Compare commits

..
5 Commits
Author SHA1 Message Date
clemensandClaude Opus 5 e7955e354c Rufnummernsuche auf den Altbestand user_old ausweiten
Die Suche lief nur gegen persons und fand deshalb den Anrufer von Anfrage
13037 nicht: dessen Mobilnummer steht in user_old, in persons ist zu
derselben Person nur eine aeltere siebenstellige Nummer gepflegt. Beide
Zeilen sind ueber tmp_user_person_map korrekt verknuepft (userid 3204 ->
person_id 2266), die Rufnummer ist bei der Migration schlicht nicht
nachgezogen worden.

Das ist kein Einzelfall. Von 2552 gemappten Paaren weicht bei 122 die
Nummer ab, 87 Nummern stehen ausschliesslich in user_old, und 176 alte
Zeilen haben gar kein Mapping. persons ist damit keine Obermenge, und die
Suche allein darueber verfehlt 87 der 2739 bekannten Rufnummern.

Der Index wird jetzt aus beiden Tabellen aufgebaut. Findet sich zu einer
alten Zeile ein aktueller Datensatz, werden dessen Daten gezeigt - die
Nummer ist nur der Schluessel, angezeigt gehoert der gepflegte Stand. Der
Vorschlag weist aus, wenn die Nummer aus dem Altbestand stammt, und
unterscheidet den Fall ohne aktuellen Datensatz.

Ein Patient darf je Nummer nur einmal erscheinen. Die Personen-ID reicht
dafuer nicht: nicht gemappte Altzeilen haben eine eigene userid und standen
dadurch neben ihrem persons-Gegenstueck, sichtbar als "2 Patienten:
Andreas Busche, Andreas Busche". Zweiter Schluessel ist deshalb Name plus
Geburtstag. Der Vergleich traegt hier, weil ohnehin nur Eintraege zu ein und
derselben Rufnummer verglichen werden - ueber den Gesamtbestand waere er
unbrauchbar, weil 0000-00-00 als Geburtstag massenhaft vorkommt.

Nachgeprueft ueber alle 2739 bekannten Rufnummern: jede liefert einen
Treffer, keine einzige eine doppelte Identitaet, groesste Trefferzahl 16 an
einer Nummer. 87 Nummern sind neu auffindbar, darunter die von Anfrage
13037, die jetzt korrekt auf person_id 2266 zeigt. Listenlogik ueber alle
12793 Anfragen unveraendert fehlerfrei; der Vorschlag steht weiterhin nie
bei einer Anfrage mit bereits zugeordnetem Patienten. Kosten bleiben zwei
Abfragen je Seitenaufruf, unabhaengig von der Zeilenzahl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 21:09:53 +02:00
clemensandClaude Opus 5 53cff55ac2 Bei Telefonanfragen den Patienten zur Rufnummer vorschlagen
Die Telefonanlage fuellt anfragen.anrufernummer inzwischen - die erste
Anfrage damit ist #13037. Wenn die Anlage den Anrufer nicht selbst zuordnen
konnte, wird die Nummer jetzt gegen persons.tele gesucht und der Treffer
angezeigt.

NormalisiereRufnummer() vergleicht ziffernweise und fuehrt +49 und 0049 auf
die nationale Schreibweise mit fuehrender 0 zurueck. Nur so finden die
Anzeige der Anlage und die von Hand gepflegten Nummern zusammen: von 2972
gepflegten Nummern stehen 2941 als reine Ziffern da, 22 mit Bindestrich,
6 international.

Bewusst nur ein Vorschlag, keine Zuordnung. 521 der 2972 Nummern gehoeren zu
mehr als einer Person - Familien teilen sich einen Anschluss, zwei Nummern
haengen sogar an 16 bzw. 18 Personen. Bei mehreren Treffern werden deshalb
die Namen aufgezaehlt statt einer ausgewaehlt, und auch der eindeutige Fall
ist als "nicht bestaetigt - nur ueber die Rufnummer erkannt" ausgewiesen.
Eine falsche Zuordnung haette die Daten des falschen Patienten neben die
Anfrage gestellt.

Nummern unter 7 Ziffern werden nicht gesucht: in persons stehen ein paar
Platzhalter mit 1 bis 5 Ziffern, die sonst wahllos zusammenfallen.

Der Index wird beim ersten Treffer einmal je Seitenaufruf aufgebaut und
danach wiederverwendet - die Liste zeigt bis zu einige tausend Zeilen, eine
Abfrage je Zeile waere zu teuer. Gemessen ueber 12800 Zeilen: genau eine
Abfrage gegen persons. Ohne Telefonanfrage mit brauchbarer Nummer wird der
Index gar nicht erst gebaut.

Angezeigt in der Anfrageliste (Kontaktspalte), auf der Hinweisseite fuer
Anfragen ohne zugeordneten Patienten und unter Antwort einsehen.

Geprueft: Normalisierung gegen internationale, geklammerte und mit
Leerzeichen geschriebene Nummern; Vorschlag bleibt aus bei Mail- und
Portalanfragen, bei fehlender, zu kurzer und unterdrueckter Nummer sowie
bei bereits zugeordnetem Patienten. Listenlogik ueber alle 12793 Anfragen
mit erzwungenen Treffern durchgerechnet - der Vorschlag steht nie bei einer
Anfrage, die schon einen Patienten hat. Fuer #13037 selbst gibt es keinen
Treffer, die Nummer ist in persons nicht gepflegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 19:24:25 +02:00
clemensandClaude Opus 5 4dabad0b22 Anrufernummer verlinken, PraxisKI-Link auf /anfragen/
Der Hinweis aus dem letzten Commit zeigte auf die Startseite von PraxisKI.
Richtig ist https://ki-praxis.praxis.local/anfragen/.

Neue Spalte anfragen.anrufernummer VARCHAR(255) NULL wird jetzt bei
Telefonanfragen als anklickbarer tel:-Link ausgegeben. GetTelefonLink()
baut das Ziel aus Ziffern und einem fuehrenden Plus, zeigt die Nummer aber
unveraendert an - Leerzeichen, Klammern, Slashes und Durchwahl-Bindestriche
stoeren den Link damit nicht. Nummern ganz ohne Ziffer, etwa bei
unterdrueckter Rufnummer, bleiben Text ohne Link. Der Rohwert wird in der
Funktion maskiert.

GetAnruferZeile() liefert die beschriftete Zeile nur fuer source='telefon'
und nur, wenn eine Nummer da ist. In der Liste steht sie in der
Kontaktspalte, wo Telefonnummern hingehoeren; dort ersetzt sie bei Anfragen
ohne zugeordneten Patienten den Platzhalter "Kontaktdaten siehe Nachricht".
In Antwortformular, Hinweisseite, Antwort einsehen und Patientenhistorie
steht sie bei den uebrigen Anfrageinformationen. Die Abfragen der letzten
beiden lasen die Spalte nicht mit, sie ist dort ergaenzt.

Die Spalte ist derzeit in allen 309 Telefonanfragen NULL - die Anlage
schreibt sie noch nicht. Bis dahin aendert sich die Anzeige nicht, der
bisherige Rueckfall greift.

Geprueft: GetTelefonLink gegen internationale, geklammerte, mit Leerzeichen
und Durchwahl geschriebene Nummern sowie gegen Leerwert, NULL, reinen Text
und einen Injektionsversuch - das Script kommt maskiert heraus, nicht als
Markup. Listenlogik ueber alle 12789 Anfragen mit simulierter Nummer
durchgerechnet: der Link erscheint genau dann, wenn source='telefon' ist
und eine Nummer vorliegt, und Nicht-Telefonzeilen bekommen kein
zusaetzliches <br> in die Kontaktspalte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 17:41:36 +02:00
clemensandClaude Opus 5 e39c00105e Bei Telefonanfragen auf die Sprachnachricht in PraxisKI hinweisen
anfragen.nachricht enthaelt bei source='telefon' nur das automatisch
erzeugte Transkript der Sprachnachricht, das Erkennungsfehler enthalten
kann. Bisher war aus der Oberflaeche nicht ersichtlich, dass es die
Aufnahme selbst noch gibt und wo sie liegt.

Neue Funktion GetSprachnachrichtHinweis() gibt fuer source='telefon' einen
Hinweis mit Link auf https://ki-praxis.praxis.local/ aus, fuer jede andere
Herkunft einen leeren String. Der Link ist nur aus dem Praxisnetz
erreichbar, das steht im Hinweis dabei.

Eingesetzt an allen fuenf Stellen, an denen eine Telefonanfrage sichtbar
wird: Anfrageliste, Antwortformular, Hinweisseite fuer Anfragen ohne
zugeordneten Patienten, Antwort einsehen und Patientenhistorie. Die
Abfragen fuer die letzten beiden lasen source bisher nicht mit, die Spalte
ist dort ergaenzt.

Nachgeprueft gegen die Datenbank: alle vier Abfragen liefern source, und
der Hinweis erscheint ueber alle 12785 Anfragen hinweg genau dann, wenn
source='telefon' ist - je Zeile geprueft, keine Ausnahme. Beide Dateien
bleiben UTF-8 ohne BOM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 13:07:29 +02:00
clemensandClaude Opus 5 f19c490f44 Telefon-Anfragen ohne Personenbezug wieder anzeigen
Seit dem 02.09.2026 legt die Telefonanlage Anfragen mit source='telefon'
an. Von 309 solchen Zeilen haben 207 keine requester_person_id, weil der
Anrufer nicht zugeordnet werden konnte - darunter alle 33 offenen. Saemtliche
Anzeigepfade haengen per INNER JOIN an persons, wodurch genau diese Zeilen
kommentarlos aus dem Ergebnis fielen. Die Ansicht "Unbeantwortete Anfragen"
zeigte dadurch 2 statt 35 Eintraege.

Bei mail- und portal-Anfragen konnte das nicht auffallen: die Formulare
legen die Person vorher in persons an und brechen sonst ab (rezepte.php),
entsprechend hat dort keine einzige Zeile eine fehlende Person. personid
und userid stehen zwar auch bei mail auf 0, sind aber tote Legacy-Spalten
und tragen den Personenbezug nicht.

Die Anzeigepfade verwenden jetzt LEFT JOIN und zeigen ohne Treffer
"Kein Patient zugeordnet" statt einer leeren Zeile: Liste, Antworten,
Loesch-Dialog, Loeschen ohne Mail und Antwort einsehen. Die beiden Knoepfe,
die zwingend eine Mailadresse brauchen - Antworten und Loeschen mit Mail -
erscheinen nur noch mit zugeordneter Person; wer das Antwortformular direkt
aufruft, bekommt einen Hinweis samt Transkript statt eines Abbruchs.
"Telefonisch beantwortet" brauchte keine Aenderung, dieser Weg fragt nur
die anfrageid ab.

Der Mailversand in functions.inc.php und die patientenbezogene Historie
behalten ihren INNER JOIN - dort ist eine Person Voraussetzung.

Neu ist GetAnfrageHerkunft(). Die Liste leitete die Herkunft bisher aus
sicherenachricht ab, was Telefonanfragen mangels Wert als "Mailanfrage"
etikettierte. Nur dieser dritte Fall kommt aus source. Die Unterscheidung
intern/Mail bleibt bewusst an sicherenachricht haengen, weil insertAnfrage()
kein source setzt und 304 interne Anfragen deshalb als source='mail' in der
Tabelle stehen; ueber source vergeben haette das Label 9871 Bestandszeilen
umetikettiert, darunter die datenschutzrelevante Unterscheidung.

Nachgeprueft gegen die Datenbank: die Ansicht liefert statt 2 nun 35 Zeilen
mit allen 33 offenen Telefon-Anfragen. Der Renderblock ueber alle 12784
Zeilen simuliert, error_reporting=E_ALL als Exception, ohne Warnung; das
Herkunfts-Label bleibt fuer 12475 Bestandszeilen unveraendert und aendert
sich nur fuer die 309 Telefon-Zeilen. Alle fuenf von der Liste erreichbaren
Aktionen laden fuer eine Zeile ohne Person.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:56:45 +02:00
2 changed files with 360 additions and 25 deletions
+86 -18
View File
@@ -115,8 +115,8 @@ if(!check_worker()){
p.ort, p.plz, p.strasse,
aa.artname
FROM anfragen a
JOIN persons p ON a.requester_person_id = p.person_id
JOIN anfrageart aa ON a.anforderungart = aa.artid
LEFT JOIN persons p ON a.requester_person_id = p.person_id
LEFT JOIN anfrageart aa ON a.anforderungart = aa.artid
";
$params = [];
@@ -201,16 +201,19 @@ if(!check_worker()){
$dateiname = e((string)$row["dateiname"]);
$anfrageart = e((string)$row["artname"]);
// artname kann seit dem LEFT JOIN leer sein.
$anfrageart = e((string)($row["artname"] ?? ""));
if($anfrageart === ""){
$anfrageart = "unbekannte Art";
}
$ordnungsid = $row["ordnungsid"];
$ordnungsstring = GetOrdnungsid($ordnungsid);
$WeitereInfos= "";
$sicherenachricht = $row["sicherenachricht"];
if($sicherenachricht == "1"){
$WeitereInfos .= "Interne Anfrage<br>";
}else{
$WeitereInfos .= "Mailanfrage<br>";
}
// Telefonanfragen liessen sicherenachricht NULL und wurden
// dadurch faelschlich als Mailanfrage etikettiert.
$WeitereInfos .= GetAnfrageHerkunft($row["source"] ?? "mail", $sicherenachricht) . "<br>";
$WeitereInfos .= GetSprachnachrichtHinweis($row["source"] ?? "mail");
if($medikamenteins){
$WeitereInfos .= "Medikament1: $medikamenteins<br>";
}
@@ -244,17 +247,44 @@ if(!check_worker()){
$farbe = GetStatusFarbe($checked);
$checkausgabe = GetStatus($checked);
// Ohne Treffer im LEFT JOIN gibt es keine Personendaten -
// typisch fuer Telefonanfragen, deren Anrufer nicht zugeordnet
// werden konnte. Frueher fielen diese Zeilen durch den INNER
// JOIN komplett aus der Liste.
$hatPerson = ($row["vorname"] !== null || $row["nachname"] !== null);
// Die Anlage haelt die Rufnummer des Anrufers getrennt von den
// Stammdaten - sie kann von der hinterlegten abweichen.
$anruferzeile = GetAnruferZeile($row["source"] ?? "mail", $row["anrufernummer"] ?? null);
// Wer koennte angerufen haben? Nur ein Vorschlag anhand der
// Rufnummer, die Auswahl trifft die Mitarbeiterin.
$anrufervorschlag = GetPatientZuRufnummerAusgabe($pdo, $row["source"] ?? "mail", $row["anrufernummer"] ?? null, $row["requester_person_id"] ?? null);
if($hatPerson){
$userausgabe = $vorname . " " . $nachname . "<br>" . $ausgabegeburstag;
$adresse = $plz . " " . $ort . "<br>" . $strasse ;
$adresse = $adresse;
echo "<tr style='background-color:". $farbe. ";'><td>$userausgabe <br> $mail <br>Tel:<a href='tel:".$tel."'>$tel</a></td><td>$adresse</td><td>$ordnungsstring - $anfrageart <br>$WeitereInfos</td><td>$datumausgabe</td><td>$update_time<br>$checkausgabe<br>$ausgabeworker</td><td>
$kontakt = "$mail <br>Tel:<a href='tel:$tel'>$tel</a>";
if($anruferzeile !== ""){
$kontakt .= "<br>" . $anruferzeile;
}
}else{
$userausgabe = "<i>Kein Patient zugeordnet</i>";
$adresse = "-";
$kontakt = ($anruferzeile !== "") ? $anruferzeile : "<i>Kontaktdaten siehe Nachricht</i>";
$kontakt .= $anrufervorschlag;
}
echo "<tr style='background-color:". $farbe. ";'><td>$userausgabe <br> $kontakt</td><td>$adresse</td><td>$ordnungsstring - $anfrageart <br>$WeitereInfos</td><td>$datumausgabe</td><td>$update_time<br>$checkausgabe<br>$ausgabeworker</td><td>";
// "Antworten" verschickt eine Mailvorlage. Ohne zugeordnete Person
// gibt es keine Adresse, der Knopf wuerde ins Leere laufen.
if($hatPerson){
echo "
<div style='float: left;margin:15px; height: 20px;'>
<form action='". self_action() ."' method=POST>
<input type=hidden name=aktion value=2>
<input type=hidden name=anfrageid value=$anfrageid>
<input type=submit class='btn btn-primary' value='Antworten'>
</form>
</div>
</div>";
}
echo "
<div style='float: left;margin:15px; height: 20px;'>
<form action='". self_action() ."' method=POST>
<input type=hidden name=aktion value=20>
@@ -308,8 +338,8 @@ if(!check_worker()){
p.ort, p.plz, p.strasse,
aa.artname
FROM anfragen a
INNER JOIN persons p ON a.requester_person_id = p.person_id
INNER JOIN anfrageart aa ON a.anforderungart = aa.artid
LEFT JOIN persons p ON a.requester_person_id = p.person_id
LEFT JOIN anfrageart aa ON a.anforderungart = aa.artid
WHERE a.anfrageid = ?
LIMIT 1
";
@@ -323,6 +353,27 @@ if(!check_worker()){
exit;
}
// Ohne zugeordnete Person gibt es keine Mailadresse - die
// Antwortvorlagen unten wuerden ins Leere laufen. Betrifft vor
// allem Telefonanfragen ohne erkannten Anrufer.
if ($row["vorname"] === null && $row["nachname"] === null) {
echo "<div class='alert alert-warning' role='alert'>";
echo "<b>Dieser Anfrage ist kein Patient zugeordnet.</b><br>";
echo "Herkunft: " . e(GetAnfrageHerkunft($row["source"] ?? "mail", $row["sicherenachricht"] ?? null)) . "<br>";
echo "Eine Antwort per Mail ist deshalb nicht moeglich. ";
echo "Bitte telefonisch beantworten und die Anfrage anschliessend ";
echo "ueber <b>Telefonisch beantwortet</b> abschliessen.";
echo "</div>";
echo GetAnruferZeile($row["source"] ?? "mail", $row["anrufernummer"] ?? null);
echo GetPatientZuRufnummerAusgabe($pdo, $row["source"] ?? "mail", $row["anrufernummer"] ?? null, $row["requester_person_id"] ?? null);
echo GetSprachnachrichtHinweis($row["source"] ?? "mail");
echo "<b>Nachricht:</b><br>" . nl2br(e((string)$row["nachricht"])) . "<br><br>";
ZuruckzumAnfrageOverView();
echo "</form>";
include(__DIR__ . "/templates/footer.inc.php");
exit;
}
// Ticket und Antwort ansehen.
$abholort = $row["abholort"] ?? null;
$sicherenachricht = $row["sicherenachricht"] ?? null;
@@ -363,6 +414,8 @@ if(!check_worker()){
$antworttext = $row["antworttext"] ?? null;
$WeitereInfos = "";
$WeitereInfos .= GetAnruferZeile($row["source"] ?? "mail", $row["anrufernummer"] ?? null);
$WeitereInfos .= GetSprachnachrichtHinweis($row["source"] ?? "mail");
if ($medikamenteins) $WeitereInfos .= "Medikament1: $medikamenteins<br>";
if ($medikamentzwei) $WeitereInfos .= "Medikament2: $medikamentzwei<br>";
if ($medikamentdrei) $WeitereInfos .= "Medikament3: $medikamentdrei<br>";
@@ -858,7 +911,7 @@ if(!check_worker()){
$stmt = $pdo->prepare("
SELECT *
FROM anfragen a
INNER JOIN persons p ON p.person_id = a.requester_person_id
LEFT JOIN persons p ON p.person_id = a.requester_person_id
WHERE a.anfrageid = :anfrageid
LIMIT 1
");
@@ -875,16 +928,22 @@ if(!check_worker()){
$vorname = $rowtime["vorname"];
$nachname = $rowtime["nachname"];
$userausgabe = $vorname . " " . $nachname;
// Telefonanfragen ohne erkannten Anrufer haben keine Person.
$hatPerson = ($vorname !== null || $nachname !== null);
$userausgabe = $hatPerson ? ($vorname . " " . $nachname) : "Kein Patient zugeordnet";
echo "Name:<br>$userausgabe<br>Anforderung: $impfstofftext<br>";
echo "Wollen Sie wirklich diesen Eintrag löschen?<br>Dieses ist nicht rückgängig zu machen!<br>Dann bestätigen Sie die Abmeldung:<br>";
// Loeschen mit Mail setzt eine Mailadresse voraus.
if ($hatPerson) {
echo "<form action='". self_action() . "' method=POST>";
echo '<input type="hidden" name="aktion" id="aktion" value="9" />';
echo '<input type="hidden" name="anfrageid" id="anfrageid" value="'. $anfrageid .'" /><br>';
echo '<input type="submit" class="btn btn-primary" id="submitbox" value="Anforderung löschen (mit Mail)" />';
echo "</form>";
echo "<br>";
}
echo "<form action='". self_action() . "' method=POST>";
echo '<input type="hidden" name="aktion" id="aktion" value="10" />';
echo '<input type="hidden" name="anfrageid" id="anfrageid" value="'. $anfrageid .'" /><br>';
@@ -971,7 +1030,7 @@ if(!check_worker()){
p.tele,
p.geburtstag
FROM anfragen a
INNER JOIN persons p ON p.person_id = a.requester_person_id
LEFT JOIN persons p ON p.person_id = a.requester_person_id
WHERE a.anfrageid = :anfrageid
LIMIT 1
");
@@ -1038,6 +1097,8 @@ if(!check_worker()){
a.medikament2,
a.sicherenachricht,
a.abholort,
a.source,
a.anrufernummer,
p.person_id,
p.vorname,
@@ -1051,8 +1112,8 @@ if(!check_worker()){
aa.artname AS anfrageart_name
FROM anfragen a
INNER JOIN persons p ON p.person_id = a.requester_person_id
INNER JOIN anfrageart aa ON a.anforderungart = aa.artid
LEFT JOIN persons p ON p.person_id = a.requester_person_id
LEFT JOIN anfrageart aa ON a.anforderungart = aa.artid
WHERE a.anfrageid = :anfrageid
LIMIT 1
");
@@ -1094,6 +1155,9 @@ if(!check_worker()){
/* --- Anzeige-Strings --- */
$WeitereInfos = "";
$WeitereInfos .= GetAnruferZeile($row["source"] ?? "mail", $row["anrufernummer"] ?? null);
$WeitereInfos .= GetPatientZuRufnummerAusgabe($pdo, $row["source"] ?? "mail", $row["anrufernummer"] ?? null, $row["person_id"] ?? null);
$WeitereInfos .= GetSprachnachrichtHinweis($row["source"] ?? "mail");
if (!empty($medikamenteins)) {
$WeitereInfos .= "Medikament1: " . htmlspecialchars($medikamenteins, ENT_QUOTES, 'UTF-8') . "<br>";
}
@@ -1284,6 +1348,8 @@ if(!check_worker()){
a.medikament4,
a.medikament5,
a.medikament6,
a.source,
a.anrufernummer,
aa.artname AS anfrageart_name,
@@ -1344,6 +1410,8 @@ if(!check_worker()){
$ordnungsstring = GetOrdnungsid($ordnungsid);
$WeitereInfos = "";
$WeitereInfos .= GetAnruferZeile($row["source"] ?? "mail", $row["anrufernummer"] ?? null);
$WeitereInfos .= GetSprachnachrichtHinweis($row["source"] ?? "mail");
if (!empty($med1)) $WeitereInfos .= "Medikament1: " . htmlspecialchars($med1, ENT_QUOTES, 'UTF-8') . "<br>";
if (!empty($med2)) $WeitereInfos .= "Medikament2: " . htmlspecialchars($med2, ENT_QUOTES, 'UTF-8') . "<br>";
if (!empty($med3)) $WeitereInfos .= "Medikament3: " . htmlspecialchars($med3, ENT_QUOTES, 'UTF-8') . "<br>";
+267
View File
@@ -611,6 +611,273 @@ function GetOrdnungsid($wert){
}
/**
* Herkunft einer Anfrage als Text.
*
* Nur der Fall "telefon" kommt aus anfragen.source. Die Unterscheidung
* intern/Mail bleibt bewusst an sicherenachricht haengen: source="portal"
* ist dafuer unbrauchbar, weil insertAnfrage() kein source setzt und interne
* Anfragen deshalb als source="mail" in der Tabelle stehen.
* Seit 02.09.2026 legt die Telefonanlage Anfragen mit source="telefon" an;
* diese haben oft keine requester_person_id, weil der Anrufer nicht
* zugeordnet werden konnte.
*/
function GetAnfrageHerkunft($source, $sicherenachricht){
if($source == "telefon"){
$text = "Telefonanfrage";
}else if($sicherenachricht == "1"){
$text = "Interne Anfrage";
}else{
$text = "Mailanfrage";
}
return $text;
}
/**
* Hinweis auf die Sprachnachricht einer Telefonanfrage.
*
* Die Telefonanlage legt in anfragen.nachricht nur das automatisch erzeugte
* Transkript ab, das Erkennungsfehler enthalten kann. Die Aufnahme selbst
* liegt in PraxisKI und ist nur aus dem Praxisnetz erreichbar.
* Fuer alle anderen Herkuenfte leer.
*/
function GetSprachnachrichtHinweis($source){
if($source != "telefon"){
return "";
}
return "Transkript der Sprachnachricht - Aufnahme anhören: "
. "<a href=\"https://ki-praxis.praxis.local/anfragen/\" target=\"_blank\" rel=\"noopener\">ki-praxis.praxis.local/anfragen</a>"
. " (nur im Praxisnetz)<br>";
}
/**
* Telefonnummer als anklickbaren tel:-Link ausgeben.
*
* Der Link vertraegt nur Ziffern und ein fuehrendes Plus; Klammern,
* Leerzeichen und Bindestriche fliegen fuer das Ziel raus, angezeigt wird
* die Nummer unveraendert. Nummern ohne jede Ziffer - etwa "unterdrueckt"
* bei unterdrueckter Rufnummer - werden nur als Text ausgegeben.
* Erwartet den Rohwert aus der Datenbank und maskiert selbst.
*/
function GetTelefonLink($nummer){
$nummer = trim((string)$nummer);
if($nummer === ""){
return "";
}
$ziel = preg_replace('/[^0-9]/', '', $nummer);
if($ziel === ""){
return htmlspecialchars($nummer, ENT_QUOTES, 'UTF-8');
}
if(substr($nummer, 0, 1) === "+"){
$ziel = "+" . $ziel;
}
return "<a href=\"tel:" . htmlspecialchars($ziel, ENT_QUOTES, 'UTF-8') . "\">"
. htmlspecialchars($nummer, ENT_QUOTES, 'UTF-8') . "</a>";
}
/**
* Rufnummer des Anrufers als beschriftete Zeile.
*
* anfragen.anrufernummer fuellt die Telefonanlage; steht dort nichts, gibt
* es keine Zeile. Fuer alle Herkuenfte ausser "telefon" leer.
*/
function GetAnruferZeile($source, $anrufernummer){
if($source != "telefon"){
return "";
}
$link = GetTelefonLink($anrufernummer);
if($link === ""){
return "";
}
return "Anruf von: " . $link . "<br>";
}
/**
* Rufnummer auf eine vergleichbare Form bringen.
*
* Nur Ziffern; +49 und 0049 werden zur nationalen Schreibweise mit
* fuehrender 0, damit die Anzeige der Telefonanlage und die von Hand
* gepflegten Nummern in persons zusammenfinden.
*/
function NormalisiereRufnummer($nummer){
$ziffern = preg_replace('/[^0-9]/', '', (string)$nummer);
if($ziffern === ""){
return "";
}
if(strpos($ziffern, "0049") === 0){
$ziffern = "0" . substr($ziffern, 4);
}elseif(strpos(trim((string)$nummer), "+") === 0 && strpos($ziffern, "49") === 0){
$ziffern = "0" . substr($ziffern, 2);
}
return $ziffern;
}
/**
* Patienten zu einer Rufnummer suchen.
*
* Gesucht wird in persons UND im Altbestand user_old. persons allein reicht
* nicht: bei 122 der 2552 migrierten Paare weicht die Nummer ab, 87 Nummern
* stehen ausschliesslich in user_old, und 176 alte Zeilen wurden nie
* gemappt. Genau so ein Fall war Anfrage 13037 - die Mobilnummer stand nur
* im Altbestand, in persons nur eine aeltere siebenstellige.
*
* Gibt es zu einer alten Zeile einen aktuellen Datensatz, werden dessen
* Daten gezeigt: die Nummer ist nur der Schluessel, angezeigt gehoert der
* gepflegte Stand. Jeder Patient taucht je Nummer hoechstens einmal auf.
*
* Liefert bewusst alle Treffer, denn 521 der gepflegten Nummern gehoeren zu
* mehr als einer Person (Familien, zwei sogar zu 16 bzw. 18). Die Auswahl
* trifft die Mitarbeiterin, nicht der Code.
*
* Nummern mit weniger als 7 Ziffern werden nicht gesucht: in den Stammdaten
* stehen ein paar Platzhalter mit 1 bis 5 Ziffern, die sonst wahllos
* zusammenfallen.
*
* Der Index wird beim ersten Treffer einmal je Seitenaufruf aufgebaut und
* danach wiederverwendet - die Anfrageliste zeigt bis zu einige tausend
* Zeilen, eine Abfrage je Zeile waere zu teuer.
*/
function FindePatientenZuRufnummer($pdo, $nummer){
static $index = null;
$gesucht = NormalisiereRufnummer($nummer);
if(strlen($gesucht) < 7){
return array();
}
if($index === null){
$index = array();
$gesehen = array();
$aufnehmen = function($nummerRoh, $schluessel, $eintrag) use (&$index, &$gesehen){
$n = NormalisiereRufnummer($nummerRoh);
if(strlen($n) < 7){
return;
}
// Zweiter Schluessel ueber Name und Geburtstag: nicht gemappte
// Altzeilen haben eine eigene userid, sind aber oft dieselbe
// Person wie ein persons-Datensatz. Ohne das stuende sie zweimal
// in der Liste. Der Vergleich traegt hier, weil ohnehin nur
// Eintraege zu ein und derselben Rufnummer verglichen werden.
$identitaet = "id:" . mb_strtolower(trim((string)$eintrag["vorname"]) . "|"
. trim((string)$eintrag["nachname"]) . "|" . (string)$eintrag["geburtstag"]);
if(isset($gesehen[$n][$schluessel]) || isset($gesehen[$n][$identitaet])){
return;
}
$gesehen[$n][$schluessel] = true;
$gesehen[$n][$identitaet] = true;
$index[$n][] = $eintrag;
};
$stmt = $pdo->query("SELECT person_id, vorname, nachname, geburtstag, email, tele
FROM persons WHERE tele IS NOT NULL AND tele <> ''");
foreach($stmt as $p){
$aufnehmen($p["tele"], "p" . $p["person_id"], array(
"person_id" => $p["person_id"],
"vorname" => $p["vorname"],
"nachname" => $p["nachname"],
"geburtstag" => $p["geburtstag"],
"email" => $p["email"],
"altbestand" => false,
));
}
$stmt = $pdo->query("SELECT u.userid, u.tele,
p.person_id AS person_id,
COALESCE(p.vorname, u.vorname) AS vorname,
COALESCE(p.nachname, u.nachname) AS nachname,
COALESCE(p.geburtstag, u.geburtstag) AS geburtstag,
COALESCE(p.email, u.mail) AS email
FROM user_old u
LEFT JOIN tmp_user_person_map m ON m.old_userid = u.userid
LEFT JOIN persons p ON p.person_id = m.person_id
WHERE u.tele IS NOT NULL AND u.tele <> ''");
foreach($stmt as $u){
$pid = $u["person_id"] !== null ? (int)$u["person_id"] : null;
$aufnehmen($u["tele"], $pid !== null ? "p" . $pid : "u" . $u["userid"], array(
"person_id" => $pid,
"vorname" => $u["vorname"],
"nachname" => $u["nachname"],
"geburtstag" => $u["geburtstag"],
"email" => $u["email"],
"altbestand" => true,
));
}
}
return isset($index[$gesucht]) ? $index[$gesucht] : array();
}
/**
* Zu einer Telefonanfrage ohne zugeordneten Patienten anzeigen, wer hinter
* der Rufnummer stecken koennte.
*
* Bewusst nur ein Vorschlag: die Rufnummer beweist nicht, wer angerufen hat.
* Bei mehreren Treffern werden die Namen aufgezaehlt, statt einen zu waehlen.
* Leer, sobald die Anfrage bereits einen Patienten hat oder nichts passt.
*/
function GetPatientZuRufnummerAusgabe($pdo, $source, $anrufernummer, $personId = null){
if($source != "telefon"){
return "";
}
if(!empty($personId)){
return "";
}
$treffer = FindePatientenZuRufnummer($pdo, $anrufernummer);
if(!$treffer){
return "";
}
$namen = array();
$ausAltbestand = false;
foreach($treffer as $p){
$name = trim(((string)$p["vorname"]) . " " . ((string)$p["nachname"]));
$namen[] = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
if(!empty($p["altbestand"])){
$ausAltbestand = true;
}
}
if(count($treffer) === 1){
$p = $treffer[0];
$text = "Rufnummer passt zu: <b>" . $namen[0] . "</b>";
$geburtstag = (string)($p["geburtstag"] ?? "");
if($geburtstag !== "" && $geburtstag !== "0000-00-00"){
$zeit = strtotime($geburtstag);
if($zeit !== false){
$geburtstag = date("d.m.Y", $zeit);
}
$text .= ", geb. " . htmlspecialchars($geburtstag, ENT_QUOTES, 'UTF-8');
}
$mail = trim((string)($p["email"] ?? ""));
if($mail !== ""){
$text .= "<br>" . htmlspecialchars($mail, ENT_QUOTES, 'UTF-8');
}
if(!empty($p["altbestand"])){
$text .= $p["person_id"] === null
? "<br><i>nur im alten Bestand, kein aktueller Patientendatensatz</i>"
: "<br><i>Rufnummer stammt aus dem alten Bestand</i>";
}
return $text . "<br><i>nicht bestätigt - nur über die Rufnummer erkannt</i><br>";
}
$gezeigt = array_slice($namen, 0, 6);
$text = "Rufnummer passt zu " . count($treffer) . " Patienten: " . implode(", ", $gezeigt);
if(count($namen) > count($gezeigt)){
$text .= " und " . (count($namen) - count($gezeigt)) . " weiteren";
}
if($ausAltbestand){
$text .= "<br><i>teilweise über den alten Bestand gefunden</i>";
}
return $text . "<br><i>nicht zugeordnet - bitte selbst prüfen</i><br>";
}
function GetPatientenartName($Patientenart){
if($Patientenart == "0"){
$impfenangebot = "Neupatient";