198 files

This commit is contained in:
Home Assistant Version Control
2026-10-11 23:51:59 +00:00
parent 4a64770282
commit 669d229519
198 changed files with 4891 additions and 2815 deletions
+1 -1
View File
@@ -256,7 +256,7 @@
},
{
"id": "8713ea9c008e4270bb28085eed5be959",
"url": "/hacsfiles/Statistics-Graph-Chart-Card/statistics-graph-chart-card.js?hacstag=1034932856405",
"url": "/hacsfiles/Statistics-Graph-Chart-Card/statistics-graph-chart-card.js?hacstag=1034932856406",
"type": "module"
},
{
@@ -102,6 +102,7 @@ from .frontend import async_register_card
from .helpers.aggregate import iter_live_tasks
from .helpers.aggregate import object_name as aggregate_object_name
from .helpers.aggregate import task_sensor_entity_id as aggregate_task_sensor_entity_id
from .helpers.assignees import person_of_user, shared_user_ids, task_is_for, task_user_ids
from .helpers.assist_sentences import async_sync as async_sync_assist_sentences
from .helpers.dates import INTERVAL_UNITS, local_date_from_iso
from .helpers.documents import DocumentStore
@@ -279,6 +280,8 @@ SERVICE_LIST_TASKS_SCHEMA = vol.Schema(
{
vol.Optional("entry_id"): cv.string,
vol.Optional("status"): vol.In(["ok", "due_soon", "overdue", "triggered", "paused"]),
# #210: only the tasks meant for this person (helpers/assignees).
vol.Optional("person"): cv.entity_id,
}
)
@@ -869,6 +872,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
"""Handle list_tasks — a queryable snapshot for automations/voice."""
wanted_entry = call.data.get("entry_id")
wanted_status = call.data.get("status")
wanted_user = _resolve_person_user_id(call.data["person"]) if call.data.get("person") else None
tasks: list[dict[str, Any]] = []
# The shared cross-object walk (loaded objects, archived tasks left
# out) — the Assist snapshot uses the same one (DRY audit 2026-09-26 B).
@@ -876,8 +880,11 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
status = str(task.get("_status", ""))
if wanted_status and status != wanted_status:
continue
if wanted_user and not task_is_for(task, wanted_user):
continue
# #151: last completion + live trigger reading, so
# notification automations don't need extra lookups.
responsible = task.get("responsible_user_id") or None
lp = task.get("last_performed")
days_since: int | None = None
if lp:
@@ -911,6 +918,13 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
"trigger_current_delta": task.get("_trigger_current_delta"),
"trigger_target": trigger_target,
"trigger_unit": trigger_unit,
# #210: who does it, as the user id and as the person
# entity (what update_task takes as responsible_user);
# shared_with is everyone under "Shared with" (on a
# rotation, the people it rotates between).
"responsible_user_id": responsible,
"responsible_user": person_of_user(hass, responsible),
"shared_with": [p for u in shared_user_ids(task) if (p := person_of_user(hass, u))],
}
)
return {"tasks": tasks, "count": len(tasks)}
@@ -1005,7 +1019,7 @@ async def _async_setup_shared(hass: HomeAssistant) -> bool:
if nm is not None:
cfg_entry = hass.config_entries.async_get_entry(entry_id)
task_cfg = ((cfg_entry.data.get(CONF_TASKS) or {}).get(task_id) or {}) if cfg_entry is not None else {}
await nm.async_dismiss_task_notification(task_id, responsible_user_id=task_cfg.get("responsible_user_id"))
await nm.async_dismiss_task_notification(task_id, user_ids=task_user_ids(task_cfg))
nm.clear_task_state(entry_id, task_id)
unsub_notification = hass.bus.async_listen("mobile_app_notification_action", _handle_notification_action)
@@ -68,6 +68,7 @@
"turn_user": "Teď je na řadě: {user} — '{task}' u zařízení {object}.",
"turn_you": "Teď jsi na řadě ty: '{task}' u zařízení {object}.",
"turn_nobody": "K úkolu '{task}' u zařízení {object} není nikdo přiřazen.",
"turn_shared": "K úkolu '{task}' u zařízení {object} teď není nikdo přiřazen; je sdílený s: {users}.",
"buy_none": "Nic nedochází — není potřeba kupovat díly.",
"buy_list": "Docházejí díly, celkem {count}: {items}.",
"buy_list_one": "Dochází jeden díl: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "{user} har tur: '{task}' på {object}.",
"turn_you": "Det er din tur: '{task}' på {object}.",
"turn_nobody": "Ingen er tildelt '{task}' på {object}.",
"turn_shared": "Ingen er tildelt '{task}' på {object} lige nu; den er delt med {users}.",
"buy_none": "Intet er ved at slippe op — der skal ikke købes reservedele.",
"buy_list": "{count} reservedele er ved at slippe op: {items}.",
"buy_list_one": "Én reservedel er ved at slippe op: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "{user} ist dran: '{task}' an {object}.",
"turn_you": "Du bist dran: '{task}' an {object}.",
"turn_nobody": "Für '{task}' an {object} ist niemand eingetragen.",
"turn_shared": "Für '{task}' an {object} ist gerade niemand eingetragen; geteilt mit {users}.",
"buy_none": "Nichts wird knapp — es müssen keine Ersatzteile gekauft werden.",
"buy_list": "{count} Ersatzteile werden knapp: {items}.",
"buy_list_one": "Ein Ersatzteil wird knapp: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "It's {user}'s turn: '{task}' on {object}.",
"turn_you": "It's your turn: '{task}' on {object}.",
"turn_nobody": "Nobody is assigned to '{task}' on {object}.",
"turn_shared": "Nobody is assigned to '{task}' on {object} right now; it is shared with {users}.",
"buy_none": "Nothing is running low — no spare parts need buying.",
"buy_list": "{count} spare parts are running low: {items}.",
"buy_list_one": "One spare part is running low: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "Le toca a {user}: '{task}' en {object}.",
"turn_you": "Te toca a ti: '{task}' en {object}.",
"turn_nobody": "Nadie tiene asignada '{task}' en {object}.",
"turn_shared": "Ahora nadie tiene asignada '{task}' en {object}; está compartida con {users}.",
"buy_none": "No se está acabando nada: no hay que comprar repuestos.",
"buy_list": "Se están acabando {count} repuestos: {items}.",
"buy_list_one": "Se está acabando un repuesto: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "Vuorossa on {user}: '{task}' kohteessa {object}.",
"turn_you": "Sinä olet vuorossa: '{task}' kohteessa {object}.",
"turn_nobody": "Tehtävälle '{task}' kohteessa {object} ei ole määritetty ketään.",
"turn_shared": "Tehtävälle '{task}' kohteessa {object} ei ole nyt määritetty ketään; se on jaettu: {users}.",
"buy_none": "Mikään ei ole loppumassa — varaosia ei tarvitse ostaa.",
"buy_list": "Varaosia on loppumassa, yhteensä {count}: {items}.",
"buy_list_one": "Yksi varaosa on loppumassa: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "C'est au tour de {user} : '{task}' sur {object}.",
"turn_you": "C'est ton tour : '{task}' sur {object}.",
"turn_nobody": "Personne n'est assigné à '{task}' sur {object}.",
"turn_shared": "Personne n'est assigné à '{task}' sur {object} pour le moment ; la tâche est partagée avec {users}.",
"buy_none": "Rien ne manque — aucune pièce à acheter.",
"buy_list": "{count} pièces arrivent en rupture : {items}.",
"buy_list_one": "Une pièce arrive en rupture : {items}.",
@@ -68,6 +68,7 @@
"turn_user": "अभी बारी है: {user} — {object} पर '{task}'।",
"turn_you": "अभी आपकी बारी है: {object} पर '{task}'।",
"turn_nobody": "{object} पर '{task}' किसी को सौंपा नहीं गया है।",
"turn_shared": "{object} पर '{task}' अभी किसी को सौंपा नहीं गया है; यह इनके साथ साझा है: {users}।",
"buy_none": "कुछ भी ख़त्म नहीं हो रहा — कोई स्पेयर पार्ट ख़रीदने की ज़रूरत नहीं है।",
"buy_list": "{count} स्पेयर पार्ट ख़त्म होने वाले हैं: {items}।",
"buy_list_one": "एक स्पेयर पार्ट ख़त्म होने वाला है: {items}।",
@@ -68,6 +68,7 @@
"turn_user": "Most {user} következik: '{task}' – {object}.",
"turn_you": "Most te következel: '{task}' – {object}.",
"turn_nobody": "Senki sincs hozzárendelve ehhez: '{task}' – {object}.",
"turn_shared": "Jelenleg senki sincs hozzárendelve ehhez: '{task}' – {object}; megosztva: {users}.",
"buy_none": "Semmi sem fogy ki — nem kell alkatrészt venni.",
"buy_list": "Kifogyóban lévő alkatrészek, összesen {count}: {items}.",
"buy_list_one": "Egy alkatrész fogyóban van: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "Tocca a {user}: '{task}' su {object}.",
"turn_you": "Tocca a te: '{task}' su {object}.",
"turn_nobody": "Nessuno è assegnato a '{task}' su {object}.",
"turn_shared": "Al momento nessuno è assegnato a '{task}' su {object}; è condivisa con {users}.",
"buy_none": "Non sta finendo niente: non serve comprare ricambi.",
"buy_list": "Stanno finendo {count} ricambi: {items}.",
"buy_list_one": "Sta finendo un ricambio: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "今の担当は{user}です:{object}の「{task}」。",
"turn_you": "今の担当はあなたです:{object}の「{task}」。",
"turn_nobody": "{object}の「{task}」には担当者がいません。",
"turn_shared": "{object}の「{task}」には今は担当者がいません。共有相手:{users}。",
"buy_none": "残り少ないものはありません。交換部品を買う必要はありません。",
"buy_list": "残り少ない交換部品が{count}件あります:{items}。",
"buy_list_one": "残り少ない交換部品が1件あります:{items}。",
@@ -68,6 +68,7 @@
"turn_user": "지금 차례: {user} — {object}의 '{task}'.",
"turn_you": "지금은 당신 차례예요: {object}의 '{task}'.",
"turn_nobody": "{object}의 '{task}' 작업에는 담당자가 없어요.",
"turn_shared": "{object}의 '{task}' 작업에는 지금 담당자가 없어요. 공유 대상: {users}.",
"buy_none": "떨어져 가는 게 없어요. 예비 부품을 살 필요가 없어요.",
"buy_list": "떨어져 가는 예비 부품이 {count}개 있어요: {items}.",
"buy_list_one": "떨어져 가는 예비 부품이 1개 있어요: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "{user} har tur: '{task}' på {object}.",
"turn_you": "Det er din tur: '{task}' på {object}.",
"turn_nobody": "Ingen er tildelt '{task}' på {object}.",
"turn_shared": "Ingen er tildelt '{task}' på {object} akkurat nå; den er delt med {users}.",
"buy_none": "Ingenting holder på å gå tomt — ingen reservedeler må kjøpes.",
"buy_list": "{count} reservedeler holder på å gå tomme: {items}.",
"buy_list_one": "Én reservedel holder på å gå tom: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "{user} is aan de beurt: '{task}' bij {object}.",
"turn_you": "Jij bent aan de beurt: '{task}' bij {object}.",
"turn_nobody": "Niemand is toegewezen aan '{task}' bij {object}.",
"turn_shared": "Niemand is op dit moment toegewezen aan '{task}' bij {object}; de taak is gedeeld met {users}.",
"buy_none": "Er raakt niets op — er hoeven geen onderdelen gekocht te worden.",
"buy_list": "{count} onderdelen raken op: {items}.",
"buy_list_one": "Eén onderdeel raakt op: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "Teraz kolej: {user} — '{task}' na urządzeniu {object}.",
"turn_you": "Teraz twoja kolej: '{task}' na urządzeniu {object}.",
"turn_nobody": "Nikt nie jest przypisany do zadania '{task}' na urządzeniu {object}.",
"turn_shared": "Nikt nie jest teraz przypisany do zadania '{task}' na urządzeniu {object}; jest udostępnione: {users}.",
"buy_none": "Nic się nie kończy — nie trzeba kupować części.",
"buy_list": "Kończą się części, łącznie {count}: {items}.",
"buy_list_one": "Kończy się jedna część: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "É a vez de {user}: '{task}' em {object}.",
"turn_you": "É a sua vez: '{task}' em {object}.",
"turn_nobody": "Ninguém está designado para '{task}' em {object}.",
"turn_shared": "Ninguém está designado para '{task}' em {object} no momento; está compartilhada com {users}.",
"buy_none": "Nada está acabando — não é preciso comprar peças.",
"buy_list": "{count} peças estão acabando: {items}.",
"buy_list_one": "Uma peça está acabando: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "É a vez de {user}: '{task}' em {object}.",
"turn_you": "É a tua vez: '{task}' em {object}.",
"turn_nobody": "Ninguém está atribuído a '{task}' em {object}.",
"turn_shared": "Ninguém está atribuído a '{task}' em {object} neste momento; está partilhada com {users}.",
"buy_none": "Nada está a acabar — não é preciso comprar peças.",
"buy_list": "Estão a acabar {count} peças: {items}.",
"buy_list_one": "Está a acabar uma peça: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "Сейчас очередь: {user} — '{task}' у устройства {object}.",
"turn_you": "Сейчас твоя очередь: '{task}' у устройства {object}.",
"turn_nobody": "За задачу '{task}' у устройства {object} никто не отвечает.",
"turn_shared": "За задачу '{task}' у устройства {object} сейчас никто не отвечает; она общая для: {users}.",
"buy_none": "Ничего не заканчивается — покупать запчасти не нужно.",
"buy_list": "Заканчиваются запчасти, всего {count}: {items}.",
"buy_list_one": "Заканчивается одна запчасть: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "{user} har turen: '{task}' på {object}.",
"turn_you": "Det är din tur: '{task}' på {object}.",
"turn_nobody": "Ingen är tilldelad '{task}' på {object}.",
"turn_shared": "Ingen är tilldelad '{task}' på {object} just nu; den delas med {users}.",
"buy_none": "Inget håller på att ta slut — inga reservdelar behöver köpas.",
"buy_list": "{count} reservdelar håller på att ta slut: {items}.",
"buy_list_one": "En reservdel håller på att ta slut: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "Sıradaki kişi: {user} — {object} için '{task}'.",
"turn_you": "Sıra sende: {object} için '{task}'.",
"turn_nobody": "{object} için '{task}' görevine kimse atanmamış.",
"turn_shared": "{object} için '{task}' görevine şu anda kimse atanmamış; paylaşılanlar: {users}.",
"buy_none": "Tükenmek üzere olan bir şey yok — yedek parça almaya gerek yok.",
"buy_list": "{count} yedek parça tükenmek üzere: {items}.",
"buy_list_one": "Bir yedek parça tükenmek üzere: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "Зараз черга: {user} — '{task}' на пристрої {object}.",
"turn_you": "Зараз твоя черга: '{task}' на пристрої {object}.",
"turn_nobody": "За завдання '{task}' на пристрої {object} ніхто не відповідає.",
"turn_shared": "За завдання '{task}' на пристрої {object} зараз ніхто не відповідає; воно спільне для: {users}.",
"buy_none": "Нічого не закінчується — купувати запчастини не потрібно.",
"buy_list": "Закінчуються запчастини, усього {count}: {items}.",
"buy_list_one": "Закінчується одна запчастина: {items}.",
@@ -68,6 +68,7 @@
"turn_user": "现在轮到{user}:{object}的“{task}”。",
"turn_you": "现在轮到你:{object}的“{task}”。",
"turn_nobody": "{object}的“{task}”没有指派任何人。",
"turn_shared": "{object}的“{task}”目前没有指派任何人;共享给:{users}。",
"buy_none": "没有快用完的东西,不需要买备件。",
"buy_list": "有{count}个备件快用完了:{items}。",
"buy_list_one": "有1个备件快用完了:{items}。",
@@ -540,6 +540,10 @@ class MaintenanceSupporterConfigFlow(ScheduleStepsMixin, TriggerConfigMixin, Con
stock = praw.get("stock") if isinstance(praw, dict) else None
if isinstance(stock, (int, float)) and not isinstance(stock, bool) and stock >= 0:
part["stock"] = stock
# #208: and a reusable part's used ones waiting.
waiting = praw.get("restoring")
if part.get("reuse") and isinstance(waiting, (int, float)) and not isinstance(waiting, bool) and waiting > 0:
part["restoring"] = waiting
parts[part["id"]] = part
data: dict[str, Any] = {
@@ -252,9 +252,14 @@ def _build_export_object(
"tasks": tasks,
"documents": documents,
# Spare parts: full static definition + the tracked stock (dynamic,
# read from the Store) so a backup/restore keeps the shelf state.
# read from the Store) so a backup/restore keeps the shelf state —
# with a reusable part's used ones waiting to be ready again (#208).
"parts": [
{**part, "stock": store.get_part_stock(part["id"]) if store is not None else None}
{
**part,
"stock": store.get_part_stock(part["id"]) if store is not None else None,
**({"restoring": store.get_part_restoring(part["id"])} if store is not None and part.get("reuse") else {}),
}
for part in (entry.data.get("parts") or {}).values()
],
}
@@ -9,6 +9,7 @@
import { expect, fixture, html } from "@open-wc/testing";
import "../components/documents-section.js";
import type { MaintenanceDocumentsSection } from "../components/documents-section";
import { DOC_MAX_BYTES } from "../helpers/photo-upload.js";
import { createMockHass } from "./_test-utils.js";
const FILE_DOC = {
@@ -198,6 +199,62 @@ describe("documents-section", () => {
}
});
describe("a refused upload says so and keeps saying it (#209)", () => {
const origFetch = window.fetch;
afterEach(() => { window.fetch = origFetch; });
const pdf = (name: string, size = 1) => {
const f = new File(["x"], name, { type: "application/pdf" });
Object.defineProperty(f, "size", { value: size });
return f;
};
const upload = async (el: MaintenanceDocumentsSection, files: File[]) => {
(el.hass as unknown as { auth: unknown }).auth = { data: { access_token: "tok" } };
await (el as unknown as { _uploadFiles: (f: File[]) => Promise<void> })._uploadFiles.call(el, files);
await el.updateComplete;
return el.shadowRoot!.querySelector(".doc-msg.error")?.textContent?.trim() ?? "";
};
it("a 413 is still on the error line after the list reloaded", async () => {
const { el, sent } = await mount(true, [FILE_DOC]);
window.fetch = (async () => new Response("{}", { status: 413 })) as typeof fetch;
const lists = () => sent.filter((m) => m.type === "maintenance_supporter/documents/list").length;
const before = lists();
const shown = await upload(el, [pdf("hvac.pdf")]);
expect(lists(), "the list reloaded").to.equal(before + 1);
expect(shown).to.equal("hvac.pdf: File is too large (max 100 MB).");
});
it("a file over the limit is refused without being sent", async () => {
const { el } = await mount(true, [FILE_DOC]);
const posted: string[] = [];
window.fetch = (async (_u: string, o: { body: FormData }) => {
posted.push((o.body.get("file") as File).name);
return new Response(JSON.stringify({ id: "n", deduped: false }), { status: 200 });
}) as unknown as typeof fetch;
const shown = await upload(el, [pdf("manual.pdf", DOC_MAX_BYTES + 1), pdf("invoice.pdf", DOC_MAX_BYTES)]);
expect(posted, "only the file within the limit went up").to.deep.equal(["invoice.pdf"]);
expect(shown).to.equal("manual.pdf: File is too large (max 100 MB).");
});
it("a dropped connection is named per file and the rest still go up", async () => {
const { el } = await mount(true, [FILE_DOC]);
let calls = 0;
window.fetch = (async () => {
if (calls++ === 0) throw new TypeError("Failed to fetch");
return new Response(JSON.stringify({ id: "n", deduped: false }), { status: 200 });
}) as typeof fetch;
const shown = await upload(el, [pdf("a.pdf"), pdf("b.pdf")]);
expect(calls).to.equal(2);
expect(shown).to.equal("a.pdf: Upload failed.");
});
it("an upload without a refusal leaves no error behind", async () => {
const { el } = await mount(true, [FILE_DOC]);
window.fetch = (async () => new Response(JSON.stringify({ id: "n", deduped: false }), { status: 200 })) as typeof fetch;
expect(await upload(el, [pdf("ok.pdf")])).to.equal("");
});
});
it("opens the file picker via keyboard on the upload label (a11y)", async () => {
const { el } = await mount();
const label = [...el.shadowRoot!.querySelectorAll("label.btn")].find(
@@ -75,6 +75,8 @@ interface Overview {
available: boolean;
configured: boolean;
task_ok?: boolean;
/** The fleet object's entry (its parts are the battery types). */
entry_id?: string | null;
total: number;
low: BatteryRow[];
soon: BatteryRow[];
@@ -84,6 +86,9 @@ interface Overview {
needs_now: Record<string, number>;
needs_soon: Record<string, number>;
types: string[];
// #208: the counted rechargeable spares — charged (stock) and waiting to
// be charged (restoring). An older backend omits it.
charging?: { part_id: string; name: string; reuse: string; stock: number; restoring: number }[];
excluded?: { entity_id: string; device_name: string }[];
// #135 follow-up: fleet-wide "track self-charging devices" opt-in.
track_self_charging?: boolean;
@@ -550,6 +555,34 @@ export class MaintenanceBatteryFleetSection extends LitElement {
: nothing}
`}
${ov.charging?.length
? html`
<div class="bf-charging">
<span class="bf-label">${t("battery_fleet_charging", L)}</span>
${ov.charging.map(
(c) => html`<span class="bf-charge-item">
<span class="bf-charge-name">${c.name}</span>
<span>${t("battery_fleet_ready", L).replace("{count}", String(c.stock))}</span>
${c.restoring > 0
? html`<span class="bf-charge-waiting">${t(`part_waiting_${c.reuse}`, L).replace("{count}", String(c.restoring))}</span>
${this._canWrite
? html`<button
class="bf-charged"
title=${t("part_ready_again_hint", L)}
.disabled=${this._marking}
@click=${() => this._act({
type: "maintenance_supporter/part/ready_again",
entry_id: ov.entry_id,
part_id: c.part_id,
})}
><ha-icon icon="mdi:battery-charging-100"></ha-icon>${t(`part_reuse_${c.reuse}`, L)}</button>`
: nothing}`
: nothing}
</span>`,
)}
</div>
`
: nothing}
${ov.soon.length
? html`
<div class="bf-soon">
@@ -730,6 +763,46 @@ export class MaintenanceBatteryFleetSection extends LitElement {
align-items: baseline;
gap: 8px;
}
.bf-charging {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin: 8px 0;
}
.bf-charge-item {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding: 2px 8px;
border-radius: 12px;
background: var(--secondary-background-color, #eee);
font-size: 13px;
}
.bf-charge-name {
font-weight: 500;
}
.bf-charge-waiting {
color: var(--warning-color, #f57c00);
}
.bf-charged {
display: inline-flex;
align-items: center;
gap: 4px;
font: inherit;
font-size: 12px;
min-height: 28px;
padding: 2px 10px;
border-radius: 14px;
border: 1px solid var(--primary-color);
background: transparent;
color: var(--primary-color);
cursor: pointer;
}
.bf-charged ha-icon {
--mdc-icon-size: 16px;
}
.bf-label {
font-size: 12px;
text-transform: uppercase;
@@ -8,7 +8,7 @@
*/
import { LitElement, html, css, nothing } from "lit";
import { isSafeHttpUrl } from "../helpers/url";
import { isSafeLink } from "../helpers/url";
import { property, state } from "lit/decorators.js";
import { t, ensureLocale, langOf } from "../styles";
import { runWs } from "../helpers/ws-run";
@@ -230,17 +230,18 @@ export class MaintenanceDocumentsSection extends LitElement {
this._hint = "";
let deduped = 0;
let dupInObject = 0;
const refused: string[] = [];
try {
// Bulk upload: one refused file (too large, rejected) is noted and
// the rest still go up — unlike the photo dialogs, which stop.
// Bulk upload: one refused file (too large, rejected, the connection
// dropped) is noted by name and the rest still go up — unlike the
// photo dialogs, which stop.
for (const file of files) {
let doc;
try {
doc = await uploadDocument(this.hass, entryId, file, [cat]);
} catch (e) {
const key = e instanceof Error ? e.message : "";
if (key !== "doc_too_large" && key !== "doc_upload_failed") throw e;
this._error = t(key, this._lang);
const key = e instanceof Error && e.message === "doc_too_large" ? "doc_too_large" : "doc_upload_failed";
refused.push(`${file.name}: ${t(key, this._lang)}`);
continue;
}
if (doc.duplicate_in_object) dupInObject++;
@@ -250,9 +251,10 @@ export class MaintenanceDocumentsSection extends LitElement {
if (dupInObject) this._hint = t("doc_dup_in_object", this._lang);
else if (deduped) this._hint = t("doc_deduped", this._lang);
await this._load();
} catch {
this._error = t("doc_upload_failed", this._lang);
} finally {
// Said AFTER the reload: _load() clears the error line, so a refusal
// set before it flashed for a moment and was gone (#209).
if (refused.length && entryId === this.entryId) this._error = refused.join(" · ");
this._busy = false;
}
}
@@ -277,7 +279,7 @@ export class MaintenanceDocumentsSection extends LitElement {
if (doc.kind === "file") void this._preview(doc);
// Only open http(s) links — never a javascript:/data: URL (the same scheme
// guard the rest of the panel applies before opening user-supplied URLs).
else if (isSafeHttpUrl(doc.url)) window.open(doc.url, "_blank", "noopener");
else if (isSafeLink(doc.url)) window.open(doc.url, "_blank", "noopener");
}
private _startEdit(doc: MaintenanceDocument): void {
@@ -340,7 +342,7 @@ export class MaintenanceDocumentsSection extends LitElement {
this._linkDescription = "";
this._addingLink = false;
},
"doc_link_invalid",
"doc_link_not_allowed",
);
}
@@ -521,7 +523,7 @@ export class MaintenanceDocumentsSection extends LitElement {
</button>`
: html`<a
class="icon-btn"
href=${isSafeHttpUrl(doc.url) ? doc.url : "#"}
href=${isSafeLink(doc.url) ? doc.url : "#"}
target="_blank"
rel="noopener noreferrer"
title=${t("doc_open", L)}
@@ -9,7 +9,7 @@
*/
import { LitElement, html, css, nothing } from "lit";
import { isSafeHttpUrl } from "../helpers/url";
import { isSafeHttpUrl, isSafeLink } from "../helpers/url";
import { renderNotesMarkdown } from "../helpers/notes-markdown";
import { property, state } from "lit/decorators.js";
import { t, langOf } from "../styles";
@@ -231,26 +231,28 @@ export class MaintenanceObjectQuickActionsDialog extends LitElement {
private _renderMetaRow(obj: MaintenanceObject) {
const L = this._lang;
const items: Array<[string, string]> = [];
const items: Array<[string, string, boolean?]> = [];
if (obj.area_id) items.push([t("area", L), obj.area_id]);
if (obj.manufacturer) items.push([t("manufacturer", L), obj.manufacturer]);
if (obj.model) items.push([t("model", L), obj.model]);
if (obj.serial_number) items.push([t("serial_number_label", L), obj.serial_number]);
if (obj.installation_date) items.push([t("installed", L), obj.installation_date]);
if (obj.warranty_expiry) items.push([t("warranty", L), obj.warranty_expiry]);
if (obj.documentation_url) items.push([t("documentation_url_label", L), obj.documentation_url]);
// The documentation link may also open an app (#207).
if (obj.documentation_url) items.push([t("documentation_url_label", L), obj.documentation_url, isSafeLink(obj.documentation_url)]);
if (items.length === 0) return nothing;
return html`
<div class="meta">
${items.map(
([label, value]) => html`
([label, value, docLink]) => html`
<div class="meta-item">
<span class="meta-label">${label}</span>
<span class="meta-value">${
// Only render http(s) values as links (never javascript:/data:);
// value-based so it works in every UI language, not just English.
isSafeHttpUrl(value)
// The documentation link was checked by the app-link rule above.
docLink || isSafeHttpUrl(value)
? html`<a href="${value}" target="_blank" rel="noopener noreferrer">${value}</a>`
: value
}</span>
@@ -22,6 +22,9 @@ import { restockUnits, unitPrice } from "../helpers/parts-cost";
// Per-part document links (v2.26) — the task-documents component in part mode.
import "./task-documents";
/** #208: what readies a used part again (helpers/parts.py REUSE_KINDS). */
export const REUSE_KINDS = ["charge", "sharpen", "clean", "dry", "refill", "restore"] as const;
interface PartForm {
id?: string;
name: string;
@@ -33,6 +36,9 @@ interface PartForm {
unit: string;
cost: string;
stock: string;
/** #208: "" = used up, else what readies a used one again. */
reuse: string;
restoring: string;
reorder_threshold: string;
restock_quantity: string;
package_size: string;
@@ -50,6 +56,8 @@ const EMPTY_FORM: PartForm = {
unit: "",
cost: "",
stock: "",
reuse: "",
restoring: "",
reorder_threshold: "",
restock_quantity: "",
package_size: "",
@@ -112,6 +120,8 @@ export class MaintenancePartsSection extends LitElement {
unit: part.unit || "",
cost: part.cost != null ? String(part.cost) : "",
stock: part.stock != null ? String(part.stock) : "",
reuse: part.reuse || "",
restoring: part.restoring ? String(part.restoring) : "",
reorder_threshold: part.reorder_threshold != null ? String(part.reorder_threshold) : "",
restock_quantity: part.restock_quantity != null ? String(part.restock_quantity) : "",
package_size: part.package_size != null ? String(part.package_size) : "",
@@ -133,6 +143,8 @@ export class MaintenancePartsSection extends LitElement {
unit: f.unit.trim() || null,
cost: num(f.cost),
stock: num(f.stock),
reuse: f.reuse || null,
...(f.reuse ? { restoring: num(f.restoring) ?? 0 } : {}),
reorder_threshold: num(f.reorder_threshold),
restock_quantity: num(f.restock_quantity),
package_size: num(f.package_size),
@@ -193,6 +205,23 @@ export class MaintenancePartsSection extends LitElement {
}
}
/** #208: the used ones of a reusable part are ready again (charged,
* sharpened, ...) — back from the pile to the stock. */
private async _readyAgain(part: MaintenancePart): Promise<void> {
if (this._busy) return;
const result = await this._send<{ stock: number; restoring: number }>({
type: "maintenance_supporter/part/ready_again",
entry_id: this.entryId,
part_id: part.id,
});
if (result) {
part.stock = result.stock;
part.restoring = result.restoring;
this.requestUpdate();
this._notifyChanged();
}
}
private _identLine(part: MaintenancePart): string {
return [part.vendor, part.mpn ? `MPN: ${part.mpn}` : "", part.gtin ? `GTIN: ${part.gtin}` : ""]
.filter(Boolean)
@@ -220,6 +249,17 @@ export class MaintenancePartsSection extends LitElement {
>`
: nothing}
</div>
${tracked && part.reuse && (part.restoring ?? 0) > 0
? html`<div class="part-waiting">
<span class="waiting-badge">${t(`part_waiting_${part.reuse}`, L).replace("{count}", formatNumber(part.restoring!, L))}</span>
${this.canWrite
? html`<button class="ready-again" title=${t("part_ready_again_hint", L)} .disabled=${this._busy}
@click=${() => this._readyAgain(part)}>
<ha-icon icon="mdi:check"></ha-icon>${t(`part_reuse_${part.reuse}`, L)}
</button>`
: nothing}
</div>`
: nothing}
<div class="part-meta">
${ident ? html`<span>${ident}</span>` : nothing}
${part.storage_location
@@ -322,6 +362,23 @@ export class MaintenancePartsSection extends LitElement {
${this._field(t("part_unit", L), "unit")}
${this._field(t("part_cost", L), "cost", { type: "number" })}
${this._field(t("part_stock", L), "stock", { type: "number" })}
<label class="form-field">
<span>${t("part_reuse", L)}</span>
<select
class="reuse-select"
.value=${f.reuse}
@change=${(e: Event) => {
this._editing = { ...this._editing!, reuse: (e.target as HTMLSelectElement).value };
}}
>
<option value="" ?selected=${!f.reuse}>${t("part_reuse_none", L)}</option>
${REUSE_KINDS.map(
(kind) => html`<option value=${kind} ?selected=${f.reuse === kind}>${t(`part_reuse_${kind}`, L)}</option>`,
)}
</select>
</label>
${f.reuse ? this._field(t("part_restoring", L), "restoring", { type: "number" }) : nothing}
${f.reuse ? html`<div class="form-hint">${t("part_reuse_hint", L)}</div>` : nothing}
${this._field(t("part_reorder_threshold", L), "reorder_threshold", { type: "number" })}
${this._field(t("part_package_size", L), "package_size", { type: "number" })}
${this._field(
@@ -477,6 +534,45 @@ export class MaintenancePartsSection extends LitElement {
.part-meta .loc ha-icon {
--mdc-icon-size: 13px;
}
.part-waiting {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-top: 4px;
}
.waiting-badge {
font-size: 12px;
padding: 1px 8px;
border-radius: 10px;
background: var(--secondary-background-color, #eee);
color: var(--primary-text-color);
}
.ready-again {
display: inline-flex;
align-items: center;
gap: 4px;
font: inherit;
font-size: 12px;
padding: 2px 10px;
min-height: 28px;
border-radius: 14px;
border: 1px solid var(--primary-color);
background: transparent;
color: var(--primary-color);
cursor: pointer;
}
.ready-again ha-icon {
--mdc-icon-size: 16px;
}
.reuse-select {
font: inherit;
padding: 6px;
border-radius: 4px;
border: 1px solid var(--divider-color, #ccc);
background: var(--card-background-color, #fff);
color: var(--primary-text-color);
}
.restock-input {
width: 64px;
padding: 4px;
@@ -12,7 +12,7 @@ import { property, state } from "lit/decorators.js";
import { t, ensureLocale, langOf } from "../styles";
import { runWs } from "../helpers/ws-run";
import { openSignedDocument } from "../helpers/document-url";
import { isSafeHttpUrl } from "../helpers/url";
import { isSafeLink } from "../helpers/url";
import { formatBytes } from "../helpers/format-bytes";
import type { HomeAssistant } from "../types";
@@ -145,7 +145,7 @@ export class MaintenanceStorageSectionCard extends LitElement {
// Defense-in-depth (mirrors task-documents/documents-section): never
// hand a non-http(s) weblink to window.open, even though the WS add
// path already rejects such schemes.
if (isSafeHttpUrl(doc.url)) window.open(doc.url, "_blank", "noopener");
if (isSafeLink(doc.url)) window.open(doc.url, "_blank", "noopener");
return;
}
await runWs(this, () => openSignedDocument(this.hass, doc.id), { onError: (m) => { this._error = m; } });
@@ -15,7 +15,7 @@ import { t, ensureLocale, langOf } from "../styles";
import { runWs } from "../helpers/ws-run";
import { downloadUrl } from "../helpers/download";
import { downloadSignedDocument, openSignedDocument } from "../helpers/document-url";
import { isSafeHttpUrl } from "../helpers/url";
import { isSafeLink } from "../helpers/url";
import { formatBytes } from "../helpers/format-bytes";
import { docCategory, docDisplayName, CATEGORY_ICONS } from "../helpers/document-categories";
import { DOC_FILTER_MIN, filterDocuments } from "../helpers/document-filter";
@@ -152,7 +152,7 @@ export class MaintenanceTaskDocuments extends LitElement {
// schemes, but a weblink stored before that check (or via a future
// path) must never reach window.open as javascript:/data: — guard the
// sink too, like documents-section does.
if (isSafeHttpUrl(doc.url)) window.open(doc.url, "_blank", "noopener");
if (isSafeLink(doc.url)) window.open(doc.url, "_blank", "noopener");
return;
}
// A per-task page hint jumps straight to the relevant page via the PDF
@@ -18,6 +18,7 @@ import type { HistoryEntry, MaintenanceObjectResponse } from "../types";
import { entrySpend } from "./parts-cost";
import { intervalSpanDays } from "./interval";
import { statusRank } from "../status-constants";
import { taskIsFor } from "./assignees";
import { addDaysIso, stampDate } from "./ha-time";
export const MAX_OCCURRENCES_PER_TASK = 5;
@@ -201,7 +202,8 @@ function projectTask(input: ProjectionInput): CalendarEvent[] {
* next_due and the overdue status count from that day,
* not from the browser's).
* @param windowDays Number of days to include (7 / 14 / 30).
* @param userFilter null/empty = all; otherwise filter tasks by responsible_user_id.
* @param userFilter null/empty = all; otherwise only the tasks meant for that
* user (helpers/assignees: theirs, or shared with nobody on duty).
*/
export function buildCalendarBuckets(
objects: MaintenanceObjectResponse[],
@@ -220,7 +222,7 @@ export function buildCalendarBuckets(
const tasks = obj.tasks || [];
for (const task of tasks) {
// User filter
if (userFilter && task.responsible_user_id !== userFilter) continue;
if (userFilter && !taskIsFor(task, userFilter)) continue;
// Disabled tasks never produce calendar events
if (task.enabled === false) continue;
const projected = projectTask({
@@ -368,7 +370,7 @@ export function buildPastBuckets(
const entryId = obj.entry_id;
const tasks = obj.tasks || [];
for (const task of tasks) {
if (userFilter && task.responsible_user_id !== userFilter) continue;
if (userFilter && !taskIsFor(task, userFilter)) continue;
const history = fullHistory[pastHistoryKey(entryId, task.id)] ?? ((task.history || []) as HistoryEntryShape[]);
for (const h of history) {
if (typeof h?.timestamp !== "string") continue;
@@ -1,11 +1,62 @@
/** Shared URL-safety guard for rendering user-supplied links.
*
* A user-entered URL (object/task documentation, spare-part shopping link,
* document weblink) must only ever be linkified when it is an absolute http(s)
* URL — never `javascript:`/`data:`/protocol-relative — else it is a stored-XSS
* vector. The `X && /^https?:\/\//i.test(X)` idiom was inlined at ~8 render
* sites; one forgotten copy is a hole, so it lives here once.
* document weblink) must never be linkified as `javascript:`/`data:`/
* protocol-relative, else it is a stored-XSS vector. The
* `X && /^https?:\/\//i.test(X)` idiom was inlined at ~8 render sites; one
* forgotten copy is a hole, so it lives here once.
*
* Since #207 a documentation link or a document's web link may also open an
* app (`obsidian://`, `onenote:`, `mailto:`): every scheme passes except the
* blocked ones, the twin of `helpers/url_safety.py` (a tripwire pins the two).
*/
/** Schemes a link may never use: they run code in the page, open the
* device's own files or the browser's internals, or start Windows handlers
* attackers have abused. */
export const BLOCKED_LINK_SCHEMES: ReadonlySet<string> = new Set([
"about",
"blob",
"data",
"file",
"filesystem",
"javascript",
"livescript",
"ms-appinstaller",
"ms-cxh",
"ms-cxh-full",
"ms-msdt",
"ms-officecmd",
"search",
"search-ms",
"view-source",
"vbscript",
]);
/** The link as a browser reads it before looking at the scheme: ASCII tabs
* and newlines anywhere dropped, control characters and spaces at either
* end trimmed ("java\tscript:" is javascript:). */
export function cleanLink(url: string): string {
// eslint-disable-next-line no-control-regex
return url.replace(/[\t\n\r]/g, "").replace(/^[\u0000-\u0020]+|[\u0000-\u0020]+$/g, "");
}
/** The lower-cased scheme of an absolute link, else null. Two characters at
* least: "C:\manual.pdf" is a drive path, not a link. */
export function linkScheme(url: string | null | undefined): string | null {
const match = /^([a-z][a-z0-9+.-]+):/i.exec(cleanLink(url || ""));
return match ? match[1].toLowerCase() : null;
}
/** An absolute link safe to open: http(s) or an app link — never one of the
* blocked schemes, never relative. */
export function isSafeLink(url: string | null | undefined): url is string {
const scheme = linkScheme(url);
return scheme !== null && !BLOCKED_LINK_SCHEMES.has(scheme);
}
/** Only an absolute http(s) URL — for links that must be web pages (a
* spare part's shop page). */
export function isSafeHttpUrl(url: string | null | undefined): url is string {
return !!url && /^https?:\/\//i.test(url);
}
@@ -888,7 +888,7 @@
"doc_link_title": "Název (nepovinné)",
"doc_open": "Otevřít",
"doc_delete_confirm": "Smazat „{name}“?",
"doc_too_large": "Soubor je příliš velký (max. 25 MB).",
"doc_too_large": "Soubor je příliš velký (max. 100 MB).",
"doc_upload_failed": "Nahrání selhalo.",
"docs_archive_too_large": "Archiv je příliš velký (max. {max}).",
"docs_archive_no_manifest": "Tento ZIP neobsahuje manifest.json, takže obnovení neví, ke kterému dokumentu soubor patří. Při opětovném zabalení ponechte manifest.json vedle složek.",
@@ -903,7 +903,7 @@
"remove": "Odebrat",
"doc_deduped": "Již uloženo jinde — sdíleno, bez dalšího místa.",
"doc_dup_in_object": "Tento soubor je již připojen k tomuto objektu.",
"doc_link_invalid": "Povoleny jsou pouze odkazy http/https.",
"doc_link_not_allowed": "Tento odkaz není povolen: odkazy, které spouštějí skripty nebo otevírají místní soubory (javascript:, data:, file: …), jsou odmítnuty.",
"doc_cat_manual": "Návod",
"doc_cat_warranty": "Záruka",
"doc_cat_invoice": "Faktura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Množství k doplnění (balení)",
"part_package_hint": "Kupuje se po baleních, spotřebovává po jednotkách: zásoba, spotřeba na úkol a hranice objednání počítají jednotky; množství k doplnění a cena platí za balení.",
"part_auto_buy": "Úkol nákupu při nízké zásobě",
"part_reuse": "Po použití",
"part_reuse_none": "Spotřebován",
"part_reuse_charge": "Nabito",
"part_reuse_sharpen": "Nabroušeno",
"part_reuse_clean": "Vyčištěno",
"part_reuse_dry": "Vysušeno",
"part_reuse_refill": "Doplněno",
"part_reuse_restore": "Obnoveno",
"part_waiting_charge": "{count} k nabití",
"part_waiting_sharpen": "{count} k nabroušení",
"part_waiting_clean": "{count} k vyčištění",
"part_waiting_dry": "{count} k vysušení",
"part_waiting_refill": "{count} k doplnění",
"part_waiting_restore": "{count} k obnovení",
"part_restoring": "Použité, zatím nepřipravené",
"part_ready_again_hint": "Všechny použité jsou opět připravené",
"part_reuse_hint": "Použitý kus nezmizí: čeká, až bude znovu připravený, a dál se počítá jako vlastněný, takže se znovu nekupuje.",
"part_restock": "Upravit zásobu",
"parts_used_by": "Používá",
"restock_quantity_label": "Zakoupené množství",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Řadit podle názvu",
"battery_fleet_sort_urgency": "Řadit podle naléhavosti",
"battery_fleet_mark_recharged": "Označit jako nabitou",
"battery_fleet_charging": "Náhradní akumulátory",
"battery_fleet_ready": "{count} připraveno",
"battery_fleet_sparkline_hint": "Stav baterie za posledních 30 dní — tečkovaně: projekce k prahu vybití",
"battery_fleet_filter_type": "Zobrazit pouze tento typ baterie",
"battery_fleet_record_replacement": "Stav poskočil kolem {date} — zaznamenat tuto výměnu do Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Titel (valgfri)",
"doc_open": "Åbn",
"doc_delete_confirm": "Slet \"{name}\"?",
"doc_too_large": "Filen er for stor (maks. 25 MB).",
"doc_too_large": "Filen er for stor (maks. 100 MB).",
"doc_upload_failed": "Upload mislykkedes.",
"docs_archive_too_large": "Arkivet er for stort (maks. {max}).",
"docs_archive_no_manifest": "Denne ZIP har ingen manifest.json, så gendannelsen ved ikke, hvilket dokument en fil hører til. Lad manifest.json ligge ved siden af mapperne, når du zipper igen.",
@@ -903,7 +903,7 @@
"remove": "Fjern",
"doc_deduped": "Allerede gemt et andet sted — delt, ingen ekstra plads.",
"doc_dup_in_object": "Denne fil er allerede vedhæftet dette objekt.",
"doc_link_invalid": "Kun http/https-links er tilladt.",
"doc_link_not_allowed": "Dette link er ikke tilladt: links, der kører scripts eller åbner lokale filer (javascript:, data:, file: …), afvises.",
"doc_cat_manual": "Vejledning",
"doc_cat_warranty": "Garanti",
"doc_cat_invoice": "Faktura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Genopfyldningsmængde (pakninger)",
"part_package_hint": "Købes i pakninger, bruges i enheden: lager, forbrug pr. opgave og genbestillingsgrænse tæller enheder; genopfyldningsmængde og pris gælder pr. pakning.",
"part_auto_buy": "Købsopgave ved lav beholdning",
"part_reuse": "Efter brug",
"part_reuse_none": "Brugt op",
"part_reuse_charge": "Opladet",
"part_reuse_sharpen": "Slebet",
"part_reuse_clean": "Rengjort",
"part_reuse_dry": "Tørret",
"part_reuse_refill": "Genopfyldt",
"part_reuse_restore": "Istandsat",
"part_waiting_charge": "{count} skal oplades",
"part_waiting_sharpen": "{count} skal slibes",
"part_waiting_clean": "{count} skal rengøres",
"part_waiting_dry": "{count} skal tørres",
"part_waiting_refill": "{count} skal genopfyldes",
"part_waiting_restore": "{count} skal istandsættes",
"part_restoring": "Brugte, ikke klar endnu",
"part_ready_again_hint": "Alle brugte er klar igen",
"part_reuse_hint": "En brugt er ikke væk: den venter, til den er klar igen, og tæller stadig som ejet, så den bliver ikke købt igen.",
"part_restock": "Justér lager",
"parts_used_by": "Bruges af",
"restock_quantity_label": "Købt mængde",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Sortér efter navn",
"battery_fleet_sort_urgency": "Sortér efter hastende grad",
"battery_fleet_mark_recharged": "Markér som genopladet",
"battery_fleet_charging": "Genopladelige reserver",
"battery_fleet_ready": "{count} klar",
"battery_fleet_sparkline_hint": "Batteriniveau de seneste 30 dage — stiplet: fremskrivning ned til lav-tærsklen",
"battery_fleet_filter_type": "Vis kun denne batteritype",
"battery_fleet_record_replacement": "Niveauet sprang omkring {date} — registrér denne udskiftning i Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Titel (optional)",
"doc_open": "Öffnen",
"doc_delete_confirm": "„{name}“ löschen?",
"doc_too_large": "Datei ist zu groß (max. 25 MB).",
"doc_too_large": "Datei ist zu groß (max. 100 MB).",
"doc_upload_failed": "Upload fehlgeschlagen.",
"docs_archive_too_large": "Das Archiv ist zu groß (max. {max}).",
"docs_archive_no_manifest": "Dieses ZIP enthält keine manifest.json, daher weiß die Wiederherstellung nicht, zu welchem Dokument eine Datei gehört. Lass die manifest.json beim erneuten Zippen neben den Ordnern liegen.",
@@ -903,7 +903,7 @@
"remove": "Entfernen",
"doc_deduped": "Bereits anderswo gespeichert — geteilt, kein zusätzlicher Speicher.",
"doc_dup_in_object": "Diese Datei ist bereits an diesem Objekt angehängt.",
"doc_link_invalid": "Nur http/https-Links sind erlaubt.",
"doc_link_not_allowed": "Dieser Link ist nicht erlaubt: Links, die Skripte ausführen oder lokale Dateien öffnen (javascript:, data:, file: …), werden abgelehnt.",
"doc_cat_manual": "Anleitung",
"doc_cat_warranty": "Garantie",
"doc_cat_invoice": "Rechnung",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Nachkaufmenge (Packungen)",
"part_package_hint": "Gekauft in Packungen, verbraucht in der Einheit: Bestand, Verbrauch pro Aufgabe und Nachbestellgrenze zählen Einheiten; Nachkaufmenge und Preis gelten pro Packung.",
"part_auto_buy": "Kauf-Task bei Niedrigbestand",
"part_reuse": "Nach Gebrauch",
"part_reuse_none": "Verbraucht",
"part_reuse_charge": "Geladen",
"part_reuse_sharpen": "Geschärft",
"part_reuse_clean": "Gereinigt",
"part_reuse_dry": "Getrocknet",
"part_reuse_refill": "Nachgefüllt",
"part_reuse_restore": "Aufbereitet",
"part_waiting_charge": "{count} zu laden",
"part_waiting_sharpen": "{count} zu schärfen",
"part_waiting_clean": "{count} zu reinigen",
"part_waiting_dry": "{count} zu trocknen",
"part_waiting_refill": "{count} nachzufüllen",
"part_waiting_restore": "{count} aufzubereiten",
"part_restoring": "Gebraucht, noch nicht bereit",
"part_ready_again_hint": "Alle gebrauchten sind wieder bereit",
"part_reuse_hint": "Ein gebrauchtes ist nicht weg: Es wartet, bis es wieder bereit ist, und zählt weiter zum Bestand, wird also nicht neu gekauft.",
"part_restock": "Bestand anpassen",
"parts_used_by": "Verwendet von",
"restock_quantity_label": "Gekaufte Menge",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Nach Name sortieren",
"battery_fleet_sort_urgency": "Nach Dringlichkeit sortieren",
"battery_fleet_mark_recharged": "Als aufgeladen markieren",
"battery_fleet_charging": "Akku-Vorrat",
"battery_fleet_ready": "{count} bereit",
"battery_fleet_sparkline_hint": "Batteriestand der letzten 30 Tage — gepunktet: Prognose bis zur Low-Schwelle",
"battery_fleet_filter_type": "Nur diesen Batterietyp anzeigen",
"battery_fleet_record_replacement": "Der Batteriestand ist um den {date} gesprungen — diesen Wechsel in Battery Notes nachtragen",
@@ -895,7 +895,7 @@
"doc_link_title": "Title (optional)",
"doc_open": "Open",
"doc_delete_confirm": "Delete \"{name}\"?",
"doc_too_large": "File is too large (max 25 MB).",
"doc_too_large": "File is too large (max 100 MB).",
"doc_upload_failed": "Upload failed.",
"docs_archive_too_large": "The archive is too large (max {max}).",
"docs_archive_no_manifest": "This ZIP has no manifest.json, so the restore cannot tell which document a file belongs to. Keep manifest.json next to the folders when zipping again.",
@@ -910,7 +910,7 @@
"remove": "Remove",
"doc_deduped": "Already stored elsewhere — shared, no extra space used.",
"doc_dup_in_object": "This file is already attached to this object.",
"doc_link_invalid": "Only http/https links are allowed.",
"doc_link_not_allowed": "This link is not allowed: links that run scripts or open local files (javascript:, data:, file: …) are refused.",
"doc_cat_manual": "Manual",
"doc_cat_warranty": "Warranty",
"doc_cat_invoice": "Invoice",
@@ -1085,6 +1085,23 @@
"part_restock_packages": "Restock amount (packages)",
"part_package_hint": "Bought in packages, used in the unit: stock, the amount a task uses and the reorder threshold count units; the restock amount and the price are per package.",
"part_auto_buy": "Auto-create buy task when low",
"part_reuse": "After use",
"part_reuse_none": "Used up",
"part_reuse_charge": "Charged",
"part_reuse_sharpen": "Sharpened",
"part_reuse_clean": "Cleaned",
"part_reuse_dry": "Dried",
"part_reuse_refill": "Refilled",
"part_reuse_restore": "Restored",
"part_waiting_charge": "{count} to charge",
"part_waiting_sharpen": "{count} to sharpen",
"part_waiting_clean": "{count} to clean",
"part_waiting_dry": "{count} to dry",
"part_waiting_refill": "{count} to refill",
"part_waiting_restore": "{count} to restore",
"part_restoring": "Used, not ready yet",
"part_ready_again_hint": "All used ones are ready again",
"part_reuse_hint": "A used one is not gone: it waits until it is ready again and still counts as owned, so it is not bought again.",
"part_restock": "Adjust stock",
"parts_used_by": "Used by",
"restock_quantity_label": "Quantity bought",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Sort by name",
"battery_fleet_sort_urgency": "Sort by urgency",
"battery_fleet_mark_recharged": "Mark as recharged",
"battery_fleet_charging": "Rechargeable spares",
"battery_fleet_ready": "{count} ready",
"battery_fleet_sparkline_hint": "Battery level over the last 30 days — dotted: projected until the low threshold",
"battery_fleet_filter_type": "Show only this battery type",
"battery_fleet_record_replacement": "The level jumped around {date} — record this replacement in Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Título (opcional)",
"doc_open": "Abrir",
"doc_delete_confirm": "¿Eliminar «{name}»?",
"doc_too_large": "El archivo es demasiado grande (máx. 25 MB).",
"doc_too_large": "El archivo es demasiado grande (máx. 100 MB).",
"doc_upload_failed": "Error al subir.",
"docs_archive_too_large": "El archivo comprimido es demasiado grande (máx. {max}).",
"docs_archive_no_manifest": "Este ZIP no contiene manifest.json, así que la restauración no sabe a qué documento pertenece cada archivo. Deja manifest.json junto a las carpetas al volver a comprimir.",
@@ -903,7 +903,7 @@
"remove": "Quitar",
"doc_deduped": "Ya almacenado en otro lugar — compartido, sin espacio adicional.",
"doc_dup_in_object": "Este archivo ya está adjunto a este objeto.",
"doc_link_invalid": "Solo se permiten enlaces http/https.",
"doc_link_not_allowed": "Este enlace no está permitido: se rechazan los enlaces que ejecutan scripts o abren archivos locales (javascript:, data:, file: …).",
"doc_cat_manual": "Manual",
"doc_cat_warranty": "Garantía",
"doc_cat_invoice": "Factura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Cantidad de reposición (envases)",
"part_package_hint": "Se compra por envases y se usa por unidades: el stock, el consumo por tarea y el umbral de pedido cuentan unidades; la cantidad de reposición y el precio son por envase.",
"part_auto_buy": "Crear tarea de compra si bajo",
"part_reuse": "Tras el uso",
"part_reuse_none": "Consumido",
"part_reuse_charge": "Recargado",
"part_reuse_sharpen": "Afilado",
"part_reuse_clean": "Limpiado",
"part_reuse_dry": "Secado",
"part_reuse_refill": "Rellenado",
"part_reuse_restore": "Restaurado",
"part_waiting_charge": "{count} por recargar",
"part_waiting_sharpen": "{count} por afilar",
"part_waiting_clean": "{count} por limpiar",
"part_waiting_dry": "{count} por secar",
"part_waiting_refill": "{count} por rellenar",
"part_waiting_restore": "{count} por restaurar",
"part_restoring": "Usados, aún no listos",
"part_ready_again_hint": "Todos los usados vuelven a estar listos",
"part_reuse_hint": "Una pieza usada no se pierde: espera hasta estar lista de nuevo y sigue contando como propia, así que no se vuelve a comprar.",
"part_restock": "Ajustar existencias",
"parts_used_by": "Usado por",
"restock_quantity_label": "Cantidad comprada",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Ordenar por nombre",
"battery_fleet_sort_urgency": "Ordenar por urgencia",
"battery_fleet_mark_recharged": "Marcar como recargada",
"battery_fleet_charging": "Repuestos recargables",
"battery_fleet_ready": "{count} listas",
"battery_fleet_sparkline_hint": "Nivel de batería de los últimos 30 días — punteado: proyección hasta el umbral bajo",
"battery_fleet_filter_type": "Mostrar solo este tipo de pila",
"battery_fleet_record_replacement": "El nivel dio un salto hacia el {date} — registrar esta sustitución en Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Otsikko (valinnainen)",
"doc_open": "Avaa",
"doc_delete_confirm": "Poistetaanko \"{name}\"?",
"doc_too_large": "Tiedosto on liian suuri (enint. 25 Mt).",
"doc_too_large": "Tiedosto on liian suuri (enint. 100 Mt).",
"doc_upload_failed": "Lataus epäonnistui.",
"docs_archive_too_large": "Arkisto on liian suuri (enintään {max}).",
"docs_archive_no_manifest": "Tässä ZIP-tiedostossa ei ole manifest.json-tiedostoa, joten palautus ei tiedä, mihin asiakirjaan tiedosto kuuluu. Pidä manifest.json kansioiden vieressä, kun pakkaat uudelleen.",
@@ -903,7 +903,7 @@
"remove": "Poista",
"doc_deduped": "Jo tallennettu muualle — jaettu, ei lisätilaa.",
"doc_dup_in_object": "Tämä tiedosto on jo liitetty tähän kohteeseen.",
"doc_link_invalid": "Vain http/https-linkit ovat sallittuja.",
"doc_link_not_allowed": "Tämä linkki ei ole sallittu: linkit, jotka suorittavat skriptejä tai avaavat paikallisia tiedostoja (javascript:, data:, file: …), hylätään.",
"doc_cat_manual": "Käyttöohje",
"doc_cat_warranty": "Takuu",
"doc_cat_invoice": "Lasku",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Täydennysmäärä (pakkausta)",
"part_package_hint": "Ostetaan pakkauksina, kulutetaan yksikköinä: varasto, tehtävän kulutus ja tilausraja lasketaan yksikköinä; täydennysmäärä ja hinta ovat pakkauskohtaisia.",
"part_auto_buy": "Ostotehtävä kun vähissä",
"part_reuse": "Käytön jälkeen",
"part_reuse_none": "Kulutettu",
"part_reuse_charge": "Ladattu",
"part_reuse_sharpen": "Teroitettu",
"part_reuse_clean": "Puhdistettu",
"part_reuse_dry": "Kuivattu",
"part_reuse_refill": "Täytetty",
"part_reuse_restore": "Kunnostettu",
"part_waiting_charge": "{count} ladattavana",
"part_waiting_sharpen": "{count} teroitettavana",
"part_waiting_clean": "{count} puhdistettavana",
"part_waiting_dry": "{count} kuivattavana",
"part_waiting_refill": "{count} täytettävänä",
"part_waiting_restore": "{count} kunnostettavana",
"part_restoring": "Käytetyt, ei vielä valmiina",
"part_ready_again_hint": "Kaikki käytetyt ovat taas valmiina",
"part_reuse_hint": "Käytetty ei katoa: se odottaa, kunnes se on taas valmiina, ja lasketaan yhä omistetuksi, joten sitä ei osteta uudelleen.",
"part_restock": "Muuta varastoa",
"parts_used_by": "Käyttäjät",
"restock_quantity_label": "Ostettu määrä",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Lajittele nimen mukaan",
"battery_fleet_sort_urgency": "Lajittele kiireellisyyden mukaan",
"battery_fleet_mark_recharged": "Merkitse ladatuksi",
"battery_fleet_charging": "Ladattavat varaosat",
"battery_fleet_ready": "{count} valmiina",
"battery_fleet_sparkline_hint": "Akun varaustaso viimeisten 30 päivän ajalta — pisteviiva: ennuste alarajaan asti",
"battery_fleet_filter_type": "Näytä vain tämä paristotyyppi",
"battery_fleet_record_replacement": "Varaustaso hyppäsi noin {date} — kirjaa tämä vaihto Battery Notesiin",
@@ -888,7 +888,7 @@
"doc_link_title": "Titre (facultatif)",
"doc_open": "Ouvrir",
"doc_delete_confirm": "Supprimer « {name} » ?",
"doc_too_large": "Fichier trop volumineux (max. 25 Mo).",
"doc_too_large": "Fichier trop volumineux (max. 100 Mo).",
"doc_upload_failed": "Échec du téléversement.",
"docs_archive_too_large": "Archive trop volumineuse (max. {max}).",
"docs_archive_no_manifest": "Ce ZIP ne contient pas de manifest.json : la restauration ne peut pas savoir à quel document appartient un fichier. Gardez manifest.json à côté des dossiers lorsque vous recompressez.",
@@ -903,7 +903,7 @@
"remove": "Retirer",
"doc_deduped": "Déjà stocké ailleurs — partagé, aucun espace supplémentaire.",
"doc_dup_in_object": "Ce fichier est déjà attaché à cet objet.",
"doc_link_invalid": "Seuls les liens http/https sont autorisés.",
"doc_link_not_allowed": "Ce lien n'est pas autorisé : les liens qui exécutent des scripts ou ouvrent des fichiers locaux (javascript:, data:, file: …) sont refusés.",
"doc_cat_manual": "Manuel",
"doc_cat_warranty": "Garantie",
"doc_cat_invoice": "Facture",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Quantité de réapprovisionnement (conditionnements)",
"part_package_hint": "Acheté par conditionnement, utilisé à l'unité : le stock, la quantité utilisée par tâche et le seuil de réapprovisionnement comptent des unités ; la quantité de réapprovisionnement et le prix sont par conditionnement.",
"part_auto_buy": "Tâche d'achat auto si bas",
"part_reuse": "Après usage",
"part_reuse_none": "Consommé",
"part_reuse_charge": "Rechargé",
"part_reuse_sharpen": "Affûté",
"part_reuse_clean": "Nettoyé",
"part_reuse_dry": "Séché",
"part_reuse_refill": "Rempli",
"part_reuse_restore": "Remis en état",
"part_waiting_charge": "{count} à recharger",
"part_waiting_sharpen": "{count} à affûter",
"part_waiting_clean": "{count} à nettoyer",
"part_waiting_dry": "{count} à sécher",
"part_waiting_refill": "{count} à remplir",
"part_waiting_restore": "{count} à remettre en état",
"part_restoring": "Utilisés, pas encore prêts",
"part_ready_again_hint": "Tous les usagés sont de nouveau prêts",
"part_reuse_hint": "Un exemplaire utilisé n'est pas perdu : il attend d'être de nouveau prêt et compte toujours comme possédé, il n'est donc pas racheté.",
"part_restock": "Ajuster le stock",
"parts_used_by": "Utilisé par",
"restock_quantity_label": "Quantité achetée",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Trier par nom",
"battery_fleet_sort_urgency": "Trier par urgence",
"battery_fleet_mark_recharged": "Marquer comme rechargée",
"battery_fleet_charging": "Piles rechargeables de réserve",
"battery_fleet_ready": "{count} prêtes",
"battery_fleet_sparkline_hint": "Niveau de batterie des 30 derniers jours — pointillé : projection jusqu'au seuil bas",
"battery_fleet_filter_type": "Afficher uniquement ce type de pile",
"battery_fleet_record_replacement": "Le niveau a bondi vers le {date} — enregistrer ce remplacement dans Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "शीर्षक (वैकल्पिक)",
"doc_open": "खोलें",
"doc_delete_confirm": "\"{name}\" हटाएँ?",
"doc_too_large": "फ़ाइल बहुत बड़ी है (अधिकतम 25 MB)।",
"doc_too_large": "फ़ाइल बहुत बड़ी है (अधिकतम 100 MB)।",
"doc_upload_failed": "अपलोड विफल रहा।",
"docs_archive_too_large": "आर्काइव बहुत बड़ा है (अधिकतम {max})।",
"docs_archive_no_manifest": "इस ZIP में manifest.json नहीं है, इसलिए पुनर्स्थापना यह नहीं जान सकती कि कोई फ़ाइल किस दस्तावेज़ की है। दोबारा ज़िप करते समय manifest.json को फ़ोल्डरों के साथ रखें।",
@@ -903,7 +903,7 @@
"remove": "हटाएँ",
"doc_deduped": "पहले से कहीं और संग्रहीत — साझा, कोई अतिरिक्त स्थान नहीं।",
"doc_dup_in_object": "यह फ़ाइल पहले से इस ऑब्जेक्ट से जुड़ी है।",
"doc_link_invalid": "केवल http/https लिंक की अनुमति है।",
"doc_link_not_allowed": "यह लिंक अनुमत नहीं है: स्क्रिप्ट चलाने वाले या स्थानीय फ़ाइलें खोलने वाले लिंक (javascript:, data:, file: …) अस्वीकार किए जाते हैं।",
"doc_cat_manual": "मैनुअल",
"doc_cat_warranty": "वारंटी",
"doc_cat_invoice": "चालान",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "दोबारा भरने की मात्रा (पैकेट)",
"part_package_hint": "पैकेट में खरीदा, इकाई में इस्तेमाल: स्टॉक, हर काम की खपत और दोबारा मंगाने की सीमा इकाइयों में गिनी जाती हैं; दोबारा भरने की मात्रा और कीमत प्रति पैकेट हैं।",
"part_auto_buy": "कम होने पर खरीद कार्य",
"part_reuse": "उपयोग के बाद",
"part_reuse_none": "खत्म",
"part_reuse_charge": "चार्ज किया गया",
"part_reuse_sharpen": "धार लगाई गई",
"part_reuse_clean": "साफ किया गया",
"part_reuse_dry": "सुखाया गया",
"part_reuse_refill": "फिर से भरा गया",
"part_reuse_restore": "फिर से तैयार",
"part_waiting_charge": "{count} चार्ज करने हैं",
"part_waiting_sharpen": "{count} पर धार लगानी है",
"part_waiting_clean": "{count} साफ करने हैं",
"part_waiting_dry": "{count} सुखाने हैं",
"part_waiting_refill": "{count} फिर से भरने हैं",
"part_waiting_restore": "{count} फिर से तैयार करने हैं",
"part_restoring": "इस्तेमाल हुए, अभी तैयार नहीं",
"part_ready_again_hint": "सभी इस्तेमाल हुए फिर से तैयार हैं",
"part_reuse_hint": "इस्तेमाल किया हुआ खत्म नहीं होता: यह फिर से तैयार होने तक इंतज़ार करता है और आपके पास गिना जाता है, इसलिए दोबारा नहीं खरीदा जाता।",
"part_restock": "स्टॉक समायोजित करें",
"parts_used_by": "द्वारा उपयोग",
"restock_quantity_label": "खरीदी गई मात्रा",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "नाम के अनुसार क्रमबद्ध करें",
"battery_fleet_sort_urgency": "तात्कालिकता के अनुसार क्रमबद्ध करें",
"battery_fleet_mark_recharged": "रिचार्ज हो गई के रूप में चिह्नित करें",
"battery_fleet_charging": "रिचार्जेबल अतिरिक्त",
"battery_fleet_ready": "{count} तैयार",
"battery_fleet_sparkline_hint": "पिछले 30 दिनों का बैटरी स्तर — बिंदीदार: लो थ्रेशोल्ड तक का अनुमान",
"battery_fleet_filter_type": "केवल यही बैटरी प्रकार दिखाएँ",
"battery_fleet_record_replacement": "स्तर लगभग {date} को उछला — इस बदलाव को Battery Notes में दर्ज करें",
@@ -895,7 +895,7 @@
"doc_link_title": "Cím (opcionális)",
"doc_open": "Megnyitás",
"doc_delete_confirm": "Törli a(z) „{name}” elemet?",
"doc_too_large": "A fájl túl nagy (max. 25 MB).",
"doc_too_large": "A fájl túl nagy (max. 100 MB).",
"doc_upload_failed": "A feltöltés nem sikerült.",
"docs_archive_too_large": "Az archívum túl nagy (max. {max}).",
"docs_archive_no_manifest": "Ebben a ZIP-ben nincs manifest.json, ezért a visszaállítás nem tudja, melyik dokumentumhoz tartozik egy fájl. Újratömörítéskor hagyd a manifest.json fájlt a mappák mellett.",
@@ -910,7 +910,7 @@
"remove": "Eltávolítás",
"doc_deduped": "Már máshol is tárolva — megosztott, nem foglal extra helyet.",
"doc_dup_in_object": "Ez a fájl már csatolva van ehhez az objektumhoz.",
"doc_link_invalid": "Csak http/https hivatkozás engedélyezett.",
"doc_link_not_allowed": "Ez a hivatkozás nem engedélyezett: a szkripteket futtató vagy helyi fájlokat megnyitó hivatkozásokat (javascript:, data:, file: …) elutasítjuk.",
"doc_cat_manual": "Kézikönyv",
"doc_cat_warranty": "Garancia",
"doc_cat_invoice": "Számla",
@@ -1085,6 +1085,23 @@
"part_restock_packages": "Utántöltési mennyiség (kiszerelés)",
"part_package_hint": "Kiszerelésben vásárolva, egységben felhasználva: a készlet, a feladatonkénti fogyás és az újrarendelési küszöb egységet számol; az utántöltési mennyiség és az ár kiszerelésenként értendő.",
"part_auto_buy": "Vásárlási feladat automatikus létrehozása, ha kevés van",
"part_reuse": "Használat után",
"part_reuse_none": "Elhasználva",
"part_reuse_charge": "Feltöltve",
"part_reuse_sharpen": "Megélezve",
"part_reuse_clean": "Megtisztítva",
"part_reuse_dry": "Kiszárítva",
"part_reuse_refill": "Utántöltve",
"part_reuse_restore": "Felújítva",
"part_waiting_charge": "{count} töltendő",
"part_waiting_sharpen": "{count} élezendő",
"part_waiting_clean": "{count} tisztítandó",
"part_waiting_dry": "{count} szárítandó",
"part_waiting_refill": "{count} utántöltendő",
"part_waiting_restore": "{count} felújítandó",
"part_restoring": "Használt, még nem kész",
"part_ready_again_hint": "Az összes használt újra kész",
"part_reuse_hint": "A használt darab nem vész el: megvárja, amíg újra kész, és továbbra is a készlethez számít, így nem kell újra megvenni.",
"part_restock": "Készlet módosítása",
"parts_used_by": "Használja",
"restock_quantity_label": "Vásárolt mennyiség",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Rendezés név szerint",
"battery_fleet_sort_urgency": "Rendezés sürgősség szerint",
"battery_fleet_mark_recharged": "Megjelölés feltöltöttként",
"battery_fleet_charging": "Tartalék akkuk",
"battery_fleet_ready": "{count} kész",
"battery_fleet_sparkline_hint": "Akkumulátorszint az elmúlt 30 napban — pontozott: előrejelzés az alacsony küszöbig",
"battery_fleet_filter_type": "Csak ez az elemtípus megjelenítése",
"battery_fleet_record_replacement": "A szint {date} körül megugrott — rögzítse ezt a cserét a Battery Notes-ban",
@@ -888,7 +888,7 @@
"doc_link_title": "Titolo (facoltativo)",
"doc_open": "Apri",
"doc_delete_confirm": "Eliminare «{name}»?",
"doc_too_large": "File troppo grande (max 25 MB).",
"doc_too_large": "File troppo grande (max 100 MB).",
"doc_upload_failed": "Caricamento non riuscito.",
"docs_archive_too_large": "L'archivio è troppo grande (max {max}).",
"docs_archive_no_manifest": "Questo ZIP non contiene manifest.json, quindi il ripristino non sa a quale documento appartiene un file. Lascia manifest.json accanto alle cartelle quando lo comprimi di nuovo.",
@@ -903,7 +903,7 @@
"remove": "Rimuovi",
"doc_deduped": "Già archiviato altrove — condiviso, nessuno spazio aggiuntivo.",
"doc_dup_in_object": "Questo file è già allegato a questo oggetto.",
"doc_link_invalid": "Sono consentiti solo link http/https.",
"doc_link_not_allowed": "Questo link non è consentito: i link che eseguono script o aprono file locali (javascript:, data:, file: …) vengono rifiutati.",
"doc_cat_manual": "Manuale",
"doc_cat_warranty": "Garanzia",
"doc_cat_invoice": "Fattura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Quantità di riordino (confezioni)",
"part_package_hint": "Acquistato a confezioni, usato a unità: scorte, consumo per attività e soglia di riordino contano unità; quantità di riordino e prezzo sono per confezione.",
"part_auto_buy": "Task d'acquisto se scarso",
"part_reuse": "Dopo l'uso",
"part_reuse_none": "Consumato",
"part_reuse_charge": "Ricaricato",
"part_reuse_sharpen": "Affilato",
"part_reuse_clean": "Pulito",
"part_reuse_dry": "Asciugato",
"part_reuse_refill": "Riempito",
"part_reuse_restore": "Ripristinato",
"part_waiting_charge": "{count} da ricaricare",
"part_waiting_sharpen": "{count} da affilare",
"part_waiting_clean": "{count} da pulire",
"part_waiting_dry": "{count} da asciugare",
"part_waiting_refill": "{count} da riempire",
"part_waiting_restore": "{count} da ripristinare",
"part_restoring": "Usati, non ancora pronti",
"part_ready_again_hint": "Tutti quelli usati sono di nuovo pronti",
"part_reuse_hint": "Un pezzo usato non è perso: aspetta di essere di nuovo pronto e conta ancora come posseduto, quindi non viene ricomprato.",
"part_restock": "Correggi scorta",
"parts_used_by": "Usato da",
"restock_quantity_label": "Quantità acquistata",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Ordina per nome",
"battery_fleet_sort_urgency": "Ordina per urgenza",
"battery_fleet_mark_recharged": "Segna come ricaricata",
"battery_fleet_charging": "Ricambi ricaricabili",
"battery_fleet_ready": "{count} pronte",
"battery_fleet_sparkline_hint": "Livello batteria degli ultimi 30 giorni — tratteggiato: proiezione fino alla soglia di batteria scarica",
"battery_fleet_filter_type": "Mostra solo questo tipo di batteria",
"battery_fleet_record_replacement": "Il livello è balzato intorno al {date} — registra questa sostituzione in Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "タイトル(任意)",
"doc_open": "開く",
"doc_delete_confirm": "「{name}」を削除しますか?",
"doc_too_large": "ファイルが大きすぎます(最大 25 MB)。",
"doc_too_large": "ファイルが大きすぎます(最大 100 MB)。",
"doc_upload_failed": "アップロードに失敗しました。",
"docs_archive_too_large": "アーカイブが大きすぎます(最大 {max})。",
"docs_archive_no_manifest": "この ZIP には manifest.json がないため、復元時にファイルがどのドキュメントのものか判断できません。再度圧縮するときは manifest.json をフォルダーと同じ場所に置いてください。",
@@ -903,7 +903,7 @@
"remove": "削除",
"doc_deduped": "既に別の場所に保存済み — 共有され、追加容量はかかりません。",
"doc_dup_in_object": "このファイルは既にこのオブジェクトに添付されています。",
"doc_link_invalid": "http/https のリンクのみ許可されています。",
"doc_link_not_allowed": "このリンクは使用できません。スクリプトを実行したりローカルファイルを開いたりするリンク(javascript:、data:、file: …)は拒否されます。",
"doc_cat_manual": "取扱説明書",
"doc_cat_warranty": "保証書",
"doc_cat_invoice": "請求書",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "補充数(包装単位)",
"part_package_hint": "包装単位で購入し、単位で使います。在庫、タスクごとの使用量、発注しきい値は単位で数え、補充数と価格は包装単位です。",
"part_auto_buy": "在庫少で購入タスク",
"part_reuse": "使用後",
"part_reuse_none": "使い切り",
"part_reuse_charge": "充電済み",
"part_reuse_sharpen": "研ぎ済み",
"part_reuse_clean": "清掃済み",
"part_reuse_dry": "乾燥済み",
"part_reuse_refill": "補充済み",
"part_reuse_restore": "再生済み",
"part_waiting_charge": "充電待ち {count}",
"part_waiting_sharpen": "研ぎ待ち {count}",
"part_waiting_clean": "清掃待ち {count}",
"part_waiting_dry": "乾燥待ち {count}",
"part_waiting_refill": "補充待ち {count}",
"part_waiting_restore": "再生待ち {count}",
"part_restoring": "使用済み・未準備",
"part_ready_again_hint": "使用済みのものがすべて再び使えます",
"part_reuse_hint": "使用済みのものはなくなりません。再び使えるようになるまで待機し、所有数に含まれるため、買い直しは不要です。",
"part_restock": "在庫を調整",
"parts_used_by": "使用タスク",
"restock_quantity_label": "購入数量",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "名前順に並べ替え",
"battery_fleet_sort_urgency": "緊急度順に並べ替え",
"battery_fleet_mark_recharged": "充電済みにする",
"battery_fleet_charging": "予備の充電池",
"battery_fleet_ready": "準備済み {count}",
"battery_fleet_sparkline_hint": "過去30日間のバッテリー残量——点線:低残量しきい値までの予測",
"battery_fleet_filter_type": "この電池タイプのみ表示",
"battery_fleet_record_replacement": "{date}頃に残量が急上昇——この交換をBattery Notesに記録する",
@@ -895,7 +895,7 @@
"doc_link_title": "제목 (선택)",
"doc_open": "열기",
"doc_delete_confirm": "\"{name}\"을(를) 삭제할까요?",
"doc_too_large": "파일이 너무 큽니다 (최대 25 MB).",
"doc_too_large": "파일이 너무 큽니다 (최대 100 MB).",
"doc_upload_failed": "업로드에 실패했습니다.",
"docs_archive_too_large": "아카이브가 너무 큽니다(최대 {max}).",
"docs_archive_no_manifest": "이 ZIP에는 manifest.json이 없어 복원 시 파일이 어느 문서에 속하는지 알 수 없습니다. 다시 압축할 때 manifest.json을 폴더 옆에 두세요.",
@@ -910,7 +910,7 @@
"remove": "제거",
"doc_deduped": "이미 다른 곳에 저장됨 — 공유되어 추가 공간을 쓰지 않습니다.",
"doc_dup_in_object": "이 파일은 이미 이 객체에 첨부되어 있습니다.",
"doc_link_invalid": "http/https 링크만 허용됩니다.",
"doc_link_not_allowed": "이 링크는 허용되지 않습니다. 스크립트를 실행하거나 로컬 파일을 여는 링크(javascript:, data:, file: …)는 거부됩니다.",
"doc_cat_manual": "설명서",
"doc_cat_warranty": "보증서",
"doc_cat_invoice": "영수증",
@@ -1085,6 +1085,23 @@
"part_restock_packages": "보충 수량(포장)",
"part_package_hint": "포장 단위로 사고 단위로 씁니다. 재고, 작업당 사용량, 재주문 기준은 단위로 세고, 보충 수량과 가격은 포장 기준입니다.",
"part_auto_buy": "재고 부족 시 구매 작업 자동 생성",
"part_reuse": "사용 후",
"part_reuse_none": "소모됨",
"part_reuse_charge": "충전됨",
"part_reuse_sharpen": "연마됨",
"part_reuse_clean": "세척됨",
"part_reuse_dry": "건조됨",
"part_reuse_refill": "리필됨",
"part_reuse_restore": "복원됨",
"part_waiting_charge": "충전 대기 {count}",
"part_waiting_sharpen": "연마 대기 {count}",
"part_waiting_clean": "세척 대기 {count}",
"part_waiting_dry": "건조 대기 {count}",
"part_waiting_refill": "리필 대기 {count}",
"part_waiting_restore": "복원 대기 {count}",
"part_restoring": "사용됨, 아직 준비 안 됨",
"part_ready_again_hint": "사용한 것이 모두 다시 준비됨",
"part_reuse_hint": "사용한 것은 없어지지 않습니다. 다시 준비될 때까지 대기하며 보유 수량에 계속 포함되므로 다시 구매하지 않습니다.",
"part_restock": "재고 조정",
"parts_used_by": "사용처",
"restock_quantity_label": "구매 수량",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "이름순 정렬",
"battery_fleet_sort_urgency": "긴급도순 정렬",
"battery_fleet_mark_recharged": "충전 완료로 표시",
"battery_fleet_charging": "예비 충전지",
"battery_fleet_ready": "준비됨 {count}",
"battery_fleet_sparkline_hint": "지난 30일간의 배터리 잔량 — 점선: 낮음 임계값까지의 예측",
"battery_fleet_filter_type": "이 배터리 유형만 표시",
"battery_fleet_record_replacement": "{date}쯤 잔량이 급상승했습니다 — 이 교체를 Battery Notes에 기록",
@@ -888,7 +888,7 @@
"doc_link_title": "Tittel (valgfritt)",
"doc_open": "Åpne",
"doc_delete_confirm": "Slette \"{name}\"?",
"doc_too_large": "Filen er for stor (maks. 25 MB).",
"doc_too_large": "Filen er for stor (maks. 100 MB).",
"doc_upload_failed": "Opplasting mislyktes.",
"docs_archive_too_large": "Arkivet er for stort (maks. {max}).",
"docs_archive_no_manifest": "Denne ZIP-filen har ingen manifest.json, så gjenopprettingen vet ikke hvilket dokument en fil hører til. La manifest.json ligge ved siden av mappene når du zipper på nytt.",
@@ -903,7 +903,7 @@
"remove": "Fjern",
"doc_deduped": "Allerede lagret et annet sted — delt, ingen ekstra plass.",
"doc_dup_in_object": "Denne filen er allerede knyttet til dette objektet.",
"doc_link_invalid": "Kun http/https-lenker er tillatt.",
"doc_link_not_allowed": "Denne lenken er ikke tillatt: lenker som kjører skript eller åpner lokale filer (javascript:, data:, file: …), avvises.",
"doc_cat_manual": "Bruksanvisning",
"doc_cat_warranty": "Garanti",
"doc_cat_invoice": "Faktura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Påfyllingsmengde (pakninger)",
"part_package_hint": "Kjøpes i pakninger, brukes i enheten: lager, forbruk per oppgave og bestillingsgrense teller enheter; påfyllingsmengde og pris gjelder per pakning.",
"part_auto_buy": "Kjøpsoppgave ved lavt lager",
"part_reuse": "Etter bruk",
"part_reuse_none": "Brukt opp",
"part_reuse_charge": "Ladet",
"part_reuse_sharpen": "Slipt",
"part_reuse_clean": "Rengjort",
"part_reuse_dry": "Tørket",
"part_reuse_refill": "Etterfylt",
"part_reuse_restore": "Satt i stand",
"part_waiting_charge": "{count} skal lades",
"part_waiting_sharpen": "{count} skal slipes",
"part_waiting_clean": "{count} skal rengjøres",
"part_waiting_dry": "{count} skal tørkes",
"part_waiting_refill": "{count} skal etterfylles",
"part_waiting_restore": "{count} skal settes i stand",
"part_restoring": "Brukte, ikke klare ennå",
"part_ready_again_hint": "Alle brukte er klare igjen",
"part_reuse_hint": "En brukt er ikke borte: den venter til den er klar igjen og teller fortsatt som eid, så den kjøpes ikke på nytt.",
"part_restock": "Juster lager",
"parts_used_by": "Brukes av",
"restock_quantity_label": "Kjøpt mengde",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Sorter etter navn",
"battery_fleet_sort_urgency": "Sorter etter hastegrad",
"battery_fleet_mark_recharged": "Merk som oppladet",
"battery_fleet_charging": "Oppladbare reserver",
"battery_fleet_ready": "{count} klare",
"battery_fleet_sparkline_hint": "Batterinivå de siste 30 dagene — stiplet: fremskrevet ned til lavnivåterskelen",
"battery_fleet_filter_type": "Vis bare denne batteritypen",
"battery_fleet_record_replacement": "Nivået hoppet rundt {date} — registrer dette byttet i Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Titel (optioneel)",
"doc_open": "Openen",
"doc_delete_confirm": "\"{name}\" verwijderen?",
"doc_too_large": "Bestand is te groot (max. 25 MB).",
"doc_too_large": "Bestand is te groot (max. 100 MB).",
"doc_upload_failed": "Uploaden mislukt.",
"docs_archive_too_large": "Het archief is te groot (max. {max}).",
"docs_archive_no_manifest": "Deze ZIP bevat geen manifest.json, dus het herstel weet niet bij welk document een bestand hoort. Laat manifest.json naast de mappen staan als je opnieuw zipt.",
@@ -903,7 +903,7 @@
"remove": "Weghalen",
"doc_deduped": "Al elders opgeslagen — gedeeld, geen extra ruimte.",
"doc_dup_in_object": "Dit bestand is al aan dit object gekoppeld.",
"doc_link_invalid": "Alleen http/https-links zijn toegestaan.",
"doc_link_not_allowed": "Deze link is niet toegestaan: links die scripts uitvoeren of lokale bestanden openen (javascript:, data:, file: …) worden geweigerd.",
"doc_cat_manual": "Handleiding",
"doc_cat_warranty": "Garantie",
"doc_cat_invoice": "Factuur",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Bijvulhoeveelheid (verpakkingen)",
"part_package_hint": "Gekocht per verpakking, gebruikt per eenheid: voorraad, verbruik per taak en bestelgrens tellen eenheden; bijvulhoeveelheid en prijs gelden per verpakking.",
"part_auto_buy": "Kooptaak bij lage voorraad",
"part_reuse": "Na gebruik",
"part_reuse_none": "Verbruikt",
"part_reuse_charge": "Opgeladen",
"part_reuse_sharpen": "Geslepen",
"part_reuse_clean": "Gereinigd",
"part_reuse_dry": "Gedroogd",
"part_reuse_refill": "Bijgevuld",
"part_reuse_restore": "Opgeknapt",
"part_waiting_charge": "{count} op te laden",
"part_waiting_sharpen": "{count} te slijpen",
"part_waiting_clean": "{count} te reinigen",
"part_waiting_dry": "{count} te drogen",
"part_waiting_refill": "{count} bij te vullen",
"part_waiting_restore": "{count} op te knappen",
"part_restoring": "Gebruikt, nog niet klaar",
"part_ready_again_hint": "Alle gebruikte zijn weer klaar",
"part_reuse_hint": "Een gebruikt exemplaar is niet weg: het wacht tot het weer klaar is en telt nog mee als bezit, dus het wordt niet opnieuw gekocht.",
"part_restock": "Voorraad aanpassen",
"parts_used_by": "Gebruikt door",
"restock_quantity_label": "Gekochte hoeveelheid",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Sorteren op naam",
"battery_fleet_sort_urgency": "Sorteren op urgentie",
"battery_fleet_mark_recharged": "Markeren als opgeladen",
"battery_fleet_charging": "Oplaadbare reserves",
"battery_fleet_ready": "{count} klaar",
"battery_fleet_sparkline_hint": "Batterijniveau van de afgelopen 30 dagen — gestippeld: prognose tot de lage drempel",
"battery_fleet_filter_type": "Alleen dit batterijtype tonen",
"battery_fleet_record_replacement": "Het niveau maakte rond {date} een sprong — deze vervanging vastleggen in Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Tytuł (opcjonalnie)",
"doc_open": "Otwórz",
"doc_delete_confirm": "Usunąć „{name}”?",
"doc_too_large": "Plik jest za duży (maks. 25 MB).",
"doc_too_large": "Plik jest za duży (maks. 100 MB).",
"doc_upload_failed": "Przesyłanie nie powiodło się.",
"docs_archive_too_large": "Archiwum jest za duże (maks. {max}).",
"docs_archive_no_manifest": "Ten ZIP nie zawiera pliku manifest.json, więc przywracanie nie wie, do którego dokumentu należy plik. Przy ponownym pakowaniu zostaw manifest.json obok folderów.",
@@ -903,7 +903,7 @@
"remove": "Usuń",
"doc_deduped": "Już zapisany gdzie indziej — współdzielony, bez dodatkowego miejsca.",
"doc_dup_in_object": "Ten plik jest już dołączony do tego obiektu.",
"doc_link_invalid": "Dozwolone są tylko linki http/https.",
"doc_link_not_allowed": "Ten link jest niedozwolony: linki uruchamiające skrypty lub otwierające pliki lokalne (javascript:, data:, file: …) są odrzucane.",
"doc_cat_manual": "Instrukcja",
"doc_cat_warranty": "Gwarancja",
"doc_cat_invoice": "Faktura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Ilość uzupełnienia (opakowania)",
"part_package_hint": "Kupowane w opakowaniach, zużywane w jednostkach: stan, zużycie na zadanie i próg zamówienia liczą jednostki; ilość uzupełnienia i cena dotyczą opakowania.",
"part_auto_buy": "Zadanie zakupu przy niskim stanie",
"part_reuse": "Po użyciu",
"part_reuse_none": "Zużyty",
"part_reuse_charge": "Naładowany",
"part_reuse_sharpen": "Naostrzony",
"part_reuse_clean": "Wyczyszczony",
"part_reuse_dry": "Wysuszony",
"part_reuse_refill": "Napełniony",
"part_reuse_restore": "Odnowiony",
"part_waiting_charge": "{count} do naładowania",
"part_waiting_sharpen": "{count} do naostrzenia",
"part_waiting_clean": "{count} do wyczyszczenia",
"part_waiting_dry": "{count} do wysuszenia",
"part_waiting_refill": "{count} do napełnienia",
"part_waiting_restore": "{count} do odnowienia",
"part_restoring": "Zużyte, jeszcze niegotowe",
"part_ready_again_hint": "Wszystkie zużyte są znów gotowe",
"part_reuse_hint": "Zużyta część nie przepada: czeka, aż znów będzie gotowa, i nadal liczy się jako posiadana, więc nie kupuje się jej ponownie.",
"part_restock": "Koryguj stan",
"parts_used_by": "Używane przez",
"restock_quantity_label": "Kupiona ilość",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Sortuj według nazwy",
"battery_fleet_sort_urgency": "Sortuj według pilności",
"battery_fleet_mark_recharged": "Oznacz jako naładowaną",
"battery_fleet_charging": "Zapasowe akumulatorki",
"battery_fleet_ready": "{count} gotowe",
"battery_fleet_sparkline_hint": "Poziom baterii z ostatnich 30 dni — kropkowana linia: prognoza do progu niskiego poziomu",
"battery_fleet_filter_type": "Pokaż tylko ten typ baterii",
"battery_fleet_record_replacement": "Poziom skoczył około {date} — zapisz tę wymianę w Battery Notes",
@@ -895,7 +895,7 @@
"doc_link_title": "Título (opcional)",
"doc_open": "Abrir",
"doc_delete_confirm": "Excluir \"{name}\"?",
"doc_too_large": "O arquivo é muito grande (máx. 25 MB).",
"doc_too_large": "O arquivo é muito grande (máx. 100 MB).",
"doc_upload_failed": "Falha no envio.",
"docs_archive_too_large": "O arquivo é grande demais (máx. {max}).",
"docs_archive_no_manifest": "Este ZIP não tem manifest.json, então a restauração não sabe a qual documento cada arquivo pertence. Mantenha o manifest.json junto às pastas ao compactar de novo.",
@@ -910,7 +910,7 @@
"remove": "Remover",
"doc_deduped": "Já armazenado em outro lugar — compartilhado, sem uso de espaço extra.",
"doc_dup_in_object": "Este arquivo já está anexado a este objeto.",
"doc_link_invalid": "Apenas links http/https são permitidos.",
"doc_link_not_allowed": "Este link não é permitido: links que executam scripts ou abrem arquivos locais (javascript:, data:, file: …) são recusados.",
"doc_cat_manual": "Manual",
"doc_cat_warranty": "Garantia",
"doc_cat_invoice": "Nota fiscal",
@@ -1085,6 +1085,23 @@
"part_restock_packages": "Quantidade de reposição (embalagens)",
"part_package_hint": "Comprado em embalagens, usado por unidade: estoque, consumo por tarefa e limite de reposição contam unidades; quantidade de reposição e preço são por embalagem.",
"part_auto_buy": "Criar tarefa de compra automaticamente quando estiver baixo",
"part_reuse": "Após o uso",
"part_reuse_none": "Consumido",
"part_reuse_charge": "Carregado",
"part_reuse_sharpen": "Afiado",
"part_reuse_clean": "Limpo",
"part_reuse_dry": "Seco",
"part_reuse_refill": "Reabastecido",
"part_reuse_restore": "Restaurado",
"part_waiting_charge": "{count} para carregar",
"part_waiting_sharpen": "{count} para afiar",
"part_waiting_clean": "{count} para limpar",
"part_waiting_dry": "{count} para secar",
"part_waiting_refill": "{count} para reabastecer",
"part_waiting_restore": "{count} para restaurar",
"part_restoring": "Usados, ainda não prontos",
"part_ready_again_hint": "Todos os usados estão prontos de novo",
"part_reuse_hint": "Uma peça usada não se perde: aguarda até ficar pronta de novo e continua contando como sua, então não é comprada de novo.",
"part_restock": "Ajustar estoque",
"parts_used_by": "Usado por",
"restock_quantity_label": "Quantidade comprada",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Ordenar por nome",
"battery_fleet_sort_urgency": "Ordenar por urgência",
"battery_fleet_mark_recharged": "Marcar como recarregada",
"battery_fleet_charging": "Reservas recarregáveis",
"battery_fleet_ready": "{count} prontas",
"battery_fleet_sparkline_hint": "Nível da bateria dos últimos 30 dias — pontilhado: projeção até o limite baixo",
"battery_fleet_filter_type": "Mostrar apenas este tipo de pilha",
"battery_fleet_record_replacement": "O nível deu um salto por volta de {date} — registrar esta substituição no Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Título (opcional)",
"doc_open": "Abrir",
"doc_delete_confirm": "Eliminar «{name}»?",
"doc_too_large": "Ficheiro demasiado grande (máx. 25 MB).",
"doc_too_large": "Ficheiro demasiado grande (máx. 100 MB).",
"doc_upload_failed": "Falha no carregamento.",
"docs_archive_too_large": "O arquivo é demasiado grande (máx. {max}).",
"docs_archive_no_manifest": "Este ZIP não contém manifest.json, pelo que o restauro não sabe a que documento pertence cada ficheiro. Mantenha o manifest.json junto às pastas ao comprimir novamente.",
@@ -903,7 +903,7 @@
"remove": "Remover",
"doc_deduped": "Já armazenado noutro local — partilhado, sem espaço extra.",
"doc_dup_in_object": "Este ficheiro já está anexado a este objeto.",
"doc_link_invalid": "Apenas ligações http/https são permitidas.",
"doc_link_not_allowed": "Esta ligação não é permitida: ligações que executam scripts ou abrem ficheiros locais (javascript:, data:, file: …) são recusadas.",
"doc_cat_manual": "Manual",
"doc_cat_warranty": "Garantia",
"doc_cat_invoice": "Fatura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Quantidade de reposição (embalagens)",
"part_package_hint": "Comprado em embalagens, usado à unidade: o stock, o consumo por tarefa e o limite de encomenda contam unidades; a quantidade de reposição e o preço são por embalagem.",
"part_auto_buy": "Tarefa de compra se baixo",
"part_reuse": "Após o uso",
"part_reuse_none": "Consumido",
"part_reuse_charge": "Carregado",
"part_reuse_sharpen": "Afiado",
"part_reuse_clean": "Limpo",
"part_reuse_dry": "Seco",
"part_reuse_refill": "Reabastecido",
"part_reuse_restore": "Restaurado",
"part_waiting_charge": "{count} por carregar",
"part_waiting_sharpen": "{count} por afiar",
"part_waiting_clean": "{count} por limpar",
"part_waiting_dry": "{count} por secar",
"part_waiting_refill": "{count} por reabastecer",
"part_waiting_restore": "{count} por restaurar",
"part_restoring": "Usados, ainda não prontos",
"part_ready_again_hint": "Todos os usados estão prontos de novo",
"part_reuse_hint": "Uma peça usada não se perde: aguarda até estar pronta de novo e continua a contar como sua, por isso não volta a ser comprada.",
"part_restock": "Ajustar estoque",
"parts_used_by": "Usado por",
"restock_quantity_label": "Quantidade comprada",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Ordenar por nome",
"battery_fleet_sort_urgency": "Ordenar por urgência",
"battery_fleet_mark_recharged": "Marcar como recarregada",
"battery_fleet_charging": "Sobressalentes recarregáveis",
"battery_fleet_ready": "{count} prontas",
"battery_fleet_sparkline_hint": "Nível da bateria dos últimos 30 dias — pontilhado: projeção até ao limiar baixo",
"battery_fleet_filter_type": "Mostrar apenas este tipo de pilha",
"battery_fleet_record_replacement": "O nível deu um salto por volta de {date} — registar esta substituição no Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Название (необязательно)",
"doc_open": "Открыть",
"doc_delete_confirm": "Удалить «{name}»?",
"doc_too_large": "Файл слишком большой (макс. 25 МБ).",
"doc_too_large": "Файл слишком большой (макс. 100 МБ).",
"doc_upload_failed": "Не удалось загрузить.",
"docs_archive_too_large": "Архив слишком большой (макс. {max}).",
"docs_archive_no_manifest": "В этом ZIP нет manifest.json, поэтому при восстановлении неизвестно, к какому документу относится файл. При повторной упаковке оставляйте manifest.json рядом с папками.",
@@ -903,7 +903,7 @@
"remove": "Убрать",
"doc_deduped": "Уже сохранён в другом месте — общий, без доп. места.",
"doc_dup_in_object": "Этот файл уже прикреплён к этому объекту.",
"doc_link_invalid": "Разрешены только ссылки http/https.",
"doc_link_not_allowed": "Эта ссылка не разрешена: ссылки, которые запускают скрипты или открывают локальные файлы (javascript:, data:, file: …), отклоняются.",
"doc_cat_manual": "Инструкция",
"doc_cat_warranty": "Гарантия",
"doc_cat_invoice": "Счёт",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Количество для пополнения (упаковок)",
"part_package_hint": "Покупается упаковками, расходуется единицами: запас, расход на задачу и порог дозаказа считаются в единицах; количество для пополнения и цена — за упаковку.",
"part_auto_buy": "Задача покупки при малом запасе",
"part_reuse": "После использования",
"part_reuse_none": "Израсходован",
"part_reuse_charge": "Заряжен",
"part_reuse_sharpen": "Заточен",
"part_reuse_clean": "Очищен",
"part_reuse_dry": "Высушен",
"part_reuse_refill": "Заправлен",
"part_reuse_restore": "Восстановлен",
"part_waiting_charge": "{count} на зарядку",
"part_waiting_sharpen": "{count} на заточку",
"part_waiting_clean": "{count} на очистку",
"part_waiting_dry": "{count} на сушку",
"part_waiting_refill": "{count} на заправку",
"part_waiting_restore": "{count} на восстановление",
"part_restoring": "Использованы, ещё не готовы",
"part_ready_again_hint": "Все использованные снова готовы",
"part_reuse_hint": "Использованная деталь не пропадает: она ждёт, пока снова будет готова, и по-прежнему считается в наличии, поэтому её не покупают заново.",
"part_restock": "Изменить запас",
"parts_used_by": "Используется",
"restock_quantity_label": "Куплено, шт.",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Сортировать по имени",
"battery_fleet_sort_urgency": "Сортировать по срочности",
"battery_fleet_mark_recharged": "Отметить как заряженную",
"battery_fleet_charging": "Запасные аккумуляторы",
"battery_fleet_ready": "{count} готово",
"battery_fleet_sparkline_hint": "Уровень заряда за последние 30 дней — пунктир: прогноз до порога разряда",
"battery_fleet_filter_type": "Показать только этот тип батарей",
"battery_fleet_record_replacement": "Уровень резко вырос примерно {date} — записать эту замену в Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Titel (valfritt)",
"doc_open": "Öppna",
"doc_delete_confirm": "Ta bort \"{name}\"?",
"doc_too_large": "Filen är för stor (max 25 MB).",
"doc_too_large": "Filen är för stor (max 100 MB).",
"doc_upload_failed": "Uppladdningen misslyckades.",
"docs_archive_too_large": "Arkivet är för stort (max {max}).",
"docs_archive_no_manifest": "Den här ZIP-filen saknar manifest.json, så återställningen vet inte vilket dokument en fil hör till. Låt manifest.json ligga bredvid mapparna när du zippar igen.",
@@ -903,7 +903,7 @@
"remove": "Ta bort",
"doc_deduped": "Redan lagrad någon annanstans — delad, inget extra utrymme.",
"doc_dup_in_object": "Den här filen är redan kopplad till detta objekt.",
"doc_link_invalid": "Endast http/https-länkar tillåts.",
"doc_link_not_allowed": "Den här länken är inte tillåten: länkar som kör skript eller öppnar lokala filer (javascript:, data:, file: …) avvisas.",
"doc_cat_manual": "Manual",
"doc_cat_warranty": "Garanti",
"doc_cat_invoice": "Faktura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Påfyllningsmängd (förpackningar)",
"part_package_hint": "Köps i förpackningar, används i enheten: lager, åtgång per uppgift och beställningsgräns räknar enheter; påfyllningsmängd och pris gäller per förpackning.",
"part_auto_buy": "Köpuppgift vid lågt lager",
"part_reuse": "Efter användning",
"part_reuse_none": "Förbrukad",
"part_reuse_charge": "Laddad",
"part_reuse_sharpen": "Slipad",
"part_reuse_clean": "Rengjord",
"part_reuse_dry": "Torkad",
"part_reuse_refill": "Påfylld",
"part_reuse_restore": "Återställd",
"part_waiting_charge": "{count} att ladda",
"part_waiting_sharpen": "{count} att slipa",
"part_waiting_clean": "{count} att rengöra",
"part_waiting_dry": "{count} att torka",
"part_waiting_refill": "{count} att fylla på",
"part_waiting_restore": "{count} att återställa",
"part_restoring": "Använda, inte klara än",
"part_ready_again_hint": "Alla använda är klara igen",
"part_reuse_hint": "En använd är inte borta: den väntar tills den är klar igen och räknas fortfarande som ägd, så den köps inte igen.",
"part_restock": "Justera lager",
"parts_used_by": "Används av",
"restock_quantity_label": "Köpt antal",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Sortera efter namn",
"battery_fleet_sort_urgency": "Sortera efter angelägenhet",
"battery_fleet_mark_recharged": "Markera som uppladdad",
"battery_fleet_charging": "Laddbara reserver",
"battery_fleet_ready": "{count} klara",
"battery_fleet_sparkline_hint": "Batterinivå de senaste 30 dagarna — prickad: prognos ner till lågnivåtröskeln",
"battery_fleet_filter_type": "Visa endast denna batterityp",
"battery_fleet_record_replacement": "Nivån hoppade omkring {date} — registrera detta byte i Battery Notes",
@@ -895,7 +895,7 @@
"doc_link_title": "Başlık (isteğe bağlı)",
"doc_open": "Aç",
"doc_delete_confirm": "\"{name}\" silinsin mi?",
"doc_too_large": "Dosya çok büyük (en fazla 25 MB).",
"doc_too_large": "Dosya çok büyük (en fazla 100 MB).",
"doc_upload_failed": "Yükleme başarısız oldu.",
"docs_archive_too_large": "Arşiv çok büyük (en fazla {max}).",
"docs_archive_no_manifest": "Bu ZIP'te manifest.json yok, bu yüzden geri yükleme bir dosyanın hangi belgeye ait olduğunu bilemez. Yeniden sıkıştırırken manifest.json dosyasını klasörlerin yanında tutun.",
@@ -910,7 +910,7 @@
"remove": "Kaldır",
"doc_deduped": "Zaten başka bir yerde saklı — paylaşıldı, ek alan kullanılmadı.",
"doc_dup_in_object": "Bu dosya zaten bu nesneye ekli.",
"doc_link_invalid": "Yalnızca http/https bağlantılarına izin verilir.",
"doc_link_not_allowed": "Bu bağlantıya izin verilmiyor: betik çalıştıran veya yerel dosyaları açan bağlantılar (javascript:, data:, file: …) reddedilir.",
"doc_cat_manual": "Kılavuz",
"doc_cat_warranty": "Garanti",
"doc_cat_invoice": "Fatura",
@@ -1085,6 +1085,23 @@
"part_restock_packages": "Yeniden stok miktarı (paket)",
"part_package_hint": "Paketle alınır, birimle kullanılır: stok, görev başına kullanım ve yeniden sipariş eşiği birim sayar; yeniden stok miktarı ve fiyat paket başınadır.",
"part_auto_buy": "Azaldığında otomatik satın alma görevi oluştur",
"part_reuse": "Kullanımdan sonra",
"part_reuse_none": "Tükendi",
"part_reuse_charge": "Şarj edildi",
"part_reuse_sharpen": "Bilendi",
"part_reuse_clean": "Temizlendi",
"part_reuse_dry": "Kurutuldu",
"part_reuse_refill": "Yeniden dolduruldu",
"part_reuse_restore": "Yenilendi",
"part_waiting_charge": "{count} şarj edilecek",
"part_waiting_sharpen": "{count} bilenecek",
"part_waiting_clean": "{count} temizlenecek",
"part_waiting_dry": "{count} kurutulacak",
"part_waiting_refill": "{count} doldurulacak",
"part_waiting_restore": "{count} yenilenecek",
"part_restoring": "Kullanılmış, henüz hazır değil",
"part_ready_again_hint": "Kullanılanların hepsi yeniden hazır",
"part_reuse_hint": "Kullanılan parça kaybolmaz: yeniden hazır olana kadar bekler ve hâlâ sahip olunan sayılır, bu yüzden yeniden satın alınmaz.",
"part_restock": "Stoku ayarla",
"parts_used_by": "Kullanan",
"restock_quantity_label": "Satın alınan miktar",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Ada göre sırala",
"battery_fleet_sort_urgency": "Aciliyete göre sırala",
"battery_fleet_mark_recharged": "Şarj edildi olarak işaretle",
"battery_fleet_charging": "Yedek şarjlı piller",
"battery_fleet_ready": "{count} hazır",
"battery_fleet_sparkline_hint": "Son 30 günün pil seviyesi — noktalı: düşük eşiğe kadar projeksiyon",
"battery_fleet_filter_type": "Yalnızca bu pil türünü göster",
"battery_fleet_record_replacement": "Seviye {date} civarında sıçradı — bu değişimi Battery Notes'a kaydet",
@@ -888,7 +888,7 @@
"doc_link_title": "Назва (необов'язково)",
"doc_open": "Відкрити",
"doc_delete_confirm": "Видалити «{name}»?",
"doc_too_large": "Файл завеликий (макс. 25 МБ).",
"doc_too_large": "Файл завеликий (макс. 100 МБ).",
"doc_upload_failed": "Не вдалося завантажити.",
"docs_archive_too_large": "Архів завеликий (макс. {max}).",
"docs_archive_no_manifest": "У цьому ZIP немає manifest.json, тому під час відновлення невідомо, до якого документа належить файл. Під час повторного пакування залишайте manifest.json поруч із теками.",
@@ -903,7 +903,7 @@
"remove": "Прибрати",
"doc_deduped": "Уже збережено в іншому місці — спільний, без додаткового місця.",
"doc_dup_in_object": "Цей файл уже прикріплено до цього об'єкта.",
"doc_link_invalid": "Дозволені лише посилання http/https.",
"doc_link_not_allowed": "Це посилання не дозволено: посилання, які запускають скрипти або відкривають локальні файли (javascript:, data:, file: …), відхиляються.",
"doc_cat_manual": "Інструкція",
"doc_cat_warranty": "Гарантія",
"doc_cat_invoice": "Рахунок",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Кількість для поповнення (упаковок)",
"part_package_hint": "Купується упаковками, витрачається одиницями: запас, витрата на завдання та поріг дозамовлення рахуються в одиницях; кількість для поповнення та ціна — за упаковку.",
"part_auto_buy": "Завдання купівлі при малому запасі",
"part_reuse": "Після використання",
"part_reuse_none": "Витрачено",
"part_reuse_charge": "Заряджено",
"part_reuse_sharpen": "Заточено",
"part_reuse_clean": "Очищено",
"part_reuse_dry": "Висушено",
"part_reuse_refill": "Заправлено",
"part_reuse_restore": "Відновлено",
"part_waiting_charge": "{count} на заряджання",
"part_waiting_sharpen": "{count} на заточування",
"part_waiting_clean": "{count} на очищення",
"part_waiting_dry": "{count} на сушіння",
"part_waiting_refill": "{count} на заправку",
"part_waiting_restore": "{count} на відновлення",
"part_restoring": "Використані, ще не готові",
"part_ready_again_hint": "Усі використані знову готові",
"part_reuse_hint": "Використана деталь не зникає: вона чекає, доки знову буде готова, і далі рахується як наявна, тож її не купують знову.",
"part_restock": "Змінити запас",
"parts_used_by": "Використовується",
"restock_quantity_label": "Куплена кількість",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Сортувати за назвою",
"battery_fleet_sort_urgency": "Сортувати за терміновістю",
"battery_fleet_mark_recharged": "Позначити як заряджену",
"battery_fleet_charging": "Запасні акумулятори",
"battery_fleet_ready": "{count} готово",
"battery_fleet_sparkline_hint": "Рівень заряду за останні 30 днів — пунктир: прогноз до порогу розряду",
"battery_fleet_filter_type": "Показати лише цей тип батарей",
"battery_fleet_record_replacement": "Рівень різко зріс приблизно {date} — записати цю заміну в Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "标题(可选)",
"doc_open": "打开",
"doc_delete_confirm": "删除“{name}”?",
"doc_too_large": "文件过大(最大 25 MB)。",
"doc_too_large": "文件过大(最大 100 MB)。",
"doc_upload_failed": "上传失败。",
"docs_archive_too_large": "压缩包过大(最大 {max})。",
"docs_archive_no_manifest": "此 ZIP 中没有 manifest.json,因此恢复时无法判断文件属于哪个文档。重新压缩时请把 manifest.json 放在各文件夹旁边。",
@@ -903,7 +903,7 @@
"remove": "移除",
"doc_deduped": "已在别处存储——共享,不占用额外空间。",
"doc_dup_in_object": "此文件已附加到该对象。",
"doc_link_invalid": "仅允许 http/https 链接。",
"doc_link_not_allowed": "不允许使用此链接:运行脚本或打开本地文件的链接(javascript:、data:、file: …)会被拒绝。",
"doc_cat_manual": "手册",
"doc_cat_warranty": "保修",
"doc_cat_invoice": "发票",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "补货数量(包)",
"part_package_hint": "按包购买、按单位使用:库存、每项任务用量和补货阈值都按单位计;补货数量和价格按包计。",
"part_auto_buy": "库存低时自动建购买任务",
"part_reuse": "使用后",
"part_reuse_none": "已用完",
"part_reuse_charge": "已充电",
"part_reuse_sharpen": "已磨利",
"part_reuse_clean": "已清洁",
"part_reuse_dry": "已烘干",
"part_reuse_refill": "已重新灌装",
"part_reuse_restore": "已修复",
"part_waiting_charge": "{count} 个待充电",
"part_waiting_sharpen": "{count} 个待磨利",
"part_waiting_clean": "{count} 个待清洁",
"part_waiting_dry": "{count} 个待烘干",
"part_waiting_refill": "{count} 个待灌装",
"part_waiting_restore": "{count} 个待修复",
"part_restoring": "已用,尚未就绪",
"part_ready_again_hint": "所有已用的都已重新就绪",
"part_reuse_hint": "用过的不会消失:它会等待重新就绪,并仍计入已有数量,因此不会被再次购买。",
"part_restock": "调整库存",
"parts_used_by": "使用于",
"restock_quantity_label": "购买数量",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "按名称排序",
"battery_fleet_sort_urgency": "按紧急程度排序",
"battery_fleet_mark_recharged": "标记为已充电",
"battery_fleet_charging": "备用充电电池",
"battery_fleet_ready": "{count} 个就绪",
"battery_fleet_sparkline_hint": "过去 30 天的电池电量——虚线:外推至低电量阈值",
"battery_fleet_filter_type": "仅显示此电池类型",
"battery_fleet_record_replacement": "电量在 {date} 前后跳升——将此次更换记录到 Battery Notes",
@@ -3,11 +3,12 @@
import { LitElement, html, css, nothing } from "lit";
import { applySubscriptionEvent, type SubscriptionEvent } from "./helpers/subscription-merge";
import { hydrateObjects } from "./helpers/hydrate-objects";
import { taskIsFor } from "./helpers/assignees";
import { property, state } from "lit/decorators.js";
import { syncLocaleFromHass, sharedStyles, STATUS_COLORS, t, ensureLocale, isLocaleLoaded, setProfilePrefs, formatDueDays, formatLastDone, formatDate, langOf, currencySymbolOf, syncCurrencyDecimals} from "./styles";
import { openSignedDocument } from "./helpers/document-url";
import { isPhotoDocument } from "./helpers/document-categories";
import { isSafeHttpUrl } from "./helpers/url";
import { isSafeLink } from "./helpers/url";
import { registerCustomCard } from "./helpers/register-card";
import { healCardRegistry } from "./helpers/registry-heal";
import { localizePickerWhenReady } from "./helpers/picker-i18n";
@@ -315,9 +316,9 @@ export class MaintenanceSupporterCard extends LitElement {
private async _openDoc(doc: CardDoc): Promise<void> {
if (doc.kind === "weblink" && doc.url) {
// Same guarded sink as every other weblink opener: never hand a
// non-http(s) URL to window.open (defense-in-depth against a link
// blocked scheme to window.open (defense-in-depth against a link
// stored before the server-side scheme check existed).
if (isSafeHttpUrl(doc.url)) window.open(doc.url, "_blank", "noopener");
if (isSafeLink(doc.url)) window.open(doc.url, "_blank", "noopener");
return;
}
try {
@@ -439,7 +440,7 @@ export class MaintenanceSupporterCard extends LitElement {
if (vf.status && task.status !== vf.status) continue;
if (vf.label && !(task.labels || []).includes(vf.label)) continue;
if (vf.priority && (task.priority || "normal") !== vf.priority) continue;
if (viewUser && task.responsible_user_id !== viewUser) continue;
if (viewUser && !taskIsFor(task, viewUser)) continue;
}
tasks.push({ entry_id: obj.entry_id, object_name: obj.object.name, task });
}
@@ -1,7 +1,7 @@
/** Maintenance Supporter Sidebar Panel. */
import { LitElement, html, nothing, type TemplateResult } from "lit";
import { isSafeHttpUrl } from "./helpers/url";
import { isSafeLink } from "./helpers/url";
import { queryTokens, scoreFields } from "./helpers/search-match";
import { objectRef, parseRef, renderRefChip, taskRef, type HasRef } from "./helpers/reference";
import { applySubscriptionEvent, type SubscriptionEvent } from "./helpers/subscription-merge";
@@ -13,6 +13,7 @@ import { LS_KEYS, lsGet, lsSet } from "./helpers/storage-keys";
import { openHtmlInNewTab, openSignedDocument, preopenTab, signApiPath } from "./helpers/document-url";
import { readObjectsCache, writeObjectsCache } from "./helpers/objects-cache";
import { hydrateObjects } from "./helpers/hydrate-objects";
import { taskIsFor, taskUserIds } from "./helpers/assignees";
import { daysProgress } from "./helpers/interval";
import { buildObjectReportHtml, type ReportLabels } from "./helpers/report";
import { warrantyStatus } from "./helpers/warranty";
@@ -1215,7 +1216,8 @@ export class MaintenanceSupporterPanel extends LitElement {
const userId = this._filterUser === "current_user"
? this._userService?.getCurrentUserId()
: this._filterUser;
if (task.responsible_user_id !== userId) continue;
// Theirs, or shared with them while nobody is responsible.
if (!taskIsFor(task, userId)) continue;
}
// Label filter (v2.26 — also captured by saved views)
@@ -1907,7 +1909,7 @@ export class MaintenanceSupporterPanel extends LitElement {
case "document":
case "content":
if (r.docKind === "weblink") {
if (isSafeHttpUrl(r.url)) window.open(r.url!, "_blank", "noopener");
if (isSafeLink(r.url)) window.open(r.url!, "_blank", "noopener");
} else if (r.docId) {
// A content hit opens the PDF on the matching page (browser viewers
// honour #page=N; the Companion app's external viewer may not).
@@ -3044,7 +3046,7 @@ export class MaintenanceSupporterPanel extends LitElement {
* Companion-safe recipe as the documents section). */
private _openManualDoc(doc: ManualDocRef): void {
if (doc.kind !== "file") {
if (isSafeHttpUrl(doc.url)) window.open(doc.url!, "_blank", "noopener");
if (isSafeLink(doc.url)) window.open(doc.url!, "_blank", "noopener");
return;
}
void openSignedDocument(this.hass, doc.id).catch(() => {
@@ -3544,12 +3546,12 @@ export class MaintenanceSupporterPanel extends LitElement {
const archivedCount = this._objects.reduce(
(n, o) => n + o.tasks.filter((tk) => tk.archived).length, 0,
);
// The people someone can filter by: everyone responsible for a task, plus
// The people someone can filter by: everyone a task is meant for, plus
// the person a saved view narrows to — otherwise that view leaves the
// select blank while it filters.
const filterPeople = new Set<string>();
for (const o of this._objects) {
for (const tk of o.tasks) if (tk.responsible_user_id) filterPeople.add(tk.responsible_user_id);
for (const tk of o.tasks) for (const id of taskUserIds(tk)) filterPeople.add(id);
}
if (this._filterUser && this._filterUser !== "current_user") filterPeople.add(this._filterUser);
const personOptions = [...filterPeople]
@@ -4411,7 +4413,7 @@ export class MaintenanceSupporterPanel extends LitElement {
// handbook attached must not render "—" here (prod: Easee vs Epson).
const manualDoc = (o.manual_docs || [])[0];
return html`<td class="oc-documentation_url">${
isSafeHttpUrl(o.documentation_url)
isSafeLink(o.documentation_url)
? html`<a href=${o.documentation_url} target="_blank" rel="noopener noreferrer"
@click=${(e: Event) => e.stopPropagation()}><ha-icon icon="mdi:file-document-outline"></ha-icon></a>`
: manualDoc
@@ -4789,7 +4791,7 @@ export class MaintenanceSupporterPanel extends LitElement {
${o.place_missing ? html`<span class="place-missing-badge">${t("place_missing", L)}</span>` : nothing}
</p>`
: nothing}
${isSafeHttpUrl(o.documentation_url)
${isSafeLink(o.documentation_url)
? html`<p class="meta">${t("documentation_url_label", L)}:
<a href=${o.documentation_url} target="_blank" rel="noopener noreferrer">${o.documentation_url}</a>
</p>`
@@ -10,7 +10,7 @@
*/
import { html, nothing } from "lit";
import { isSafeHttpUrl } from "../helpers/url";
import { isSafeLink } from "../helpers/url";
import { renderNotesMarkdown } from "../helpers/notes-markdown";
import { renderPersonAvatar, type PersonDisplay } from "../helpers/person";
import { t, formatDate, formatRecurrence, formatCost, formatDuration } from "../styles";
@@ -310,9 +310,9 @@ function renderChecklistCard(task: MaintenanceTask, ctx: TaskDetailContext) {
* documentation_url for quick access to the device manual without having to
* navigate back to the object detail. */
function renderTaskMeta(task: MaintenanceTask, ctx: TaskDetailContext) {
const safeTaskUrl = isSafeHttpUrl(task.documentation_url)
const safeTaskUrl = isSafeLink(task.documentation_url)
? task.documentation_url : null;
const safeObjUrl = isSafeHttpUrl(ctx.objectDocUrl) ? ctx.objectDocUrl : null;
const safeObjUrl = isSafeLink(ctx.objectDocUrl) ? ctx.objectDocUrl : null;
// Same fallback rule as the objects table: an UPLOADED manual (category
// "manual") stands in when the object's URL field is empty.
const manualDoc = safeObjUrl ? null : (ctx.objectManualDocs || [])[0];
@@ -1274,10 +1274,13 @@ export const sharedStyles = css`
font-weight: 500;
font-size: 14px;
margin-bottom: 4px;
min-width: 0;
overflow-wrap: anywhere;
}
.group-card-actions {
display: flex;
flex-shrink: 0;
gap: 0;
}
.group-card-actions ha-icon-button {
@@ -1324,6 +1327,8 @@ export const sharedStyles = css`
border-radius: 10px;
background: var(--secondary-background-color, #f5f5f5);
color: var(--primary-text-color);
max-width: 100%;
overflow-wrap: anywhere;
}
/* Adaptive scheduling suggestion badge */
@@ -1712,7 +1717,19 @@ export const sharedStyles = css`
suffix is hidden instead — the bar and the title carry the ratio. */
.budget-tile-max { display: none; }
.group-card { min-width: 0; max-width: 100%; }
/* #206 — one group per row. flex: 1 with min-width 0 put every group
into ONE row: three groups were 96 px each on a 360 px phone, and the
delete button of one lay over the next group. The buttons get the
same 44 px targets as the task rows, apart from each other; the
negative margin lets them reach into the card's padding instead of
pushing the tasks down. */
.group-card { flex: 1 1 100%; min-width: 0; max-width: 100%; }
.group-card-actions { gap: 8px; margin: -8px -8px -8px 0; }
.group-card-actions ha-icon-button {
--ha-icon-button-size: 44px;
--mdc-icon-button-size: 44px;
--mdc-icon-size: 22px;
}
.filter-chip { padding: 6px 12px; font-size: 13px; }
@@ -427,8 +427,14 @@ export interface MaintenancePart {
package_size?: number | null;
auto_buy_task?: boolean;
doc_id?: string | null;
/** Tracked on-hand count; null = catalog-only (not tracked). */
/** #208: what readies a used one again (charge, sharpen, clean, dry,
* refill, restore); null/absent = the part is used up. */
reuse?: string | null;
/** Tracked on-hand count; null = catalog-only (not tracked). For a
* reusable part: the ready ones. */
stock?: number | null;
/** #208: used ones of a reusable part waiting to be ready again. */
restoring?: number;
/** Derived: tracked stock at/below the reorder threshold. */
is_low?: boolean;
/** Derived: product_url or the configured shopping-search link. */
@@ -888,7 +888,7 @@
"doc_link_title": "Název (nepovinné)",
"doc_open": "Otevřít",
"doc_delete_confirm": "Smazat „{name}“?",
"doc_too_large": "Soubor je příliš velký (max. 25 MB).",
"doc_too_large": "Soubor je příliš velký (max. 100 MB).",
"doc_upload_failed": "Nahrání selhalo.",
"docs_archive_too_large": "Archiv je příliš velký (max. {max}).",
"docs_archive_no_manifest": "Tento ZIP neobsahuje manifest.json, takže obnovení neví, ke kterému dokumentu soubor patří. Při opětovném zabalení ponechte manifest.json vedle složek.",
@@ -903,7 +903,7 @@
"remove": "Odebrat",
"doc_deduped": "Již uloženo jinde — sdíleno, bez dalšího místa.",
"doc_dup_in_object": "Tento soubor je již připojen k tomuto objektu.",
"doc_link_invalid": "Povoleny jsou pouze odkazy http/https.",
"doc_link_not_allowed": "Tento odkaz není povolen: odkazy, které spouštějí skripty nebo otevírají místní soubory (javascript:, data:, file: …), jsou odmítnuty.",
"doc_cat_manual": "Návod",
"doc_cat_warranty": "Záruka",
"doc_cat_invoice": "Faktura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Množství k doplnění (balení)",
"part_package_hint": "Kupuje se po baleních, spotřebovává po jednotkách: zásoba, spotřeba na úkol a hranice objednání počítají jednotky; množství k doplnění a cena platí za balení.",
"part_auto_buy": "Úkol nákupu při nízké zásobě",
"part_reuse": "Po použití",
"part_reuse_none": "Spotřebován",
"part_reuse_charge": "Nabito",
"part_reuse_sharpen": "Nabroušeno",
"part_reuse_clean": "Vyčištěno",
"part_reuse_dry": "Vysušeno",
"part_reuse_refill": "Doplněno",
"part_reuse_restore": "Obnoveno",
"part_waiting_charge": "{count} k nabití",
"part_waiting_sharpen": "{count} k nabroušení",
"part_waiting_clean": "{count} k vyčištění",
"part_waiting_dry": "{count} k vysušení",
"part_waiting_refill": "{count} k doplnění",
"part_waiting_restore": "{count} k obnovení",
"part_restoring": "Použité, zatím nepřipravené",
"part_ready_again_hint": "Všechny použité jsou opět připravené",
"part_reuse_hint": "Použitý kus nezmizí: čeká, až bude znovu připravený, a dál se počítá jako vlastněný, takže se znovu nekupuje.",
"part_restock": "Upravit zásobu",
"parts_used_by": "Používá",
"restock_quantity_label": "Zakoupené množství",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Řadit podle názvu",
"battery_fleet_sort_urgency": "Řadit podle naléhavosti",
"battery_fleet_mark_recharged": "Označit jako nabitou",
"battery_fleet_charging": "Náhradní akumulátory",
"battery_fleet_ready": "{count} připraveno",
"battery_fleet_sparkline_hint": "Stav baterie za posledních 30 dní — tečkovaně: projekce k prahu vybití",
"battery_fleet_filter_type": "Zobrazit pouze tento typ baterie",
"battery_fleet_record_replacement": "Stav poskočil kolem {date} — zaznamenat tuto výměnu do Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Titel (valgfri)",
"doc_open": "Åbn",
"doc_delete_confirm": "Slet \"{name}\"?",
"doc_too_large": "Filen er for stor (maks. 25 MB).",
"doc_too_large": "Filen er for stor (maks. 100 MB).",
"doc_upload_failed": "Upload mislykkedes.",
"docs_archive_too_large": "Arkivet er for stort (maks. {max}).",
"docs_archive_no_manifest": "Denne ZIP har ingen manifest.json, så gendannelsen ved ikke, hvilket dokument en fil hører til. Lad manifest.json ligge ved siden af mapperne, når du zipper igen.",
@@ -903,7 +903,7 @@
"remove": "Fjern",
"doc_deduped": "Allerede gemt et andet sted — delt, ingen ekstra plads.",
"doc_dup_in_object": "Denne fil er allerede vedhæftet dette objekt.",
"doc_link_invalid": "Kun http/https-links er tilladt.",
"doc_link_not_allowed": "Dette link er ikke tilladt: links, der kører scripts eller åbner lokale filer (javascript:, data:, file: …), afvises.",
"doc_cat_manual": "Vejledning",
"doc_cat_warranty": "Garanti",
"doc_cat_invoice": "Faktura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Genopfyldningsmængde (pakninger)",
"part_package_hint": "Købes i pakninger, bruges i enheden: lager, forbrug pr. opgave og genbestillingsgrænse tæller enheder; genopfyldningsmængde og pris gælder pr. pakning.",
"part_auto_buy": "Købsopgave ved lav beholdning",
"part_reuse": "Efter brug",
"part_reuse_none": "Brugt op",
"part_reuse_charge": "Opladet",
"part_reuse_sharpen": "Slebet",
"part_reuse_clean": "Rengjort",
"part_reuse_dry": "Tørret",
"part_reuse_refill": "Genopfyldt",
"part_reuse_restore": "Istandsat",
"part_waiting_charge": "{count} skal oplades",
"part_waiting_sharpen": "{count} skal slibes",
"part_waiting_clean": "{count} skal rengøres",
"part_waiting_dry": "{count} skal tørres",
"part_waiting_refill": "{count} skal genopfyldes",
"part_waiting_restore": "{count} skal istandsættes",
"part_restoring": "Brugte, ikke klar endnu",
"part_ready_again_hint": "Alle brugte er klar igen",
"part_reuse_hint": "En brugt er ikke væk: den venter, til den er klar igen, og tæller stadig som ejet, så den bliver ikke købt igen.",
"part_restock": "Justér lager",
"parts_used_by": "Bruges af",
"restock_quantity_label": "Købt mængde",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Sortér efter navn",
"battery_fleet_sort_urgency": "Sortér efter hastende grad",
"battery_fleet_mark_recharged": "Markér som genopladet",
"battery_fleet_charging": "Genopladelige reserver",
"battery_fleet_ready": "{count} klar",
"battery_fleet_sparkline_hint": "Batteriniveau de seneste 30 dage — stiplet: fremskrivning ned til lav-tærsklen",
"battery_fleet_filter_type": "Vis kun denne batteritype",
"battery_fleet_record_replacement": "Niveauet sprang omkring {date} — registrér denne udskiftning i Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Titel (optional)",
"doc_open": "Öffnen",
"doc_delete_confirm": "„{name}“ löschen?",
"doc_too_large": "Datei ist zu groß (max. 25 MB).",
"doc_too_large": "Datei ist zu groß (max. 100 MB).",
"doc_upload_failed": "Upload fehlgeschlagen.",
"docs_archive_too_large": "Das Archiv ist zu groß (max. {max}).",
"docs_archive_no_manifest": "Dieses ZIP enthält keine manifest.json, daher weiß die Wiederherstellung nicht, zu welchem Dokument eine Datei gehört. Lass die manifest.json beim erneuten Zippen neben den Ordnern liegen.",
@@ -903,7 +903,7 @@
"remove": "Entfernen",
"doc_deduped": "Bereits anderswo gespeichert — geteilt, kein zusätzlicher Speicher.",
"doc_dup_in_object": "Diese Datei ist bereits an diesem Objekt angehängt.",
"doc_link_invalid": "Nur http/https-Links sind erlaubt.",
"doc_link_not_allowed": "Dieser Link ist nicht erlaubt: Links, die Skripte ausführen oder lokale Dateien öffnen (javascript:, data:, file: …), werden abgelehnt.",
"doc_cat_manual": "Anleitung",
"doc_cat_warranty": "Garantie",
"doc_cat_invoice": "Rechnung",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Nachkaufmenge (Packungen)",
"part_package_hint": "Gekauft in Packungen, verbraucht in der Einheit: Bestand, Verbrauch pro Aufgabe und Nachbestellgrenze zählen Einheiten; Nachkaufmenge und Preis gelten pro Packung.",
"part_auto_buy": "Kauf-Task bei Niedrigbestand",
"part_reuse": "Nach Gebrauch",
"part_reuse_none": "Verbraucht",
"part_reuse_charge": "Geladen",
"part_reuse_sharpen": "Geschärft",
"part_reuse_clean": "Gereinigt",
"part_reuse_dry": "Getrocknet",
"part_reuse_refill": "Nachgefüllt",
"part_reuse_restore": "Aufbereitet",
"part_waiting_charge": "{count} zu laden",
"part_waiting_sharpen": "{count} zu schärfen",
"part_waiting_clean": "{count} zu reinigen",
"part_waiting_dry": "{count} zu trocknen",
"part_waiting_refill": "{count} nachzufüllen",
"part_waiting_restore": "{count} aufzubereiten",
"part_restoring": "Gebraucht, noch nicht bereit",
"part_ready_again_hint": "Alle gebrauchten sind wieder bereit",
"part_reuse_hint": "Ein gebrauchtes ist nicht weg: Es wartet, bis es wieder bereit ist, und zählt weiter zum Bestand, wird also nicht neu gekauft.",
"part_restock": "Bestand anpassen",
"parts_used_by": "Verwendet von",
"restock_quantity_label": "Gekaufte Menge",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Nach Name sortieren",
"battery_fleet_sort_urgency": "Nach Dringlichkeit sortieren",
"battery_fleet_mark_recharged": "Als aufgeladen markieren",
"battery_fleet_charging": "Akku-Vorrat",
"battery_fleet_ready": "{count} bereit",
"battery_fleet_sparkline_hint": "Batteriestand der letzten 30 Tage — gepunktet: Prognose bis zur Low-Schwelle",
"battery_fleet_filter_type": "Nur diesen Batterietyp anzeigen",
"battery_fleet_record_replacement": "Der Batteriestand ist um den {date} gesprungen — diesen Wechsel in Battery Notes nachtragen",
@@ -895,7 +895,7 @@
"doc_link_title": "Title (optional)",
"doc_open": "Open",
"doc_delete_confirm": "Delete \"{name}\"?",
"doc_too_large": "File is too large (max 25 MB).",
"doc_too_large": "File is too large (max 100 MB).",
"doc_upload_failed": "Upload failed.",
"docs_archive_too_large": "The archive is too large (max {max}).",
"docs_archive_no_manifest": "This ZIP has no manifest.json, so the restore cannot tell which document a file belongs to. Keep manifest.json next to the folders when zipping again.",
@@ -910,7 +910,7 @@
"remove": "Remove",
"doc_deduped": "Already stored elsewhere — shared, no extra space used.",
"doc_dup_in_object": "This file is already attached to this object.",
"doc_link_invalid": "Only http/https links are allowed.",
"doc_link_not_allowed": "This link is not allowed: links that run scripts or open local files (javascript:, data:, file: …) are refused.",
"doc_cat_manual": "Manual",
"doc_cat_warranty": "Warranty",
"doc_cat_invoice": "Invoice",
@@ -1085,6 +1085,23 @@
"part_restock_packages": "Restock amount (packages)",
"part_package_hint": "Bought in packages, used in the unit: stock, the amount a task uses and the reorder threshold count units; the restock amount and the price are per package.",
"part_auto_buy": "Auto-create buy task when low",
"part_reuse": "After use",
"part_reuse_none": "Used up",
"part_reuse_charge": "Charged",
"part_reuse_sharpen": "Sharpened",
"part_reuse_clean": "Cleaned",
"part_reuse_dry": "Dried",
"part_reuse_refill": "Refilled",
"part_reuse_restore": "Restored",
"part_waiting_charge": "{count} to charge",
"part_waiting_sharpen": "{count} to sharpen",
"part_waiting_clean": "{count} to clean",
"part_waiting_dry": "{count} to dry",
"part_waiting_refill": "{count} to refill",
"part_waiting_restore": "{count} to restore",
"part_restoring": "Used, not ready yet",
"part_ready_again_hint": "All used ones are ready again",
"part_reuse_hint": "A used one is not gone: it waits until it is ready again and still counts as owned, so it is not bought again.",
"part_restock": "Adjust stock",
"parts_used_by": "Used by",
"restock_quantity_label": "Quantity bought",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Sort by name",
"battery_fleet_sort_urgency": "Sort by urgency",
"battery_fleet_mark_recharged": "Mark as recharged",
"battery_fleet_charging": "Rechargeable spares",
"battery_fleet_ready": "{count} ready",
"battery_fleet_sparkline_hint": "Battery level over the last 30 days — dotted: projected until the low threshold",
"battery_fleet_filter_type": "Show only this battery type",
"battery_fleet_record_replacement": "The level jumped around {date} — record this replacement in Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Título (opcional)",
"doc_open": "Abrir",
"doc_delete_confirm": "¿Eliminar «{name}»?",
"doc_too_large": "El archivo es demasiado grande (máx. 25 MB).",
"doc_too_large": "El archivo es demasiado grande (máx. 100 MB).",
"doc_upload_failed": "Error al subir.",
"docs_archive_too_large": "El archivo comprimido es demasiado grande (máx. {max}).",
"docs_archive_no_manifest": "Este ZIP no contiene manifest.json, así que la restauración no sabe a qué documento pertenece cada archivo. Deja manifest.json junto a las carpetas al volver a comprimir.",
@@ -903,7 +903,7 @@
"remove": "Quitar",
"doc_deduped": "Ya almacenado en otro lugar — compartido, sin espacio adicional.",
"doc_dup_in_object": "Este archivo ya está adjunto a este objeto.",
"doc_link_invalid": "Solo se permiten enlaces http/https.",
"doc_link_not_allowed": "Este enlace no está permitido: se rechazan los enlaces que ejecutan scripts o abren archivos locales (javascript:, data:, file: …).",
"doc_cat_manual": "Manual",
"doc_cat_warranty": "Garantía",
"doc_cat_invoice": "Factura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Cantidad de reposición (envases)",
"part_package_hint": "Se compra por envases y se usa por unidades: el stock, el consumo por tarea y el umbral de pedido cuentan unidades; la cantidad de reposición y el precio son por envase.",
"part_auto_buy": "Crear tarea de compra si bajo",
"part_reuse": "Tras el uso",
"part_reuse_none": "Consumido",
"part_reuse_charge": "Recargado",
"part_reuse_sharpen": "Afilado",
"part_reuse_clean": "Limpiado",
"part_reuse_dry": "Secado",
"part_reuse_refill": "Rellenado",
"part_reuse_restore": "Restaurado",
"part_waiting_charge": "{count} por recargar",
"part_waiting_sharpen": "{count} por afilar",
"part_waiting_clean": "{count} por limpiar",
"part_waiting_dry": "{count} por secar",
"part_waiting_refill": "{count} por rellenar",
"part_waiting_restore": "{count} por restaurar",
"part_restoring": "Usados, aún no listos",
"part_ready_again_hint": "Todos los usados vuelven a estar listos",
"part_reuse_hint": "Una pieza usada no se pierde: espera hasta estar lista de nuevo y sigue contando como propia, así que no se vuelve a comprar.",
"part_restock": "Ajustar existencias",
"parts_used_by": "Usado por",
"restock_quantity_label": "Cantidad comprada",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Ordenar por nombre",
"battery_fleet_sort_urgency": "Ordenar por urgencia",
"battery_fleet_mark_recharged": "Marcar como recargada",
"battery_fleet_charging": "Repuestos recargables",
"battery_fleet_ready": "{count} listas",
"battery_fleet_sparkline_hint": "Nivel de batería de los últimos 30 días — punteado: proyección hasta el umbral bajo",
"battery_fleet_filter_type": "Mostrar solo este tipo de pila",
"battery_fleet_record_replacement": "El nivel dio un salto hacia el {date} — registrar esta sustitución en Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Otsikko (valinnainen)",
"doc_open": "Avaa",
"doc_delete_confirm": "Poistetaanko \"{name}\"?",
"doc_too_large": "Tiedosto on liian suuri (enint. 25 Mt).",
"doc_too_large": "Tiedosto on liian suuri (enint. 100 Mt).",
"doc_upload_failed": "Lataus epäonnistui.",
"docs_archive_too_large": "Arkisto on liian suuri (enintään {max}).",
"docs_archive_no_manifest": "Tässä ZIP-tiedostossa ei ole manifest.json-tiedostoa, joten palautus ei tiedä, mihin asiakirjaan tiedosto kuuluu. Pidä manifest.json kansioiden vieressä, kun pakkaat uudelleen.",
@@ -903,7 +903,7 @@
"remove": "Poista",
"doc_deduped": "Jo tallennettu muualle — jaettu, ei lisätilaa.",
"doc_dup_in_object": "Tämä tiedosto on jo liitetty tähän kohteeseen.",
"doc_link_invalid": "Vain http/https-linkit ovat sallittuja.",
"doc_link_not_allowed": "Tämä linkki ei ole sallittu: linkit, jotka suorittavat skriptejä tai avaavat paikallisia tiedostoja (javascript:, data:, file: …), hylätään.",
"doc_cat_manual": "Käyttöohje",
"doc_cat_warranty": "Takuu",
"doc_cat_invoice": "Lasku",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Täydennysmäärä (pakkausta)",
"part_package_hint": "Ostetaan pakkauksina, kulutetaan yksikköinä: varasto, tehtävän kulutus ja tilausraja lasketaan yksikköinä; täydennysmäärä ja hinta ovat pakkauskohtaisia.",
"part_auto_buy": "Ostotehtävä kun vähissä",
"part_reuse": "Käytön jälkeen",
"part_reuse_none": "Kulutettu",
"part_reuse_charge": "Ladattu",
"part_reuse_sharpen": "Teroitettu",
"part_reuse_clean": "Puhdistettu",
"part_reuse_dry": "Kuivattu",
"part_reuse_refill": "Täytetty",
"part_reuse_restore": "Kunnostettu",
"part_waiting_charge": "{count} ladattavana",
"part_waiting_sharpen": "{count} teroitettavana",
"part_waiting_clean": "{count} puhdistettavana",
"part_waiting_dry": "{count} kuivattavana",
"part_waiting_refill": "{count} täytettävänä",
"part_waiting_restore": "{count} kunnostettavana",
"part_restoring": "Käytetyt, ei vielä valmiina",
"part_ready_again_hint": "Kaikki käytetyt ovat taas valmiina",
"part_reuse_hint": "Käytetty ei katoa: se odottaa, kunnes se on taas valmiina, ja lasketaan yhä omistetuksi, joten sitä ei osteta uudelleen.",
"part_restock": "Muuta varastoa",
"parts_used_by": "Käyttäjät",
"restock_quantity_label": "Ostettu määrä",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Lajittele nimen mukaan",
"battery_fleet_sort_urgency": "Lajittele kiireellisyyden mukaan",
"battery_fleet_mark_recharged": "Merkitse ladatuksi",
"battery_fleet_charging": "Ladattavat varaosat",
"battery_fleet_ready": "{count} valmiina",
"battery_fleet_sparkline_hint": "Akun varaustaso viimeisten 30 päivän ajalta — pisteviiva: ennuste alarajaan asti",
"battery_fleet_filter_type": "Näytä vain tämä paristotyyppi",
"battery_fleet_record_replacement": "Varaustaso hyppäsi noin {date} — kirjaa tämä vaihto Battery Notesiin",
@@ -888,7 +888,7 @@
"doc_link_title": "Titre (facultatif)",
"doc_open": "Ouvrir",
"doc_delete_confirm": "Supprimer « {name} » ?",
"doc_too_large": "Fichier trop volumineux (max. 25 Mo).",
"doc_too_large": "Fichier trop volumineux (max. 100 Mo).",
"doc_upload_failed": "Échec du téléversement.",
"docs_archive_too_large": "Archive trop volumineuse (max. {max}).",
"docs_archive_no_manifest": "Ce ZIP ne contient pas de manifest.json : la restauration ne peut pas savoir à quel document appartient un fichier. Gardez manifest.json à côté des dossiers lorsque vous recompressez.",
@@ -903,7 +903,7 @@
"remove": "Retirer",
"doc_deduped": "Déjà stocké ailleurs — partagé, aucun espace supplémentaire.",
"doc_dup_in_object": "Ce fichier est déjà attaché à cet objet.",
"doc_link_invalid": "Seuls les liens http/https sont autorisés.",
"doc_link_not_allowed": "Ce lien n'est pas autorisé : les liens qui exécutent des scripts ou ouvrent des fichiers locaux (javascript:, data:, file: …) sont refusés.",
"doc_cat_manual": "Manuel",
"doc_cat_warranty": "Garantie",
"doc_cat_invoice": "Facture",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Quantité de réapprovisionnement (conditionnements)",
"part_package_hint": "Acheté par conditionnement, utilisé à l'unité : le stock, la quantité utilisée par tâche et le seuil de réapprovisionnement comptent des unités ; la quantité de réapprovisionnement et le prix sont par conditionnement.",
"part_auto_buy": "Tâche d'achat auto si bas",
"part_reuse": "Après usage",
"part_reuse_none": "Consommé",
"part_reuse_charge": "Rechargé",
"part_reuse_sharpen": "Affûté",
"part_reuse_clean": "Nettoyé",
"part_reuse_dry": "Séché",
"part_reuse_refill": "Rempli",
"part_reuse_restore": "Remis en état",
"part_waiting_charge": "{count} à recharger",
"part_waiting_sharpen": "{count} à affûter",
"part_waiting_clean": "{count} à nettoyer",
"part_waiting_dry": "{count} à sécher",
"part_waiting_refill": "{count} à remplir",
"part_waiting_restore": "{count} à remettre en état",
"part_restoring": "Utilisés, pas encore prêts",
"part_ready_again_hint": "Tous les usagés sont de nouveau prêts",
"part_reuse_hint": "Un exemplaire utilisé n'est pas perdu : il attend d'être de nouveau prêt et compte toujours comme possédé, il n'est donc pas racheté.",
"part_restock": "Ajuster le stock",
"parts_used_by": "Utilisé par",
"restock_quantity_label": "Quantité achetée",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Trier par nom",
"battery_fleet_sort_urgency": "Trier par urgence",
"battery_fleet_mark_recharged": "Marquer comme rechargée",
"battery_fleet_charging": "Piles rechargeables de réserve",
"battery_fleet_ready": "{count} prêtes",
"battery_fleet_sparkline_hint": "Niveau de batterie des 30 derniers jours — pointillé : projection jusqu'au seuil bas",
"battery_fleet_filter_type": "Afficher uniquement ce type de pile",
"battery_fleet_record_replacement": "Le niveau a bondi vers le {date} — enregistrer ce remplacement dans Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "शीर्षक (वैकल्पिक)",
"doc_open": "खोलें",
"doc_delete_confirm": "\"{name}\" हटाएँ?",
"doc_too_large": "फ़ाइल बहुत बड़ी है (अधिकतम 25 MB)।",
"doc_too_large": "फ़ाइल बहुत बड़ी है (अधिकतम 100 MB)।",
"doc_upload_failed": "अपलोड विफल रहा।",
"docs_archive_too_large": "आर्काइव बहुत बड़ा है (अधिकतम {max})।",
"docs_archive_no_manifest": "इस ZIP में manifest.json नहीं है, इसलिए पुनर्स्थापना यह नहीं जान सकती कि कोई फ़ाइल किस दस्तावेज़ की है। दोबारा ज़िप करते समय manifest.json को फ़ोल्डरों के साथ रखें।",
@@ -903,7 +903,7 @@
"remove": "हटाएँ",
"doc_deduped": "पहले से कहीं और संग्रहीत — साझा, कोई अतिरिक्त स्थान नहीं।",
"doc_dup_in_object": "यह फ़ाइल पहले से इस ऑब्जेक्ट से जुड़ी है।",
"doc_link_invalid": "केवल http/https लिंक की अनुमति है।",
"doc_link_not_allowed": "यह लिंक अनुमत नहीं है: स्क्रिप्ट चलाने वाले या स्थानीय फ़ाइलें खोलने वाले लिंक (javascript:, data:, file: …) अस्वीकार किए जाते हैं।",
"doc_cat_manual": "मैनुअल",
"doc_cat_warranty": "वारंटी",
"doc_cat_invoice": "चालान",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "दोबारा भरने की मात्रा (पैकेट)",
"part_package_hint": "पैकेट में खरीदा, इकाई में इस्तेमाल: स्टॉक, हर काम की खपत और दोबारा मंगाने की सीमा इकाइयों में गिनी जाती हैं; दोबारा भरने की मात्रा और कीमत प्रति पैकेट हैं।",
"part_auto_buy": "कम होने पर खरीद कार्य",
"part_reuse": "उपयोग के बाद",
"part_reuse_none": "खत्म",
"part_reuse_charge": "चार्ज किया गया",
"part_reuse_sharpen": "धार लगाई गई",
"part_reuse_clean": "साफ किया गया",
"part_reuse_dry": "सुखाया गया",
"part_reuse_refill": "फिर से भरा गया",
"part_reuse_restore": "फिर से तैयार",
"part_waiting_charge": "{count} चार्ज करने हैं",
"part_waiting_sharpen": "{count} पर धार लगानी है",
"part_waiting_clean": "{count} साफ करने हैं",
"part_waiting_dry": "{count} सुखाने हैं",
"part_waiting_refill": "{count} फिर से भरने हैं",
"part_waiting_restore": "{count} फिर से तैयार करने हैं",
"part_restoring": "इस्तेमाल हुए, अभी तैयार नहीं",
"part_ready_again_hint": "सभी इस्तेमाल हुए फिर से तैयार हैं",
"part_reuse_hint": "इस्तेमाल किया हुआ खत्म नहीं होता: यह फिर से तैयार होने तक इंतज़ार करता है और आपके पास गिना जाता है, इसलिए दोबारा नहीं खरीदा जाता।",
"part_restock": "स्टॉक समायोजित करें",
"parts_used_by": "द्वारा उपयोग",
"restock_quantity_label": "खरीदी गई मात्रा",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "नाम के अनुसार क्रमबद्ध करें",
"battery_fleet_sort_urgency": "तात्कालिकता के अनुसार क्रमबद्ध करें",
"battery_fleet_mark_recharged": "रिचार्ज हो गई के रूप में चिह्नित करें",
"battery_fleet_charging": "रिचार्जेबल अतिरिक्त",
"battery_fleet_ready": "{count} तैयार",
"battery_fleet_sparkline_hint": "पिछले 30 दिनों का बैटरी स्तर — बिंदीदार: लो थ्रेशोल्ड तक का अनुमान",
"battery_fleet_filter_type": "केवल यही बैटरी प्रकार दिखाएँ",
"battery_fleet_record_replacement": "स्तर लगभग {date} को उछला — इस बदलाव को Battery Notes में दर्ज करें",
@@ -895,7 +895,7 @@
"doc_link_title": "Cím (opcionális)",
"doc_open": "Megnyitás",
"doc_delete_confirm": "Törli a(z) „{name}” elemet?",
"doc_too_large": "A fájl túl nagy (max. 25 MB).",
"doc_too_large": "A fájl túl nagy (max. 100 MB).",
"doc_upload_failed": "A feltöltés nem sikerült.",
"docs_archive_too_large": "Az archívum túl nagy (max. {max}).",
"docs_archive_no_manifest": "Ebben a ZIP-ben nincs manifest.json, ezért a visszaállítás nem tudja, melyik dokumentumhoz tartozik egy fájl. Újratömörítéskor hagyd a manifest.json fájlt a mappák mellett.",
@@ -910,7 +910,7 @@
"remove": "Eltávolítás",
"doc_deduped": "Már máshol is tárolva — megosztott, nem foglal extra helyet.",
"doc_dup_in_object": "Ez a fájl már csatolva van ehhez az objektumhoz.",
"doc_link_invalid": "Csak http/https hivatkozás engedélyezett.",
"doc_link_not_allowed": "Ez a hivatkozás nem engedélyezett: a szkripteket futtató vagy helyi fájlokat megnyitó hivatkozásokat (javascript:, data:, file: …) elutasítjuk.",
"doc_cat_manual": "Kézikönyv",
"doc_cat_warranty": "Garancia",
"doc_cat_invoice": "Számla",
@@ -1085,6 +1085,23 @@
"part_restock_packages": "Utántöltési mennyiség (kiszerelés)",
"part_package_hint": "Kiszerelésben vásárolva, egységben felhasználva: a készlet, a feladatonkénti fogyás és az újrarendelési küszöb egységet számol; az utántöltési mennyiség és az ár kiszerelésenként értendő.",
"part_auto_buy": "Vásárlási feladat automatikus létrehozása, ha kevés van",
"part_reuse": "Használat után",
"part_reuse_none": "Elhasználva",
"part_reuse_charge": "Feltöltve",
"part_reuse_sharpen": "Megélezve",
"part_reuse_clean": "Megtisztítva",
"part_reuse_dry": "Kiszárítva",
"part_reuse_refill": "Utántöltve",
"part_reuse_restore": "Felújítva",
"part_waiting_charge": "{count} töltendő",
"part_waiting_sharpen": "{count} élezendő",
"part_waiting_clean": "{count} tisztítandó",
"part_waiting_dry": "{count} szárítandó",
"part_waiting_refill": "{count} utántöltendő",
"part_waiting_restore": "{count} felújítandó",
"part_restoring": "Használt, még nem kész",
"part_ready_again_hint": "Az összes használt újra kész",
"part_reuse_hint": "A használt darab nem vész el: megvárja, amíg újra kész, és továbbra is a készlethez számít, így nem kell újra megvenni.",
"part_restock": "Készlet módosítása",
"parts_used_by": "Használja",
"restock_quantity_label": "Vásárolt mennyiség",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Rendezés név szerint",
"battery_fleet_sort_urgency": "Rendezés sürgősség szerint",
"battery_fleet_mark_recharged": "Megjelölés feltöltöttként",
"battery_fleet_charging": "Tartalék akkuk",
"battery_fleet_ready": "{count} kész",
"battery_fleet_sparkline_hint": "Akkumulátorszint az elmúlt 30 napban — pontozott: előrejelzés az alacsony küszöbig",
"battery_fleet_filter_type": "Csak ez az elemtípus megjelenítése",
"battery_fleet_record_replacement": "A szint {date} körül megugrott — rögzítse ezt a cserét a Battery Notes-ban",
@@ -888,7 +888,7 @@
"doc_link_title": "Titolo (facoltativo)",
"doc_open": "Apri",
"doc_delete_confirm": "Eliminare «{name}»?",
"doc_too_large": "File troppo grande (max 25 MB).",
"doc_too_large": "File troppo grande (max 100 MB).",
"doc_upload_failed": "Caricamento non riuscito.",
"docs_archive_too_large": "L'archivio è troppo grande (max {max}).",
"docs_archive_no_manifest": "Questo ZIP non contiene manifest.json, quindi il ripristino non sa a quale documento appartiene un file. Lascia manifest.json accanto alle cartelle quando lo comprimi di nuovo.",
@@ -903,7 +903,7 @@
"remove": "Rimuovi",
"doc_deduped": "Già archiviato altrove — condiviso, nessuno spazio aggiuntivo.",
"doc_dup_in_object": "Questo file è già allegato a questo oggetto.",
"doc_link_invalid": "Sono consentiti solo link http/https.",
"doc_link_not_allowed": "Questo link non è consentito: i link che eseguono script o aprono file locali (javascript:, data:, file: …) vengono rifiutati.",
"doc_cat_manual": "Manuale",
"doc_cat_warranty": "Garanzia",
"doc_cat_invoice": "Fattura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Quantità di riordino (confezioni)",
"part_package_hint": "Acquistato a confezioni, usato a unità: scorte, consumo per attività e soglia di riordino contano unità; quantità di riordino e prezzo sono per confezione.",
"part_auto_buy": "Task d'acquisto se scarso",
"part_reuse": "Dopo l'uso",
"part_reuse_none": "Consumato",
"part_reuse_charge": "Ricaricato",
"part_reuse_sharpen": "Affilato",
"part_reuse_clean": "Pulito",
"part_reuse_dry": "Asciugato",
"part_reuse_refill": "Riempito",
"part_reuse_restore": "Ripristinato",
"part_waiting_charge": "{count} da ricaricare",
"part_waiting_sharpen": "{count} da affilare",
"part_waiting_clean": "{count} da pulire",
"part_waiting_dry": "{count} da asciugare",
"part_waiting_refill": "{count} da riempire",
"part_waiting_restore": "{count} da ripristinare",
"part_restoring": "Usati, non ancora pronti",
"part_ready_again_hint": "Tutti quelli usati sono di nuovo pronti",
"part_reuse_hint": "Un pezzo usato non è perso: aspetta di essere di nuovo pronto e conta ancora come posseduto, quindi non viene ricomprato.",
"part_restock": "Correggi scorta",
"parts_used_by": "Usato da",
"restock_quantity_label": "Quantità acquistata",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Ordina per nome",
"battery_fleet_sort_urgency": "Ordina per urgenza",
"battery_fleet_mark_recharged": "Segna come ricaricata",
"battery_fleet_charging": "Ricambi ricaricabili",
"battery_fleet_ready": "{count} pronte",
"battery_fleet_sparkline_hint": "Livello batteria degli ultimi 30 giorni — tratteggiato: proiezione fino alla soglia di batteria scarica",
"battery_fleet_filter_type": "Mostra solo questo tipo di batteria",
"battery_fleet_record_replacement": "Il livello è balzato intorno al {date} — registra questa sostituzione in Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "タイトル(任意)",
"doc_open": "開く",
"doc_delete_confirm": "「{name}」を削除しますか?",
"doc_too_large": "ファイルが大きすぎます(最大 25 MB)。",
"doc_too_large": "ファイルが大きすぎます(最大 100 MB)。",
"doc_upload_failed": "アップロードに失敗しました。",
"docs_archive_too_large": "アーカイブが大きすぎます(最大 {max})。",
"docs_archive_no_manifest": "この ZIP には manifest.json がないため、復元時にファイルがどのドキュメントのものか判断できません。再度圧縮するときは manifest.json をフォルダーと同じ場所に置いてください。",
@@ -903,7 +903,7 @@
"remove": "削除",
"doc_deduped": "既に別の場所に保存済み — 共有され、追加容量はかかりません。",
"doc_dup_in_object": "このファイルは既にこのオブジェクトに添付されています。",
"doc_link_invalid": "http/https のリンクのみ許可されています。",
"doc_link_not_allowed": "このリンクは使用できません。スクリプトを実行したりローカルファイルを開いたりするリンク(javascript:、data:、file: …)は拒否されます。",
"doc_cat_manual": "取扱説明書",
"doc_cat_warranty": "保証書",
"doc_cat_invoice": "請求書",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "補充数(包装単位)",
"part_package_hint": "包装単位で購入し、単位で使います。在庫、タスクごとの使用量、発注しきい値は単位で数え、補充数と価格は包装単位です。",
"part_auto_buy": "在庫少で購入タスク",
"part_reuse": "使用後",
"part_reuse_none": "使い切り",
"part_reuse_charge": "充電済み",
"part_reuse_sharpen": "研ぎ済み",
"part_reuse_clean": "清掃済み",
"part_reuse_dry": "乾燥済み",
"part_reuse_refill": "補充済み",
"part_reuse_restore": "再生済み",
"part_waiting_charge": "充電待ち {count}",
"part_waiting_sharpen": "研ぎ待ち {count}",
"part_waiting_clean": "清掃待ち {count}",
"part_waiting_dry": "乾燥待ち {count}",
"part_waiting_refill": "補充待ち {count}",
"part_waiting_restore": "再生待ち {count}",
"part_restoring": "使用済み・未準備",
"part_ready_again_hint": "使用済みのものがすべて再び使えます",
"part_reuse_hint": "使用済みのものはなくなりません。再び使えるようになるまで待機し、所有数に含まれるため、買い直しは不要です。",
"part_restock": "在庫を調整",
"parts_used_by": "使用タスク",
"restock_quantity_label": "購入数量",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "名前順に並べ替え",
"battery_fleet_sort_urgency": "緊急度順に並べ替え",
"battery_fleet_mark_recharged": "充電済みにする",
"battery_fleet_charging": "予備の充電池",
"battery_fleet_ready": "準備済み {count}",
"battery_fleet_sparkline_hint": "過去30日間のバッテリー残量——点線:低残量しきい値までの予測",
"battery_fleet_filter_type": "この電池タイプのみ表示",
"battery_fleet_record_replacement": "{date}頃に残量が急上昇——この交換をBattery Notesに記録する",
@@ -895,7 +895,7 @@
"doc_link_title": "제목 (선택)",
"doc_open": "열기",
"doc_delete_confirm": "\"{name}\"을(를) 삭제할까요?",
"doc_too_large": "파일이 너무 큽니다 (최대 25 MB).",
"doc_too_large": "파일이 너무 큽니다 (최대 100 MB).",
"doc_upload_failed": "업로드에 실패했습니다.",
"docs_archive_too_large": "아카이브가 너무 큽니다(최대 {max}).",
"docs_archive_no_manifest": "이 ZIP에는 manifest.json이 없어 복원 시 파일이 어느 문서에 속하는지 알 수 없습니다. 다시 압축할 때 manifest.json을 폴더 옆에 두세요.",
@@ -910,7 +910,7 @@
"remove": "제거",
"doc_deduped": "이미 다른 곳에 저장됨 — 공유되어 추가 공간을 쓰지 않습니다.",
"doc_dup_in_object": "이 파일은 이미 이 객체에 첨부되어 있습니다.",
"doc_link_invalid": "http/https 링크만 허용됩니다.",
"doc_link_not_allowed": "이 링크는 허용되지 않습니다. 스크립트를 실행하거나 로컬 파일을 여는 링크(javascript:, data:, file: …)는 거부됩니다.",
"doc_cat_manual": "설명서",
"doc_cat_warranty": "보증서",
"doc_cat_invoice": "영수증",
@@ -1085,6 +1085,23 @@
"part_restock_packages": "보충 수량(포장)",
"part_package_hint": "포장 단위로 사고 단위로 씁니다. 재고, 작업당 사용량, 재주문 기준은 단위로 세고, 보충 수량과 가격은 포장 기준입니다.",
"part_auto_buy": "재고 부족 시 구매 작업 자동 생성",
"part_reuse": "사용 후",
"part_reuse_none": "소모됨",
"part_reuse_charge": "충전됨",
"part_reuse_sharpen": "연마됨",
"part_reuse_clean": "세척됨",
"part_reuse_dry": "건조됨",
"part_reuse_refill": "리필됨",
"part_reuse_restore": "복원됨",
"part_waiting_charge": "충전 대기 {count}",
"part_waiting_sharpen": "연마 대기 {count}",
"part_waiting_clean": "세척 대기 {count}",
"part_waiting_dry": "건조 대기 {count}",
"part_waiting_refill": "리필 대기 {count}",
"part_waiting_restore": "복원 대기 {count}",
"part_restoring": "사용됨, 아직 준비 안 됨",
"part_ready_again_hint": "사용한 것이 모두 다시 준비됨",
"part_reuse_hint": "사용한 것은 없어지지 않습니다. 다시 준비될 때까지 대기하며 보유 수량에 계속 포함되므로 다시 구매하지 않습니다.",
"part_restock": "재고 조정",
"parts_used_by": "사용처",
"restock_quantity_label": "구매 수량",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "이름순 정렬",
"battery_fleet_sort_urgency": "긴급도순 정렬",
"battery_fleet_mark_recharged": "충전 완료로 표시",
"battery_fleet_charging": "예비 충전지",
"battery_fleet_ready": "준비됨 {count}",
"battery_fleet_sparkline_hint": "지난 30일간의 배터리 잔량 — 점선: 낮음 임계값까지의 예측",
"battery_fleet_filter_type": "이 배터리 유형만 표시",
"battery_fleet_record_replacement": "{date}쯤 잔량이 급상승했습니다 — 이 교체를 Battery Notes에 기록",
@@ -888,7 +888,7 @@
"doc_link_title": "Tittel (valgfritt)",
"doc_open": "Åpne",
"doc_delete_confirm": "Slette \"{name}\"?",
"doc_too_large": "Filen er for stor (maks. 25 MB).",
"doc_too_large": "Filen er for stor (maks. 100 MB).",
"doc_upload_failed": "Opplasting mislyktes.",
"docs_archive_too_large": "Arkivet er for stort (maks. {max}).",
"docs_archive_no_manifest": "Denne ZIP-filen har ingen manifest.json, så gjenopprettingen vet ikke hvilket dokument en fil hører til. La manifest.json ligge ved siden av mappene når du zipper på nytt.",
@@ -903,7 +903,7 @@
"remove": "Fjern",
"doc_deduped": "Allerede lagret et annet sted — delt, ingen ekstra plass.",
"doc_dup_in_object": "Denne filen er allerede knyttet til dette objektet.",
"doc_link_invalid": "Kun http/https-lenker er tillatt.",
"doc_link_not_allowed": "Denne lenken er ikke tillatt: lenker som kjører skript eller åpner lokale filer (javascript:, data:, file: …), avvises.",
"doc_cat_manual": "Bruksanvisning",
"doc_cat_warranty": "Garanti",
"doc_cat_invoice": "Faktura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Påfyllingsmengde (pakninger)",
"part_package_hint": "Kjøpes i pakninger, brukes i enheten: lager, forbruk per oppgave og bestillingsgrense teller enheter; påfyllingsmengde og pris gjelder per pakning.",
"part_auto_buy": "Kjøpsoppgave ved lavt lager",
"part_reuse": "Etter bruk",
"part_reuse_none": "Brukt opp",
"part_reuse_charge": "Ladet",
"part_reuse_sharpen": "Slipt",
"part_reuse_clean": "Rengjort",
"part_reuse_dry": "Tørket",
"part_reuse_refill": "Etterfylt",
"part_reuse_restore": "Satt i stand",
"part_waiting_charge": "{count} skal lades",
"part_waiting_sharpen": "{count} skal slipes",
"part_waiting_clean": "{count} skal rengjøres",
"part_waiting_dry": "{count} skal tørkes",
"part_waiting_refill": "{count} skal etterfylles",
"part_waiting_restore": "{count} skal settes i stand",
"part_restoring": "Brukte, ikke klare ennå",
"part_ready_again_hint": "Alle brukte er klare igjen",
"part_reuse_hint": "En brukt er ikke borte: den venter til den er klar igjen og teller fortsatt som eid, så den kjøpes ikke på nytt.",
"part_restock": "Juster lager",
"parts_used_by": "Brukes av",
"restock_quantity_label": "Kjøpt mengde",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Sorter etter navn",
"battery_fleet_sort_urgency": "Sorter etter hastegrad",
"battery_fleet_mark_recharged": "Merk som oppladet",
"battery_fleet_charging": "Oppladbare reserver",
"battery_fleet_ready": "{count} klare",
"battery_fleet_sparkline_hint": "Batterinivå de siste 30 dagene — stiplet: fremskrevet ned til lavnivåterskelen",
"battery_fleet_filter_type": "Vis bare denne batteritypen",
"battery_fleet_record_replacement": "Nivået hoppet rundt {date} — registrer dette byttet i Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Titel (optioneel)",
"doc_open": "Openen",
"doc_delete_confirm": "\"{name}\" verwijderen?",
"doc_too_large": "Bestand is te groot (max. 25 MB).",
"doc_too_large": "Bestand is te groot (max. 100 MB).",
"doc_upload_failed": "Uploaden mislukt.",
"docs_archive_too_large": "Het archief is te groot (max. {max}).",
"docs_archive_no_manifest": "Deze ZIP bevat geen manifest.json, dus het herstel weet niet bij welk document een bestand hoort. Laat manifest.json naast de mappen staan als je opnieuw zipt.",
@@ -903,7 +903,7 @@
"remove": "Weghalen",
"doc_deduped": "Al elders opgeslagen — gedeeld, geen extra ruimte.",
"doc_dup_in_object": "Dit bestand is al aan dit object gekoppeld.",
"doc_link_invalid": "Alleen http/https-links zijn toegestaan.",
"doc_link_not_allowed": "Deze link is niet toegestaan: links die scripts uitvoeren of lokale bestanden openen (javascript:, data:, file: …) worden geweigerd.",
"doc_cat_manual": "Handleiding",
"doc_cat_warranty": "Garantie",
"doc_cat_invoice": "Factuur",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Bijvulhoeveelheid (verpakkingen)",
"part_package_hint": "Gekocht per verpakking, gebruikt per eenheid: voorraad, verbruik per taak en bestelgrens tellen eenheden; bijvulhoeveelheid en prijs gelden per verpakking.",
"part_auto_buy": "Kooptaak bij lage voorraad",
"part_reuse": "Na gebruik",
"part_reuse_none": "Verbruikt",
"part_reuse_charge": "Opgeladen",
"part_reuse_sharpen": "Geslepen",
"part_reuse_clean": "Gereinigd",
"part_reuse_dry": "Gedroogd",
"part_reuse_refill": "Bijgevuld",
"part_reuse_restore": "Opgeknapt",
"part_waiting_charge": "{count} op te laden",
"part_waiting_sharpen": "{count} te slijpen",
"part_waiting_clean": "{count} te reinigen",
"part_waiting_dry": "{count} te drogen",
"part_waiting_refill": "{count} bij te vullen",
"part_waiting_restore": "{count} op te knappen",
"part_restoring": "Gebruikt, nog niet klaar",
"part_ready_again_hint": "Alle gebruikte zijn weer klaar",
"part_reuse_hint": "Een gebruikt exemplaar is niet weg: het wacht tot het weer klaar is en telt nog mee als bezit, dus het wordt niet opnieuw gekocht.",
"part_restock": "Voorraad aanpassen",
"parts_used_by": "Gebruikt door",
"restock_quantity_label": "Gekochte hoeveelheid",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Sorteren op naam",
"battery_fleet_sort_urgency": "Sorteren op urgentie",
"battery_fleet_mark_recharged": "Markeren als opgeladen",
"battery_fleet_charging": "Oplaadbare reserves",
"battery_fleet_ready": "{count} klaar",
"battery_fleet_sparkline_hint": "Batterijniveau van de afgelopen 30 dagen — gestippeld: prognose tot de lage drempel",
"battery_fleet_filter_type": "Alleen dit batterijtype tonen",
"battery_fleet_record_replacement": "Het niveau maakte rond {date} een sprong — deze vervanging vastleggen in Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Tytuł (opcjonalnie)",
"doc_open": "Otwórz",
"doc_delete_confirm": "Usunąć „{name}”?",
"doc_too_large": "Plik jest za duży (maks. 25 MB).",
"doc_too_large": "Plik jest za duży (maks. 100 MB).",
"doc_upload_failed": "Przesyłanie nie powiodło się.",
"docs_archive_too_large": "Archiwum jest za duże (maks. {max}).",
"docs_archive_no_manifest": "Ten ZIP nie zawiera pliku manifest.json, więc przywracanie nie wie, do którego dokumentu należy plik. Przy ponownym pakowaniu zostaw manifest.json obok folderów.",
@@ -903,7 +903,7 @@
"remove": "Usuń",
"doc_deduped": "Już zapisany gdzie indziej — współdzielony, bez dodatkowego miejsca.",
"doc_dup_in_object": "Ten plik jest już dołączony do tego obiektu.",
"doc_link_invalid": "Dozwolone są tylko linki http/https.",
"doc_link_not_allowed": "Ten link jest niedozwolony: linki uruchamiające skrypty lub otwierające pliki lokalne (javascript:, data:, file: …) są odrzucane.",
"doc_cat_manual": "Instrukcja",
"doc_cat_warranty": "Gwarancja",
"doc_cat_invoice": "Faktura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Ilość uzupełnienia (opakowania)",
"part_package_hint": "Kupowane w opakowaniach, zużywane w jednostkach: stan, zużycie na zadanie i próg zamówienia liczą jednostki; ilość uzupełnienia i cena dotyczą opakowania.",
"part_auto_buy": "Zadanie zakupu przy niskim stanie",
"part_reuse": "Po użyciu",
"part_reuse_none": "Zużyty",
"part_reuse_charge": "Naładowany",
"part_reuse_sharpen": "Naostrzony",
"part_reuse_clean": "Wyczyszczony",
"part_reuse_dry": "Wysuszony",
"part_reuse_refill": "Napełniony",
"part_reuse_restore": "Odnowiony",
"part_waiting_charge": "{count} do naładowania",
"part_waiting_sharpen": "{count} do naostrzenia",
"part_waiting_clean": "{count} do wyczyszczenia",
"part_waiting_dry": "{count} do wysuszenia",
"part_waiting_refill": "{count} do napełnienia",
"part_waiting_restore": "{count} do odnowienia",
"part_restoring": "Zużyte, jeszcze niegotowe",
"part_ready_again_hint": "Wszystkie zużyte są znów gotowe",
"part_reuse_hint": "Zużyta część nie przepada: czeka, aż znów będzie gotowa, i nadal liczy się jako posiadana, więc nie kupuje się jej ponownie.",
"part_restock": "Koryguj stan",
"parts_used_by": "Używane przez",
"restock_quantity_label": "Kupiona ilość",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Sortuj według nazwy",
"battery_fleet_sort_urgency": "Sortuj według pilności",
"battery_fleet_mark_recharged": "Oznacz jako naładowaną",
"battery_fleet_charging": "Zapasowe akumulatorki",
"battery_fleet_ready": "{count} gotowe",
"battery_fleet_sparkline_hint": "Poziom baterii z ostatnich 30 dni — kropkowana linia: prognoza do progu niskiego poziomu",
"battery_fleet_filter_type": "Pokaż tylko ten typ baterii",
"battery_fleet_record_replacement": "Poziom skoczył około {date} — zapisz tę wymianę w Battery Notes",
@@ -895,7 +895,7 @@
"doc_link_title": "Título (opcional)",
"doc_open": "Abrir",
"doc_delete_confirm": "Excluir \"{name}\"?",
"doc_too_large": "O arquivo é muito grande (máx. 25 MB).",
"doc_too_large": "O arquivo é muito grande (máx. 100 MB).",
"doc_upload_failed": "Falha no envio.",
"docs_archive_too_large": "O arquivo é grande demais (máx. {max}).",
"docs_archive_no_manifest": "Este ZIP não tem manifest.json, então a restauração não sabe a qual documento cada arquivo pertence. Mantenha o manifest.json junto às pastas ao compactar de novo.",
@@ -910,7 +910,7 @@
"remove": "Remover",
"doc_deduped": "Já armazenado em outro lugar — compartilhado, sem uso de espaço extra.",
"doc_dup_in_object": "Este arquivo já está anexado a este objeto.",
"doc_link_invalid": "Apenas links http/https são permitidos.",
"doc_link_not_allowed": "Este link não é permitido: links que executam scripts ou abrem arquivos locais (javascript:, data:, file: …) são recusados.",
"doc_cat_manual": "Manual",
"doc_cat_warranty": "Garantia",
"doc_cat_invoice": "Nota fiscal",
@@ -1085,6 +1085,23 @@
"part_restock_packages": "Quantidade de reposição (embalagens)",
"part_package_hint": "Comprado em embalagens, usado por unidade: estoque, consumo por tarefa e limite de reposição contam unidades; quantidade de reposição e preço são por embalagem.",
"part_auto_buy": "Criar tarefa de compra automaticamente quando estiver baixo",
"part_reuse": "Após o uso",
"part_reuse_none": "Consumido",
"part_reuse_charge": "Carregado",
"part_reuse_sharpen": "Afiado",
"part_reuse_clean": "Limpo",
"part_reuse_dry": "Seco",
"part_reuse_refill": "Reabastecido",
"part_reuse_restore": "Restaurado",
"part_waiting_charge": "{count} para carregar",
"part_waiting_sharpen": "{count} para afiar",
"part_waiting_clean": "{count} para limpar",
"part_waiting_dry": "{count} para secar",
"part_waiting_refill": "{count} para reabastecer",
"part_waiting_restore": "{count} para restaurar",
"part_restoring": "Usados, ainda não prontos",
"part_ready_again_hint": "Todos os usados estão prontos de novo",
"part_reuse_hint": "Uma peça usada não se perde: aguarda até ficar pronta de novo e continua contando como sua, então não é comprada de novo.",
"part_restock": "Ajustar estoque",
"parts_used_by": "Usado por",
"restock_quantity_label": "Quantidade comprada",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Ordenar por nome",
"battery_fleet_sort_urgency": "Ordenar por urgência",
"battery_fleet_mark_recharged": "Marcar como recarregada",
"battery_fleet_charging": "Reservas recarregáveis",
"battery_fleet_ready": "{count} prontas",
"battery_fleet_sparkline_hint": "Nível da bateria dos últimos 30 dias — pontilhado: projeção até o limite baixo",
"battery_fleet_filter_type": "Mostrar apenas este tipo de pilha",
"battery_fleet_record_replacement": "O nível deu um salto por volta de {date} — registrar esta substituição no Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Título (opcional)",
"doc_open": "Abrir",
"doc_delete_confirm": "Eliminar «{name}»?",
"doc_too_large": "Ficheiro demasiado grande (máx. 25 MB).",
"doc_too_large": "Ficheiro demasiado grande (máx. 100 MB).",
"doc_upload_failed": "Falha no carregamento.",
"docs_archive_too_large": "O arquivo é demasiado grande (máx. {max}).",
"docs_archive_no_manifest": "Este ZIP não contém manifest.json, pelo que o restauro não sabe a que documento pertence cada ficheiro. Mantenha o manifest.json junto às pastas ao comprimir novamente.",
@@ -903,7 +903,7 @@
"remove": "Remover",
"doc_deduped": "Já armazenado noutro local — partilhado, sem espaço extra.",
"doc_dup_in_object": "Este ficheiro já está anexado a este objeto.",
"doc_link_invalid": "Apenas ligações http/https são permitidas.",
"doc_link_not_allowed": "Esta ligação não é permitida: ligações que executam scripts ou abrem ficheiros locais (javascript:, data:, file: …) são recusadas.",
"doc_cat_manual": "Manual",
"doc_cat_warranty": "Garantia",
"doc_cat_invoice": "Fatura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Quantidade de reposição (embalagens)",
"part_package_hint": "Comprado em embalagens, usado à unidade: o stock, o consumo por tarefa e o limite de encomenda contam unidades; a quantidade de reposição e o preço são por embalagem.",
"part_auto_buy": "Tarefa de compra se baixo",
"part_reuse": "Após o uso",
"part_reuse_none": "Consumido",
"part_reuse_charge": "Carregado",
"part_reuse_sharpen": "Afiado",
"part_reuse_clean": "Limpo",
"part_reuse_dry": "Seco",
"part_reuse_refill": "Reabastecido",
"part_reuse_restore": "Restaurado",
"part_waiting_charge": "{count} por carregar",
"part_waiting_sharpen": "{count} por afiar",
"part_waiting_clean": "{count} por limpar",
"part_waiting_dry": "{count} por secar",
"part_waiting_refill": "{count} por reabastecer",
"part_waiting_restore": "{count} por restaurar",
"part_restoring": "Usados, ainda não prontos",
"part_ready_again_hint": "Todos os usados estão prontos de novo",
"part_reuse_hint": "Uma peça usada não se perde: aguarda até estar pronta de novo e continua a contar como sua, por isso não volta a ser comprada.",
"part_restock": "Ajustar estoque",
"parts_used_by": "Usado por",
"restock_quantity_label": "Quantidade comprada",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Ordenar por nome",
"battery_fleet_sort_urgency": "Ordenar por urgência",
"battery_fleet_mark_recharged": "Marcar como recarregada",
"battery_fleet_charging": "Sobressalentes recarregáveis",
"battery_fleet_ready": "{count} prontas",
"battery_fleet_sparkline_hint": "Nível da bateria dos últimos 30 dias — pontilhado: projeção até ao limiar baixo",
"battery_fleet_filter_type": "Mostrar apenas este tipo de pilha",
"battery_fleet_record_replacement": "O nível deu um salto por volta de {date} — registar esta substituição no Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Название (необязательно)",
"doc_open": "Открыть",
"doc_delete_confirm": "Удалить «{name}»?",
"doc_too_large": "Файл слишком большой (макс. 25 МБ).",
"doc_too_large": "Файл слишком большой (макс. 100 МБ).",
"doc_upload_failed": "Не удалось загрузить.",
"docs_archive_too_large": "Архив слишком большой (макс. {max}).",
"docs_archive_no_manifest": "В этом ZIP нет manifest.json, поэтому при восстановлении неизвестно, к какому документу относится файл. При повторной упаковке оставляйте manifest.json рядом с папками.",
@@ -903,7 +903,7 @@
"remove": "Убрать",
"doc_deduped": "Уже сохранён в другом месте — общий, без доп. места.",
"doc_dup_in_object": "Этот файл уже прикреплён к этому объекту.",
"doc_link_invalid": "Разрешены только ссылки http/https.",
"doc_link_not_allowed": "Эта ссылка не разрешена: ссылки, которые запускают скрипты или открывают локальные файлы (javascript:, data:, file: …), отклоняются.",
"doc_cat_manual": "Инструкция",
"doc_cat_warranty": "Гарантия",
"doc_cat_invoice": "Счёт",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Количество для пополнения (упаковок)",
"part_package_hint": "Покупается упаковками, расходуется единицами: запас, расход на задачу и порог дозаказа считаются в единицах; количество для пополнения и цена — за упаковку.",
"part_auto_buy": "Задача покупки при малом запасе",
"part_reuse": "После использования",
"part_reuse_none": "Израсходован",
"part_reuse_charge": "Заряжен",
"part_reuse_sharpen": "Заточен",
"part_reuse_clean": "Очищен",
"part_reuse_dry": "Высушен",
"part_reuse_refill": "Заправлен",
"part_reuse_restore": "Восстановлен",
"part_waiting_charge": "{count} на зарядку",
"part_waiting_sharpen": "{count} на заточку",
"part_waiting_clean": "{count} на очистку",
"part_waiting_dry": "{count} на сушку",
"part_waiting_refill": "{count} на заправку",
"part_waiting_restore": "{count} на восстановление",
"part_restoring": "Использованы, ещё не готовы",
"part_ready_again_hint": "Все использованные снова готовы",
"part_reuse_hint": "Использованная деталь не пропадает: она ждёт, пока снова будет готова, и по-прежнему считается в наличии, поэтому её не покупают заново.",
"part_restock": "Изменить запас",
"parts_used_by": "Используется",
"restock_quantity_label": "Куплено, шт.",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Сортировать по имени",
"battery_fleet_sort_urgency": "Сортировать по срочности",
"battery_fleet_mark_recharged": "Отметить как заряженную",
"battery_fleet_charging": "Запасные аккумуляторы",
"battery_fleet_ready": "{count} готово",
"battery_fleet_sparkline_hint": "Уровень заряда за последние 30 дней — пунктир: прогноз до порога разряда",
"battery_fleet_filter_type": "Показать только этот тип батарей",
"battery_fleet_record_replacement": "Уровень резко вырос примерно {date} — записать эту замену в Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "Titel (valfritt)",
"doc_open": "Öppna",
"doc_delete_confirm": "Ta bort \"{name}\"?",
"doc_too_large": "Filen är för stor (max 25 MB).",
"doc_too_large": "Filen är för stor (max 100 MB).",
"doc_upload_failed": "Uppladdningen misslyckades.",
"docs_archive_too_large": "Arkivet är för stort (max {max}).",
"docs_archive_no_manifest": "Den här ZIP-filen saknar manifest.json, så återställningen vet inte vilket dokument en fil hör till. Låt manifest.json ligga bredvid mapparna när du zippar igen.",
@@ -903,7 +903,7 @@
"remove": "Ta bort",
"doc_deduped": "Redan lagrad någon annanstans — delad, inget extra utrymme.",
"doc_dup_in_object": "Den här filen är redan kopplad till detta objekt.",
"doc_link_invalid": "Endast http/https-länkar tillåts.",
"doc_link_not_allowed": "Den här länken är inte tillåten: länkar som kör skript eller öppnar lokala filer (javascript:, data:, file: …) avvisas.",
"doc_cat_manual": "Manual",
"doc_cat_warranty": "Garanti",
"doc_cat_invoice": "Faktura",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Påfyllningsmängd (förpackningar)",
"part_package_hint": "Köps i förpackningar, används i enheten: lager, åtgång per uppgift och beställningsgräns räknar enheter; påfyllningsmängd och pris gäller per förpackning.",
"part_auto_buy": "Köpuppgift vid lågt lager",
"part_reuse": "Efter användning",
"part_reuse_none": "Förbrukad",
"part_reuse_charge": "Laddad",
"part_reuse_sharpen": "Slipad",
"part_reuse_clean": "Rengjord",
"part_reuse_dry": "Torkad",
"part_reuse_refill": "Påfylld",
"part_reuse_restore": "Återställd",
"part_waiting_charge": "{count} att ladda",
"part_waiting_sharpen": "{count} att slipa",
"part_waiting_clean": "{count} att rengöra",
"part_waiting_dry": "{count} att torka",
"part_waiting_refill": "{count} att fylla på",
"part_waiting_restore": "{count} att återställa",
"part_restoring": "Använda, inte klara än",
"part_ready_again_hint": "Alla använda är klara igen",
"part_reuse_hint": "En använd är inte borta: den väntar tills den är klar igen och räknas fortfarande som ägd, så den köps inte igen.",
"part_restock": "Justera lager",
"parts_used_by": "Används av",
"restock_quantity_label": "Köpt antal",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Sortera efter namn",
"battery_fleet_sort_urgency": "Sortera efter angelägenhet",
"battery_fleet_mark_recharged": "Markera som uppladdad",
"battery_fleet_charging": "Laddbara reserver",
"battery_fleet_ready": "{count} klara",
"battery_fleet_sparkline_hint": "Batterinivå de senaste 30 dagarna — prickad: prognos ner till lågnivåtröskeln",
"battery_fleet_filter_type": "Visa endast denna batterityp",
"battery_fleet_record_replacement": "Nivån hoppade omkring {date} — registrera detta byte i Battery Notes",
@@ -895,7 +895,7 @@
"doc_link_title": "Başlık (isteğe bağlı)",
"doc_open": "Aç",
"doc_delete_confirm": "\"{name}\" silinsin mi?",
"doc_too_large": "Dosya çok büyük (en fazla 25 MB).",
"doc_too_large": "Dosya çok büyük (en fazla 100 MB).",
"doc_upload_failed": "Yükleme başarısız oldu.",
"docs_archive_too_large": "Arşiv çok büyük (en fazla {max}).",
"docs_archive_no_manifest": "Bu ZIP'te manifest.json yok, bu yüzden geri yükleme bir dosyanın hangi belgeye ait olduğunu bilemez. Yeniden sıkıştırırken manifest.json dosyasını klasörlerin yanında tutun.",
@@ -910,7 +910,7 @@
"remove": "Kaldır",
"doc_deduped": "Zaten başka bir yerde saklı — paylaşıldı, ek alan kullanılmadı.",
"doc_dup_in_object": "Bu dosya zaten bu nesneye ekli.",
"doc_link_invalid": "Yalnızca http/https bağlantılarına izin verilir.",
"doc_link_not_allowed": "Bu bağlantıya izin verilmiyor: betik çalıştıran veya yerel dosyaları açan bağlantılar (javascript:, data:, file: …) reddedilir.",
"doc_cat_manual": "Kılavuz",
"doc_cat_warranty": "Garanti",
"doc_cat_invoice": "Fatura",
@@ -1085,6 +1085,23 @@
"part_restock_packages": "Yeniden stok miktarı (paket)",
"part_package_hint": "Paketle alınır, birimle kullanılır: stok, görev başına kullanım ve yeniden sipariş eşiği birim sayar; yeniden stok miktarı ve fiyat paket başınadır.",
"part_auto_buy": "Azaldığında otomatik satın alma görevi oluştur",
"part_reuse": "Kullanımdan sonra",
"part_reuse_none": "Tükendi",
"part_reuse_charge": "Şarj edildi",
"part_reuse_sharpen": "Bilendi",
"part_reuse_clean": "Temizlendi",
"part_reuse_dry": "Kurutuldu",
"part_reuse_refill": "Yeniden dolduruldu",
"part_reuse_restore": "Yenilendi",
"part_waiting_charge": "{count} şarj edilecek",
"part_waiting_sharpen": "{count} bilenecek",
"part_waiting_clean": "{count} temizlenecek",
"part_waiting_dry": "{count} kurutulacak",
"part_waiting_refill": "{count} doldurulacak",
"part_waiting_restore": "{count} yenilenecek",
"part_restoring": "Kullanılmış, henüz hazır değil",
"part_ready_again_hint": "Kullanılanların hepsi yeniden hazır",
"part_reuse_hint": "Kullanılan parça kaybolmaz: yeniden hazır olana kadar bekler ve hâlâ sahip olunan sayılır, bu yüzden yeniden satın alınmaz.",
"part_restock": "Stoku ayarla",
"parts_used_by": "Kullanan",
"restock_quantity_label": "Satın alınan miktar",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Ada göre sırala",
"battery_fleet_sort_urgency": "Aciliyete göre sırala",
"battery_fleet_mark_recharged": "Şarj edildi olarak işaretle",
"battery_fleet_charging": "Yedek şarjlı piller",
"battery_fleet_ready": "{count} hazır",
"battery_fleet_sparkline_hint": "Son 30 günün pil seviyesi — noktalı: düşük eşiğe kadar projeksiyon",
"battery_fleet_filter_type": "Yalnızca bu pil türünü göster",
"battery_fleet_record_replacement": "Seviye {date} civarında sıçradı — bu değişimi Battery Notes'a kaydet",
@@ -888,7 +888,7 @@
"doc_link_title": "Назва (необов'язково)",
"doc_open": "Відкрити",
"doc_delete_confirm": "Видалити «{name}»?",
"doc_too_large": "Файл завеликий (макс. 25 МБ).",
"doc_too_large": "Файл завеликий (макс. 100 МБ).",
"doc_upload_failed": "Не вдалося завантажити.",
"docs_archive_too_large": "Архів завеликий (макс. {max}).",
"docs_archive_no_manifest": "У цьому ZIP немає manifest.json, тому під час відновлення невідомо, до якого документа належить файл. Під час повторного пакування залишайте manifest.json поруч із теками.",
@@ -903,7 +903,7 @@
"remove": "Прибрати",
"doc_deduped": "Уже збережено в іншому місці — спільний, без додаткового місця.",
"doc_dup_in_object": "Цей файл уже прикріплено до цього об'єкта.",
"doc_link_invalid": "Дозволені лише посилання http/https.",
"doc_link_not_allowed": "Це посилання не дозволено: посилання, які запускають скрипти або відкривають локальні файли (javascript:, data:, file: …), відхиляються.",
"doc_cat_manual": "Інструкція",
"doc_cat_warranty": "Гарантія",
"doc_cat_invoice": "Рахунок",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "Кількість для поповнення (упаковок)",
"part_package_hint": "Купується упаковками, витрачається одиницями: запас, витрата на завдання та поріг дозамовлення рахуються в одиницях; кількість для поповнення та ціна — за упаковку.",
"part_auto_buy": "Завдання купівлі при малому запасі",
"part_reuse": "Після використання",
"part_reuse_none": "Витрачено",
"part_reuse_charge": "Заряджено",
"part_reuse_sharpen": "Заточено",
"part_reuse_clean": "Очищено",
"part_reuse_dry": "Висушено",
"part_reuse_refill": "Заправлено",
"part_reuse_restore": "Відновлено",
"part_waiting_charge": "{count} на заряджання",
"part_waiting_sharpen": "{count} на заточування",
"part_waiting_clean": "{count} на очищення",
"part_waiting_dry": "{count} на сушіння",
"part_waiting_refill": "{count} на заправку",
"part_waiting_restore": "{count} на відновлення",
"part_restoring": "Використані, ще не готові",
"part_ready_again_hint": "Усі використані знову готові",
"part_reuse_hint": "Використана деталь не зникає: вона чекає, доки знову буде готова, і далі рахується як наявна, тож її не купують знову.",
"part_restock": "Змінити запас",
"parts_used_by": "Використовується",
"restock_quantity_label": "Куплена кількість",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "Сортувати за назвою",
"battery_fleet_sort_urgency": "Сортувати за терміновістю",
"battery_fleet_mark_recharged": "Позначити як заряджену",
"battery_fleet_charging": "Запасні акумулятори",
"battery_fleet_ready": "{count} готово",
"battery_fleet_sparkline_hint": "Рівень заряду за останні 30 днів — пунктир: прогноз до порогу розряду",
"battery_fleet_filter_type": "Показати лише цей тип батарей",
"battery_fleet_record_replacement": "Рівень різко зріс приблизно {date} — записати цю заміну в Battery Notes",
@@ -888,7 +888,7 @@
"doc_link_title": "标题(可选)",
"doc_open": "打开",
"doc_delete_confirm": "删除“{name}”?",
"doc_too_large": "文件过大(最大 25 MB)。",
"doc_too_large": "文件过大(最大 100 MB)。",
"doc_upload_failed": "上传失败。",
"docs_archive_too_large": "压缩包过大(最大 {max})。",
"docs_archive_no_manifest": "此 ZIP 中没有 manifest.json,因此恢复时无法判断文件属于哪个文档。重新压缩时请把 manifest.json 放在各文件夹旁边。",
@@ -903,7 +903,7 @@
"remove": "移除",
"doc_deduped": "已在别处存储——共享,不占用额外空间。",
"doc_dup_in_object": "此文件已附加到该对象。",
"doc_link_invalid": "仅允许 http/https 链接。",
"doc_link_not_allowed": "不允许使用此链接:运行脚本或打开本地文件的链接(javascript:、data:、file: …)会被拒绝。",
"doc_cat_manual": "手册",
"doc_cat_warranty": "保修",
"doc_cat_invoice": "发票",
@@ -1078,6 +1078,23 @@
"part_restock_packages": "补货数量(包)",
"part_package_hint": "按包购买、按单位使用:库存、每项任务用量和补货阈值都按单位计;补货数量和价格按包计。",
"part_auto_buy": "库存低时自动建购买任务",
"part_reuse": "使用后",
"part_reuse_none": "已用完",
"part_reuse_charge": "已充电",
"part_reuse_sharpen": "已磨利",
"part_reuse_clean": "已清洁",
"part_reuse_dry": "已烘干",
"part_reuse_refill": "已重新灌装",
"part_reuse_restore": "已修复",
"part_waiting_charge": "{count} 个待充电",
"part_waiting_sharpen": "{count} 个待磨利",
"part_waiting_clean": "{count} 个待清洁",
"part_waiting_dry": "{count} 个待烘干",
"part_waiting_refill": "{count} 个待灌装",
"part_waiting_restore": "{count} 个待修复",
"part_restoring": "已用,尚未就绪",
"part_ready_again_hint": "所有已用的都已重新就绪",
"part_reuse_hint": "用过的不会消失:它会等待重新就绪,并仍计入已有数量,因此不会被再次购买。",
"part_restock": "调整库存",
"parts_used_by": "使用于",
"restock_quantity_label": "购买数量",
@@ -1255,6 +1272,8 @@
"battery_fleet_sort_name": "按名称排序",
"battery_fleet_sort_urgency": "按紧急程度排序",
"battery_fleet_mark_recharged": "标记为已充电",
"battery_fleet_charging": "备用充电电池",
"battery_fleet_ready": "{count} 个就绪",
"battery_fleet_sparkline_hint": "过去 30 天的电池电量——虚线:外推至低电量阈值",
"battery_fleet_filter_type": "仅显示此电池类型",
"battery_fleet_record_replacement": "电量在 {date} 前后跳升——将此次更换记录到 Battery Notes",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
/*! maintenance_supporter frontend 2.100.0 */
import{e as y}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-QKL6SKIH.js";import{d as m}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-M2CMCNR4.js";import{a as h}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-5USKKRUA.js";import{H as b,a as o,b as u,c as n,f as l,h as v,l as g,m as d,s as r,u as f}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZW2EWBUR.js";var a=class extends v{constructor(){super(...arguments);this._open=!1;this._loading=!1;this._adopting=!1;this._error="";this._sensors=[];this._selected=new Set;this._objectNames={};this._objects=[];this._covered={};this._users=[];this._responsible="";this._forMinutes="0";this._localeReady=!1;this._userService=null;this._toggle=t=>{let i=new Set(this._selected);i.has(t)?i.delete(t):i.add(t),this._selected=i};this._toggleAll=()=>{this._selected.size===this._sensors.length?this._selected=new Set:this._selected=new Set(this._sensors.map(t=>t.entity_id))};this._adopt=async()=>{if(this._selected.size===0||this._adopting)return;this._error="";let t=this._sensors.filter(e=>this._selected.has(e.entity_id)).map(e=>({entity_id:e.entity_id,name:e.name,entry_id:this._existingEntryFor(e)??void 0,object_name:this._effectiveName(e),device_id:e.device_id??void 0,part_id:e.suggested_part_id??void 0,responsible_user_id:this._responsible||void 0,for_minutes:parseInt(this._forMinutes,10)>0?parseInt(this._forMinutes,10):void 0})),i=await h(this,{type:"maintenance_supporter/problem_sensors/adopt",selections:t},{busy:e=>{this._adopting=e},onError:e=>{this._error=e}});i!==void 0&&(this.dispatchEvent(new CustomEvent("problem-sensors-adopted",{bubbles:!0,composed:!0,detail:i})),this._open=!1)}}get _lang(){return b(this.hass)}updated(t){t.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,f(this._lang).then(()=>this.requestUpdate()))}async open(){this._open=!0,this._loading=!0,this._error="",this._sensors=[],this._selected=new Set,this._objectNames={},this._responsible="",this._forMinutes="0",this._userService?this._userService.updateHass(this.hass):this._userService=new y(this.hass);let[t,i,e]=await Promise.all([h(this,{type:"maintenance_supporter/problem_sensors/discover"},{onError:s=>{this._error=s}}),this._userService.getUsers().catch(()=>[]),this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"}).catch(()=>({objects:[]}))]);this._loading=!1,t!==void 0&&(this._objects=(e?.objects||[]).filter(s=>!s.object.archived_at).map(s=>({entry_id:s.entry_id,name:s.object.name,tasks:(s.tasks||[]).length})),this._sensors=t?.sensors||[],this._covered=Object.fromEntries(this._sensors.map(s=>[s.entity_id,s.covered_by??null])),this._selected=new Set(this._sensors.filter(s=>!s.covered_by).map(s=>s.entity_id)),this._users=i)}_defaultName(t){return t.suggested_entry_id?t.suggested_object_name:t.candidate?.name??t.suggested_object_name}_effectiveName(t){return(this._objectNames[t.entity_id]??"").trim()||this._defaultName(t)}_existingEntryFor(t){let i=this._effectiveName(t),e=t.suggested_entry_id||t.target_entry_id||null;if(i===this._defaultName(t)&&e)return e;let s=this._objects.find(c=>c.name.trim().toLowerCase()===i.toLowerCase());return s?s.entry_id:null}_schedulePreview(){clearTimeout(this._previewTimer),this._previewTimer=setTimeout(()=>{this._preview()},350)}async _preview(){let t=this._sensors.map(e=>({entity_id:e.entity_id,name:e.name,entry_id:this._existingEntryFor(e)})),i=await h(this,{type:"maintenance_supporter/problem_sensors/preview",selections:t},{onError:()=>{}});i?.covered&&(this._covered={...this._covered,...i.covered})}_summary(t){let i=new Map;for(let e of this._sensors){if(!this._selected.has(e.entity_id))continue;let s=this._existingEntryFor(e),c=this._effectiveName(e),_=s??`new:${c.toLowerCase()}`,p=i.get(_)??{name:c,entry:s,adding:0};p.adding+=1,i.set(_,p)}return i.size===0?l:n`
/*! maintenance_supporter frontend 2.102.0 */
import{e as y}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-KFG47RQD.js";import{d as m}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-UKOBV3FF.js";import{a as h}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-RIYW54TU.js";import{H as b,a as o,b as u,c as n,f as l,h as v,l as g,m as d,s as r,u as f}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4CECVKQ7.js";var a=class extends v{constructor(){super(...arguments);this._open=!1;this._loading=!1;this._adopting=!1;this._error="";this._sensors=[];this._selected=new Set;this._objectNames={};this._objects=[];this._covered={};this._users=[];this._responsible="";this._forMinutes="0";this._localeReady=!1;this._userService=null;this._toggle=t=>{let i=new Set(this._selected);i.has(t)?i.delete(t):i.add(t),this._selected=i};this._toggleAll=()=>{this._selected.size===this._sensors.length?this._selected=new Set:this._selected=new Set(this._sensors.map(t=>t.entity_id))};this._adopt=async()=>{if(this._selected.size===0||this._adopting)return;this._error="";let t=this._sensors.filter(e=>this._selected.has(e.entity_id)).map(e=>({entity_id:e.entity_id,name:e.name,entry_id:this._existingEntryFor(e)??void 0,object_name:this._effectiveName(e),device_id:e.device_id??void 0,part_id:e.suggested_part_id??void 0,responsible_user_id:this._responsible||void 0,for_minutes:parseInt(this._forMinutes,10)>0?parseInt(this._forMinutes,10):void 0})),i=await h(this,{type:"maintenance_supporter/problem_sensors/adopt",selections:t},{busy:e=>{this._adopting=e},onError:e=>{this._error=e}});i!==void 0&&(this.dispatchEvent(new CustomEvent("problem-sensors-adopted",{bubbles:!0,composed:!0,detail:i})),this._open=!1)}}get _lang(){return b(this.hass)}updated(t){t.has("hass")&&this.hass&&!this._localeReady&&(this._localeReady=!0,f(this._lang).then(()=>this.requestUpdate()))}async open(){this._open=!0,this._loading=!0,this._error="",this._sensors=[],this._selected=new Set,this._objectNames={},this._responsible="",this._forMinutes="0",this._userService?this._userService.updateHass(this.hass):this._userService=new y(this.hass);let[t,i,e]=await Promise.all([h(this,{type:"maintenance_supporter/problem_sensors/discover"},{onError:s=>{this._error=s}}),this._userService.getUsers().catch(()=>[]),this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"}).catch(()=>({objects:[]}))]);this._loading=!1,t!==void 0&&(this._objects=(e?.objects||[]).filter(s=>!s.object.archived_at).map(s=>({entry_id:s.entry_id,name:s.object.name,tasks:(s.tasks||[]).length})),this._sensors=t?.sensors||[],this._covered=Object.fromEntries(this._sensors.map(s=>[s.entity_id,s.covered_by??null])),this._selected=new Set(this._sensors.filter(s=>!s.covered_by).map(s=>s.entity_id)),this._users=i)}_defaultName(t){return t.suggested_entry_id?t.suggested_object_name:t.candidate?.name??t.suggested_object_name}_effectiveName(t){return(this._objectNames[t.entity_id]??"").trim()||this._defaultName(t)}_existingEntryFor(t){let i=this._effectiveName(t),e=t.suggested_entry_id||t.target_entry_id||null;if(i===this._defaultName(t)&&e)return e;let s=this._objects.find(c=>c.name.trim().toLowerCase()===i.toLowerCase());return s?s.entry_id:null}_schedulePreview(){clearTimeout(this._previewTimer),this._previewTimer=setTimeout(()=>{this._preview()},350)}async _preview(){let t=this._sensors.map(e=>({entity_id:e.entity_id,name:e.name,entry_id:this._existingEntryFor(e)})),i=await h(this,{type:"maintenance_supporter/problem_sensors/preview",selections:t},{onError:()=>{}});i?.covered&&(this._covered={...this._covered,...i.covered})}_summary(t){let i=new Map;for(let e of this._sensors){if(!this._selected.has(e.entity_id))continue;let s=this._existingEntryFor(e),c=this._effectiveName(e),_=s??`new:${c.toLowerCase()}`,p=i.get(_)??{name:c,entry:s,adding:0};p.adding+=1,i.set(_,p)}return i.size===0?l:n`
<div class="summary">
<div class="summary-title">${r("adopt_summary_title",t)}</div>
${[...i.values()].map(e=>{if(!e.entry)return n`<div class="summary-line new">
@@ -1,5 +1,5 @@
/*! maintenance_supporter frontend 2.100.0 */
import{a as X}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-W3SEDH34.js";import{a as o}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-UO7KFJ6R.js";import{f as E,g as M,h as z,i as Q,j as U,k as Y,l as q,m as G,n as R,o as A,p as W}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZFJHBIVS.js";import{a as B}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-JFENKV7A.js";import{e as P,f as K}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-GN3YV45M.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-U7674FZW.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-Q7CGGH4O.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-PO272ATG.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-C6NCKN5O.js";import{B as S,H as F,K as g,L as v,O as N,P as I,U as w,W as T,a as $,c as p,f as y,h as H,l as k,m as j,n as C,q as D,s as a,u as L,y as O}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZW2EWBUR.js";function J(b,i,t,s){let l=b.entries.filter(e=>e.type==="completed").sort((e,n)=>e.ts-n.ts),d=`${b.from?t.date(b.from):"\u2026"} \u2013 ${b.to?t.date(b.to):"\u2026"}`,r=b.totals,h=[[i.completions,t.number(r.completions)],[i.totalCost,t.cost(r.totalCost)],[i.avgCost,r.avgCost!=null?t.cost(r.avgCost):i.none],[i.totalTime,r.totalDuration?t.duration(r.totalDuration):i.none]].map(([e,n])=>`<div class="kpi"><div class="k">${o(e)}</div><div class="v">${o(n)}</div></div>`).join(""),u=b.byObject.map(e=>`<tr>
/*! maintenance_supporter frontend 2.102.0 */
import{a as X}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-VWOWFA6G.js";import{a as o}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-GH6PGWHU.js";import{f as E,g as M,h as z,i as Q,j as U,k as Y,l as q,m as G,n as R,o as A,p as W}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-7W4GNWDJ.js";import{a as B}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-DVPW5S27.js";import{e as P,f as K}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-XWO2WENS.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZXKKAVZ3.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-7SOUIRZ7.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-OGIYAEPJ.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-U4NJYI3P.js";import{B as S,H as F,K as g,L as v,O as N,P as I,U as w,W as T,a as $,c as p,f as y,h as H,l as k,m as j,n as C,q as D,s as a,u as L,y as O}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4CECVKQ7.js";function J(b,i,t,s){let l=b.entries.filter(e=>e.type==="completed").sort((e,n)=>e.ts-n.ts),d=`${b.from?t.date(b.from):"\u2026"} \u2013 ${b.to?t.date(b.to):"\u2026"}`,r=b.totals,h=[[i.completions,t.number(r.completions)],[i.totalCost,t.cost(r.totalCost)],[i.avgCost,r.avgCost!=null?t.cost(r.avgCost):i.none],[i.totalTime,r.totalDuration?t.duration(r.totalDuration):i.none]].map(([e,n])=>`<div class="kpi"><div class="k">${o(e)}</div><div class="v">${o(n)}</div></div>`).join(""),u=b.byObject.map(e=>`<tr>
<td>${o(e.objectName)}</td>
<td class="num">${o(t.number(e.completions))}</td>
<td class="num">${o(t.cost(e.cost))}</td>
@@ -1,5 +1,5 @@
/*! maintenance_supporter frontend 2.100.0 */
import{a as O}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-W3SEDH34.js";import{q as j,r as E,s as L,t as R,u as C}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZFJHBIVS.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-U7674FZW.js";import{a as _,b as S,c as A}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-HFUWDHSL.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-Q7CGGH4O.js";import{H as g,K as k,L as v,O as w,a as l,c as a,f as m,h as y,l as p,m as b,s as r,u as $,y as f}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZW2EWBUR.js";var q=[{key:"name",label:"area",num:!1},{key:"objects",label:"objects",num:!0},{key:"tasks",label:"tasks",num:!0},{key:"overdue",label:"overdue",num:!0},{key:"due_soon",label:"due_soon",num:!0},{key:"cost_year",label:"area_cost_year",num:!0},{key:"cost_total",label:"total_cost",num:!0},{key:"last",label:"last_performed",num:!1}],i=class extends y{constructor(){super(...arguments);this.objects=[];this.showArchived=!1;this.currencySymbol="";this._query="";this._sort=E(S(_.areaSort));this._histories=new O(this);this._localeReady=!1;this._memo=null}createRenderRoot(){return this}get _lang(){return g(this.hass)}willUpdate(t){if(super.willUpdate(t),t.has("objects")||t.has("showArchived")||t.has("hass")&&t.get("hass")===void 0){let o=this.objects.filter(s=>this.showArchived||!s.object.archived).flatMap(s=>s.tasks.map(n=>({entryId:s.entry_id,task:n})));this._histories.sync(this.hass,o)}}updated(t){super.updated(t),!this._localeReady&&this.hass&&(this._localeReady=!0,$(this._lang).then(()=>this.requestUpdate()))}_rows(t){let o=this._lang,s=[this.objects,this.showArchived,this._histories.version,this.hass?.areas,o,t];if(this._memo&&this._memo.deps.every((c,h)=>c===s[h]))return this._memo.rows;let n=j(this.objects,{showArchived:this.showArchived,areas:this.hass?.areas,noAreaLabel:r("no_area",o),year:t,historyOf:(c,h)=>this._histories.historyOf(c,h)});return this._memo={deps:s,rows:n},n}_setSort(t){this._sort=L(this._sort,t),A(_.areaSort,`${this._sort.key}:${this._sort.dir}`)}_open(t){this.dispatchEvent(new CustomEvent("open-area",{detail:{areaKey:t},bubbles:!0,composed:!0}))}_toggleArchived(){this.dispatchEvent(new CustomEvent("archived-toggle",{bubbles:!0,composed:!0}))}_headerLabel(t,o,s){return t==="area_cost_year"?r("area_cost_year",s).replace("{year}",String(o)):r(t,s)}render(){if(!this.hass)return m;let t=this._lang,o=Number(f().slice(0,4)),s=this.currencySymbol,n=this._rows(o),c=R(C(n,this._query),this._sort),h=this.objects.filter(e=>e.object.archived).length,d=e=>k(e,t);return a`
/*! maintenance_supporter frontend 2.102.0 */
import{a as O}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-VWOWFA6G.js";import{q as j,r as E,s as L,t as R,u as C}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-7W4GNWDJ.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZXKKAVZ3.js";import{a as _,b as S,c as A}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-Y6A3V724.js";import"/maintenance_supporter_panelfiles/panel-chunks/chunk-7SOUIRZ7.js";import{H as g,K as k,L as v,O as w,a as l,c as a,f as m,h as y,l as p,m as b,s as r,u as $,y as f}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4CECVKQ7.js";var q=[{key:"name",label:"area",num:!1},{key:"objects",label:"objects",num:!0},{key:"tasks",label:"tasks",num:!0},{key:"overdue",label:"overdue",num:!0},{key:"due_soon",label:"due_soon",num:!0},{key:"cost_year",label:"area_cost_year",num:!0},{key:"cost_total",label:"total_cost",num:!0},{key:"last",label:"last_performed",num:!1}],i=class extends y{constructor(){super(...arguments);this.objects=[];this.showArchived=!1;this.currencySymbol="";this._query="";this._sort=E(S(_.areaSort));this._histories=new O(this);this._localeReady=!1;this._memo=null}createRenderRoot(){return this}get _lang(){return g(this.hass)}willUpdate(t){if(super.willUpdate(t),t.has("objects")||t.has("showArchived")||t.has("hass")&&t.get("hass")===void 0){let o=this.objects.filter(s=>this.showArchived||!s.object.archived).flatMap(s=>s.tasks.map(n=>({entryId:s.entry_id,task:n})));this._histories.sync(this.hass,o)}}updated(t){super.updated(t),!this._localeReady&&this.hass&&(this._localeReady=!0,$(this._lang).then(()=>this.requestUpdate()))}_rows(t){let o=this._lang,s=[this.objects,this.showArchived,this._histories.version,this.hass?.areas,o,t];if(this._memo&&this._memo.deps.every((c,h)=>c===s[h]))return this._memo.rows;let n=j(this.objects,{showArchived:this.showArchived,areas:this.hass?.areas,noAreaLabel:r("no_area",o),year:t,historyOf:(c,h)=>this._histories.historyOf(c,h)});return this._memo={deps:s,rows:n},n}_setSort(t){this._sort=L(this._sort,t),A(_.areaSort,`${this._sort.key}:${this._sort.dir}`)}_open(t){this.dispatchEvent(new CustomEvent("open-area",{detail:{areaKey:t},bubbles:!0,composed:!0}))}_toggleArchived(){this.dispatchEvent(new CustomEvent("archived-toggle",{bubbles:!0,composed:!0}))}_headerLabel(t,o,s){return t==="area_cost_year"?r("area_cost_year",s).replace("{year}",String(o)):r(t,s)}render(){if(!this.hass)return m;let t=this._lang,o=Number(f().slice(0,4)),s=this.currencySymbol,n=this._rows(o),c=R(C(n,this._query),this._sort),h=this.objects.filter(e=>e.object.archived).length,d=e=>k(e,t);return a`
<div class="filter-bar area-filter-bar">
<input
type="search"
@@ -1,5 +1,5 @@
/*! maintenance_supporter frontend 2.100.0 */
import"/maintenance_supporter_panelfiles/panel-chunks/chunk-TX5O4PHL.js";import{H as u,a as n,b as _,c as o,f as p,h as c,l as g,m as a,s}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZW2EWBUR.js";var d=["round_robin","least_completed","random"],v=["low","normal","high"],m=365,i=class extends c{constructor(){super(...arguments);this._open=!1;this._mode="assign";this._count=0;this._users=[];this._present=[];this._assignKind="one";this._person="";this._pool=new Set;this._rotation=d[0];this._addText="";this._remove=new Set;this._setWarning=!1;this._warning=7;this._setPriority=!1;this._priority="normal";this._setMute=!1;this._mute=!1;this._resolve=null}get _lang(){return u(this.hass)}open(t,e,r){return this._resolve?.(null),this._mode=t,this._count=e.length,this._users=[...r].sort((l,h)=>l.name.localeCompare(h.name)),this._present=[...new Set(e.flatMap(l=>l.labels||[]))].sort((l,h)=>l.localeCompare(h)),this._assignKind="one",this._person="",this._pool=new Set,this._rotation=d[0],this._addText="",this._remove=new Set,this._setWarning=!1,this._warning=7,this._setPriority=!1,this._priority="normal",this._setMute=!1,this._mute=!1,this._open=!0,new Promise(l=>{this._resolve=l})}_finish(t){this._open=!1;let e=this._resolve;this._resolve=null,e?.(t)}changes(){if(this._mode==="assign"){if(this._assignKind==="one")return{responsible_user_id:this._person||null,assignee_pool:null,rotation_strategy:null};let e=this._users.map(r=>r.id).filter(r=>this._pool.has(r));return e.length<2?null:{assignee_pool:e,rotation_strategy:this._rotation,responsible_user_id:null}}if(this._mode==="labels"){let e=[...new Set(this._addText.split(",").map(l=>l.trim()).filter(Boolean))],r=[...this._remove];return e.length||r.length?{labels_add:e,labels_remove:r}:null}let t={};return this._setWarning&&(t.warning_days=this._warning),this._setPriority&&(t.priority=this._priority),this._setMute&&(t.notify_enabled=!this._mute),Object.keys(t).length?t:null}_toggle(t,e){let r=new Set(t);return r.has(e)?r.delete(e):r.add(e),r}_renderAssign(t){return o`
/*! maintenance_supporter frontend 2.102.0 */
import"/maintenance_supporter_panelfiles/panel-chunks/chunk-I7RCCBTV.js";import{H as u,a as n,b as _,c as o,f as p,h as c,l as g,m as a,s}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4CECVKQ7.js";var d=["round_robin","least_completed","random"],v=["low","normal","high"],m=365,i=class extends c{constructor(){super(...arguments);this._open=!1;this._mode="assign";this._count=0;this._users=[];this._present=[];this._assignKind="one";this._person="";this._pool=new Set;this._rotation=d[0];this._addText="";this._remove=new Set;this._setWarning=!1;this._warning=7;this._setPriority=!1;this._priority="normal";this._setMute=!1;this._mute=!1;this._resolve=null}get _lang(){return u(this.hass)}open(t,e,r){return this._resolve?.(null),this._mode=t,this._count=e.length,this._users=[...r].sort((l,h)=>l.name.localeCompare(h.name)),this._present=[...new Set(e.flatMap(l=>l.labels||[]))].sort((l,h)=>l.localeCompare(h)),this._assignKind="one",this._person="",this._pool=new Set,this._rotation=d[0],this._addText="",this._remove=new Set,this._setWarning=!1,this._warning=7,this._setPriority=!1,this._priority="normal",this._setMute=!1,this._mute=!1,this._open=!0,new Promise(l=>{this._resolve=l})}_finish(t){this._open=!1;let e=this._resolve;this._resolve=null,e?.(t)}changes(){if(this._mode==="assign"){if(this._assignKind==="one")return{responsible_user_id:this._person||null,assignee_pool:null,rotation_strategy:null};let e=this._users.map(r=>r.id).filter(r=>this._pool.has(r));return e.length<2?null:{assignee_pool:e,rotation_strategy:this._rotation,responsible_user_id:null}}if(this._mode==="labels"){let e=[...new Set(this._addText.split(",").map(l=>l.trim()).filter(Boolean))],r=[...this._remove];return e.length||r.length?{labels_add:e,labels_remove:r}:null}let t={};return this._setWarning&&(t.warning_days=this._warning),this._setPriority&&(t.priority=this._priority),this._setMute&&(t.notify_enabled=!this._mute),Object.keys(t).length?t:null}_toggle(t,e){let r=new Set(t);return r.has(e)?r.delete(e):r.add(e),r}_renderAssign(t){return o`
<div class="hint">${s("bulk_assign_hint",t)}</div>
<label class="choice">
<input type="radio" name="kind" .checked=${this._assignKind==="one"} @change=${()=>this._assignKind="one"} />
@@ -1,5 +1,5 @@
/*! maintenance_supporter frontend 2.100.0 */
import{a as q}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-C6NCKN5O.js";import{H as k,a,b as f,c as n,f as u,h as x,l as h,m as c,s as e}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZW2EWBUR.js";function d(r){return r.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function _(r){return!r.startsWith("data:image/svg+xml,")&&!r.startsWith("data:image/png;base64,")?"":d(r)}function y(r){return r.replace(/[/\\:*?"<>|#%]+/g,"").replace(/\s+/g,"-").toLowerCase().substring(0,100)}var l=class extends x{constructor(){super(...arguments);this.lang="";this._open=!1;this._loading=!1;this._error="";this._viewResult=null;this._completeResult=null;this._quickResult=null;this._urlMode="companion";this._entryId="";this._taskId=null;this._objectName="";this._taskName="";this._generateSeq=0}get _dialogLang(){return this.lang||k(this.hass)}openForObject(t,i){this._entryId=t,this._taskId=null,this._objectName=i,this._taskName="",this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._quickResult=null,this._open=!0,this._generate()}openForTask(t,i,s,o){this._entryId=t,this._taskId=i,this._objectName=s,this._taskName=o,this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._quickResult=null,this._open=!0,this._generate()}async _generate(){let t=++this._generateSeq;this._loading=!0,this._error="",this._viewResult=null,this._completeResult=null,this._quickResult=null;try{let i={type:"maintenance_supporter/qr/generate",entry_id:this._entryId,url_mode:this._urlMode};this._taskId&&(i.task_id=this._taskId);let s=[this.hass.connection.sendMessagePromise({...i,action:"view"})];this._taskId&&(s.push(this.hass.connection.sendMessagePromise({...i,action:"complete"})),await this._hasQuickCompleteDefaults()&&s.push(this.hass.connection.sendMessagePromise({...i,action:"quick_complete"})));let o=await Promise.all(s);if(t!==this._generateSeq)return;this._viewResult=o[0],o.length>1&&(this._completeResult=o[1]),o.length>2&&(this._quickResult=o[2])}catch(i){if(t!==this._generateSeq)return;let s=i?.code,o=i?.message;this._error=s==="no_url"||typeof o=="string"&&o.includes("No Home Assistant URL")?e("qr_error_no_url",this._dialogLang):e("qr_error",this._dialogLang)}finally{t===this._generateSeq&&(this._loading=!1)}}async _hasQuickCompleteDefaults(){try{let i=((await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:this._entryId})).tasks||[]).find(s=>s.id===this._taskId);return!!i?.quick_complete_defaults&&Object.keys(i.quick_complete_defaults).length>0}catch{return!1}}_setUrlMode(t){this._urlMode!==t&&(this._urlMode=t,this._generate())}_print(){if(!this._viewResult)return;let t=this._viewResult,i=t.label.task_name?`${t.label.object_name} \u2014 ${t.label.task_name}`:t.label.object_name,s=[t.label.manufacturer,t.label.model].filter(Boolean).join(" "),o=window.open("","_blank","width=600,height=500");if(!o)return;let p=this._dialogLang,m=d(i),g=d(s),v=!!this._completeResult,b=!!this._quickResult,w=d(e("qr_action_view",p)),$=d(e("qr_action_complete",p)),R=d(e("qr_action_quick_complete",p));o.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
/*! maintenance_supporter frontend 2.102.0 */
import{a as q}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-U4NJYI3P.js";import{H as k,a,b as f,c as n,f as u,h as x,l as h,m as c,s as e}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4CECVKQ7.js";function d(r){return r.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function _(r){return!r.startsWith("data:image/svg+xml,")&&!r.startsWith("data:image/png;base64,")?"":d(r)}function y(r){return r.replace(/[/\\:*?"<>|#%]+/g,"").replace(/\s+/g,"-").toLowerCase().substring(0,100)}var l=class extends x{constructor(){super(...arguments);this.lang="";this._open=!1;this._loading=!1;this._error="";this._viewResult=null;this._completeResult=null;this._quickResult=null;this._urlMode="companion";this._entryId="";this._taskId=null;this._objectName="";this._taskName="";this._generateSeq=0}get _dialogLang(){return this.lang||k(this.hass)}openForObject(t,i){this._entryId=t,this._taskId=null,this._objectName=i,this._taskName="",this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._quickResult=null,this._open=!0,this._generate()}openForTask(t,i,s,o){this._entryId=t,this._taskId=i,this._objectName=s,this._taskName=o,this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._quickResult=null,this._open=!0,this._generate()}async _generate(){let t=++this._generateSeq;this._loading=!0,this._error="",this._viewResult=null,this._completeResult=null,this._quickResult=null;try{let i={type:"maintenance_supporter/qr/generate",entry_id:this._entryId,url_mode:this._urlMode};this._taskId&&(i.task_id=this._taskId);let s=[this.hass.connection.sendMessagePromise({...i,action:"view"})];this._taskId&&(s.push(this.hass.connection.sendMessagePromise({...i,action:"complete"})),await this._hasQuickCompleteDefaults()&&s.push(this.hass.connection.sendMessagePromise({...i,action:"quick_complete"})));let o=await Promise.all(s);if(t!==this._generateSeq)return;this._viewResult=o[0],o.length>1&&(this._completeResult=o[1]),o.length>2&&(this._quickResult=o[2])}catch(i){if(t!==this._generateSeq)return;let s=i?.code,o=i?.message;this._error=s==="no_url"||typeof o=="string"&&o.includes("No Home Assistant URL")?e("qr_error_no_url",this._dialogLang):e("qr_error",this._dialogLang)}finally{t===this._generateSeq&&(this._loading=!1)}}async _hasQuickCompleteDefaults(){try{let i=((await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object",entry_id:this._entryId})).tasks||[]).find(s=>s.id===this._taskId);return!!i?.quick_complete_defaults&&Object.keys(i.quick_complete_defaults).length>0}catch{return!1}}_setUrlMode(t){this._urlMode!==t&&(this._urlMode=t,this._generate())}_print(){if(!this._viewResult)return;let t=this._viewResult,i=t.label.task_name?`${t.label.object_name} \u2014 ${t.label.task_name}`:t.label.object_name,s=[t.label.manufacturer,t.label.model].filter(Boolean).join(" "),o=window.open("","_blank","width=600,height=500");if(!o)return;let p=this._dialogLang,m=d(i),g=d(s),v=!!this._completeResult,b=!!this._quickResult,w=d(e("qr_action_view",p)),$=d(e("qr_action_complete",p)),R=d(e("qr_action_quick_complete",p));o.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8">
<meta name="color-scheme" content="light">
<title>${m}</title>
<style>
@@ -1,2 +1,2 @@
/*! maintenance_supporter frontend 2.100.0 */
/*! maintenance_supporter frontend 2.102.0 */
function c(n){return n==="use"?"use":"purchase"}var s=n=>typeof n=="number"&&Number.isFinite(n)?n:null;function a(n){let t=s(n?.cost);if(t===null)return null;let o=s(n?.package_size);return o&&o>0?t/o:t}function p(n,t){let o=s(t)&&t>0?t:s(n.restock_quantity)||1,e=s(n.package_size);return Math.round(o*(e&&e>0?e:1)*100)/100}function l(n){let t=s(n.cost);if(n.cost_basis!=="use")return t;let o=s(n.parts_cost),e=n.purchase?null:t;return e===null&&o===null?null:(e??0)+(o??0)}function _(n){if(!n)return[];let t=[],o=n.photo_doc_id;typeof o=="string"&&t.push(o);let e=n.photo_doc_ids;Array.isArray(e)&&t.push(...e);let r=[];for(let u of t){if(typeof u!="string")continue;let i=u.trim();if(!(!i||r.includes(i))&&(r.push(i),r.length>=10))break}return r}export{_ as a,c as b,a as c,p as d,l as e};
@@ -1,2 +1,2 @@
/*! maintenance_supporter frontend 2.100.0 */
import{O as n,s as o}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZW2EWBUR.js";var l={"Completed from dashboard button":"hist_note_button","Completed from the To-do list":"hist_note_todo","Completed by voice":"hist_note_voice","Completed from the notification":"hist_note_notification","Completed via NFC tag":"hist_note_nfc","Completed from the shopping list":"hist_note_shopping_list","Completed from a mirrored to-do list":"hist_note_todo_mirror","Skipped from notification":"hist_note_skipped_notification","Skipped from dashboard button":"hist_note_skipped_button","Sensor trigger activated":"hist_note_sensor_triggered","Initial value set during task creation":"hist_note_initial"},m=[[/^Auto-completed: sensor recovered \((.+)\)$/,"hist_note_auto_recovered",t=>({value:t[1]})],[/^Reset to (\d{4}-\d{2}-\d{2})$/,"hist_note_reset",(t,e)=>({date:n(t[1],e)})],[/^Trigger entity replaced: (.+) → (.+)$/,"hist_note_trigger_replaced",t=>({old:t[1],new:t[2]})],[/^Sensor trigger removed \(entity was: (.+)\)\. Schedule converted to (\w+)\.$/,"hist_note_trigger_removed",(t,e)=>({entity:t[1],schedule:o(t[2],e)})],[/^Entity (.+) removed from compound trigger; (\d+) conditions remain\.$/,"hist_note_compound_removed",t=>({entity:t[1],count:t[2]})]];function h(t,e){if(!t)return"";let i=l[t.trim()];if(i)return o(i,e);for(let[s,d,_]of m){let r=t.trim().match(s);if(r){let c=_(r,e);return o(d,e).replace(/\{(\w+)\}/g,(a,g)=>c[g]??a)}}return t}export{h as a};
/*! maintenance_supporter frontend 2.102.0 */
import{O as n,s as o}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4CECVKQ7.js";var l={"Completed from dashboard button":"hist_note_button","Completed from the To-do list":"hist_note_todo","Completed by voice":"hist_note_voice","Completed from the notification":"hist_note_notification","Completed via NFC tag":"hist_note_nfc","Completed from the shopping list":"hist_note_shopping_list","Completed from a mirrored to-do list":"hist_note_todo_mirror","Skipped from notification":"hist_note_skipped_notification","Skipped from dashboard button":"hist_note_skipped_button","Sensor trigger activated":"hist_note_sensor_triggered","Initial value set during task creation":"hist_note_initial"},m=[[/^Auto-completed: sensor recovered \((.+)\)$/,"hist_note_auto_recovered",t=>({value:t[1]})],[/^Reset to (\d{4}-\d{2}-\d{2})$/,"hist_note_reset",(t,e)=>({date:n(t[1],e)})],[/^Trigger entity replaced: (.+) → (.+)$/,"hist_note_trigger_replaced",t=>({old:t[1],new:t[2]})],[/^Sensor trigger removed \(entity was: (.+)\)\. Schedule converted to (\w+)\.$/,"hist_note_trigger_removed",(t,e)=>({entity:t[1],schedule:o(t[2],e)})],[/^Entity (.+) removed from compound trigger; (\d+) conditions remain\.$/,"hist_note_compound_removed",t=>({entity:t[1],count:t[2]})]];function h(t,e){if(!t)return"";let i=l[t.trim()];if(i)return o(i,e);for(let[s,d,_]of m){let r=t.trim().match(s);if(r){let c=_(r,e);return o(d,e).replace(/\{(\w+)\}/g,(a,g)=>c[g]??a)}}return t}export{h as a};
@@ -1,5 +1,5 @@
/*! maintenance_supporter frontend 2.100.0 */
import{a as p}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-5USKKRUA.js";import{H as v,a as r,b as _,c as l,f as o,h,l as c,m as n,s as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-ZW2EWBUR.js";var s=class extends h{constructor(){super(...arguments);this.objects=[];this.placesEnabled=!1;this._open=!1;this._loading=!1;this._error="";this._name="";this._manufacturer="";this._model="";this._serialNumber="";this._areaId="";this._installationDate="";this._warrantyExpiry="";this._documentationUrl="";this._notes="";this._haDeviceId="";this._parentEntryId="";this._place="";this._remindOnSite=!1;this._entryId=null;this._replacing=!1;this._oldDeviceId="";this._replaceDevice="keep"}get _lang(){return v(this.hass)}openCreate(){this._entryId=null,this._name="",this._manufacturer="",this._model="",this._serialNumber="",this._areaId="",this._installationDate="",this._warrantyExpiry="",this._documentationUrl="",this._notes="",this._haDeviceId="",this._parentEntryId="",this._place="",this._remindOnSite=!1,this._replacing=!1,this._error="",this._open=!0}openReplace(e,a){this._entryId=e,this._replacing=!0,this._name=a.name||"",this._oldDeviceId=a.ha_device_id||"",this._haDeviceId="",this._replaceDevice="keep",this._error="",this._open=!0}openEdit(e,a){this._entryId=e,this._name=a.name||"",this._manufacturer=a.manufacturer||"",this._model=a.model||"",this._serialNumber=a.serial_number||"",this._areaId=a.area_id||"",this._installationDate=a.installation_date||"",this._warrantyExpiry=a.warranty_expiry||"",this._documentationUrl=a.documentation_url||"",this._notes=a.notes||"",this._haDeviceId=a.ha_device_id||"",this._parentEntryId=a.parent_entry_id||"",this._place=a.place||"",this._remindOnSite=!!a.remind_on_site,this._replacing=!1,this._error="",this._open=!0}async _replace(){if(this._loading||!this._entryId)return;let e={type:"maintenance_supporter/object/replace",entry_id:this._entryId,name:this._name.trim()||null};this._oldDeviceId?(this._replaceDevice==="other"&&(e.ha_device_id=this._haDeviceId),this._replaceDevice==="none"&&(e.ha_device_id=null)):this._haDeviceId&&(e.ha_device_id=this._haDeviceId);let a=await p(this,e,{busy:t=>{this._loading=t},fallbackKey:"save_error",onError:t=>{this._error=t}});a!==void 0&&(this._open=!1,this.dispatchEvent(new CustomEvent("object-replaced",{detail:{entry_id:a?.entry_id,device_swap:a?.device_swap}})))}_deviceName(e){let t=this.hass?.devices?.[e];return t?.name_by_user||t?.name||e}_deviceSelector(e){return l`<ha-form
/*! maintenance_supporter frontend 2.102.0 */
import{a as p}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-RIYW54TU.js";import{H as v,a as r,b as _,c as l,f as o,h,l as c,m as n,s as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-4CECVKQ7.js";var s=class extends h{constructor(){super(...arguments);this.objects=[];this.placesEnabled=!1;this._open=!1;this._loading=!1;this._error="";this._name="";this._manufacturer="";this._model="";this._serialNumber="";this._areaId="";this._installationDate="";this._warrantyExpiry="";this._documentationUrl="";this._notes="";this._haDeviceId="";this._parentEntryId="";this._place="";this._remindOnSite=!1;this._entryId=null;this._replacing=!1;this._oldDeviceId="";this._replaceDevice="keep"}get _lang(){return v(this.hass)}openCreate(){this._entryId=null,this._name="",this._manufacturer="",this._model="",this._serialNumber="",this._areaId="",this._installationDate="",this._warrantyExpiry="",this._documentationUrl="",this._notes="",this._haDeviceId="",this._parentEntryId="",this._place="",this._remindOnSite=!1,this._replacing=!1,this._error="",this._open=!0}openReplace(e,a){this._entryId=e,this._replacing=!0,this._name=a.name||"",this._oldDeviceId=a.ha_device_id||"",this._haDeviceId="",this._replaceDevice="keep",this._error="",this._open=!0}openEdit(e,a){this._entryId=e,this._name=a.name||"",this._manufacturer=a.manufacturer||"",this._model=a.model||"",this._serialNumber=a.serial_number||"",this._areaId=a.area_id||"",this._installationDate=a.installation_date||"",this._warrantyExpiry=a.warranty_expiry||"",this._documentationUrl=a.documentation_url||"",this._notes=a.notes||"",this._haDeviceId=a.ha_device_id||"",this._parentEntryId=a.parent_entry_id||"",this._place=a.place||"",this._remindOnSite=!!a.remind_on_site,this._replacing=!1,this._error="",this._open=!0}async _replace(){if(this._loading||!this._entryId)return;let e={type:"maintenance_supporter/object/replace",entry_id:this._entryId,name:this._name.trim()||null};this._oldDeviceId?(this._replaceDevice==="other"&&(e.ha_device_id=this._haDeviceId),this._replaceDevice==="none"&&(e.ha_device_id=null)):this._haDeviceId&&(e.ha_device_id=this._haDeviceId);let a=await p(this,e,{busy:t=>{this._loading=t},fallbackKey:"save_error",onError:t=>{this._error=t}});a!==void 0&&(this._open=!1,this.dispatchEvent(new CustomEvent("object-replaced",{detail:{entry_id:a?.entry_id,device_swap:a?.device_swap}})))}_deviceName(e){let t=this.hass?.devices?.[e];return t?.name_by_user||t?.name||e}_deviceSelector(e){return l`<ha-form
.hass=${this.hass}
.data=${{device:this._haDeviceId||void 0}}
.schema=${[{name:"device",selector:{device:{}}}]}
@@ -1,2 +0,0 @@
/*! maintenance_supporter frontend 2.100.0 */
import{b as i}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-Q7CGGH4O.js";import{a as l}from"/maintenance_supporter_panelfiles/panel-chunks/chunk-M2CMCNR4.js";var a={operatorWriteEnabled:!1,operatorIds:[]};function m(e,t=a){return e?e.is_admin?!0:t.operatorWriteEnabled&&t.operatorIds.includes(e.id):!1}var _=["buttons_compact","buttons","icons"];function p(e){let[t,n]=l.default_warning_days,s=e?.general?.default_warning_days,r=e?.general?.row_action_style;return{features:{...o.features,...e?.features??{}},defaultWarningDays:typeof s=="number"&&Number.isFinite(s)&&s>=t&&s<=n?s:o.defaultWarningDays,rowActionStyle:_.includes(r??"")?r:o.rowActionStyle,access:{operatorWriteEnabled:e?.operator_write_enabled===!0,operatorIds:Array.isArray(e?.admin_panel_user_ids)?e.admin_panel_user_ids.filter(d=>typeof d=="string"):[]},budget:e?.budget??null,rowActionNoticePending:e?.general?.row_action_notice_pending===!0,refsInLists:e?.general?.ref_numbers_in_lists===!0,objectsTableColumns:Array.isArray(e?.objects_table_columns)?e.objects_table_columns:void 0,partsCostMode:i(e?.budget?.parts_cost_mode)}}var o={features:{adaptive:!1,seasonal:!1,environmental:!1,budget:!1,groups:!1,checklists:!1,schedule_time:!1,completion_actions:!1,places:!1},defaultWarningDays:7,rowActionStyle:"buttons_compact",access:a,budget:null,rowActionNoticePending:!1,refsInLists:!1,objectsTableColumns:void 0,partsCostMode:"purchase"};function c(){let e=window;return e.__msSettingsCache??={promise:null}}function S(e){let t=c();if(t.promise)return t.promise;let n=e.connection.sendMessagePromise({type:"maintenance_supporter/settings"}).then(s=>p(s)).catch(()=>(t.promise===n&&(t.promise=null),o));return t.promise=n,n}function y(){c().promise=null}var u=class{constructor(){this._handle=null}schedule(t,n=4e3){this.clear(),this._handle=setTimeout(()=>{this._handle=null,t()},n)}clear(){this._handle!==null&&(clearTimeout(this._handle),this._handle=null)}get pending(){return this._handle!==null}};export{a,m as b,_ as c,p as d,S as e,y as f,u as g};

Some files were not shown because too many files have changed in this diff Show More