Inhalt

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

duesk_client_minimal_tinker.py

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

"""
DÜSK - Tkinter Version (keine PyQt6 nötig)
"""

import tkinter as tk
from tkinter import ttk, messagebox
import requests
import math
import threading

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

# Normwerte
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]}

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]}


class DueskApp:
    def __init__(self, root):
        self.root = root
        self.root.title("DÜSK - Düsseldorfer Schülerinventar")
        self.root.geometry("1000x600")
        
        self.user_id = None
        self.session = None
        self.profiles = []
        
        # Login Frame
        self.login_frame = ttk.Frame(root)
        self.login_frame.pack(fill="both", expand=True)
        
        ttk.Label(self.login_frame, text="DÜSK - Düsseldorfer Schülerinventar", 
                  font=("Arial", 20, "bold")).pack(pady=20)
        
        form = ttk.Frame(self.login_frame)
        form.pack(pady=20)
        
        ttk.Label(form, text="Benutzername:").grid(row=0, column=0, padx=5, pady=5)
        self.username_entry = ttk.Entry(form)
        self.username_entry.grid(row=0, column=1, padx=5, pady=5)
        self.username_entry.insert(0, "gast")
        
        ttk.Label(form, text="Passwort:").grid(row=1, column=0, padx=5, pady=5)
        self.password_entry = ttk.Entry(form, show="*")
        self.password_entry.grid(row=1, column=1, padx=5, pady=5)
        self.password_entry.insert(0, "gast")
        
        ttk.Button(self.login_frame, text="Anmelden", command=self.do_login).pack(pady=20)
        
        # Main Frame (wird nach Login sichtbar)
        self.main_frame = ttk.Frame(root)
        
        # Toolbar
        toolbar = ttk.Frame(self.main_frame)
        toolbar.pack(fill="x", padx=5, pady=5)
        
        ttk.Button(toolbar, text="Aktualisieren", command=self.load_profiles).pack(side="left", padx=5)
        ttk.Button(toolbar, text="Abmelden", command=self.logout).pack(side="left", padx=5)
        
        # Tabelle
        self.tree = ttk.Treeview(self.main_frame, columns=("name", "gruppe", "id"), show="headings")
        self.tree.heading("name", text="Name")
        self.tree.heading("gruppe", text="Gruppe")
        self.tree.heading("id", text="ProfilID")
        self.tree.column("name", width=200)
        self.tree.column("gruppe", width=150)
        self.tree.column("id", width=80)
        self.tree.pack(fill="both", expand=True, padx=5, pady=5)
        
        # Doppelklick auf Profil
        self.tree.bind("<Double-1>", self.on_profile_double_click)
        
        # Statusbar
        self.status_var = tk.StringVar()
        self.status_var.set("Bereit")
        statusbar = ttk.Label(self.main_frame, textvariable=self.status_var, relief="sunken")
        statusbar.pack(fill="x", padx=5, pady=5)
        
    def do_login(self):
        username = self.username_entry.get()
        password = self.password_entry.get()
        
        try:
            resp = requests.post(API_BASE_URL + "api_login.php", 
                                json={"username": username, "password": password})
            data = resp.json()
            
            if data.get('success') or data.get('userID'):
                self.user_id = data['userID']
                self.session = data['session']
                self.login_frame.pack_forget()
                self.main_frame.pack(fill="both", expand=True)
                self.load_profiles()
            else:
                messagebox.showerror("Fehler", "Anmeldung fehlgeschlagen")
        except Exception as e:
            messagebox.showerror("Fehler", f"Verbindungsfehler: {e}")
            
    def load_profiles(self):
        self.status_var.set("Lade Profile...")
        self.root.update()
        
        try:
            headers = {'X-User-ID': str(self.user_id), 'X-Session': self.session}
            resp = requests.get(API_BASE_URL + "api_profiles.php", headers=headers)
            profiles = resp.json()
            
            if isinstance(profiles, list):
                self.profiles = profiles
                # Tabelle leeren
                for item in self.tree.get_children():
                    self.tree.delete(item)
                # Einträge hinzufügen
                for p in profiles:
                    self.tree.insert("", "end", values=(p.get('name', ''), p.get('gruppename', ''), p.get('profilID', '')))
                self.status_var.set(f"{len(profiles)} Profile geladen")
            else:
                self.status_var.set("Keine Profile gefunden")
        except Exception as e:
            self.status_var.set(f"Fehler: {e}")
            
    def on_profile_double_click(self, event):
        selection = self.tree.selection()
        if selection:
            item = self.tree.item(selection[0])
            profile_id = item['values'][2]
            self.show_profile(profile_id)
            
    def show_profile(self, profile_id):
        # Profil laden
        headers = {'X-User-ID': str(self.user_id), 'X-Session': self.session}
        resp = requests.get(API_BASE_URL + "api_profiles.php", params={"id": profile_id}, headers=headers)
        profile = resp.json()
        
        if isinstance(profile, dict):
            self.open_profile_window(profile)
            
    def open_profile_window(self, profile):
        """Öffnet ein neues Fenster mit der Profilansicht"""
        win = tk.Toplevel(self.root)
        win.title(f"Profil: {profile.get('name', 'Unbekannt')}")
        win.geometry("900x700")
        
        notebook = ttk.Notebook(win)
        notebook.pack(fill="both", expand=True, padx=10, pady=10)
        
        # Tab: Selbsteinschätzung
        se_frame = ttk.Frame(notebook)
        notebook.add(se_frame, text="Selbsteinschätzung")
        self.create_competence_view(se_frame, profile, "se")
        
        # Tab: Fremdeinschätzung
        fe_frame = ttk.Frame(notebook)
        notebook.add(fe_frame, text="Fremdeinschätzung")
        self.create_competence_view(fe_frame, profile, "fe")
        
        # Tab: Statistik
        stats_frame = ttk.Frame(notebook)
        notebook.add(stats_frame, text="Statistik")
        self.create_stats_view(stats_frame, profile)
        
        # Tab: Alle Items
        items_frame = ttk.Frame(notebook)
        notebook.add(items_frame, text="Alle Items")
        self.create_items_view(items_frame, profile)
        
    def create_competence_view(self, parent, profile, prefix):
        """Erstellt die Ansicht für Kompetenzen (SE oder FE)"""
        # Normauswahl
        norm_frame = ttk.Frame(parent)
        norm_frame.pack(fill="x", padx=10, pady=5)
        
        ttk.Label(norm_frame, text="Normtabelle:").pack(side="left", padx=5)
        norm_var = tk.StringVar(value="HS")
        hs_radio = ttk.Radiobutton(norm_frame, text="Hauptschule (HS)", variable=norm_var, value="HS")
        fs_radio = ttk.Radiobutton(norm_frame, text="Förderschule (FS)", variable=norm_var, value="FS")
        hs_radio.pack(side="left", padx=5)
        fs_radio.pack(side="left", padx=5)
        
        # Tabelle für Kompetenzen
        table_frame = ttk.Frame(parent)
        table_frame.pack(fill="both", expand=True, padx=10, pady=5)
        
        tree = ttk.Treeview(table_frame, columns=("kompetenz", "1", "2", "3", "4", "5"), show="headings", height=6)
        tree.heading("kompetenz", text="Kompetenz")
        tree.heading("1", text="1")
        tree.heading("2", text="2")
        tree.heading("3", text="3")
        tree.heading("4", text="4")
        tree.heading("5", text="5")
        tree.column("kompetenz", width=150)
        for col in ["1", "2", "3", "4", "5"]:
            tree.column(col, width=50, anchor="center")
        tree.pack(fill="both", expand=True)
        
        # Update-Funktion bei Normwechsel
        def update_view(*args):
            # Items extrahieren
            items = [int(profile.get(f'{prefix}item{i}', 2)) for i in range(1, 37)]
            
            # Summen berechnen
            sums = [0] * 7
            sums[1] = sum(items[0:10])
            sums[2] = sum(items[10:20])
            sums[3] = sum(items[20:28]) + items[8] + items[9]
            sums[4] = sum(items[28:36])
            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]
            
            # Normen auswählen
            if norm_var.get() == "HS":
                norm = NORM_SE_HS if prefix == "se" else NORM_FE_HS
            else:
                norm = NORM_SE_FS if prefix == "se" else NORM_FE_FS
            
            # Profilwerte berechnen (0-4)
            values = [0] * 6
            kompetenzen = ["Arbeitsverhalten", "Lernverhalten", "Sozialverhalten", 
                           "Fachkompetenz", "Personale Kompetenz", "Methodenkompetenz"]
            for k in range(1, 7):
                placed = False
                for p in range(5):
                    if sums[k] < norm[k][p]:
                        values[k-1] = p
                        placed = True
                        break
                if not placed:
                    values[k-1] = 4
            
            # Tabelle füllen
            for item in tree.get_children():
                tree.delete(item)
            for i, (komp, val) in enumerate(zip(kompetenzen, values)):
                row = [komp]
                for j in range(5):
                    row.append("X" if j == val else "")
                tree.insert("", "end", values=row)
        
        norm_var.trace_add("write", update_view)
        update_view()
        
    def create_stats_view(self, parent, profile):
        """Statistik-Ansicht"""
        frame = ttk.Frame(parent)
        frame.pack(fill="both", expand=True, padx=10, pady=10)
        
        # Items extrahieren
        se_items = [int(profile.get(f'item{i}', 2)) for i in range(1, 37)]
        fe_items = [int(profile.get(f'feitem{i}', 2)) for i in range(1, 37)]
        
        # Summen für Korrelation (vereinfacht)
        se_sums = [0] * 6
        fe_sums = [0] * 6
        
        for i in range(6):
            se_sums[i] = sum(se_items[i*10:(i+1)*10]) if i < 3 else sum(se_items[28:36]) if i == 3 else 0
            fe_sums[i] = sum(fe_items[i*10:(i+1)*10]) if i < 3 else sum(fe_items[28:36]) if i == 3 else 0
        
        # Korrelation berechnen
        def calc_corr(a, b):
            ma, mb = sum(a)/6, sum(b)/6
            num = sum((a[i]-ma)*(b[i]-mb) for i in range(6))
            den = math.sqrt(sum((a[i]-ma)**2 for i in range(6)) * sum((b[i]-mb)**2 for i in range(6)))
            return num/den if den != 0 else 0
        
        corr = calc_corr(se_sums, fe_sums)
        agree = sum(1 for s, f in zip(se_items, fe_items) if s == f) * 100 / 36
        
        ttk.Label(frame, text=f"Korrelation: {corr:.2f}", font=("Arial", 12)).pack(pady=5)
        ttk.Label(frame, text=f"Übereinstimmung: {agree:.1f}%", font=("Arial", 12)).pack(pady=5)
        
        # Interpretation
        text = tk.Text(frame, wrap="word", height=10)
        text.pack(fill="both", expand=True, pady=10)
        
        interpretation = f"Die Korrelation von {corr:.2f} bedeutet: "
        if corr >= 0.8:
            interpretation += "Sehr gute Übereinstimmung zwischen Selbst- und Fremdeinschätzung."
        elif corr >= 0.5:
            interpretation += "Mäßige Übereinstimmung zwischen Selbst- und Fremdeinschätzung."
        elif corr >= 0.3:
            interpretation += "Schwache Übereinstimmung zwischen Selbst- und Fremdeinschätzung."
        else:
            interpretation += "Keine signifikante Übereinstimmung zwischen Selbst- und Fremdeinschätzung."
        
        text.insert("1.0", interpretation)
        text.config(state="disabled")
        
    def create_items_view(self, parent, profile):
        """Alle Items anzeigen"""
        frame = ttk.Frame(parent)
        frame.pack(fill="both", expand=True, padx=10, pady=10)
        
        # Scrollbare Tabelle
        container = ttk.Frame(frame)
        container.pack(fill="both", expand=True)
        
        scroll_y = ttk.Scrollbar(container, orient="vertical")
        scroll_x = ttk.Scrollbar(container, orient="horizontal")
        
        tree = ttk.Treeview(container, columns=("item", "se", "fe"), show="headings",
                           yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)
        tree.heading("item", text="Item")
        tree.heading("se", text="Selbst")
        tree.heading("fe", text="Fremd")
        tree.column("item", width=250)
        tree.column("se", width=50, anchor="center")
        tree.column("fe", width=50, anchor="center")
        
        scroll_y.config(command=tree.yview)
        scroll_x.config(command=tree.xview)
        scroll_y.pack(side="right", fill="y")
        scroll_x.pack(side="bottom", fill="x")
        tree.pack(fill="both", expand=True)
        
        items = [
            "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(items, 1):
            se_val = profile.get(f'item{i}', 2)
            fe_val = profile.get(f'feitem{i}', 2)
            tree.insert("", "end", values=(name, se_val, fe_val))
            
    def logout(self):
        try:
            headers = {'X-User-ID': str(self.user_id), 'X-Session': self.session}
            requests.post(API_BASE_URL + "api_logout.php", headers=headers)
        except:
            pass
        self.main_frame.pack_forget()
        self.login_frame.pack(fill="both", expand=True)
        self.user_id = None
        self.session = None


def main():
    root = tk.Tk()
    app = DueskApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()