Inhalt

Aktueller Ordner: duesseldorfer-schuelerinventar-python-client
⬅ Übergeordnet

duesk_client.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Düsseldorfer Schülerinventar (DÜSK) - Python Client
Mit exakter PHP-Berechnungslogik - KORRIGIERT
"""

import sys
import subprocess
import requests
import math

# GUI imports
try:
    from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, 
                                  QHBoxLayout, QLabel, QLineEdit, QPushButton, 
                                  QTableWidget, QTableWidgetItem, QMessageBox,
                                  QHeaderView, QDialog, QDialogButtonBox, 
                                  QFormLayout, QProgressDialog, QGroupBox,
                                  QTabWidget, QComboBox)
    from PyQt6.QtCore import Qt, QThread, pyqtSignal
    from PyQt6.QtGui import QFont
except ImportError:
    print("Installiere PyQt6...")
    subprocess.check_call([sys.executable, "-m", "pip", "install", "PyQt6", "requests"])
    from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, 
                                  QHBoxLayout, QLabel, QLineEdit, QPushButton, 
                                  QTableWidget, QTableWidgetItem, QMessageBox,
                                  QHeaderView, QDialog, QDialogButtonBox, 
                                  QFormLayout, QProgressDialog, QGroupBox,
                                  QTabWidget, QComboBox)
    from PyQt6.QtCore import Qt, QThread, pyqtSignal
    from PyQt6.QtGui import QFont

API_BASE_URL = "https://paul-koop.org/api/"


class APIWorker(QThread):
    finished = pyqtSignal(object)
    error = pyqtSignal(str)
    
    def __init__(self, endpoint, method="GET", data=None, params=None, user_id=None, session=None):
        super().__init__()
        self.endpoint = endpoint
        self.method = method
        self.data = data
        self.params = params
        self.user_id = user_id
        self.session = session
        
    def run(self):
        try:
            url = API_BASE_URL + self.endpoint
            headers = {'Content-Type': 'application/json'}
            
            if self.user_id and self.session:
                headers['X-User-ID'] = str(self.user_id)
                headers['X-Session'] = self.session
                
            if self.method == "GET":
                response = requests.get(url, headers=headers, params=self.params, timeout=30)
            elif self.method == "POST":
                response = requests.post(url, json=self.data, headers=headers, timeout=30)
            else:
                self.error.emit(f"Unbekannte Methode: {self.method}")
                return
                
            if response.status_code == 200:
                self.finished.emit(response.json())
            else:
                self.error.emit(f"HTTP Fehler {response.status_code}")
        except Exception as e:
            self.error.emit(str(e))


class ProfileViewDialog(QDialog):
    """Profilansicht mit exakter PHP-Berechnungslogik"""
    
    # Normwerte aus Ihrer Datenbank (normSEhs)
    NORM_SE_HS = {
        1: [21.33, 25.33, 29.33, 33.32, 37.32],
        2: [20.87, 24.95, 29.03, 33.13, 37.18],
        3: [17.93, 21.37, 24.80, 28.23, 31.67],
        4: [13.98, 17.71, 21.44, 25.17, 28.90],
        5: [24.60, 28.55, 33.04, 37.53, 42.01],
        6: [15.53, 18.97, 22.40, 25.83, 29.27]
    }
    
    NORM_FE_HS = {
        1: [12.66, 18.16, 23.66, 29.16, 34.66],
        2: [13.33, 18.42, 23.51, 28.60, 33.69],
        3: [10.75, 15.41, 20.07, 24.73, 29.39],
        4: [14.22, 15.30, 16.38, 17.46, 18.54],
        5: [14.12, 20.21, 26.30, 32.39, 38.48],
        6: [10.53, 14.51, 18.49, 22.47, 26.45]
    }
    
    # Normwerte Förderschule (normSEfs)
    NORM_SE_FS = {
        1: [17.54, 24.03, 30.53, 37.02, 43.51],
        2: [17.80, 24.26, 30.73, 37.19, 43.65],
        3: [18.03, 22.41, 26.79, 31.17, 35.55],
        4: [14.28, 15.55, 16.83, 18.10, 19.37],
        5: [20.69, 27.49, 34.29, 41.09, 47.89],
        6: [12.44, 18.06, 23.68, 29.29, 34.91]
    }
    
    NORM_FE_FS = {
        1: [15.30, 19.79, 24.28, 28.77, 33.26],
        2: [14.63, 18.94, 23.25, 27.56, 31.87],
        3: [14.62, 17.81, 21.00, 24.19, 27.38],
        4: [15.00, 15.55, 16.10, 16.65, 17.20],
        5: [18.44, 22.61, 26.78, 30.95, 35.12],
        6: [9.79, 13.97, 18.15, 22.33, 26.51]
    }
    
    def __init__(self, profile_data, parent=None):
        super().__init__(parent)
        self.profile_data = profile_data
        self.current_norm = "HS"  # HS oder FS
        self.init_ui()
        self.calculate_and_display()
        
    def init_ui(self):
        self.setWindowTitle(f"Profil: {self.profile_data.get('name', 'Unbekannt')}")
        self.setMinimumSize(1000, 700)
        
        layout = QVBoxLayout()
        
        # Name
        name_label = QLabel(f"<h2>{self.profile_data.get('name', 'Unbekannt')}</h2>")
        name_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(name_label)
        
        # Norm-Auswahl
        norm_layout = QHBoxLayout()
        norm_label = QLabel("Normtabelle:")
        self.norm_combo = QComboBox()
        self.norm_combo.addItem("Hauptschule (HS)", "HS")
        self.norm_combo.addItem("Förderschule (FS)", "FS")
        self.norm_combo.currentIndexChanged.connect(self.on_norm_changed)
        
        norm_layout.addWidget(norm_label)
        norm_layout.addWidget(self.norm_combo)
        norm_layout.addStretch()
        layout.addLayout(norm_layout)
        
        # Hauptinhalt mit zwei Spalten
        main_widget = QWidget()
        main_layout = QHBoxLayout(main_widget)
        
        # Selbsteinschätzung
        se_widget = QWidget()
        se_layout = QVBoxLayout(se_widget)
        
        se_title = QLabel("<b>Selbsteinschätzung</b>")
        se_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
        se_layout.addWidget(se_title)
        
        self.se_table = QTableWidget(6, 5)
        self.se_table.setHorizontalHeaderLabels(["1", "2", "3", "4", "5"])
        self.se_table.setVerticalHeaderLabels([
            "Arbeitsverhalten", "Lernverhalten", "Sozialverhalten", 
            "Fachkompetenz", "Personale Kompetenz", "Methodenkompetenz"
        ])
        self.se_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
        se_layout.addWidget(self.se_table)
        
        # Fremdeinschätzung
        fe_widget = QWidget()
        fe_layout = QVBoxLayout(fe_widget)
        
        fe_title = QLabel("<b>Fremdeinschätzung</b>")
        fe_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
        fe_layout.addWidget(fe_title)
        
        self.fe_table = QTableWidget(6, 5)
        self.fe_table.setHorizontalHeaderLabels(["1", "2", "3", "4", "5"])
        self.fe_table.setVerticalHeaderLabels([
            "Arbeitsverhalten", "Lernverhalten", "Sozialverhalten", 
            "Fachkompetenz", "Personale Kompetenz", "Methodenkompetenz"
        ])
        self.fe_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
        fe_layout.addWidget(self.fe_table)
        
        main_layout.addWidget(se_widget)
        main_layout.addWidget(fe_widget)
        layout.addWidget(main_widget)
        
        # Item-Tabelle
        items_group = QGroupBox("Item-Werte")
        items_layout = QVBoxLayout()
        
        self.items_table = QTableWidget(36, 3)
        self.items_table.setHorizontalHeaderLabels(["Item", "Selbst", "Fremd"])
        self.items_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
        
        item_names = [
            "Zuverlässigkeit", "Arbeitstempo", "Arbeitsplanung", "Organisationsfähigkeit",
            "Geschicklichkeit", "Ordnung", "Sorgfalt", "Kreativität", "Problemlösungsfähigkeit",
            "Abstraktionsvermögen", "Selbstständigkeit", "Belastbarkeit", "Konzentrationsfähigkeit",
            "Verantwortungsbewusstsein", "Eigeninitiative", "Leistungsbereitschaft", "Auffassungsgabe",
            "Merkfähigkeit", "Motivationsfähigkeit", "Reflektionsfähigkeit", "Teamfähigkeit",
            "Hilfsbereitschaft", "Kontaktfähigkeit", "Respektvoller Umgang", "Kommunikationsfähigkeit",
            "Einfühlungsvermögen", "Konfliktfähigkeit", "Kritikfähigkeit", "Schreiben", "Lesen",
            "Mathematik", "Naturwissenschaft", "Fremdsprachen", "Präsentationsfähigkeit",
            "PC Kenntnisse", "Fächerübergreifendes Denken"
        ]
        
        for i, name in enumerate(item_names, 1):
            se_val = self.profile_data.get(f'item{i}', 0)
            fe_val = self.profile_data.get(f'feitem{i}', 0)
            self.items_table.setItem(i-1, 0, QTableWidgetItem(name))
            self.items_table.setItem(i-1, 1, QTableWidgetItem(str(se_val)))
            self.items_table.setItem(i-1, 2, QTableWidgetItem(str(fe_val)))
        
        items_layout.addWidget(self.items_table)
        items_group.setLayout(items_layout)
        layout.addWidget(items_group)
        
        # Schließen-Button
        close_btn = QPushButton("Schließen")
        close_btn.clicked.connect(self.accept)
        layout.addWidget(close_btn)
        
        self.setLayout(layout)
        
    def on_norm_changed(self):
        """Wird aufgerufen, wenn die Normtabelle geändert wird"""
        self.current_norm = self.norm_combo.currentData()
        self.calculate_and_display()
        
    def calculate_sums(self, items):
        """Exakte PHP-Berechnung der Summen"""
        if len(items) < 36:
            items.extend([2] * (36 - len(items)))
        
        sums = [0] * 7  # Index 1-6
        
        sums[1] = sum(items[0:10])   # item1-item10
        sums[2] = sum(items[10:20])  # item11-item20
        sums[3] = sum(items[20:28]) + items[8] + items[9]  # item21-item28 + item9 + item10
        sums[4] = sum(items[28:36])  # item29-item36
        sums[5] = items[0] + items[1] + items[5] + items[6] + items[7] + items[8] + items[9] + items[11] + items[12] + items[13] + items[14]
        sums[6] = items[2] + items[3] + items[4] + items[8] + items[9] + items[10] + items[16] + items[17]
        
        return sums
        
    def calculate_profile_values(self, sums, norm):
        """Exakte PHP-Berechnung der Profilwerte (wo das X gesetzt wird)"""
        values = [0] * 6
        
        for kompetenz in range(1, 7):
            x_gesetzt = False
            for punkt in range(5):
                if sums[kompetenz] < norm[kompetenz][punkt]:
                    values[kompetenz - 1] = punkt
                    x_gesetzt = True
                    break
            if not x_gesetzt:
                values[kompetenz - 1] = 4
                
        return values
        
    def fill_table(self, table, values):
        """Füllt die Tabelle mit X an der richtigen Position"""
        for i, value in enumerate(values):
            for j in range(5):
                if j == value:
                    table.setItem(i, j, QTableWidgetItem("X"))
                else:
                    table.setItem(i, j, QTableWidgetItem(""))
                table.item(i, j).setTextAlignment(Qt.AlignmentFlag.AlignCenter)
        table.resizeColumnsToContents()
        
    def calculate_and_display(self):
        """Hauptberechnung - exakt wie in PHP"""
        # Items aus dem Profil extrahieren
        se_items = []
        fe_items = []
        
        for i in range(1, 37):
            se_val = self.profile_data.get(f'item{i}', 2)
            fe_val = self.profile_data.get(f'feitem{i}', 2)
            se_items.append(int(se_val) if se_val else 2)
            fe_items.append(int(fe_val) if fe_val else 2)
        
        # Summen berechnen
        se_sums = self.calculate_sums(se_items)
        fe_sums = self.calculate_sums(fe_items)
        
        # Normen auswählen
        if self.current_norm == "HS":
            norm_se = self.NORM_SE_HS
            norm_fe = self.NORM_FE_HS
        else:
            norm_se = self.NORM_SE_FS
            norm_fe = self.NORM_FE_FS
        
        # Profilwerte berechnen
        se_values = self.calculate_profile_values(se_sums, norm_se)
        fe_values = self.calculate_profile_values(fe_sums, norm_fe)
        
        # Tabellen füllen
        self.fill_table(self.se_table, se_values)
        self.fill_table(self.fe_table, fe_values)


class LoginDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.user_id = None
        self.session = None
        self.init_ui()
        
    def init_ui(self):
        self.setWindowTitle("DÜSK - Anmeldung")
        self.setModal(True)
        self.setMinimumSize(300, 200)
        
        layout = QVBoxLayout()
        
        title = QLabel("DÜSK - Anmeldung")
        title.setFont(QFont("Arial", 16, QFont.Weight.Bold))
        title.setAlignment(Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(title)
        
        form_layout = QFormLayout()
        self.username_input = QLineEdit()
        self.username_input.setText("gast")
        self.password_input = QLineEdit()
        self.password_input.setText("gast")
        self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
        
        form_layout.addRow("Benutzername:", self.username_input)
        form_layout.addRow("Passwort:", self.password_input)
        layout.addLayout(form_layout)
        
        button_box = QDialogButtonBox()
        login_btn = button_box.addButton("Anmelden", QDialogButtonBox.ButtonRole.AcceptRole)
        cancel_btn = button_box.addButton("Abbrechen", QDialogButtonBox.ButtonRole.RejectRole)
        login_btn.clicked.connect(self.do_login)
        cancel_btn.clicked.connect(self.reject)
        layout.addWidget(button_box)
        
        self.setLayout(layout)
        
    def do_login(self):
        username = self.username_input.text().strip()
        password = self.password_input.text().strip()
        
        self.worker = APIWorker("api_login.php", "POST", {"username": username, "password": password})
        self.worker.finished.connect(self.login_success)
        self.worker.error.connect(self.login_error)
        self.worker.start()
        
        self.progress = QProgressDialog("Anmeldung...", None, 0, 0, self)
        self.progress.setWindowModality(Qt.WindowModality.WindowModal)
        self.progress.show()
        
    def login_success(self, response):
        self.progress.close()
        if response.get('success') or response.get('userID'):
            self.user_id = response['userID']
            self.session = response['session']
            self.accept()
        else:
            QMessageBox.warning(self, "Fehler", "Anmeldung fehlgeschlagen")
            
    def login_error(self, error):
        self.progress.close()
        QMessageBox.critical(self, "Fehler", f"Verbindungsfehler: {error}")


class MainWindow(QMainWindow):
    def __init__(self, user_id, session):
        super().__init__()
        self.user_id = user_id
        self.session = session
        self.init_ui()
        self.load_profiles()
        
    def init_ui(self):
        self.setWindowTitle("DÜSK - Düsseldorfer Schülerinventar")
        self.setMinimumSize(800, 500)
        
        central = QWidget()
        self.setCentralWidget(central)
        layout = QVBoxLayout(central)
        
        # Header
        header = QLabel("DÜSK - Düsseldorfer Schülerinventar")
        header.setAlignment(Qt.AlignmentFlag.AlignCenter)
        header.setStyleSheet("font-size: 20px; font-weight: bold; background-color: #6699CC; color: white; padding: 10px;")
        layout.addWidget(header)
        
        # Refresh Button
        refresh_btn = QPushButton("Aktualisieren")
        refresh_btn.clicked.connect(self.load_profiles)
        layout.addWidget(refresh_btn)
        
        # Tabelle
        self.table = QTableWidget()
        self.table.setColumnCount(3)
        self.table.setHorizontalHeaderLabels(["Name", "Gruppe", "Aktion"])
        self.table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)
        self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
        layout.addWidget(self.table)
        
        # Footer
        footer = QLabel("Paul Koop M.A. - Thomashofstrasse 19, 52070 Aachen")
        footer.setAlignment(Qt.AlignmentFlag.AlignCenter)
        footer.setStyleSheet("background-color: #6699CC; color: white; padding: 8px; margin-top: 10px;")
        layout.addWidget(footer)
        
    def load_profiles(self):
        self.worker = APIWorker("api_profiles.php", "GET", user_id=self.user_id, session=self.session)
        self.worker.finished.connect(self.profiles_loaded)
        self.worker.error.connect(lambda e: QMessageBox.critical(self, "Fehler", f"Fehler: {e}"))
        self.worker.start()
        
    def profiles_loaded(self, response):
        if isinstance(response, list):
            self.table.setRowCount(len(response))
            for i, p in enumerate(response):
                self.table.setItem(i, 0, QTableWidgetItem(p.get('name', '')))
                self.table.setItem(i, 1, QTableWidgetItem(p.get('gruppename', '')))
                
                view_btn = QPushButton("Profil anzeigen")
                profile_id = p.get('profilID')
                view_btn.clicked.connect(lambda checked, pid=profile_id: self.load_and_show_profile(pid))
                self.table.setCellWidget(i, 2, view_btn)
            self.table.resizeRowsToContents()
            
    def load_and_show_profile(self, profile_id):
        self.progress = QProgressDialog("Lade Profil...", None, 0, 0, self)
        self.progress.setWindowModality(Qt.WindowModality.WindowModal)
        self.progress.show()
        
        self.worker2 = APIWorker("api_profiles.php", "GET", params={"id": profile_id},
                                 user_id=self.user_id, session=self.session)
        self.worker2.finished.connect(lambda r: self.show_profile(r))
        self.worker2.error.connect(lambda e: self.on_profile_error(e))
        self.worker2.start()
        
    def on_profile_error(self, error):
        self.progress.close()
        QMessageBox.critical(self, "Fehler", f"Konnte Profil nicht laden: {error}")
        
    def show_profile(self, profile_data):
        self.progress.close()
        if isinstance(profile_data, dict):
            if 'error' in profile_data:
                QMessageBox.critical(self, "Fehler", profile_data.get('error'))
            else:
                dialog = ProfileViewDialog(profile_data)
                dialog.exec()
        else:
            QMessageBox.critical(self, "Fehler", "Unerwartete Antwort vom Server")


def main():
    app = QApplication(sys.argv)
    
    login = LoginDialog()
    if login.exec() == QDialog.DialogCode.Accepted:
        window = MainWindow(login.user_id, login.session)
        window.show()
        sys.exit(app.exec())
    else:
        sys.exit(0)


if __name__ == "__main__":
    main()