Inhalt

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

duesk_client_tinker_Diagramme.py

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

"""
Düsseldorfer Schülerinventar (DÜSK) - Tkinter Vollversion mit Grafiken
Profile und Zeitreihen mit grafischen Diagrammen
"""

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

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

KOMPETENZEN = ["Arbeitsverhalten", "Lernverhalten", "Sozialverhalten", 
               "Fachkompetenz", "Personale Kompetenz", "Methodenkompetenz"]

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


class ProfileChart(tk.Canvas):
    """Diagramm für ein Profil (SE und FE im Vergleich)"""
    
    def __init__(self, parent, width=500, height=250):
        super().__init__(parent, width=width, height=height, bg='white', highlightthickness=0)
        self.width = width
        self.height = height
        self.se_values = [3, 3, 3, 3, 3, 3]
        self.fe_values = [3, 3, 3, 3, 3, 3]
        
    def set_values(self, se_values, fe_values):
        self.se_values = se_values[:6]
        self.fe_values = fe_values[:6]
        self.draw()
        
    def draw(self):
        self.delete("all")
        w, h = self.width, self.height
        
        # Diagramm-Bereich
        top, bottom = 40, h - 40
        left, right = 100, w - 20
        chart_h = bottom - top
        row_h = chart_h / 6
        
        # Hintergrund
        self.create_rectangle(left, top, right, bottom, fill='#f8f9fa', outline='#ccc')
        
        # Horizontale Gitterlinien
        for i in range(7):
            y = top + i * row_h
            self.create_line(left - 5, y, right, y, fill='#ddd', dash=(2, 2))
        
        # Vertikale Linien für Werte 1-5
        for i in range(6):
            x = left + i * ((right - left) / 5)
            self.create_line(x, top, x, bottom, fill='#ddd', dash=(2, 2))
        
        # Y-Achsen-Beschriftungen (Kompetenzen)
        for i, label in enumerate(KOMPETENZEN):
            y = top + i * row_h + row_h / 2
            self.create_text(95, y, text=label, anchor='e', font=('Arial', 8))
        
        # X-Achsen-Beschriftungen (Werte 1-5)
        for i in range(5):
            x = left + i * ((right - left) / 5) + ((right - left) / 5) / 2
            self.create_text(x, bottom + 15, text=str(i + 1), font=('Arial', 9))
        
        # SE Linie (blau)
        se_points = []
        for i, val in enumerate(self.se_values):
            x = left + (val - 1) * ((right - left) / 4)
            y = top + i * row_h + row_h / 2
            se_points.append((x, y))
        
        for i in range(len(se_points) - 1):
            self.create_line(se_points[i][0], se_points[i][1], 
                           se_points[i+1][0], se_points[i+1][1], 
                           fill='#0066cc', width=2)
        
        for x, y in se_points:
            self.create_oval(x - 5, y - 5, x + 5, y + 5, fill='#0066cc', outline='white', width=1)
        
        # FE Linie (rot)
        fe_points = []
        for i, val in enumerate(self.fe_values):
            x = left + (val - 1) * ((right - left) / 4)
            y = top + i * row_h + row_h / 2
            fe_points.append((x, y))
        
        for i in range(len(fe_points) - 1):
            self.create_line(fe_points[i][0], fe_points[i][1], 
                           fe_points[i+1][0], fe_points[i+1][1], 
                           fill='#cc0000', width=2)
        
        for x, y in fe_points:
            self.create_oval(x - 5, y - 5, x + 5, y + 5, fill='#cc0000', outline='white', width=1)
        
        # Legende
        self.create_rectangle(right - 80, top - 25, right - 50, top - 15, fill='#0066cc', outline='')
        self.create_text(right - 45, top - 20, text='SE', anchor='w', font=('Arial', 9))
        self.create_rectangle(right - 30, top - 25, right, top - 15, fill='#cc0000', outline='')
        self.create_text(right - 25, top - 20, text='FE', anchor='w', font=('Arial', 9))
        
        # Titel
        self.create_text((left + right) / 2, top - 15, text='Profil-Vergleich (SE vs. FE)', 
                        font=('Arial', 10, 'bold'))


class TimeSeriesChart(tk.Canvas):
    """Zeitreihen-Diagramm für eine Gruppe"""
    
    def __init__(self, parent, width=800, height=300):
        super().__init__(parent, width=width, height=height, bg='white', highlightthickness=0)
        self.width = width
        self.height = height
        self.data = []
        self.kompetenz_index = 0
        
    def set_data(self, data, kompetenz_index=0):
        self.data = data
        self.kompetenz_index = kompetenz_index
        self.draw()
        
    def draw(self):
        self.delete("all")
        if not self.data:
            return
            
        w, h = self.width, self.height
        top, bottom = 50, h - 50
        left, right = 80, w - 20
        
        # Titel
        kompetenz = KOMPETENZEN[self.kompetenz_index]
        self.create_text((left + right) / 2, 25, text=f'Zeitreihe: {kompetenz}', 
                        font=('Arial', 12, 'bold'))
        
        # Gitter
        for i in range(6):
            y = top + i * ((bottom - top) / 5)
            self.create_line(left, y, right, y, fill='#ddd', dash=(2, 2))
            self.create_text(left - 10, y, text=str(5 - i), anchor='e', font=('Arial', 8))
        
        # X-Achse
        n = len(self.data)
        if n == 0:
            return
        step = (right - left) / max(n - 1, 1)
        
        # Datenpunkte sammeln
        all_values = []
        for name, values in self.data:
            val = values[self.kompetenz_index] if self.kompetenz_index < len(values) else 0
            all_values.append((name, val))
        
        # Linie und Punkte
        points = []
        for i, (name, val) in enumerate(all_values):
            x = left + i * step
            y = bottom - (val - 1) * ((bottom - top) / 4)
            points.append((x, y))
        
        # Linie zeichnen
        for i in range(len(points) - 1):
            self.create_line(points[i][0], points[i][1], points[i+1][0], points[i+1][1], 
                           fill='#0066cc', width=2)
        
        # Punkte und Beschriftungen
        for i, (x, y) in enumerate(points):
            self.create_oval(x - 6, y - 6, x + 6, y + 6, fill='#0066cc', outline='white', width=1)
            self.create_text(x, y - 12, text=str(all_values[i][1]), font=('Arial', 8))
            self.create_text(x, bottom + 15, text=all_values[i][0][:15], angle=45, 
                           anchor='nw', font=('Arial', 7))
        
        # Y-Achsen-Beschriftung
        self.create_text(left - 25, (top + bottom) / 2, text='Wert (1-5)', 
                        angle=90, font=('Arial', 9))


class DueskApp:
    def __init__(self, root):
        self.root = root
        self.root.title("DÜSK - Düsseldorfer Schülerinventar")
        self.root.geometry("1200x750")
        
        self.user_id = None
        self.session = None
        self.profiles = []
        self.groups = []
        
        self.setup_login_ui()
        
    def setup_login_ui(self):
        self.login_frame = ttk.Frame(self.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=30)
        
        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, width=20)
        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, width=20, 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)
        ttk.Label(self.login_frame, text="Server: paul-koop.org\nBenutzung mit gast/gast möglich",
                  font=("Arial", 9), foreground="gray").pack()
        
    def setup_main_ui(self):
        self.main_frame = ttk.Frame(self.root)
        self.main_frame.pack(fill="both", expand=True)
        
        # Toolbar
        toolbar = ttk.Frame(self.main_frame)
        toolbar.pack(fill="x", padx=5, pady=5)
        
        ttk.Button(toolbar, text="Neues Profil", command=self.new_profile).pack(side="left", padx=5)
        ttk.Button(toolbar, text="Aktualisieren", command=self.load_profiles).pack(side="left", padx=5)
        ttk.Button(toolbar, text="Gruppen verwalten", command=self.manage_groups).pack(side="left", padx=5)
        ttk.Button(toolbar, text="Abmelden", command=self.logout).pack(side="right", padx=5)
        
        # Tabelle
        columns = ("Name", "Gruppe", "ProfilID")
        self.tree = ttk.Treeview(self.main_frame, columns=columns, show="headings", height=12)
        for col in columns:
            self.tree.heading(col, text=col)
            self.tree.column(col, width=200 if col == "Name" else 150)
        self.tree.pack(fill="both", expand=True, padx=5, pady=5)
        
        # Buttons unter der Tabelle
        btn_frame = ttk.Frame(self.main_frame)
        btn_frame.pack(fill="x", padx=5, pady=5)
        ttk.Button(btn_frame, text="Anzeigen", command=self.view_profile).pack(side="left", padx=5)
        ttk.Button(btn_frame, text="Bearbeiten", command=self.edit_profile).pack(side="left", padx=5)
        ttk.Button(btn_frame, text="Löschen", command=self.delete_profile).pack(side="left", padx=5)
        ttk.Button(btn_frame, text="Zeitreihe (Gruppe)", command=self.show_time_series).pack(side="left", padx=5)
        
        # 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)
        
        self.load_profiles()
        self.load_groups()
        
    def get_selected_profile(self):
        selection = self.tree.selection()
        if not selection:
            messagebox.showwarning("Keine Auswahl", "Bitte wählen Sie ein Profil aus.")
            return None
        item = self.tree.item(selection[0])
        profile_id = item['values'][2]
        for p in self.profiles:
            if str(p.get('profilID')) == str(profile_id):
                return p
        return None
        
    def do_login(self):
        username = self.username_entry.get().strip()
        password = self.password_entry.get().strip()
        
        try:
            resp = requests.post(API_BASE_URL + "api_login.php", 
                                json={"username": username, "password": password}, timeout=30)
            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.setup_main_ui()
            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, timeout=30)
            profiles = resp.json()
            
            if isinstance(profiles, list):
                self.profiles = profiles
                for item in self.tree.get_children():
                    self.tree.delete(item)
                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 load_groups(self):
        try:
            headers = {'X-User-ID': str(self.user_id), 'X-Session': self.session}
            resp = requests.get(API_BASE_URL + "api_groups.php", headers=headers, timeout=30)
            if resp.status_code == 200:
                self.groups = resp.json() if isinstance(resp.json(), list) else []
        except:
            self.groups = []
            
    def new_profile(self):
        dialog = ProfileEditDialog(self.root, self.user_id, self.session, self.groups)
        if dialog.result:
            self.load_profiles()
            
    def edit_profile(self):
        profile = self.get_selected_profile()
        if profile:
            headers = {'X-User-ID': str(self.user_id), 'X-Session': self.session}
            resp = requests.get(API_BASE_URL + "api_profiles.php", 
                               params={"id": profile.get('profilID')}, headers=headers, timeout=30)
            full_profile = resp.json()
            if isinstance(full_profile, dict):
                dialog = ProfileEditDialog(self.root, self.user_id, self.session, self.groups, full_profile)
                if dialog.result:
                    self.load_profiles()
                
    def view_profile(self):
        profile = self.get_selected_profile()
        if profile:
            headers = {'X-User-ID': str(self.user_id), 'X-Session': self.session}
            resp = requests.get(API_BASE_URL + "api_profiles.php", 
                               params={"id": profile.get('profilID')}, headers=headers, timeout=30)
            full_profile = resp.json()
            if isinstance(full_profile, dict):
                ProfileViewDialog(self.root, full_profile)
                
    def delete_profile(self):
        profile = self.get_selected_profile()
        if profile:
            if messagebox.askyesno("Löschen", f"Profil '{profile.get('name')}' wirklich löschen?"):
                headers = {'X-User-ID': str(self.user_id), 'X-Session': self.session}
                requests.delete(API_BASE_URL + "api_profiles.php", 
                               params={"id": profile.get('profilID')}, headers=headers, timeout=30)
                self.load_profiles()
                
    def show_time_series(self):
        profile = self.get_selected_profile()
        if not profile:
            return
        group_id = profile.get('gruppeID')
        group_name = profile.get('gruppename', 'Unbekannt')
        
        # Alle Profile der gleichen Gruppe mit vollständigen Daten laden
        group_profiles = []
        for p in self.profiles:
            if p.get('gruppeID') == group_id:
                headers = {'X-User-ID': str(self.user_id), 'X-Session': self.session}
                resp = requests.get(API_BASE_URL + "api_profiles.php", 
                                   params={"id": p.get('profilID')}, headers=headers, timeout=30)
                full = resp.json()
                if isinstance(full, dict):
                    group_profiles.append(full)
                    
        if group_profiles:
            TimeSeriesDialog(self.root, group_name, group_profiles)
        else:
            messagebox.showinfo("Info", "Keine weiteren Profile in dieser Gruppe.")
            
    def manage_groups(self):
        dialog = GroupManagerDialog(self.root, self.user_id, self.session, self.groups, self.load_groups)
        if dialog.result:
            self.load_groups()
            self.load_profiles()
            
    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, timeout=30)
        except:
            pass
        self.main_frame.pack_forget()
        self.setup_login_ui()


class ProfileEditDialog:
    def __init__(self, parent, user_id, session, groups, profile=None):
        self.parent = parent
        self.user_id = user_id
        self.session = session
        self.groups = groups
        self.profile = profile
        self.result = False
        self.se_vars = {}
        self.fe_vars = {}
        
        self.dialog = tk.Toplevel(parent)
        self.dialog.title("Profil bearbeiten" if profile else "Neues Profil")
        self.dialog.geometry("900x700")
        
        self.setup_ui()
        if profile:
            self.load_values()
            
        self.dialog.transient(parent)
        self.dialog.grab_set()
        parent.wait_window(self.dialog)
        
    def setup_ui(self):
        # Basis
        info_frame = ttk.LabelFrame(self.dialog, text="Profil-Informationen")
        info_frame.pack(fill="x", padx=10, pady=5)
        
        ttk.Label(info_frame, text="Name:").grid(row=0, column=0, padx=5, pady=5)
        self.name_entry = ttk.Entry(info_frame, width=30)
        self.name_entry.grid(row=0, column=1, padx=5, pady=5)
        if self.profile:
            self.name_entry.insert(0, self.profile.get('name', ''))
            
        ttk.Label(info_frame, text="Gruppe:").grid(row=1, column=0, padx=5, pady=5)
        self.group_combo = ttk.Combobox(info_frame, width=27)
        group_names = [g.get('name', '') for g in self.groups]
        self.group_combo['values'] = group_names
        if self.groups:
            self.group_combo.current(0)
        if self.profile and self.profile.get('gruppename'):
            for i, g in enumerate(self.groups):
                if g.get('name') == self.profile.get('gruppename'):
                    self.group_combo.current(i)
                    break
        self.group_combo.grid(row=1, column=1, padx=5, pady=5)
        
        ttk.Label(info_frame, text="Neue Gruppe:").grid(row=2, column=0, padx=5, pady=5)
        self.new_group_entry = ttk.Entry(info_frame, width=30)
        self.new_group_entry.grid(row=2, column=1, padx=5, pady=5)
        
        # Notebook
        notebook = ttk.Notebook(self.dialog)
        notebook.pack(fill="both", expand=True, padx=10, pady=5)
        
        se_frame = ttk.Frame(notebook)
        notebook.add(se_frame, text="Selbsteinschätzung")
        self.create_item_grid(se_frame, self.se_vars, "se")
        
        fe_frame = ttk.Frame(notebook)
        notebook.add(fe_frame, text="Fremdeinschätzung")
        self.create_item_grid(fe_frame, self.fe_vars, "fe")
        
        # Buttons
        btn_frame = ttk.Frame(self.dialog)
        btn_frame.pack(fill="x", padx=10, pady=10)
        ttk.Button(btn_frame, text="Speichern", command=self.save).pack(side="right", padx=5)
        ttk.Button(btn_frame, text="Abbrechen", command=self.dialog.destroy).pack(side="right", padx=5)
        
    def create_item_grid(self, parent, vars_dict, prefix):
        canvas = tk.Canvas(parent)
        scrollbar = ttk.Scrollbar(parent, orient="vertical", command=canvas.yview)
        scrollable_frame = ttk.Frame(canvas)
        
        scrollable_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
        canvas.configure(yscrollcommand=scrollbar.set)
        
        for i, item_name in enumerate(ITEMS, 1):
            frame = ttk.LabelFrame(scrollable_frame, text=f"{i:2d}. {item_name}")
            frame.pack(fill="x", padx=5, pady=2)
            
            var = tk.IntVar(value=2)
            vars_dict[i] = var
            
            for val, label in [(4, "trifft voll zu (4)"), (3, "trifft zu (3)"), 
                              (2, "trifft teilweise zu (2)"), (1, "trifft nicht zu (1)")]:
                rb = ttk.Radiobutton(frame, text=label, variable=var, value=val)
                rb.pack(side="left", padx=5)
                
        canvas.pack(side="left", fill="both", expand=True)
        scrollbar.pack(side="right", fill="y")
        
    def load_values(self):
        for i in range(1, 37):
            se_val = self.profile.get(f'item{i}', 2)
            if se_val and i in self.se_vars:
                self.se_vars[i].set(se_val)
            fe_val = self.profile.get(f'feitem{i}', 2)
            if fe_val and i in self.fe_vars:
                self.fe_vars[i].set(fe_val)
                
    def save(self):
        name = self.name_entry.get().strip()
        if not name:
            messagebox.showerror("Fehler", "Bitte Name eingeben")
            return
            
        group_name = self.group_combo.get()
        new_group = self.new_group_entry.get().strip()
        
        data = {'name': name}
        
        if new_group:
            data['namegruppe'] = new_group
        elif group_name:
            for g in self.groups:
                if g.get('name') == group_name:
                    data['gruppeID'] = g.get('gruppeID')
                    break
                    
        for i in range(1, 37):
            data[f'item{i}'] = self.se_vars[i].get()
            data[f'feitem{i}'] = self.fe_vars[i].get()
            
        if self.profile:
            data['profilID'] = self.profile.get('profilID')
            method = requests.put
        else:
            method = requests.post
            
        try:
            headers = {'Content-Type': 'application/json', 'X-User-ID': str(self.user_id), 'X-Session': self.session}
            resp = method(API_BASE_URL + "api_profiles.php", json=data, headers=headers, timeout=30)
            result = resp.json()
            if result.get('success'):
                self.result = True
                self.dialog.destroy()
            else:
                messagebox.showerror("Fehler", result.get('error', 'Unbekannter Fehler'))
        except Exception as e:
            messagebox.showerror("Fehler", str(e))


class ProfileViewDialog:
    def __init__(self, parent, profile):
        self.parent = parent
        self.profile = profile
        self.current_norm = "HS"
        
        self.dialog = tk.Toplevel(parent)
        self.dialog.title(f"Profil: {profile.get('name', 'Unbekannt')}")
        self.dialog.geometry("1100x800")
        
        self.setup_ui()
        self.calculate()
        
    def setup_ui(self):
        # Header
        header = ttk.Frame(self.dialog)
        header.pack(fill="x", padx=10, pady=5)
        ttk.Label(header, text=f"Name: {self.profile.get('name', 'Unbekannt')}", 
                  font=("Arial", 14, "bold")).pack(side="left")
        ttk.Label(header, text=f"Profil-ID: {self.profile.get('profilID', '?')}").pack(side="right")
        
        # Normauswahl
        norm_frame = ttk.Frame(self.dialog)
        norm_frame.pack(fill="x", padx=10, pady=5)
        ttk.Label(norm_frame, text="Normtabelle:").pack(side="left")
        self.norm_var = tk.StringVar(value="HS")
        hs_radio = ttk.Radiobutton(norm_frame, text="Hauptschule (HS)", variable=self.norm_var, value="HS", command=self.calculate)
        fs_radio = ttk.Radiobutton(norm_frame, text="Förderschule (FS)", variable=self.norm_var, value="FS", command=self.calculate)
        hs_radio.pack(side="left", padx=5)
        fs_radio.pack(side="left", padx=5)
        
        # Notebook
        self.notebook = ttk.Notebook(self.dialog)
        self.notebook.pack(fill="both", expand=True, padx=10, pady=5)
        
        self.se_frame = ttk.Frame(self.notebook)
        self.notebook.add(self.se_frame, text="Selbsteinschätzung (SE)")
        
        self.fe_frame = ttk.Frame(self.notebook)
        self.notebook.add(self.fe_frame, text="Fremdeinschätzung (FE)")
        
        self.stats_frame = ttk.Frame(self.notebook)
        self.notebook.add(self.stats_frame, text="Statistik")
        
        self.items_frame = ttk.Frame(self.notebook)
        self.notebook.add(self.items_frame, text="Alle Items (36)")
        
    def calculate_sums(self, items):
        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]
        return sums
        
    def get_profile_values(self, sums, norm):
        values = [0] * 6
        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
        return values
        
    def create_competence_view(self, parent, title, items, norm, show_se=True, show_fe=True):
        # Clear Frame
        for widget in parent.winfo_children():
            widget.destroy()
            
        # Diagramm
        chart = ProfileChart(parent, width=500, height=220)
        chart.pack(pady=5)
            
        # Tabelle
        ttk.Label(parent, text=title, font=("Arial", 12, "bold")).pack(pady=5)
        
        tree = ttk.Treeview(parent, columns=("kompetenz", "1", "2", "3", "4", "5"), show="headings", height=6)
        tree.heading("kompetenz", text="Kompetenz")
        for i in range(1, 6):
            tree.heading(str(i), text=str(i))
        tree.column("kompetenz", width=150)
        for i in range(1, 6):
            tree.column(str(i), width=50, anchor="center")
        tree.pack(pady=5)
        
        sums = self.calculate_sums(items)
        values = self.get_profile_values(sums, norm)
        chart_values = [v + 1 for v in values]
        
        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)
            
        text = " | ".join([f"{k}: {v}" for k, v in zip(KOMPETENZEN, chart_values)])
        ttk.Label(parent, text=text, font=("Courier", 9)).pack(pady=5)
        
        return chart_values, chart
        
    def calculate(self):
        se_items = [int(self.profile.get(f'item{i}', 2)) for i in range(1, 37)]
        fe_items = [int(self.profile.get(f'feitem{i}', 2)) for i in range(1, 37)]
        
        if self.norm_var.get() == "HS":
            norm_se, norm_fe = NORM_SE_HS, NORM_FE_HS
        else:
            norm_se, norm_fe = NORM_SE_FS, NORM_FE_FS
            
        # Selbsteinschätzung
        se_chart_values, se_chart = self.create_competence_view(
            self.se_frame, "Selbsteinschätzung", se_items, norm_se, show_se=True, show_fe=False)
        se_chart.set_values(se_chart_values, [0]*6)
        
        # Fremdeinschätzung
        fe_chart_values, fe_chart = self.create_competence_view(
            self.fe_frame, "Fremdeinschätzung", fe_items, norm_fe, show_se=False, show_fe=True)
        fe_chart.set_values([0]*6, fe_chart_values)
        
        self.create_stats_view(se_chart_values, fe_chart_values, se_items, fe_items)
        self.create_items_view(se_items, fe_items)
        
    def create_stats_view(self, se_chart, fe_chart, se_items, fe_items):
        for widget in self.stats_frame.winfo_children():
            widget.destroy()
            
        # Vergleichs-Diagramm (SE und FE gemeinsam)
        compare_chart = ProfileChart(self.stats_frame, width=600, height=250)
        compare_chart.pack(pady=10)
        compare_chart.set_values(se_chart, fe_chart)
        
        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_chart, fe_chart)
        agree = sum(1 for s, f in zip(se_items, fe_items) if s == f) * 100 / 36
        
        # Statistik-Labels
        stats_frame = ttk.Frame(self.stats_frame)
        stats_frame.pack(pady=10)
        ttk.Label(stats_frame, text=f"Korrelation: {corr:.2f}", font=("Arial", 12, "bold")).pack(side="left", padx=20)
        ttk.Label(stats_frame, text=f"Übereinstimmung: {agree:.1f}%", font=("Arial", 12, "bold")).pack(side="left", padx=20)
        
        # Interpretation
        text = tk.Text(self.stats_frame, wrap="word", height=10, width=80)
        text.pack(pady=10, padx=10, fill="both", expand=True)
        
        interpretation = f"Die Korrelation von {corr:.2f} bedeutet: "
        if corr >= 0.8:
            interpretation += "Sehr gute Übereinstimmung zwischen Selbst- und Fremdeinschätzung.\n\n"
        elif corr >= 0.5:
            interpretation += "Mäßige Übereinstimmung zwischen Selbst- und Fremdeinschätzung.\n\n"
        elif corr >= 0.3:
            interpretation += "Schwache Übereinstimmung zwischen Selbst- und Fremdeinschätzung.\n\n"
        else:
            interpretation += "Keine signifikante Übereinstimmung zwischen Selbst- und Fremdeinschätzung.\n\n"
            
        ratings = ["weit unterdurchschnittlich", "unterdurchschnittlich", "durchschnittlich", 
                   "überdurchschnittlich", "weit überdurchschnittlich"]
        
        interpretation += "Auswertung der Kompetenzen:\n"
        interpretation += "-" * 50 + "\n"
        interpretation += "Selbsteinschätzung:\n"
        for i, v in enumerate(se_chart):
            interpretation += f"  • {KOMPETENZEN[i]}: {ratings[v-1]}\n"
        interpretation += "\nFremdeinschätzung:\n"
        for i, v in enumerate(fe_chart):
            interpretation += f"  • {KOMPETENZEN[i]}: {ratings[v-1]}\n"
            
        text.insert("1.0", interpretation)
        text.config(state="disabled")
        
    def create_items_view(self, se_items, fe_items):
        for widget in self.items_frame.winfo_children():
            widget.destroy()
            
        container = ttk.Frame(self.items_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=280)
        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)
        
        for i, name in enumerate(ITEMS, 1):
            tree.insert("", "end", values=(f"{i:2d}. {name}", se_items[i-1], fe_items[i-1]))


class TimeSeriesDialog:
    def __init__(self, parent, group_name, profiles):
        self.dialog = tk.Toplevel(parent)
        self.dialog.title(f"Zeitreihe - {group_name}")
        self.dialog.geometry("1000x700")
        
        ttk.Label(self.dialog, text=f"Gruppe: {group_name}", font=("Arial", 14, "bold")).pack(pady=10)
        ttk.Label(self.dialog, text=f"Anzahl Profile: {len(profiles)}").pack()
        
        # Auswahl der Kompetenz für Zeitreihe
        select_frame = ttk.Frame(self.dialog)
        select_frame.pack(pady=10)
        ttk.Label(select_frame, text="Kompetenz für Zeitreihe:").pack(side="left", padx=5)
        
        self.kompetenz_var = tk.IntVar(value=0)
        for i, komp in enumerate(KOMPETENZEN):
            rb = ttk.Radiobutton(select_frame, text=komp, variable=self.kompetenz_var, value=i,
                                command=self.update_chart)
            rb.pack(side="left", padx=3)
        
        # Zeitreihen-Diagramm
        self.chart = TimeSeriesChart(self.dialog, width=900, height=300)
        self.chart.pack(pady=10)
        
        # Tabelle mit Kompetenzwerten
        columns = ("ProfilID", "Name") + tuple(KOMPETENZEN)
        tree_frame = ttk.Frame(self.dialog)
        tree_frame.pack(fill="both", expand=True, padx=10, pady=10)
        
        scroll_y = ttk.Scrollbar(tree_frame, orient="vertical")
        scroll_x = ttk.Scrollbar(tree_frame, orient="horizontal")
        
        self.tree = ttk.Treeview(tree_frame, columns=columns, show="headings",
                                 yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)
        for col in columns:
            self.tree.heading(col, text=col)
            width = 80 if col != "Name" else 100
            self.tree.column(col, width=width, anchor="center" if col != "Name" else "w")
        
        scroll_y.config(command=self.tree.yview)
        scroll_x.config(command=self.tree.xview)
        scroll_y.pack(side="right", fill="y")
        scroll_x.pack(side="bottom", fill="x")
        self.tree.pack(fill="both", expand=True)
        
        # Daten vorbereiten
        self.profile_data = []  # (name, [kompetenzwerte])
        for p in sorted(profiles, key=lambda x: int(x.get('profilID', 0))):
            # Berechne Kompetenzwerte
            items = [int(p.get(f'item{i}', 2)) for i in range(1, 37)]
            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]
            
            values = [0] * 6
            for k in range(1, 7):
                placed = False
                for pu in range(5):
                    if sums[k] < NORM_SE_HS[k][pu]:
                        values[k-1] = pu
                        placed = True
                        break
                if not placed:
                    values[k-1] = 4
            kompetenz_werte = [v + 1 for v in values]
            
            self.profile_data.append((p.get('name', ''), kompetenz_werte))
            
            # Tabelle füllen
            self.tree.insert("", "end", values=(
                p.get('profilID', ''),
                p.get('name', ''),
                kompetenz_werte[0], kompetenz_werte[1], kompetenz_werte[2],
                kompetenz_werte[3], kompetenz_werte[4], kompetenz_werte[5]
            ))
        
        self.update_chart()
        
        ttk.Button(self.dialog, text="Schließen", command=self.dialog.destroy).pack(pady=10)
        
    def update_chart(self):
        kompetenz_idx = self.kompetenz_var.get()
        chart_data = [(name, values) for name, values in self.profile_data]
        self.chart.set_data(chart_data, kompetenz_idx)


class GroupManagerDialog:
    def __init__(self, parent, user_id, session, groups, refresh_callback):
        self.parent = parent
        self.user_id = user_id
        self.session = session
        self.groups = groups
        self.refresh_callback = refresh_callback
        self.result = False
        
        self.dialog = tk.Toplevel(parent)
        self.dialog.title("Gruppenverwaltung")
        self.dialog.geometry("400x400")
        
        self.tree = ttk.Treeview(self.dialog, columns=("name",), show="headings", height=10)
        self.tree.heading("name", text="Gruppenname")
        self.tree.column("name", width=250)
        self.tree.pack(fill="both", expand=True, padx=10, pady=10)
        
        self.refresh_list()
        
        add_frame = ttk.Frame(self.dialog)
        add_frame.pack(fill="x", padx=10, pady=5)
        self.new_name_entry = ttk.Entry(add_frame, width=25)
        self.new_name_entry.pack(side="left", padx=5)
        ttk.Button(add_frame, text="Hinzufügen", command=self.add_group).pack(side="left", padx=5)
        
        btn_frame = ttk.Frame(self.dialog)
        btn_frame.pack(fill="x", padx=10, pady=10)
        ttk.Button(btn_frame, text="Löschen", command=self.delete_group).pack(side="left", padx=5)
        ttk.Button(btn_frame, text="Schließen", command=self.dialog.destroy).pack(side="right", padx=5)
        
        self.dialog.transient(parent)
        self.dialog.grab_set()
        parent.wait_window(self.dialog)
        
    def refresh_list(self):
        for item in self.tree.get_children():
            self.tree.delete(item)
        for g in self.groups:
            self.tree.insert("", "end", values=(g.get('name', ''),), tags=(g.get('gruppeID'),))
            
    def get_selected_group(self):
        selection = self.tree.selection()
        if selection:
            item = self.tree.item(selection[0])
            group_name = item['values'][0]
            for g in self.groups:
                if g.get('name') == group_name:
                    return g
        return None
        
    def add_group(self):
        name = self.new_name_entry.get().strip()
        if not name:
            return
        try:
            headers = {'Content-Type': 'application/json', 'X-User-ID': str(self.user_id), 'X-Session': self.session}
            resp = requests.post(API_BASE_URL + "api_groups.php", json={"name": name}, headers=headers, timeout=30)
            if resp.status_code == 200:
                self.new_name_entry.delete(0, tk.END)
                self.result = True
                self.refresh_callback()
                self.load_groups()
        except:
            pass
            
    def delete_group(self):
        group = self.get_selected_group()
        if group and messagebox.askyesno("Löschen", f"Gruppe '{group.get('name')}' wirklich löschen?"):
            try:
                headers = {'X-User-ID': str(self.user_id), 'X-Session': self.session}
                requests.delete(API_BASE_URL + "api_groups.php", params={"id": group.get('gruppeID')}, headers=headers, timeout=30)
                self.result = True
                self.refresh_callback()
                self.load_groups()
            except:
                pass
                
    def load_groups(self):
        try:
            headers = {'X-User-ID': str(self.user_id), 'X-Session': self.session}
            resp = requests.get(API_BASE_URL + "api_groups.php", headers=headers, timeout=30)
            if resp.status_code == 200:
                self.groups = resp.json() if isinstance(resp.json(), list) else []
                self.refresh_list()
        except:
            pass


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

if __name__ == "__main__":
    main()