init
This commit is contained in:
commit
832ebf099a
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.env
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -0,0 +1,19 @@
|
||||
FROM golang:1.21-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN mkdir -p static/css && mkdir -p static/images && go mod init ldap_phonebook_html
|
||||
COPY *.go .
|
||||
#COPY static static
|
||||
RUN go mod tidy && go mod download && go build -o ldap_phonebook_html /app
|
||||
|
||||
#ENV LDAP_SERVER="<Server>"
|
||||
#ENV LDAP_PORT="636"
|
||||
#ENV LDAP_BIND_DN="CN=<User>,CN=Users,DC=<example>,DC=local"
|
||||
#ENV LDAP_BIND_PASSWORD="<password>"
|
||||
#ENV LDAP_BASE_DN="CN=Users,DC=<example>,DC=local"
|
||||
#ENV LDAP_FILTER=(objectClass=person)
|
||||
#ENV SERVER_PORT="8080"
|
||||
|
||||
|
||||
CMD ["/app/ldap_phonebook_html"]
|
26
LICENSE.md
Normal file
26
LICENSE.md
Normal file
@ -0,0 +1,26 @@
|
||||
# LICENSE
|
||||
|
||||
## Telefonbuch-Programm
|
||||
Copyright (C) 2023 Steffen Probst
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
## Full License Text
|
||||
|
||||
For the full license text, please visit:
|
||||
[GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
## Development Assistance
|
||||
|
||||
This program was developed with the assistance of Claude.ai, an AI assistant created by Anthropic, PBC. All copyrights for the resulting code remain with the author, Steffen Probst.
|
152
MANUAL.md
Normal file
152
MANUAL.md
Normal file
@ -0,0 +1,152 @@
|
||||
# Telefonbuch-Programm Benutzerhandbuch
|
||||
|
||||
## Inhaltsverzeichnis
|
||||
- [Einführung](#einführung)
|
||||
- [Installation](#installation)
|
||||
- [Konfiguration](#konfiguration)
|
||||
- [Umgebungsvariablen](#umgebungsvariablen)
|
||||
- [config.yaml](#configyaml)
|
||||
- [Container-Rollout mit Podman](#container-rollout-mit-podman)
|
||||
- [Active Directory Konfiguration](#active-directory-konfiguration)
|
||||
- [Verwendung der Benutzeroberfläche](#verwendung-der-benutzeroberfläche)
|
||||
- [Fehlerbehebung](#fehlerbehebung)
|
||||
|
||||
## Einführung
|
||||
|
||||
Dieses Telefonbuch-Programm ermöglicht es Ihnen, Kontaktinformationen aus Ihrem Active Directory (AD) zu extrahieren und in einer benutzerfreundlichen Weboberfläche darzustellen. Es unterstützt sowohl Benutzer als auch Kontakte aus dem AD und wird als Podman-Container bereitgestellt.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Stellen Sie sicher, dass Podman und podman-compose auf Ihrem System installiert sind.
|
||||
2. Klonen Sie das Repository oder laden Sie den Quellcode herunter.
|
||||
3. Navigieren Sie zum Projektverzeichnis.
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Umgebungsvariablen
|
||||
|
||||
Erstellen Sie eine `.env`-Datei im Projektverzeichnis mit folgenden Variablen:
|
||||
|
||||
```
|
||||
LDAP_SERVER=<Ihr LDAP-Server>
|
||||
BIND_DN=<Ihr Bind DN>
|
||||
BIND_PASSWORD=<Ihr Bind-Passwort>
|
||||
SEARCH_BASE=<Ihr Suchbasis-DN>
|
||||
```
|
||||
|
||||
### config.yaml
|
||||
|
||||
Überprüfen und passen Sie die `config.yaml` Datei im `static`-Verzeichnis an:
|
||||
|
||||
```yaml
|
||||
phone_rules:
|
||||
invalid_number: "+49 5331 89"
|
||||
country:
|
||||
prefix: "49"
|
||||
area_codes:
|
||||
- "5331"
|
||||
internal_prefix: "89"
|
||||
```
|
||||
|
||||
## Container-Rollout mit Podman
|
||||
|
||||
1. Bauen und Starten des Containers:
|
||||
```
|
||||
podman-compose up -d
|
||||
```
|
||||
|
||||
2. Überprüfen Sie, ob der Container läuft:
|
||||
```
|
||||
podman ps
|
||||
```
|
||||
|
||||
3. Zugriff auf die Anwendung:
|
||||
Öffnen Sie einen Webbrowser und navigieren Sie zu `http://localhost:8082`.
|
||||
|
||||
4. Stoppen des Containers:
|
||||
```
|
||||
podman-compose down
|
||||
```
|
||||
|
||||
5. Logs anzeigen:
|
||||
```
|
||||
podman logs <container-name>
|
||||
```
|
||||
|
||||
## Active Directory Konfiguration
|
||||
|
||||
### Erforderliche Attribute
|
||||
|
||||
Für Benutzer und Kontakte im AD sollten folgende Attribute korrekt gefüllt sein:
|
||||
|
||||
- `givenName`: Vorname
|
||||
- `sn`: Nachname
|
||||
- `mail`: E-Mail-Adresse
|
||||
- `telephoneNumber`: Bürotelefonnummer > Konfigurationsformat `+49 5331 89xxx` oder `+49 5331 89xxxxx`
|
||||
- `mobile`: Mobiltelefonnummer > Konfigurationsformat `+49 1(5..7) xxxxxx`
|
||||
- `otherTelephone`: Weitere Telefonnummern > Konfigurationsformat `+49 xxxx xxxxxx`
|
||||
- `physicalDeliveryOfficeName`: Abteilung
|
||||
|
||||
Für Kontakte:
|
||||
- Erstellen Sie Kontaktobjekte in Ihrem AD
|
||||
- Füllen Sie die oben genannten Attribute entsprechend aus
|
||||
|
||||
### Anzeigeverhalten von Einträgen
|
||||
|
||||
Das Telefonbuch-Programm filtert die Einträge aus dem Active Directory, um sicherzustellen, dass nur relevante und vollständige Informationen angezeigt werden. Rufnummer die in keine weiterführend Nummer haben, hinter dem Prefix, werden über InvalidNumber(unvollständige Rufnummer) in der `config.yaml` zusätzlich konfiguriert. -> Es werden nur vollständige Rufnmmer angezeigt, die im Format +49 5331 89xxx(xx) vorliegen. Aus der korrekten Rufnummer wird dann auch die interne Rufnummer generiert. Diese Funktion betrifft auch nur Attribute `telephoneNumber` aus dem AD.
|
||||
|
||||
#### Allgemeine Bedingungen für alle Einträge
|
||||
|
||||
Ein Eintrag (Benutzer oder Kontakt) wird nur dann angezeigt, wenn:
|
||||
|
||||
1. Der Vorname (`givenName`) nicht leer ist.
|
||||
2. Der Nachname (`sn`) nicht leer ist.
|
||||
|
||||
#### Spezifische Bedingungen für Benutzer
|
||||
|
||||
Ein Benutzer wird im Telefonbuch angezeigt, wenn:
|
||||
|
||||
- Die allgemeinen Bedingungen erfüllt sind UND
|
||||
- Mindestens eine der folgenden Bedingungen zutrifft:
|
||||
* Die Bürotelefonnummer (`telephoneNumber`) ist vorhanden und nicht als ungültig markiert (siehe `config.yaml`). -> `InvalidNumber`
|
||||
* Eine Mobiltelefonnummer (`mobile`) ist vorhanden.
|
||||
* Die E-Mail-Adresse (`mail`) ist vorhanden.
|
||||
|
||||
Ein Benutzer wird NICHT angezeigt, wenn:
|
||||
|
||||
1. Keine Telefonnummer (weder Büro noch Mobil) vorhanden ist ODER
|
||||
2. Die einzige vorhandene Telefonnummer als ungültig markiert ist (siehe `config.yaml`) UND
|
||||
3. Keine E-Mail-Adresse vorhanden ist.
|
||||
|
||||
#### Spezifische Bedingungen für Kontakte
|
||||
|
||||
Ein Kontakt wird im Telefonbuch angezeigt, wenn die allgemeinen Bedingungen erfüllt sind. Kontakte werden großzügiger behandelt als Benutzer und werden angezeigt, auch wenn keine Telefonnummer oder E-Mail-Adresse vorhanden ist.
|
||||
|
||||
#### Hinweise
|
||||
|
||||
- Die Abteilung (`physicalDeliveryOfficeName`) ist optional und beeinflusst nicht die Anzeige im Telefonbuch.
|
||||
- Zusätzliche Telefonnummern (`otherTelephone`) werden angezeigt, wenn vorhanden, beeinflussen aber nicht die Entscheidung, ob ein Eintrag im Telefonbuch erscheint.
|
||||
- Die Unterscheidung zwischen Benutzern und Kontakten basiert auf dem `objectClass`-Attribut im Active Directory.
|
||||
|
||||
## Verwendung der Benutzeroberfläche
|
||||
|
||||
1. Starten Sie das Programm.
|
||||
2. Öffnen Sie einen Webbrowser und navigieren Sie zu `http://localhost:<SERVER_PORT>`.
|
||||
|
||||
### Suchfunktion
|
||||
- Verwenden Sie das Suchfeld oben auf der Seite, um nach Namen, Telefonnummern oder anderen Informationen zu suchen.
|
||||
- Die Suche ist Case-Sensitive (unterscheidet zwischen Groß- und Kleinschreibung).
|
||||
|
||||
### Sortierung
|
||||
- Klicken Sie auf die Spaltenüberschriften, um die Tabelle nach dieser Spalte zu sortieren.
|
||||
- Ein erneuter Klick auf dieselbe Spalte kehrt die Sortierreihenfolge um.
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
- Überprüfen Sie die Container-Logs mit `podman logs <container-name>`.
|
||||
- Stellen Sie sicher, dass alle Umgebungsvariablen in der `.env`-Datei korrekt gesetzt sind.
|
||||
- Überprüfen Sie die Netzwerkverbindung zum LDAP-Server vom Host-System aus.
|
||||
- Verifizieren Sie die LDAP-Anmeldeinformationen und Zugriffsrechte.
|
||||
- Bei Problemen mit dem Volume-Mount, überprüfen Sie die Berechtigungen des `static`-Verzeichnisses.
|
||||
|
||||
Wenn Probleme weiterhin bestehen, erstellen Sie bitte ein Issue im GitHub-Repository oder kontaktieren Sie den Entwickler.
|
68
README.md
Normal file
68
README.md
Normal file
@ -0,0 +1,68 @@
|
||||
# README
|
||||
|
||||
## Beschreibung
|
||||
Telefonbuch für MKN, in Go programmiert und als Podman-Container bereitgestellt.
|
||||
|
||||
## Features
|
||||
- Anmeldung an die Domäne AD/LDAP
|
||||
- Auslesen von Benutzern und Kontakten
|
||||
- Formatierung der Rufnummern
|
||||
- Ausgabe über internen Webserver in Go
|
||||
- Suche und Sortierung in JavaScript
|
||||
- Unterstützung für Benutzer und Kontakte aus dem Active Directory
|
||||
- Bereitstellung als Podman-Container
|
||||
|
||||
## Programmablauf
|
||||
1. Start des Podman-Containers
|
||||
2. Initialisierung der Go-Anwendung:
|
||||
- Laden der Umgebungsvariablen
|
||||
- Laden der YAML-Konfiguration
|
||||
- Laden des HTML-Templates
|
||||
3. Start des HTTP-Servers
|
||||
4. Periodische Aktualisierung des Caches im Hintergrund
|
||||
5. Verarbeitung eingehender HTTP-Anfragen
|
||||
6. Beenden der Anwendung bei Container-Stopp
|
||||
|
||||
## Installation und Konfiguration
|
||||
|
||||
1. Vorbereitung:
|
||||
- Stellen Sie sicher, dass Podman und podman-compose auf Ihrem System installiert sind.
|
||||
- Klonen Sie das Repository in ein lokales Verzeichnis.
|
||||
|
||||
2. Konfiguration:
|
||||
- Erstellen Sie eine `.env`-Datei im Projektverzeichnis mit folgenden Variablen:
|
||||
```
|
||||
LDAP_SERVER=<Ihr LDAP-Server>
|
||||
BIND_DN=<Ihr Bind DN>
|
||||
BIND_PASSWORD=<Ihr Bind-Passwort>
|
||||
SEARCH_BASE=<Ihr Suchbasis-DN>
|
||||
```
|
||||
- Passen Sie die `config.yaml` im `static`-Verzeichnis nach Bedarf an.
|
||||
|
||||
3. Container bauen und starten:
|
||||
```
|
||||
podman-compose up -d
|
||||
```
|
||||
|
||||
4. Zugriff auf die Anwendung:
|
||||
Öffnen Sie einen Webbrowser und navigieren Sie zu `http://localhost:8082`.
|
||||
|
||||
5. Container stoppen:
|
||||
```
|
||||
podman-compose down
|
||||
```
|
||||
|
||||
Für detailliertere Anweisungen, siehe MANUAL.md.
|
||||
|
||||
## Lizenz
|
||||
|
||||
Entwickler: Steffen Probst
|
||||
E-Mail: [pts@mkn.de](mailto:pts@mkn.de)
|
||||
|
||||
Dieses Projekt ist lizenziert unter der [GNU General Public License Version 3 (GPL-3.0)](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
|
||||
### Entwicklungsunterstützung
|
||||
|
||||
Dieses Programm wurde mit Unterstützung von Claude.ai, einem KI-Assistenten entwickelt von Anthropic, PBC, erstellt. Claude.ai wurde für Code-Generierung, Problemlösung und Optimierung eingesetzt.
|
||||
|
||||
Hinweis: Während Claude.ai bei der Entwicklung half, liegt das Urheberrecht des resultierenden Codes beim Autor, Steffen Probst. Claude.ai ist ein Werkzeug und beansprucht keine Rechte an dem erstellten Code.
|
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@ -0,0 +1,20 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
go-app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- LDAP_SERVER=${LDAP_SERVER}
|
||||
- BIND_DN=${BIND_DN}
|
||||
- BIND_PASSWORD=${BIND_PASSWORD}
|
||||
- SEARCH_BASE=${SEARCH_BASE}
|
||||
- SERVER_PORT=8080
|
||||
volumes:
|
||||
- ./static:/app/static # Map the static directory from host to container
|
||||
|
||||
volumes:
|
||||
go-app-data:
|
468
main.go
Normal file
468
main.go
Normal file
@ -0,0 +1,468 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
Copyright (C) 2023 Steffen Probst
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Entwickelt mit Unterstützung von Claude.ai, einem KI-Assistenten von Anthropic, PBC.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"encoding/xml"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
LDAPServer string
|
||||
BindDN string
|
||||
BindPassword string
|
||||
SearchBase string
|
||||
ServerPort string
|
||||
}
|
||||
|
||||
type AddressBook struct {
|
||||
XMLName xml.Name `xml:"AddressBook"`
|
||||
Version string `xml:"version,attr"`
|
||||
PBGroups []PBGroup `xml:"pbgroup"`
|
||||
Contacts []Contact `xml:"Contact"`
|
||||
}
|
||||
|
||||
type PBGroup struct {
|
||||
ID int `xml:"id,attr"`
|
||||
Name string `xml:"name"`
|
||||
}
|
||||
|
||||
type Contact struct {
|
||||
FirstName string `xml:"FirstName"`
|
||||
LastName string `xml:"LastName"`
|
||||
Frequent int `xml:"Frequent"`
|
||||
Phones []Phone `xml:"Phone"`
|
||||
Department string `xml:"Department,omitempty"`
|
||||
Group int `xml:"Group"`
|
||||
}
|
||||
|
||||
type Phone struct {
|
||||
Type string `xml:"type,attr"`
|
||||
PhoneNumber string `xml:"phonenumber"`
|
||||
AccountIndex int `xml:"accountindex"`
|
||||
}
|
||||
|
||||
type YealinkPhoneBook struct {
|
||||
XMLName xml.Name `xml:"YealinkIPPhoneDirectory"`
|
||||
Entries []YealinkEntry `xml:"DirectoryEntry"`
|
||||
}
|
||||
|
||||
type YealinkEntry struct {
|
||||
Name string `xml:"Name"`
|
||||
Telephone string `xml:"Telephone"`
|
||||
Mobile string `xml:"Mobile,omitempty"`
|
||||
OtherMobile string `xml:"Other,omitempty"`
|
||||
}
|
||||
|
||||
// Neue Struktur für das Contact-Format
|
||||
type ContactXML struct {
|
||||
XMLName xml.Name `xml:"contacts"`
|
||||
Contacts []ContactEntry `xml:"contact"`
|
||||
}
|
||||
|
||||
type ContactEntry struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Number string `xml:"number,attr"`
|
||||
FirstName string `xml:"firstname,attr"`
|
||||
LastName string `xml:"lastname,attr"`
|
||||
Phone string `xml:"phone,attr"`
|
||||
Mobile string `xml:"mobile,attr"`
|
||||
Email string `xml:"email,attr"`
|
||||
Address string `xml:"address,attr"`
|
||||
City string `xml:"city,attr"`
|
||||
State string `xml:"state,attr"`
|
||||
Zip string `xml:"zip,attr"`
|
||||
Comment string `xml:"comment,attr"`
|
||||
ID string `xml:"id,attr"`
|
||||
Info string `xml:"info,attr"`
|
||||
Presence string `xml:"presence,attr"`
|
||||
Starred string `xml:"starred,attr"`
|
||||
Directory string `xml:"directory,attr"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
config Config
|
||||
cache struct {
|
||||
grandstreamFile *os.File
|
||||
yealinkFile *os.File
|
||||
contactFile *os.File
|
||||
hash []byte
|
||||
lastUpdate time.Time
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
config = Config{
|
||||
LDAPServer: os.Getenv("LDAP_SERVER"),
|
||||
BindDN: os.Getenv("BIND_DN"),
|
||||
BindPassword: os.Getenv("BIND_PASSWORD"),
|
||||
SearchBase: os.Getenv("SEARCH_BASE"),
|
||||
ServerPort: os.Getenv("SERVER_PORT"),
|
||||
}
|
||||
|
||||
if config.ServerPort == "" {
|
||||
config.ServerPort = "8080"
|
||||
}
|
||||
|
||||
// Initialisiere den Cache vor dem Start des Servers
|
||||
err = initializeCache()
|
||||
if err != nil {
|
||||
log.Fatalf("Fehler beim Initialisieren des Caches: %v", err)
|
||||
}
|
||||
|
||||
http.HandleFunc("/phonebook.xml", grandstreamPhoneBookHandler)
|
||||
http.HandleFunc("/yphonebook.xml", yealinkPhoneBookHandler)
|
||||
http.HandleFunc("/Contacts.xml", contactXMLHandler)
|
||||
http.HandleFunc("/phonebook.csv", csvExportHandler)
|
||||
|
||||
log.Printf("Server starting on port %s", config.ServerPort)
|
||||
log.Fatal(http.ListenAndServe(":"+config.ServerPort, nil))
|
||||
}
|
||||
|
||||
func grandstreamPhoneBookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
servePhoneBook(w, r, cache.grandstreamFile)
|
||||
}
|
||||
|
||||
func yealinkPhoneBookHandler(w http.ResponseWriter, r *http.Request) {
|
||||
servePhoneBook(w, r, cache.yealinkFile)
|
||||
}
|
||||
|
||||
func contactXMLHandler(w http.ResponseWriter, r *http.Request) {
|
||||
servePhoneBook(w, r, cache.contactFile)
|
||||
}
|
||||
|
||||
func csvExportHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := updateCachedXMLIfNeeded()
|
||||
if err != nil {
|
||||
http.Error(w, "Fehler beim Aktualisieren der Kontakte", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
contacts, err := getADContacts()
|
||||
if err != nil {
|
||||
http.Error(w, "Fehler beim Abrufen der Kontakte", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/csv; charset=windows-1252")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=phonebook.csv")
|
||||
|
||||
if err := exportToCSV(contacts, w); err != nil {
|
||||
http.Error(w, "Fehler beim Exportieren als CSV", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func servePhoneBook(w http.ResponseWriter, r *http.Request, file *os.File) {
|
||||
err := updateCachedXMLIfNeeded()
|
||||
if err != nil {
|
||||
http.Error(w, "Fehler beim Aktualisieren der Kontakte", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cache.mutex.RLock()
|
||||
defer cache.mutex.RUnlock()
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
_, err = file.Seek(0, 0)
|
||||
if err != nil {
|
||||
log.Printf("Fehler beim Zurücksetzen des Datei-Offsets: %v", err)
|
||||
http.Error(w, "Interner Serverfehler", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = io.Copy(w, file)
|
||||
if err != nil {
|
||||
log.Printf("Fehler beim Kopieren der XML-Datei: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func initializeCache() error {
|
||||
log.Println("Initialisiere Cache...")
|
||||
|
||||
// Führen Sie updateCachedXMLIfNeeded aus, um die Caching-Dateien zu erstellen
|
||||
err := updateCachedXMLIfNeeded()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Fehler beim Initialisieren des Caches: %v", err)
|
||||
}
|
||||
|
||||
// Überprüfen Sie, ob alle erforderlichen Dateien erstellt wurden
|
||||
if cache.grandstreamFile == nil || cache.yealinkFile == nil || cache.contactFile == nil {
|
||||
return fmt.Errorf("Nicht alle erforderlichen Cache-Dateien wurden erstellt")
|
||||
}
|
||||
|
||||
log.Println("Cache erfolgreich initialisiert")
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateCachedXMLIfNeeded() error {
|
||||
cache.mutex.Lock()
|
||||
defer cache.mutex.Unlock()
|
||||
|
||||
contacts, err := getADContacts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newHash := calculateHash(contacts)
|
||||
if cache.grandstreamFile != nil && cache.yealinkFile != nil && cache.contactFile != nil && bytes.Equal(newHash, cache.hash) {
|
||||
return nil
|
||||
}
|
||||
|
||||
addressBook := AddressBook{
|
||||
Version: "1.0",
|
||||
PBGroups: []PBGroup{
|
||||
{ID: 1, Name: "Blacklist"},
|
||||
{ID: 2, Name: "Work"},
|
||||
{ID: 3, Name: "Friend"},
|
||||
},
|
||||
Contacts: contacts,
|
||||
}
|
||||
|
||||
yealinkPhoneBook := YealinkPhoneBook{
|
||||
Entries: make([]YealinkEntry, len(contacts)),
|
||||
}
|
||||
|
||||
for i, c := range contacts {
|
||||
yealinkPhoneBook.Entries[i] = YealinkEntry{
|
||||
Name: c.FirstName + " " + c.LastName,
|
||||
Telephone: getPhoneByType(c.Phones, "Work"),
|
||||
Mobile: getPhoneByType(c.Phones, "Mobile"),
|
||||
OtherMobile: getPhoneByType(c.Phones, "Home"),
|
||||
}
|
||||
}
|
||||
|
||||
// Erstellen der Contacts.xml
|
||||
contactXML := ContactXML{
|
||||
Contacts: make([]ContactEntry, len(contacts)),
|
||||
}
|
||||
|
||||
for i, c := range contacts {
|
||||
contactXML.Contacts[i] = ContactEntry{
|
||||
Name: c.FirstName + " " + c.LastName,
|
||||
Number: getPhoneByType(c.Phones, "Work"),
|
||||
FirstName: c.FirstName,
|
||||
LastName: c.LastName,
|
||||
Phone: getPhoneByType(c.Phones, "Work"),
|
||||
Mobile: getPhoneByType(c.Phones, "Mobile"),
|
||||
Presence: "0",
|
||||
Starred: "0",
|
||||
Directory: "0",
|
||||
}
|
||||
}
|
||||
|
||||
if err := saveToTempFile(&addressBook, &cache.grandstreamFile); err != nil {
|
||||
return fmt.Errorf("Fehler beim Speichern der Grandstream-XML: %v", err)
|
||||
}
|
||||
|
||||
if err := saveToTempFile(&yealinkPhoneBook, &cache.yealinkFile); err != nil {
|
||||
return fmt.Errorf("Fehler beim Speichern der Yealink-XML: %v", err)
|
||||
}
|
||||
|
||||
if err := saveToTempFile(&contactXML, &cache.contactFile); err != nil {
|
||||
return fmt.Errorf("Fehler beim Speichern der Contact-XML: %v", err)
|
||||
}
|
||||
|
||||
csvFile, err := os.OpenFile("phonebook.csv", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Fehler beim Erstellen der CSV-Datei: %v", err)
|
||||
}
|
||||
defer csvFile.Close()
|
||||
|
||||
if err := exportToCSV(contacts, csvFile); err != nil {
|
||||
return fmt.Errorf("Fehler beim Exportieren als CSV: %v", err)
|
||||
}
|
||||
|
||||
cache.hash = newHash
|
||||
cache.lastUpdate = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveToTempFile(data interface{}, file **os.File) error {
|
||||
tempFile, err := os.CreateTemp("", "phonebook_*.xml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Fehler beim Erstellen der temporären Datei: %v", err)
|
||||
}
|
||||
|
||||
enc := xml.NewEncoder(tempFile)
|
||||
enc.Indent("", " ")
|
||||
if err := enc.Encode(data); err != nil {
|
||||
tempFile.Close()
|
||||
os.Remove(tempFile.Name())
|
||||
return fmt.Errorf("Fehler beim Kodieren der XML: %v", err)
|
||||
}
|
||||
|
||||
if *file != nil {
|
||||
(*file).Close()
|
||||
os.Remove((*file).Name())
|
||||
}
|
||||
|
||||
*file = tempFile
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculateHash(contacts []Contact) []byte {
|
||||
h := md5.New()
|
||||
for _, contact := range contacts {
|
||||
io.WriteString(h, contact.FirstName)
|
||||
io.WriteString(h, contact.LastName)
|
||||
for _, phone := range contact.Phones {
|
||||
io.WriteString(h, phone.PhoneNumber)
|
||||
}
|
||||
}
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
func getADContacts() ([]Contact, error) {
|
||||
l, err := ldap.DialTLS("tcp", config.LDAPServer+":636", &tls.Config{InsecureSkipVerify: true})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LDAP-Verbindung fehlgeschlagen: %v", err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
err = l.Bind(config.BindDN, config.BindPassword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LDAP-Bindung fehlgeschlagen: %v", err)
|
||||
}
|
||||
|
||||
// Erweiterter LDAP-Filter für Benutzer und Kontakte
|
||||
searchFilter := "(&(|(objectClass=user)(objectClass=contact))(|(telephoneNumber=*)(otherTelephone=*)(mobile=*)(homePhone=*))(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"
|
||||
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
config.SearchBase,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
searchFilter,
|
||||
[]string{"givenName", "sn", "displayName", "telephoneNumber", "otherTelephone", "mobile", "homePhone"},
|
||||
nil,
|
||||
)
|
||||
|
||||
sr, err := l.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LDAP-Suche fehlgeschlagen: %v", err)
|
||||
}
|
||||
|
||||
var contacts []Contact
|
||||
for _, entry := range sr.Entries {
|
||||
firstName := entry.GetAttributeValue("givenName")
|
||||
lastName := entry.GetAttributeValue("sn")
|
||||
|
||||
if firstName == "" || lastName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
phone := formatPhoneNumber(entry.GetAttributeValue("telephoneNumber"))
|
||||
otherMobile := formatPhoneNumber(entry.GetAttributeValue("otherMobile"))
|
||||
mobile := formatPhoneNumber(entry.GetAttributeValue("mobile"))
|
||||
|
||||
if isExcludedNumber(phone) || isExcludedNumber(otherMobile) || isExcludedNumber(mobile) {
|
||||
continue
|
||||
}
|
||||
|
||||
if phone != "" || otherMobile != "" || mobile != "" {
|
||||
contact := Contact{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Phones: []Phone{},
|
||||
Group: 2, // Standardmäßig zur "Work"-Gruppe hinzufügen
|
||||
}
|
||||
if phone != "" {
|
||||
contact.Phones = append(contact.Phones, Phone{Type: "Work", PhoneNumber: phone, AccountIndex: 0})
|
||||
}
|
||||
if mobile != "" {
|
||||
contact.Phones = append(contact.Phones, Phone{Type: "Mobile", PhoneNumber: mobile, AccountIndex: 0})
|
||||
}
|
||||
if otherMobile != "" {
|
||||
contact.Phones = append(contact.Phones, Phone{Type: "Home", PhoneNumber: otherMobile, AccountIndex: 0})
|
||||
}
|
||||
contacts = append(contacts, contact)
|
||||
}
|
||||
}
|
||||
|
||||
return contacts, nil
|
||||
}
|
||||
|
||||
func removeDuplicateNumbers(phones []Phone) []Phone {
|
||||
seen := make(map[string]bool)
|
||||
result := []Phone{}
|
||||
|
||||
for _, phone := range phones {
|
||||
if !seen[phone.PhoneNumber] {
|
||||
seen[phone.PhoneNumber] = true
|
||||
result = append(result, phone)
|
||||
} else if phone.Type == "Work" {
|
||||
// Wenn die Nummer bereits gesehen wurde und dies die "Work" Nummer ist,
|
||||
// ersetzen wir die vorherige (wahrscheinlich "Mobile") mit dieser.
|
||||
for i, p := range result {
|
||||
if p.PhoneNumber == phone.PhoneNumber {
|
||||
result[i] = phone
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func isExcludedNumber(number string) bool {
|
||||
excludedNumbers := []string{
|
||||
"+49 5331 89",
|
||||
"+49533189",
|
||||
"+ 49 5331 89",
|
||||
}
|
||||
|
||||
// Zuerst prüfen wir auf die ausgeschlossenen Nummern
|
||||
for _, excluded := range excludedNumbers {
|
||||
if strings.Contains(number, excluded) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Dann prüfen wir, ob die Nummer mit "089" endet
|
||||
if len(number) >= 3 && number[len(number)-3:] == "089" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Wenn keine der obigen Bedingungen zutrifft, ist die Nummer nicht ausgeschlossen
|
||||
return false
|
||||
}
|
||||
|
||||
func
|
26
static/config.yaml
Normal file
26
static/config.yaml
Normal file
@ -0,0 +1,26 @@
|
||||
phone_rules:
|
||||
# Präfixe, die von Telefonnummern entfernt werden sollen
|
||||
# trim_prefixes:
|
||||
# - "0049"
|
||||
# - "+49"
|
||||
# - "49"
|
||||
# - "00"
|
||||
# - "0"
|
||||
|
||||
# Regeln zur Formatierung von Telefonnummern
|
||||
#format_rules:
|
||||
# "+49": "0" # Ersetzt internationale Vorwahl durch nationale
|
||||
# "00": "0" # Ersetzt internationale Vorwahl durch nationale
|
||||
|
||||
# Standard-Präfix für Nummern ohne Ländervorwahl
|
||||
#default_prefix: "+49"
|
||||
|
||||
# Ungültige Telefonnummer, die ignoriert werden soll
|
||||
invalid_number: "+49 5331 89"
|
||||
|
||||
# Konfiguration für Deutschland
|
||||
country:
|
||||
prefix: "49" # Ländervorwahl ohne '+' oder '00'
|
||||
area_codes:
|
||||
- "5331" # Vorwahl für Wolfenbüttel
|
||||
internal_prefix: "89" # Präfix für interne Nummern
|
169
static/css/main.css
Normal file
169
static/css/main.css
Normal file
@ -0,0 +1,169 @@
|
||||
:root {
|
||||
--background-body: #f5f5f5;
|
||||
--background: #e0e0e0;
|
||||
--text-main: #363636;
|
||||
--text-bright: #000;
|
||||
--links: #0076d1;
|
||||
--focus: rgba(0,150,191,0.67);
|
||||
--border: #dbdbdb;
|
||||
--code: #000;
|
||||
--animation-duration: 0.1s;
|
||||
--button-hover: #9b9b9b;
|
||||
--scrollbar-thumb: #aaa;
|
||||
--form-placeholder: #949494;
|
||||
--form-text: #1d1d1d;
|
||||
--table-header: #d0d0d0;
|
||||
--table-row-odd: #e8e8e8;
|
||||
--table-row-even: #f0f0f0;
|
||||
--sort-arrow: #666;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
line-height: 1.4;
|
||||
max-width: 80%;
|
||||
margin: 0 auto;
|
||||
padding: 0 10%;
|
||||
color: var(--text-main);
|
||||
background: var(--background-body);
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--table-header);
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-right: 20px; /* Space for sort indicator */
|
||||
}
|
||||
|
||||
tr:nth-child(odd) {
|
||||
background-color: var(--table-row-odd);
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: var(--table-row-even);
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
flex-grow: 1;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
#searchInput {
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
padding: 12px 20px;
|
||||
margin: 8px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Sort indicators */
|
||||
th::after {
|
||||
content: '\25B2'; /* Upward triangle */
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
opacity: 0.3;
|
||||
color: var(--sort-arrow);
|
||||
}
|
||||
|
||||
th.asc::after {
|
||||
content: '\25B2'; /* Upward triangle */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
th.desc::after {
|
||||
content: '\25BC'; /* Downward triangle */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
th:hover::after {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 20px;
|
||||
padding: 10px 0;
|
||||
background-color: var(--background);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
a[href^="tel:"]:before {
|
||||
content: "📞 ";
|
||||
}
|
||||
|
||||
a[href^="mailto:"]:before {
|
||||
content: "📧 ";
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
max-width: 95%;
|
||||
padding: 0 2.5%;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility improvements */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
107
static/css/print.css
Normal file
107
static/css/print.css
Normal file
@ -0,0 +1,107 @@
|
||||
@media print {
|
||||
@page {
|
||||
size: A4 landscape;
|
||||
margin: 0.8cm;
|
||||
@bottom-right {
|
||||
content: "Seite " counter(page) " von " counter(pages);
|
||||
font-size: 8pt;
|
||||
margin-right: -0.5cm;
|
||||
margin-bottom: -0.5cm;
|
||||
}
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 9pt;
|
||||
line-height: 1.3;
|
||||
background: none;
|
||||
color: #000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.dynamic-table-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.5cm;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
max-width: 29.7cm; /* A4 landscape width minus margins */
|
||||
border-collapse: collapse;
|
||||
page-break-inside: auto;
|
||||
}
|
||||
|
||||
thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
tbody {
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.1cm;
|
||||
border: 1px solid #000;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f0f0f0 !important;
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Hide elements not needed for print */
|
||||
#searchInput, .sort-icon::after, .header-container, footer, .print-date {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Column widths */
|
||||
th:nth-child(1), td:nth-child(1),
|
||||
th:nth-child(2), td:nth-child(2) {
|
||||
width: 15%;
|
||||
}
|
||||
th:nth-child(3), td:nth-child(3) {
|
||||
width: 20%;
|
||||
}
|
||||
th:nth-child(4), td:nth-child(4) {
|
||||
width: 10%;
|
||||
}
|
||||
th:nth-child(5), td:nth-child(5) {
|
||||
width: 25%;
|
||||
}
|
||||
th:nth-child(6), td:nth-child(6) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
/* Remove top margin for the first page */
|
||||
.dynamic-table-container:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Ensure content on subsequent pages starts at the top */
|
||||
@page :not(:first) {
|
||||
margin-top: 0.8cm;
|
||||
}
|
||||
}
|
21
static/images/ccheart_black.svg
Normal file
21
static/images/ccheart_black.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW 2018 (64-Bit Evaluation Version) -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="20.4978in" height="18.0152in" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 46296.26 40689.13"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.fil0 {fill:black}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<g id="_1761831900240">
|
||||
<path class="fil0" d="M23204.91 7530.98c2944.63,-3188.84 6384.04,-4639.01 10366.38,-4077.21 4110.34,579.88 7609.97,3518.41 8854.17,7479.01 957.39,3047.58 559.96,6460.83 -722.09,9573.35 -1993.98,4840.97 -7886.31,11722.09 -18555.24,16532.85 -10668.92,-4810.76 -16561.25,-11691.88 -18555.24,-16532.85 -1282.05,-3112.52 -1679.47,-6525.77 -722.09,-9573.35 1244.19,-3960.6 4743.83,-6899.13 8854.17,-7479.01 3982.46,-561.82 7421.94,888.46 10366.64,4077.48 5.4,5.84 56.52,61.37 56.53,61.36 0.04,0.04 51.9,-56.36 56.79,-61.63zm-56.79 -4522.44c-6431.69,-5048.01 -16512.25,-3730.83 -21147.65,3855.94 -1539.08,2519.03 -2117.14,5447.75 -1981.3,8355.45 235.64,5043.59 2412.75,9452.27 5610.61,13256.78 4306.02,5122.9 10531.26,9148.59 17382.21,12152.72 9.53,4.18 88.63,38.56 136.13,59.69 41.66,-17.53 114.6,-50.41 137.01,-60.3 6815.65,-3004.07 13075.56,-7030.12 17381.33,-12152.12 3198.08,-3804.33 5374.97,-8213.2 5610.61,-13256.78 135.85,-2907.7 -442.2,-5836.43 -1981.3,-8355.45 -4635.4,-7586.77 -14715.95,-8903.95 -21147.65,-3855.94z"/>
|
||||
<path class="fil0" d="M22983.64 21630.19l-2928.01 -1451.38c-1017.73,1483.99 -1758.21,2488.33 -3897.08,1902.25 -1678.91,-460.05 -2175.85,-2300.18 -2239.67,-3843.76 -87.17,-2108.39 649.94,-4543.46 3168.15,-4413.24 1609.13,83.19 2294.75,1032.23 2661.15,1885.36l3196.99 -1638.9c-1574.75,-3004.31 -5265.13,-4026.05 -8393.32,-3188.81 -3328.66,890.9 -5014.61,3952.95 -4955.5,7255.23 60.43,3375.58 1680.8,6291.51 5161.55,7052.54 1697.16,371.06 3545.13,284.81 5116.74,-503.18 1216.27,-609.83 2567.56,-1786.86 3109,-3056.12zm13802.46 0l-2928.01 -1451.38c-1017.73,1483.99 -1758.21,2488.33 -3897.08,1902.25 -1678.91,-460.05 -2175.86,-2300.18 -2239.67,-3843.76 -87.18,-2108.39 649.94,-4543.46 3168.15,-4413.24 1609.13,83.19 2294.74,1032.23 2661.15,1885.36l3196.99 -1638.9c-1574.75,-3004.31 -5265.14,-4026.05 -8393.32,-3188.81 -3328.66,890.9 -5014.61,3952.95 -4955.5,7255.23 60.42,3375.58 1680.8,6291.51 5161.55,7052.54 1697.16,371.06 3545.13,284.81 5116.74,-503.18 1216.27,-609.83 2567.56,-1786.86 3109,-3056.12z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
BIN
static/images/ccheart_black.svg_.zip
Normal file
BIN
static/images/ccheart_black.svg_.zip
Normal file
Binary file not shown.
BIN
static/images/favicon-16x16.png
Normal file
BIN
static/images/favicon-16x16.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 659 B |
BIN
static/images/favicon-32x32.png
Normal file
BIN
static/images/favicon-32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
static/images/favicon.ico
Normal file
BIN
static/images/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
BIN
static/images/favicon_io.zip
Normal file
BIN
static/images/favicon_io.zip
Normal file
Binary file not shown.
21
static/images/logo.svg
Normal file
21
static/images/logo.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Creator: CorelDRAW 2018 (64-Bit Evaluation Version) -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="20.4978in" height="18.0152in" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||
viewBox="0 0 46296.26 40689.13"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
<![CDATA[
|
||||
.fil0 {fill:black}
|
||||
]]>
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_x0020_1">
|
||||
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||
<g id="_1761831900240">
|
||||
<path class="fil0" d="M23204.91 7530.98c2944.63,-3188.84 6384.04,-4639.01 10366.38,-4077.21 4110.34,579.88 7609.97,3518.41 8854.17,7479.01 957.39,3047.58 559.96,6460.83 -722.09,9573.35 -1993.98,4840.97 -7886.31,11722.09 -18555.24,16532.85 -10668.92,-4810.76 -16561.25,-11691.88 -18555.24,-16532.85 -1282.05,-3112.52 -1679.47,-6525.77 -722.09,-9573.35 1244.19,-3960.6 4743.83,-6899.13 8854.17,-7479.01 3982.46,-561.82 7421.94,888.46 10366.64,4077.48 5.4,5.84 56.52,61.37 56.53,61.36 0.04,0.04 51.9,-56.36 56.79,-61.63zm-56.79 -4522.44c-6431.69,-5048.01 -16512.25,-3730.83 -21147.65,3855.94 -1539.08,2519.03 -2117.14,5447.75 -1981.3,8355.45 235.64,5043.59 2412.75,9452.27 5610.61,13256.78 4306.02,5122.9 10531.26,9148.59 17382.21,12152.72 9.53,4.18 88.63,38.56 136.13,59.69 41.66,-17.53 114.6,-50.41 137.01,-60.3 6815.65,-3004.07 13075.56,-7030.12 17381.33,-12152.12 3198.08,-3804.33 5374.97,-8213.2 5610.61,-13256.78 135.85,-2907.7 -442.2,-5836.43 -1981.3,-8355.45 -4635.4,-7586.77 -14715.95,-8903.95 -21147.65,-3855.94z"/>
|
||||
<path class="fil0" d="M22983.64 21630.19l-2928.01 -1451.38c-1017.73,1483.99 -1758.21,2488.33 -3897.08,1902.25 -1678.91,-460.05 -2175.85,-2300.18 -2239.67,-3843.76 -87.17,-2108.39 649.94,-4543.46 3168.15,-4413.24 1609.13,83.19 2294.75,1032.23 2661.15,1885.36l3196.99 -1638.9c-1574.75,-3004.31 -5265.13,-4026.05 -8393.32,-3188.81 -3328.66,890.9 -5014.61,3952.95 -4955.5,7255.23 60.43,3375.58 1680.8,6291.51 5161.55,7052.54 1697.16,371.06 3545.13,284.81 5116.74,-503.18 1216.27,-609.83 2567.56,-1786.86 3109,-3056.12zm13802.46 0l-2928.01 -1451.38c-1017.73,1483.99 -1758.21,2488.33 -3897.08,1902.25 -1678.91,-460.05 -2175.86,-2300.18 -2239.67,-3843.76 -87.18,-2108.39 649.94,-4543.46 3168.15,-4413.24 1609.13,83.19 2294.74,1032.23 2661.15,1885.36l3196.99 -1638.9c-1574.75,-3004.31 -5265.14,-4026.05 -8393.32,-3188.81 -3328.66,890.9 -5014.61,3952.95 -4955.5,7255.23 60.42,3375.58 1680.8,6291.51 5161.55,7052.54 1697.16,371.06 3545.13,284.81 5116.74,-503.18 1216.27,-609.83 2567.56,-1786.86 3109,-3056.12z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
67
static/index.html
Normal file
67
static/index.html
Normal file
@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/static/css/main.css">
|
||||
<link rel="stylesheet" href="/static/css/print.css">
|
||||
<title>☎ Telefonbuch ☎</title>
|
||||
<link rel="icon" type="image/x-icon" href="/static/images/favicon.ico">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
|
||||
<script src="/static/js/phonebook.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header-container">
|
||||
<img src="/static/images/logo.svg" alt="MKN Raute" class="logo">
|
||||
<h1>☎ Telefonbuch ☎</h1>
|
||||
<img src="/static/images/logo.svg" alt="MKN Raute" class="logo">
|
||||
</div>
|
||||
<input type="text" id="searchInput" onkeyup="searchTable()" placeholder="Suche nach Namen, Telefonnummern, Abteilung oder E-Mail...">
|
||||
<div class="dynamic-table-container">
|
||||
<table id="phonebookTable" class="dynamic-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="desc" aria-sort="descending">Nachname</th>
|
||||
<th class="min-width-cell">Vorname</th>
|
||||
<th class="min-width-cell dynamic-width">Telefonnummer</th>
|
||||
<th class="min-width-cell">Interne Rufnummer</th>
|
||||
<th class="min-width-cell">E-Mail</th>
|
||||
<th class="min-width-cell dynamic-width">Abteilung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .}}
|
||||
<tr>
|
||||
<td class="min-width-cell">{{.LastName}}</td>
|
||||
<td class="min-width-cell">{{.FirstName}}</td>
|
||||
<td class="min-width-cell dynamic-width">
|
||||
{{range .Phones}}
|
||||
({{.Type}}) <a href="tel:{{.PhoneNumber}}">{{.PhoneNumber}}</a><br>
|
||||
{{else}}-{{end}}
|
||||
</td>
|
||||
<td class="min-width-cell">{{if .InternalPhone}}<a href="tel:{{.InternalPhone}}">{{.InternalPhone}}</a>{{else}}-{{end}}</td>
|
||||
<td class="min-width-cell"><a href="mailto:{{.Email}}">{{.Email}}</a></td>
|
||||
<td class="min-width-cell dynamic-width">{{if .Department}}{{.Department}}{{else}}-{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="print-date">Stand der Liste: <span id="currentDate"></span></div>
|
||||
<footer>
|
||||
<div class="footer-container">
|
||||
<div class="developer">
|
||||
Entwickelt von Steffen Probst
|
||||
</div>
|
||||
<div class="license">
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html" target="_blank">GPL-3.0 Lizenz</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
document.getElementById('currentDate').textContent = new Date().toLocaleDateString('de-DE');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
84
static/js/phonebook.js
Normal file
84
static/js/phonebook.js
Normal file
@ -0,0 +1,84 @@
|
||||
function searchTable() {
|
||||
const input = document.getElementById("searchInput");
|
||||
const filter = input.value; // Entfernung von .toUpperCase()
|
||||
const table = document.getElementById("phonebookTable");
|
||||
const tr = table.getElementsByTagName("tr");
|
||||
|
||||
for (let i = 1; i < tr.length; i++) {
|
||||
let visible = false;
|
||||
const td = tr[i].getElementsByTagName("td");
|
||||
for (let j = 0; j < td.length; j++) {
|
||||
if (td[j]) {
|
||||
const txtValue = td[j].textContent || td[j].innerText;
|
||||
if (txtValue.indexOf(filter) > -1) { // Entfernung von .toUpperCase()
|
||||
visible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
tr[i].style.display = visible ? "" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
let lastSortedColumn = 0;
|
||||
let sortDirections = Array(document.querySelectorAll('#phonebookTable th').length).fill('asc');
|
||||
|
||||
// Array to keep track of the sort state for each column
|
||||
let sortStates = [];
|
||||
|
||||
function sortTable(n) {
|
||||
const table = document.getElementById("phonebookTable");
|
||||
const tbody = table.querySelector("tbody");
|
||||
const rows = Array.from(tbody.querySelectorAll("tr"));
|
||||
const th = table.querySelectorAll("th")[n];
|
||||
|
||||
// Initialize sort state if not set
|
||||
if (sortStates[n] === undefined) {
|
||||
sortStates[n] = 'asc';
|
||||
} else {
|
||||
// Toggle sort direction
|
||||
sortStates[n] = sortStates[n] === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
|
||||
const isAsc = sortStates[n] === 'asc';
|
||||
|
||||
// Clear all sorting classes
|
||||
table.querySelectorAll("th").forEach(header => header.classList.remove("asc", "desc"));
|
||||
|
||||
// Set the appropriate class for the clicked header
|
||||
th.classList.add(isAsc ? "asc" : "desc");
|
||||
|
||||
rows.sort((a, b) => {
|
||||
let aValue = a.cells[n].textContent.trim();
|
||||
let bValue = b.cells[n].textContent.trim();
|
||||
|
||||
// Check if the values are numbers
|
||||
if (!isNaN(aValue) && !isNaN(bValue)) {
|
||||
return isAsc ? aValue - bValue : bValue - aValue;
|
||||
}
|
||||
|
||||
// For non-numeric values, use localeCompare for proper string comparison
|
||||
return isAsc ?
|
||||
aValue.localeCompare(bValue, undefined, {sensitivity: 'base'}) :
|
||||
bValue.localeCompare(aValue, undefined, {sensitivity: 'base'});
|
||||
});
|
||||
|
||||
// Remove all existing rows
|
||||
while (tbody.firstChild) {
|
||||
tbody.removeChild(tbody.firstChild);
|
||||
}
|
||||
|
||||
// Add sorted rows
|
||||
tbody.append(...rows);
|
||||
}
|
||||
|
||||
// Add event listeners to table headers and initial sort
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const headers = document.querySelectorAll('#phonebookTable th');
|
||||
headers.forEach((header, index) => {
|
||||
header.addEventListener('click', () => sortTable(index));
|
||||
});
|
||||
|
||||
// Initial sort on the first column (index 0) in ascending order
|
||||
sortTable(0);
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user