Inhalt
Aktueller Ordner:
duesseldorfer-schuelerinventar-swift-client/Duesk/ViewsProfileDetailView.swift
import SwiftUI
import Charts
struct ProfileDetailView: View {
let profile: Profile
@StateObject private var viewModel: ProfileDetailViewModel
@Environment(\.dismiss) private var dismiss
@State private var showingExportSheet = false
init(profile: Profile) {
self.profile = profile
_viewModel = StateObject(wrappedValue: ProfileDetailViewModel(profile: profile))
}
var body: some View {
NavigationView {
TabView {
// SE Tab
ScrollView {
VStack(spacing: 20) {
competenceCard(title: "Selbsteinschätzung (SE)", values: viewModel.seValues)
chartCard(title: "Selbsteinschätzung - Profil", values: viewModel.seValues, color: .blue)
}
.padding()
}
.tabItem {
Label("Selbsteinschätzung", systemImage: "person.fill")
}
// FE Tab
ScrollView {
VStack(spacing: 20) {
competenceCard(title: "Fremdeinschätzung (FE)", values: viewModel.feValues)
chartCard(title: "Fremdeinschätzung - Profil", values: viewModel.feValues, color: .red)
}
.padding()
}
.tabItem {
Label("Fremdeinschätzung", systemImage: "person.2.fill")
}
// Statistik Tab
ScrollView {
VStack(spacing: 20) {
// Vergleichschart
comparisonCard
// Kennzahlen
metricsCard
// Interpretation
interpretationCard
}
.padding()
}
.tabItem {
Label("Statistik", systemImage: "chart.bar.fill")
}
// Items Tab
itemsTab
.tabItem {
Label("Alle Items", systemImage: "list.bullet")
}
}
.navigationTitle(profile.name)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Schließen") { dismiss() }
}
ToolbarItem(placement: .primaryAction) {
HStack {
Picker("Norm", selection: $viewModel.currentNorm) {
ForEach(ProfileDetailViewModel.NormType.allCases, id: \.self) { norm in
Text(norm.rawValue).tag(norm)
}
}
.pickerStyle(.segmented)
.frame(width: 180)
.onChange(of: viewModel.currentNorm) { _ in
viewModel.calculate()
}
Button(action: { showingExportSheet = true }) {
Label("Exportieren", systemImage: "square.and.arrow.up")
}
}
}
}
.fileExporter(
isPresented: $showingExportSheet,
document: ProfileDocument(profile: profile, viewModel: viewModel),
contentType: .plainText,
defaultFilename: "Profil_\(profile.name).txt"
) { result in
if case .failure(let error) = result {
print("Export fehlgeschlagen: \(error)")
}
}
}
}
// MARK: - Competence Table Card
@ViewBuilder
private func competenceCard(title: String, values: [Int]) -> some View {
VStack(alignment: .leading, spacing: 12) {
Text(title)
.font(.headline)
Grid(horizontalSpacing: 8, verticalSpacing: 8) {
GridRow {
Text("Kompetenz")
.font(.caption)
.frame(width: 120, alignment: .leading)
ForEach(1...5, id: \.self) { i in
Text("\(i)")
.font(.caption)
.frame(width: 35)
}
Text("Bewertung")
.font(.caption)
.frame(width: 120, alignment: .leading)
}
.foregroundColor(.secondary)
Divider()
ForEach(0..<6, id: \.self) { i in
GridRow {
Text(Norms.kompetenzen[i])
.font(.caption)
.frame(width: 120, alignment: .leading)
ForEach(1...5, id: \.self) { j in
Text(values[i] + 1 == j ? "✓" : "")
.frame(width: 35)
.foregroundColor(values[i] + 1 == j ? .green : .secondary)
}
Text(Calculator.getRating(value: values[i] + 1))
.font(.caption)
.frame(width: 120, alignment: .leading)
}
if i < 5 { Divider() }
}
}
.padding()
.background(Color(.textBackgroundColor))
.cornerRadius(12)
}
}
// MARK: - Chart Card
@ViewBuilder
private func chartCard(title: String, values: [Int], color: Color) -> some View {
VStack(alignment: .leading, spacing: 12) {
Text(title)
.font(.headline)
Chart {
ForEach(0..<6, id: \.self) { i in
LineMark(
x: .value("Kompetenz", Norms.kompetenzen[i]),
y: .value("Wert", values[i] + 1)
)
.foregroundStyle(color)
.lineStyle(StrokeStyle(lineWidth: 2))
PointMark(
x: .value("Kompetenz", Norms.kompetenzen[i]),
y: .value("Wert", values[i] + 1)
)
.foregroundStyle(color)
.symbolSize(8)
}
}
.frame(height: 250)
.chartYScale(domain: 1...5)
.chartYAxis {
AxisMarks(values: [1, 2, 3, 4, 5]) { value in
AxisValueLabel()
AxisGridLine()
}
}
}
.padding()
.background(Color(.textBackgroundColor))
.cornerRadius(12)
}
// MARK: - Comparison Card
@ViewBuilder
private var comparisonCard: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Vergleich SE vs. FE")
.font(.headline)
Chart {
ForEach(0..<6, id: \.self) { i in
LineMark(
x: .value("Kompetenz", Norms.kompetenzen[i]),
y: .value("Wert", viewModel.seValues[i] + 1)
)
.foregroundStyle(.blue)
.lineStyle(StrokeStyle(lineWidth: 2))
PointMark(
x: .value("Kompetenz", Norms.kompetenzen[i]),
y: .value("Wert", viewModel.seValues[i] + 1)
)
.foregroundStyle(.blue)
.symbolSize(8)
LineMark(
x: .value("Kompetenz", Norms.kompetenzen[i]),
y: .value("Wert", viewModel.feValues[i] + 1)
)
.foregroundStyle(.red)
.lineStyle(StrokeStyle(lineWidth: 2, dash: [5, 5]))
PointMark(
x: .value("Kompetenz", Norms.kompetenzen[i]),
y: .value("Wert", viewModel.feValues[i] + 1)
)
.foregroundStyle(.red)
.symbolSize(8)
}
}
.frame(height: 250)
.chartYScale(domain: 1...5)
.chartLegend(position: .top) {
HStack(spacing: 20) {
HStack {
Circle().fill(.blue).frame(width: 8, height: 8)
Text("Selbsteinschätzung (SE)")
}
HStack {
Circle().fill(.red).frame(width: 8, height: 8)
Text("Fremdeinschätzung (FE)")
}
}
.font(.caption)
}
}
.padding()
.background(Color(.textBackgroundColor))
.cornerRadius(12)
}
// MARK: - Metrics Card
@ViewBuilder
private var metricsCard: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Kennzahlen")
.font(.headline)
HStack(spacing: 30) {
MetricView(
title: "Korrelation",
value: String(format: "%.2f", viewModel.correlation),
description: correlationDescription
)
MetricView(
title: "Übereinstimmung",
value: String(format: "%.1f", viewModel.agreement) + "%",
description: agreementDescription
)
}
}
.padding()
.background(Color(.textBackgroundColor))
.cornerRadius(12)
}
private var correlationDescription: String {
if viewModel.correlation >= 0.8 { return "sehr gut" }
if viewModel.correlation >= 0.6 { return "gut" }
if viewModel.correlation >= 0.4 { return "mäßig" }
if viewModel.correlation >= 0.2 { return "schwach" }
return "keine"
}
private var agreementDescription: String {
if viewModel.agreement >= 80 { return "hohe" }
if viewModel.agreement >= 60 { return "mittlere" }
if viewModel.agreement >= 40 { return "geringe" }
return "sehr geringe"
}
// MARK: - Interpretation Card
@ViewBuilder
private var interpretationCard: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Interpretation")
.font(.headline)
Text(viewModel.interpretation)
.font(.body)
.lineSpacing(4)
}
.padding()
.background(Color(.textBackgroundColor))
.cornerRadius(12)
}
// MARK: - Items Tab
@ViewBuilder
private var itemsTab: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
// Header
HStack {
Text("Item")
.font(.headline)
.frame(width: 250, alignment: .leading)
Text("SE")
.font(.headline)
.frame(width: 50)
Text("FE")
.font(.headline)
.frame(width: 50)
}
.padding(.horizontal)
.padding(.top)
Divider()
ForEach(0..<36, id: \.self) { i in
HStack {
Text("\(i+1). \(Norms.items[i])")
.font(.subheadline)
.frame(width: 250, alignment: .leading)
Text("\(viewModel.seItems[i])")
.font(.subheadline)
.frame(width: 50)
.foregroundColor(.blue)
Text("\(viewModel.feItems[i])")
.font(.subheadline)
.frame(width: 50)
.foregroundColor(.red)
}
.padding(.horizontal)
if i < 35 {
Divider()
.padding(.leading, 260)
}
}
}
.padding(.vertical)
}
}
}
// MARK: - Supporting Views
struct MetricView: View {
let title: String
let value: String
let description: String
var body: some View {
VStack(spacing: 8) {
Text(title)
.font(.caption)
.foregroundColor(.secondary)
Text(value)
.font(.title2)
.fontWeight(.bold)
Text(description)
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
}
// MARK: - Export Document
struct ProfileDocument: FileDocument {
static var readableContentTypes: [UTType] { [.plainText] }
let content: String
init(profile: Profile, viewModel: ProfileDetailViewModel) {
var text = """
DÜSK - Profilauswertung
========================
Name: \(profile.name)
Gruppe: \(profile.gruppename ?? "Keine Gruppe")
Profil-ID: \(profile.profilID)
Normtabelle: \(viewModel.currentNorm.rawValue)
"""
text += "\nKompetenzwerte:\n"
text += "----------------------------------------\n"
for i in 0..<6 {
text += "\(Norms.kompetenzen[i]): SE=\(viewModel.seValues[i]+1)/5, FE=\(viewModel.feValues[i]+1)/5\n"
}
text += "\nKorrelation: \(String(format: "%.2f", viewModel.correlation))\n"
text += "Übereinstimmung: \(String(format: "%.1f", viewModel.agreement))%\n\n"
text += viewModel.interpretation
self.content = text
}
init(configuration: ReadConfiguration) throws {
content = ""
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
return FileWrapper(regularFileWithContents: content.data(using: .utf8)!)
}
}