vCard-Import fürs Autohaus, Selbst-Update-Logging und UI-Feinschliff
- Service -> Autohaus -> Zahnrad hat jetzt einen "vCard importieren"-Knopf (öffnet den Dateiauswahldialog für .vcf). Liest FN/ORG, TEL, EMAIL, ADR und befüllt das Formular - in beiden Frontends, jeweils passend zum bestehenden Speicherverhalten (Panel speichert sofort, companion-app erst über den vorhandenen "Speichern"-Knopf). - update_pruefen/update_installieren loggten bisher nur bei einem Fehler - ein erfolgreicher Aufruf war im Log nicht von "noch nicht ausgeführt" zu unterscheiden. Jetzt auch eine Erfolgsmeldung. - Panel: der feste Hinweis auf den fehlenden Gitea-Token stand dauerhaft in der Integration-Update-Kachel, auch wenn ein Token längst hinterlegt war. Erscheint jetzt nur noch als Teil einer echten Fehlermeldung. Details in AGENTS.md, Abschnitte J (Nachtrag) und K. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -2296,6 +2296,47 @@ store, never `configuration.yaml`. A read-only-scoped Gitea token is enough; the
|
|||||||
now say install.ps1 is the only way *to install*; the self-update is now the preferred way *to
|
now say install.ps1 is the only way *to install*; the self-update is now the preferred way *to
|
||||||
update* an already-running instance.
|
update* an already-running instance.
|
||||||
|
|
||||||
|
**Two fixes found during the owner's own first live test (2026-08-24, same day).** First attempt
|
||||||
|
against the test container ("nothing changed") turned out to be a real user error caught by log
|
||||||
|
inspection, not a bug: the check was clicked before the token had actually been saved in the
|
||||||
|
Options Flow — `dienste.py`'s log showed exactly why (`Kein Gitea-Token hinterlegt`), confirming the
|
||||||
|
error path works. But this surfaced a genuine observability gap: `update_pruefen`/
|
||||||
|
`update_installieren` only ever logged on *failure* — a successful check was silent, so "nothing in
|
||||||
|
the log" was ambiguous between "not clicked yet" and "worked fine." Fixed by adding an `_LOGGER.info`
|
||||||
|
line on the success path of both services. Separately, the owner asked for the panel's idle-state
|
||||||
|
text ("Ein Gitea-Zugriffstoken muss zuvor... eingetragen sein") to only appear as part of an actual
|
||||||
|
error, not as a permanent reminder cluttering the tile before the first check — removed from the
|
||||||
|
idle branch; the same information already surfaces via `integration_update.fehler` when it's
|
||||||
|
actually true. companion-app never had this line in the first place, so no parity fix needed there.
|
||||||
|
|
||||||
|
### K) vCard import for the workshop contact (built 2026-08-24)
|
||||||
|
|
||||||
|
Service → Autohaus (gear icon) now has a "vCard importieren" button next to the existing form
|
||||||
|
fields, opening a file picker for a `.vcf` contact exported from a phone/desktop address book. Reads
|
||||||
|
`FN`/`ORG` (name, `FN` preferred), `TEL`, `EMAIL`, and `ADR` (the seven-part vCard address —
|
||||||
|
PO-Box/extended/street/locality/region/postal-code/country — joined into one readable line, since
|
||||||
|
the form's address field is a single string). Deliberately not a full vCard parser: only the four
|
||||||
|
fields the Autohaus form already has. Unfolds RFC 6350 §3.2 folded lines (CRLF + one WSP removed
|
||||||
|
entirely, not replaced with a space) and un-escapes `\,`/`\;`/`\n`/`\\` before use. Multiple
|
||||||
|
`TEL`/`EMAIL`/`ADR` lines: first one wins.
|
||||||
|
|
||||||
|
- Panel: `vcardParsen()` in `audi-dashboard-app.js`, wired into `vWerkstatt()`'s new hidden
|
||||||
|
`data-vcard-import` file input via the existing central `change` listener — same
|
||||||
|
parse-then-`hinweis()`-on-error pattern as the existing JSON profile/backup import handlers.
|
||||||
|
Updates `CAR.service.autohaus` fields directly and calls the existing `profilSpeichern()` (saves
|
||||||
|
immediately, matching this field's existing per-keystroke-save behavior).
|
||||||
|
Same isolated-function shape as `entpacken_pruefen_tauschen()` in section J's self-updater: parse
|
||||||
|
logic takes no dependency on `HASS`/global state, so it's directly testable.
|
||||||
|
- companion-app: new `src/screens/vcard.ts` (`vcardParsen()`, 7 unit tests in `vcard.test.ts`
|
||||||
|
covering folding, escaping, ORG fallback, first-wins on repeats, and the two "nothing readable"
|
||||||
|
error cases), wired into `Werkstatt()` in `Service.tsx`. Unlike the panel, this only populates the
|
||||||
|
local `entwurf` draft state — the existing explicit "Speichern" button still does the actual save,
|
||||||
|
matching this screen's existing edit-then-confirm pattern rather than the panel's live-save one.
|
||||||
|
Empty fields from the vCard never overwrite non-empty draft values.
|
||||||
|
|
||||||
|
Verified: `tsc --noEmit` clean, all 134 companion-app tests pass (127 + 7 new), panel JS re-checked
|
||||||
|
with `node --check`. Manifest bumped to `2026.8.24.5`, `npm run ota` rerun (both frontends changed).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Working conventions (observed — keep them)
|
## Working conventions (observed — keep them)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* eingestellten Intervallen abweichen.
|
* eingestellten Intervallen abweichen.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useRef, useState } from "react"
|
||||||
|
|
||||||
import { ActionButton, Feld, Tile } from "@audi-dash/ui"
|
import { ActionButton, Feld, Tile } from "@audi-dash/ui"
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ import type { SeitenName } from "../navigation"
|
|||||||
import { SymbolInspektion, SymbolOelwechsel } from "../symbole"
|
import { SymbolInspektion, SymbolOelwechsel } from "../symbole"
|
||||||
import { Leerzustand, NaviKachel, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
import { Leerzustand, NaviKachel, Wertzeile, Werteliste, bestaetigen } from "./bausteine"
|
||||||
import { kalenderDateiLaden } from "./kalender"
|
import { kalenderDateiLaden } from "./kalender"
|
||||||
|
import { vcardParsen } from "./vcard"
|
||||||
|
|
||||||
export interface ServicebuchEintrag {
|
export interface ServicebuchEintrag {
|
||||||
datum?: string
|
datum?: string
|
||||||
@@ -206,6 +207,8 @@ export function Werkstatt() {
|
|||||||
const [entwurf, setzeEntwurf] = useState<Autohaus>({ ...autohausRoh })
|
const [entwurf, setzeEntwurf] = useState<Autohaus>({ ...autohausRoh })
|
||||||
const [laeuft, setzeLaeuft] = useState(false)
|
const [laeuft, setzeLaeuft] = useState(false)
|
||||||
const [gespeichert, setzeGespeichert] = useState(false)
|
const [gespeichert, setzeGespeichert] = useState(false)
|
||||||
|
const [vcardFehler, setzeVcardFehler] = useState<string | null>(null)
|
||||||
|
const vcardDatei = useRef<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
if (!fahrzeug) return null
|
if (!fahrzeug) return null
|
||||||
|
|
||||||
@@ -221,6 +224,33 @@ export function Werkstatt() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Befüllt nur den Entwurf, speichert nicht selbst - der Nutzer prüft die
|
||||||
|
// übernommenen Felder wie bei jeder anderen Änderung erst im Formular,
|
||||||
|
// bevor "Speichern" etwas an Home Assistant schickt. Leere Felder aus der
|
||||||
|
// vCard überschreiben vorhandene Werte im Entwurf nicht.
|
||||||
|
const vcardImportieren = (datei: File) => {
|
||||||
|
setzeVcardFehler(null)
|
||||||
|
setzeGespeichert(false)
|
||||||
|
const leser = new FileReader()
|
||||||
|
leser.onload = () => {
|
||||||
|
try {
|
||||||
|
const felder = vcardParsen(String(leser.result))
|
||||||
|
setzeEntwurf((alt) => ({
|
||||||
|
...alt,
|
||||||
|
...(felder.name ? { name: felder.name } : {}),
|
||||||
|
...(felder.adresse ? { adresse: felder.adresse } : {}),
|
||||||
|
...(felder.tel ? { tel: felder.tel } : {}),
|
||||||
|
...(felder.mail ? { mail: felder.mail } : {}),
|
||||||
|
}))
|
||||||
|
} catch (fehler) {
|
||||||
|
setzeVcardFehler(
|
||||||
|
fehler instanceof Error ? fehler.message : "Die vCard ließ sich nicht lesen.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
leser.readAsText(datei)
|
||||||
|
}
|
||||||
|
|
||||||
const feld = (schluessel: keyof Autohaus, beschriftung: string, letztes = false) => (
|
const feld = (schluessel: keyof Autohaus, beschriftung: string, letztes = false) => (
|
||||||
<Feld label={beschriftung} last={letztes}>
|
<Feld label={beschriftung} last={letztes}>
|
||||||
<input
|
<input
|
||||||
@@ -238,12 +268,25 @@ export function Werkstatt() {
|
|||||||
{feld("adresse", "Adresse")}
|
{feld("adresse", "Adresse")}
|
||||||
{feld("tel", "Telefon")}
|
{feld("tel", "Telefon")}
|
||||||
{feld("mail", "E-Mail", true)}
|
{feld("mail", "E-Mail", true)}
|
||||||
|
{vcardFehler && <p className="dm-fehler">{vcardFehler}</p>}
|
||||||
{gespeichert && <p className="dm-erfolg">Gespeichert.</p>}
|
{gespeichert && <p className="dm-erfolg">Gespeichert.</p>}
|
||||||
<div style={{ marginTop: 14 }}>
|
<div className="dm-knopfreihe" style={{ marginTop: 14 }}>
|
||||||
<ActionButton onClick={() => void speichern()} disabled={laeuft}>
|
<ActionButton onClick={() => void speichern()} disabled={laeuft}>
|
||||||
{laeuft ? "Speichere …" : "Speichern"}
|
{laeuft ? "Speichere …" : "Speichern"}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
<ActionButton onClick={() => vcardDatei.current?.click()}>vCard importieren</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
<input
|
||||||
|
ref={vcardDatei}
|
||||||
|
type="file"
|
||||||
|
accept=".vcf,text/vcard"
|
||||||
|
hidden
|
||||||
|
onChange={(e) => {
|
||||||
|
const datei = e.target.files?.[0]
|
||||||
|
if (datei) vcardImportieren(datei)
|
||||||
|
e.target.value = ""
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Tile>
|
</Tile>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { describe, expect, it } from "vitest"
|
||||||
|
|
||||||
|
import { vcardParsen } from "./vcard"
|
||||||
|
|
||||||
|
describe("vcardParsen", () => {
|
||||||
|
it("liest FN, TEL, EMAIL und eine zusammengesetzte ADR", () => {
|
||||||
|
const vcard = [
|
||||||
|
"BEGIN:VCARD",
|
||||||
|
"VERSION:3.0",
|
||||||
|
"FN:Autohaus Mustermann",
|
||||||
|
"TEL;TYPE=WORK,VOICE:+49 123 4567890",
|
||||||
|
"EMAIL;TYPE=INTERNET:[email protected]",
|
||||||
|
"ADR;TYPE=WORK:;;Musterstraße 1;Musterstadt;;12345;Deutschland",
|
||||||
|
"END:VCARD",
|
||||||
|
].join("\r\n")
|
||||||
|
|
||||||
|
expect(vcardParsen(vcard)).toEqual({
|
||||||
|
name: "Autohaus Mustermann",
|
||||||
|
adresse: "Musterstraße 1, 12345 Musterstadt, Deutschland",
|
||||||
|
tel: "+49 123 4567890",
|
||||||
|
mail: "[email protected]",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fällt ohne FN auf ORG zurück", () => {
|
||||||
|
const vcard = ["BEGIN:VCARD", "VERSION:3.0", "ORG:Autohaus GmbH", "END:VCARD"].join("\n")
|
||||||
|
expect(vcardParsen(vcard).name).toBe("Autohaus GmbH")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("entfaltet gefaltete Zeilen vor dem Parsen", () => {
|
||||||
|
// CRLF + genau ein WSP-Zeichen werden beim Entfalten vollständig entfernt
|
||||||
|
// (RFC 6350 §3.2) - hier mitten im Wort "Mustermann" gefaltet, damit ein
|
||||||
|
// falsches Verhalten (WSP durch ein Leerzeichen statt durch nichts
|
||||||
|
// ersetzt) den Test sichtbar brechen würde.
|
||||||
|
const vcard = ["BEGIN:VCARD", "VERSION:3.0", "FN:Autohaus Muster\n mann", "END:VCARD"].join(
|
||||||
|
"\n",
|
||||||
|
)
|
||||||
|
expect(vcardParsen(vcard).name).toBe("Autohaus Mustermann")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("entfernt vCard-Escapezeichen aus Feldwerten", () => {
|
||||||
|
const vcard = ["BEGIN:VCARD", "VERSION:3.0", "FN:Mueller\\, Schmidt und Partner"].join("\n")
|
||||||
|
expect(vcardParsen(vcard).name).toBe("Mueller, Schmidt und Partner")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("nimmt jeweils nur die erste Zeile bei mehrfachen TEL/EMAIL/ADR", () => {
|
||||||
|
const vcard = [
|
||||||
|
"BEGIN:VCARD",
|
||||||
|
"VERSION:3.0",
|
||||||
|
"TEL;TYPE=WORK:+49 111",
|
||||||
|
"TEL;TYPE=CELL:+49 222",
|
||||||
|
"EMAIL:[email protected]",
|
||||||
|
"EMAIL:[email protected]",
|
||||||
|
"END:VCARD",
|
||||||
|
].join("\n")
|
||||||
|
const f = vcardParsen(vcard)
|
||||||
|
expect(f.tel).toBe("+49 111")
|
||||||
|
expect(f.mail).toBe("[email protected]")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("wirft einen Fehler ohne jedes lesbare Feld", () => {
|
||||||
|
const vcard = ["BEGIN:VCARD", "VERSION:3.0", "END:VCARD"].join("\n")
|
||||||
|
expect(() => vcardParsen(vcard)).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("wirft einen Fehler bei völlig fremdem Dateiinhalt", () => {
|
||||||
|
expect(() => vcardParsen("Das ist keine vCard.")).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Liest Name, Adresse, Telefon und E-Mail aus einer vCard (.vcf) - deckt
|
||||||
|
* vCard 3.0/4.0 so weit ab, wie ein typischer Werkstatt-Kontakt aus dem
|
||||||
|
* Adressbuch (iOS/Android/Outlook) sie befüllt. Kein vollständiger Parser,
|
||||||
|
* nur die vier Felder, die das Autohaus-Formular (Werkstatt() in
|
||||||
|
* Service.tsx) kennt. Deckungsgleich mit vcardParsen() im Panel
|
||||||
|
* (audi-dashboard-app.js).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface VcardFelder {
|
||||||
|
name: string | null
|
||||||
|
adresse: string | null
|
||||||
|
tel: string | null
|
||||||
|
mail: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Entfernt vCard-Escapes (\n, \,, \;, \\) aus einem Feldwert. */
|
||||||
|
function entwerten(s: string): string {
|
||||||
|
return s.replace(/\\n/gi, "\n").replace(/\\,/g, ",").replace(/\\;/g, ";").replace(/\\\\/g, "\\")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function vcardParsen(text: string): VcardFelder {
|
||||||
|
// Gefaltete Zeilen (Fortsetzung mit führendem Leerzeichen/Tab, RFC 6350
|
||||||
|
// §3.2) vor dem zeilenweisen Parsen entfalten.
|
||||||
|
const entfaltet = text.replace(/\r\n/g, "\n").replace(/\n[ \t]/g, "")
|
||||||
|
const zeilen = entfaltet
|
||||||
|
.split("\n")
|
||||||
|
.map((z) => z.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
let name: string | null = null
|
||||||
|
let org: string | null = null
|
||||||
|
let tel: string | null = null
|
||||||
|
let mail: string | null = null
|
||||||
|
let adresse: string | null = null
|
||||||
|
|
||||||
|
for (const zeile of zeilen) {
|
||||||
|
const doppelpunkt = zeile.indexOf(":")
|
||||||
|
if (doppelpunkt < 0) continue
|
||||||
|
const eigenschaft = zeile.slice(0, doppelpunkt).split(";")[0]!.toUpperCase()
|
||||||
|
const wert = zeile.slice(doppelpunkt + 1)
|
||||||
|
|
||||||
|
if (eigenschaft === "FN" && !name) {
|
||||||
|
name = entwerten(wert)
|
||||||
|
} else if (eigenschaft === "ORG" && !org) {
|
||||||
|
org = entwerten(wert.split(";")[0]!)
|
||||||
|
} else if (eigenschaft === "TEL" && !tel) {
|
||||||
|
tel = entwerten(wert)
|
||||||
|
} else if (eigenschaft === "EMAIL" && !mail) {
|
||||||
|
mail = entwerten(wert)
|
||||||
|
} else if (eigenschaft === "ADR" && !adresse) {
|
||||||
|
// PO-Box(0), Zusatz(1), Straße(2), Ort(3), Region(4), PLZ(5), Land(6) -
|
||||||
|
// zu einer lesbaren Ein-Zeilen-Adresse zusammengefasst, weil das
|
||||||
|
// Formularfeld nur eine Zeile kennt.
|
||||||
|
const teile = wert.split(";").map(entwerten)
|
||||||
|
const strasse = [teile[1], teile[2]].filter(Boolean).join(" ")
|
||||||
|
const ort = [teile[5], teile[3]].filter(Boolean).join(" ")
|
||||||
|
adresse = [strasse, ort, teile[6]].filter(Boolean).join(", ") || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name && !org && !tel && !mail && !adresse) {
|
||||||
|
throw new Error("Keine lesbaren Kontaktfelder gefunden - ist das eine .vcf-Datei?")
|
||||||
|
}
|
||||||
|
return { name: name ?? org, adresse, tel, mail }
|
||||||
|
}
|
||||||
@@ -228,6 +228,10 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None:
|
|||||||
jetzt = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
jetzt = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||||
try:
|
try:
|
||||||
ergebnis = await aktualisierung.version_pruefen(hass, token, k.version)
|
ergebnis = await aktualisierung.version_pruefen(hass, token, k.version)
|
||||||
|
_LOGGER.info(
|
||||||
|
"Update-Prüfung: Gitea hat Version %s (installiert: %s, verfügbar: %s)",
|
||||||
|
ergebnis["version"], k.version, ergebnis["verfuegbar"],
|
||||||
|
)
|
||||||
k.letzte_update_pruefung = {
|
k.letzte_update_pruefung = {
|
||||||
"verfuegbar": ergebnis["verfuegbar"],
|
"verfuegbar": ergebnis["verfuegbar"],
|
||||||
"version": ergebnis["version"],
|
"version": ergebnis["version"],
|
||||||
@@ -255,6 +259,10 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None:
|
|||||||
jetzt = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
jetzt = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||||
try:
|
try:
|
||||||
ergebnis = await aktualisierung.update_installieren(hass, token)
|
ergebnis = await aktualisierung.update_installieren(hass, token)
|
||||||
|
_LOGGER.info(
|
||||||
|
"Update installiert: Version %s - Home Assistant muss neu gestartet werden.",
|
||||||
|
ergebnis["version"],
|
||||||
|
)
|
||||||
k.letzte_update_pruefung = {
|
k.letzte_update_pruefung = {
|
||||||
"verfuegbar": False,
|
"verfuegbar": False,
|
||||||
"version": ergebnis["version"],
|
"version": ergebnis["version"],
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":"2026.8.24.4","sha256":"cbce1baa785a3577fc65168b9f8b2094ecd844aadc2ac36f99d6be8ccb4cb8c1","bytes":231567,"gebaut":"2026-08-24T09:39:59Z"}
|
{"version":"2026.8.24.5","sha256":"378a1cbc7f5daa3d8c5d74734a4c09f30557b243a8ce4d720240bf911fbeff76","bytes":232096,"gebaut":"2026-08-24T10:29:33Z"}
|
||||||
Binary file not shown.
@@ -1969,6 +1969,44 @@ function termine() {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Liest Name, Adresse, Telefon und E-Mail aus einer vCard (.vcf) - deckt
|
||||||
|
vCard 3.0/4.0 so weit ab, wie ein typischer Werkstatt-Kontakt aus dem
|
||||||
|
Adressbuch (iOS/Android/Outlook) sie befüllt. Kein vollständiger Parser,
|
||||||
|
nur die vier Felder, die das Autohaus-Formular kennt.
|
||||||
|
|
||||||
|
Gefaltete Zeilen (Fortsetzung mit führendem Leerzeichen/Tab, RFC 6350
|
||||||
|
§3.2) werden vor dem zeilenweisen Parsen entfaltet; ADR setzt sich aus
|
||||||
|
PO-Box, Zusatz, Straße, Ort, Region, PLZ, Land zusammen (in dieser
|
||||||
|
Reihenfolge, mit Semikolon getrennt) - hier zu einer lesbaren
|
||||||
|
Ein-Zeilen-Adresse zusammengefasst, weil das Formularfeld nur eine
|
||||||
|
Zeile kennt. Mehrfache TEL/EMAIL/ADR-Zeilen: die erste gewinnt. */
|
||||||
|
function vcardParsen(text) {
|
||||||
|
const entfaltet = String(text).replace(/\r\n/g, "\n").replace(/\n[ \t]/g, "");
|
||||||
|
const zeilen = entfaltet.split("\n").map((z) => z.trim()).filter(Boolean);
|
||||||
|
const entwerten = (s) => s.replace(/\\n/gi, "\n").replace(/\\,/g, ",").replace(/\\;/g, ";").replace(/\\\\/g, "\\");
|
||||||
|
let name = null, org = null, tel = null, mail = null, adresse = null;
|
||||||
|
for (const zeile of zeilen) {
|
||||||
|
const doppelpunkt = zeile.indexOf(":");
|
||||||
|
if (doppelpunkt < 0) continue;
|
||||||
|
const eigenschaft = zeile.slice(0, doppelpunkt).split(";")[0].toUpperCase();
|
||||||
|
const wert = zeile.slice(doppelpunkt + 1);
|
||||||
|
if (eigenschaft === "FN" && !name) name = entwerten(wert);
|
||||||
|
else if (eigenschaft === "ORG" && !org) org = entwerten(wert.split(";")[0]);
|
||||||
|
else if (eigenschaft === "TEL" && !tel) tel = entwerten(wert);
|
||||||
|
else if (eigenschaft === "EMAIL" && !mail) mail = entwerten(wert);
|
||||||
|
else if (eigenschaft === "ADR" && !adresse) {
|
||||||
|
const teile = wert.split(";").map(entwerten);
|
||||||
|
const strasse = [teile[1], teile[2]].filter(Boolean).join(" ");
|
||||||
|
const ort = [teile[5], teile[3]].filter(Boolean).join(" ");
|
||||||
|
adresse = [strasse, ort, teile[6]].filter(Boolean).join(", ") || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!name && !org && !tel && !mail && !adresse) {
|
||||||
|
throw new Error("Keine lesbaren Kontaktfelder gefunden - ist das eine .vcf-Datei?");
|
||||||
|
}
|
||||||
|
return { name: name || org || null, adresse, tel, mail };
|
||||||
|
}
|
||||||
|
|
||||||
function vWerkstatt() {
|
function vWerkstatt() {
|
||||||
const a = CAR.service.autohaus;
|
const a = CAR.service.autohaus;
|
||||||
return `
|
return `
|
||||||
@@ -1978,6 +2016,8 @@ function vWerkstatt() {
|
|||||||
<div class="feld"><label>Telefon</label><input type="tel" value="${esc(a.tel)}" data-ah="tel"></div>
|
<div class="feld"><label>Telefon</label><input type="tel" value="${esc(a.tel)}" data-ah="tel"></div>
|
||||||
<div class="feld" style="border-bottom:0"><label>E-Mail</label>
|
<div class="feld" style="border-bottom:0"><label>E-Mail</label>
|
||||||
<input type="email" value="${esc(a.mail || "")}" data-ah="mail" placeholder="noch offen"></div>
|
<input type="email" value="${esc(a.mail || "")}" data-ah="mail" placeholder="noch offen"></div>
|
||||||
|
<label class="aktion" style="display:block;text-align:center;margin-top:16px;cursor:pointer">
|
||||||
|
vCard importieren<input type="file" accept=".vcf,text/vcard" data-vcard-import hidden></label>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2405,9 +2445,7 @@ function vEinst() {
|
|||||||
<button class="aktion" style="margin-top:14px" data-update="installieren">Update installieren</button>`;
|
<button class="aktion" style="margin-top:14px" data-update="installieren">Update installieren</button>`;
|
||||||
}
|
}
|
||||||
return `<span style="display:block;margin-top:8px;color:var(--fg2);font-size:13.5px;line-height:1.6">
|
return `<span style="display:block;margin-top:8px;color:var(--fg2);font-size:13.5px;line-height:1.6">
|
||||||
${u ? `Integration ist aktuell${u.geprueft_am ? ` (geprüft ${dedat(new Date(u.geprueft_am))})` : ""}.` : "Noch nicht geprüft."}
|
${u ? `Integration ist aktuell${u.geprueft_am ? ` (geprüft ${dedat(new Date(u.geprueft_am))})` : ""}.` : "Noch nicht geprüft."}</span>
|
||||||
Ein Gitea-Zugriffstoken muss zuvor unter Einstellungen -> Geräte & Dienste -> Audi
|
|
||||||
Dashboard -> Konfigurieren eingetragen sein.</span>
|
|
||||||
<button class="aktion" style="margin-top:14px" data-update="pruefen">Auf Update prüfen</button>`;
|
<button class="aktion" style="margin-top:14px" data-update="pruefen">Auf Update prüfen</button>`;
|
||||||
})()}
|
})()}
|
||||||
${INTEGRATION_UPDATE && INTEGRATION_UPDATE.fehler
|
${INTEGRATION_UPDATE && INTEGRATION_UPDATE.fehler
|
||||||
@@ -4612,6 +4650,23 @@ function ereignisseVerdrahten() {
|
|||||||
leser.readAsText(datei);
|
leser.readAsText(datei);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (e.target.dataset.vcardImport !== undefined) {
|
||||||
|
const datei = e.target.files[0]; if (!datei) return;
|
||||||
|
const leser = new FileReader();
|
||||||
|
leser.onload = () => {
|
||||||
|
try {
|
||||||
|
const felder = vcardParsen(leser.result);
|
||||||
|
if (felder.name) CAR.service.autohaus.name = felder.name;
|
||||||
|
if (felder.adresse) CAR.service.autohaus.adresse = felder.adresse;
|
||||||
|
if (felder.tel) CAR.service.autohaus.tel = felder.tel;
|
||||||
|
if (felder.mail) CAR.service.autohaus.mail = felder.mail;
|
||||||
|
profilSpeichern(); render();
|
||||||
|
} catch (err) { hinweis("vCard konnte nicht gelesen werden", err.message); }
|
||||||
|
};
|
||||||
|
leser.readAsText(datei);
|
||||||
|
e.target.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (e.target.dataset.bildupload !== undefined) {
|
if (e.target.dataset.bildupload !== undefined) {
|
||||||
const datei = e.target.files[0]; if (!datei) return;
|
const datei = e.target.files[0]; if (!datei) return;
|
||||||
const dateiname = e.target.dataset.bildupload;
|
const dateiname = e.target.dataset.bildupload;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "audi_dashboard",
|
"domain": "audi_dashboard",
|
||||||
"name": "Audi Dashboard",
|
"name": "Audi Dashboard",
|
||||||
"version": "2026.8.24.4",
|
"version": "2026.8.24.5",
|
||||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||||
"codeowners": ["@paul"],
|
"codeowners": ["@paul"],
|
||||||
|
|||||||
Reference in New Issue
Block a user