CineBook Projekt hinzugefügt
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.vs/
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
139
App.xaml
Normal file
139
App.xaml
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<Application x:Class="CineBook.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
StartupUri="Views/LoginWindow.xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<SolidColorBrush x:Key="BrushBlack" Color="#0A0A0A"/>
|
||||||
|
<SolidColorBrush x:Key="BrushGold" Color="#F5C518"/>
|
||||||
|
<SolidColorBrush x:Key="BrushRed" Color="#C0392B"/>
|
||||||
|
<SolidColorBrush x:Key="BrushCream" Color="#F5F0E8"/>
|
||||||
|
<SolidColorBrush x:Key="BrushGray" Color="#2C2C2C"/>
|
||||||
|
<SolidColorBrush x:Key="BrushDarkGray" Color="#1A1A1A"/>
|
||||||
|
<SolidColorBrush x:Key="BrushLightGray" Color="#444444"/>
|
||||||
|
<SolidColorBrush x:Key="BrushGreen" Color="#27AE60"/>
|
||||||
|
<SolidColorBrush x:Key="BrushTransparent" Color="Transparent"/>
|
||||||
|
|
||||||
|
<!-- Global Button Style -->
|
||||||
|
<Style x:Key="RetroButton" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="{StaticResource BrushGold}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource BrushBlack}"/>
|
||||||
|
<Setter Property="FontFamily" Value="Courier New"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="Padding" Value="14,7"/>
|
||||||
|
<Setter Property="BorderThickness" Value="2"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#A0820F"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
CornerRadius="2">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Margin="{TemplateBinding Padding}"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="#FFD700"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#F5C518"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter Property="Background" Value="#A0820F"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.5"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="RetroButtonRed" TargetType="Button" BasedOn="{StaticResource RetroButton}">
|
||||||
|
<Setter Property="Background" Value="{StaticResource BrushRed}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#922B21"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="RetroButtonGray" TargetType="Button" BasedOn="{StaticResource RetroButton}">
|
||||||
|
<Setter Property="Background" Value="{StaticResource BrushLightGray}"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource BrushCream}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#222"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="RetroButtonGreen" TargetType="Button" BasedOn="{StaticResource RetroButton}">
|
||||||
|
<Setter Property="Background" Value="{StaticResource BrushGreen}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#1E8449"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- TextBox Style -->
|
||||||
|
<Style x:Key="RetroTextBox" TargetType="TextBox">
|
||||||
|
<Setter Property="Background" Value="#111"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource BrushGold}"/>
|
||||||
|
<Setter Property="CaretBrush" Value="{StaticResource BrushGold}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#444"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="FontFamily" Value="Courier New"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="Padding" Value="6,4"/>
|
||||||
|
<Setter Property="SelectionBrush" Value="{StaticResource BrushGold}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- PasswordBox Style -->
|
||||||
|
<Style x:Key="RetroPasswordBox" TargetType="PasswordBox">
|
||||||
|
<Setter Property="Background" Value="#111"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource BrushGold}"/>
|
||||||
|
<Setter Property="CaretBrush" Value="{StaticResource BrushGold}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#444"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="FontFamily" Value="Courier New"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="Padding" Value="6,4"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ComboBox Style -->
|
||||||
|
<Style x:Key="RetroComboBox" TargetType="ComboBox">
|
||||||
|
<Setter Property="Background" Value="#111"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource BrushGold}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#444"/>
|
||||||
|
<Setter Property="FontFamily" Value="Courier New"/>
|
||||||
|
<Setter Property="Padding" Value="6,4"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Label Style -->
|
||||||
|
<Style x:Key="RetroLabel" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource BrushCream}"/>
|
||||||
|
<Setter Property="FontFamily" Value="Courier New"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="RetroHeader" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource BrushGold}"/>
|
||||||
|
<Setter Property="FontFamily" Value="Courier New"/>
|
||||||
|
<Setter Property="FontSize" Value="20"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- DataGrid Style -->
|
||||||
|
<Style x:Key="RetroDataGrid" TargetType="DataGrid">
|
||||||
|
<Setter Property="Background" Value="#111"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource BrushCream}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#333"/>
|
||||||
|
<Setter Property="GridLinesVisibility" Value="Horizontal"/>
|
||||||
|
<Setter Property="HorizontalGridLinesBrush" Value="#2a2a2a"/>
|
||||||
|
<Setter Property="FontFamily" Value="Courier New"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="RowBackground" Value="#111"/>
|
||||||
|
<Setter Property="AlternatingRowBackground" Value="#161616"/>
|
||||||
|
<Setter Property="SelectionMode" Value="Single"/>
|
||||||
|
<Setter Property="AutoGenerateColumns" Value="False"/>
|
||||||
|
<Setter Property="CanUserAddRows" Value="False"/>
|
||||||
|
<Setter Property="CanUserDeleteRows" Value="False"/>
|
||||||
|
<Setter Property="IsReadOnly" Value="True"/>
|
||||||
|
<Setter Property="HeadersVisibility" Value="Column"/>
|
||||||
|
</Style>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
5
App.xaml.cs
Normal file
5
App.xaml.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace CineBook;
|
||||||
|
|
||||||
|
public partial class App : Application { }
|
||||||
17
CineBook.csproj
Normal file
17
CineBook.csproj
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<AssemblyName>CineBook</AssemblyName>
|
||||||
|
<RootNamespace>CineBook</RootNamespace>
|
||||||
|
<RestorePackagesWithLockFile>false</RestorePackagesWithLockFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Quelle: https://mysqlconnector.net/ (abgerufen: 2026-01-29) -->
|
||||||
|
<PackageReference Include="MySqlConnector" Version="2.3.7" />
|
||||||
|
<!-- BCrypt nicht nötig, da SHA256 aus System.Security.Cryptography verwendet wird -->
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
122
Data/Db.cs
Normal file
122
Data/Db.cs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
using MySqlConnector;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
|
||||||
|
namespace CineBook.Data;
|
||||||
|
|
||||||
|
// Quelle: https://mysqlconnector.net/ (abgerufen: 2026-01-29)
|
||||||
|
// Zweck: MySQL Zugriff mit parameterisierten Queries (Schutz vor SQL-Injection) + Fehlerbehandlung
|
||||||
|
public class Db
|
||||||
|
{
|
||||||
|
private readonly string _cs;
|
||||||
|
public Db(string connectionString) => _cs = connectionString;
|
||||||
|
|
||||||
|
// Quelle/Hilfe: https://mysqlconnector.net/transactions/ (abgerufen: 2026-03-05)
|
||||||
|
// Zweck: Mehrere DB-Statements atomar ausführen (z.B. Buchung + Seat-Update + Counter-Update)
|
||||||
|
public T InTransaction<T>(Func<MySqlConnection, MySqlTransaction, T> work)
|
||||||
|
{
|
||||||
|
using var con = new MySqlConnection(_cs);
|
||||||
|
con.Open();
|
||||||
|
using var tx = con.BeginTransaction();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = work(con, tx);
|
||||||
|
tx.Commit();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
try { tx.Rollback(); } catch { /* ignore rollback errors */ }
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Execute(MySqlConnection con, MySqlTransaction tx, string sql, Dictionary<string, object?> p)
|
||||||
|
{
|
||||||
|
using var cmd = new MySqlCommand(sql, con, tx);
|
||||||
|
foreach (var kv in p)
|
||||||
|
cmd.Parameters.AddWithValue(kv.Key, kv.Value ?? DBNull.Value);
|
||||||
|
return cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> Query<T>(MySqlConnection con, MySqlTransaction tx, string sql, Dictionary<string, object?> p, Func<MySqlDataReader, T> map)
|
||||||
|
{
|
||||||
|
using var cmd = new MySqlCommand(sql, con, tx);
|
||||||
|
foreach (var kv in p)
|
||||||
|
cmd.Parameters.AddWithValue(kv.Key, kv.Value ?? DBNull.Value);
|
||||||
|
using var r = cmd.ExecuteReader();
|
||||||
|
var list = new List<T>();
|
||||||
|
while (r.Read()) list.Add(map(r));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> Query<T>(string sql, Dictionary<string, object?> p, Func<MySqlDataReader, T> map)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var con = new MySqlConnection(_cs);
|
||||||
|
con.Open();
|
||||||
|
using var cmd = new MySqlCommand(sql, con);
|
||||||
|
foreach (var kv in p)
|
||||||
|
cmd.Parameters.AddWithValue(kv.Key, kv.Value ?? DBNull.Value);
|
||||||
|
using var r = cmd.ExecuteReader();
|
||||||
|
var list = new List<T>();
|
||||||
|
while (r.Read()) list.Add(map(r));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
catch (MySqlException ex)
|
||||||
|
{
|
||||||
|
throw new Exception("DB-Fehler: " + ex.Message, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> Query<T>(string sql, Func<MySqlDataReader, T> map)
|
||||||
|
=> Query(sql, new Dictionary<string, object?>(), map);
|
||||||
|
|
||||||
|
public int Execute(string sql, Dictionary<string, object?> p)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var con = new MySqlConnection(_cs);
|
||||||
|
con.Open();
|
||||||
|
using var cmd = new MySqlCommand(sql, con);
|
||||||
|
foreach (var kv in p)
|
||||||
|
cmd.Parameters.AddWithValue(kv.Key, kv.Value ?? DBNull.Value);
|
||||||
|
return cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
catch (MySqlException ex)
|
||||||
|
{
|
||||||
|
throw new Exception("DB-Fehler: " + ex.Message, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public long InsertAndGetId(string sql, Dictionary<string, object?> p)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var con = new MySqlConnection(_cs);
|
||||||
|
con.Open();
|
||||||
|
using var cmd = new MySqlCommand(sql, con);
|
||||||
|
foreach (var kv in p)
|
||||||
|
cmd.Parameters.AddWithValue(kv.Key, kv.Value ?? DBNull.Value);
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
return cmd.LastInsertedId;
|
||||||
|
}
|
||||||
|
catch (MySqlException ex)
|
||||||
|
{
|
||||||
|
throw new Exception("DB-Fehler: " + ex.Message, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TestConnection()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var con = new MySqlConnection(_cs);
|
||||||
|
con.Open();
|
||||||
|
return con.State == ConnectionState.Open;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Data/DbConfig.cs
Normal file
11
Data/DbConfig.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
namespace CineBook.Data
|
||||||
|
{
|
||||||
|
public static class DbConfig
|
||||||
|
{
|
||||||
|
public static string GetConnectionString()
|
||||||
|
{
|
||||||
|
return "Server=mysql.pb.bib.de;Port=3306;Database=cinebook;Uid=pbt3h24akv;Pwd=8DwwBCvrMUtJ;";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
408
Data/Repository.cs
Normal file
408
Data/Repository.cs
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
using CineBook.Models;
|
||||||
|
using MySqlConnector;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CineBook.Data;
|
||||||
|
|
||||||
|
// Quelle: Eigene Implementierung basierend auf Unterrichtsmaterial + MySqlConnector Dokumentation
|
||||||
|
public class Repository
|
||||||
|
{
|
||||||
|
private readonly Db _db;
|
||||||
|
public Repository(Db db) => _db = db;
|
||||||
|
|
||||||
|
// ==================== USERS ====================
|
||||||
|
public User? GetUserByCredentials(string username, string password)
|
||||||
|
{
|
||||||
|
var hash = HashPassword(password);
|
||||||
|
var users = _db.Query(
|
||||||
|
"SELECT * FROM Users WHERE Username = @u AND PasswordHash = @p LIMIT 1",
|
||||||
|
new Dictionary<string, object?> { ["@u"] = username, ["@p"] = hash },
|
||||||
|
r => new User
|
||||||
|
{
|
||||||
|
UserID = r.GetInt32("UserID"),
|
||||||
|
Username = r.GetString("Username"),
|
||||||
|
PasswordHash = r.GetString("PasswordHash"),
|
||||||
|
Role = r.GetString("Role"),
|
||||||
|
Email = r.IsDBNull(r.GetOrdinal("Email")) ? "" : r.GetString("Email"),
|
||||||
|
CreatedAt = r.GetDateTime("CreatedAt")
|
||||||
|
});
|
||||||
|
return users.Count > 0 ? users[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<User> GetAllUsers() => _db.Query(
|
||||||
|
"SELECT * FROM Users ORDER BY Username",
|
||||||
|
r => new User
|
||||||
|
{
|
||||||
|
UserID = r.GetInt32("UserID"),
|
||||||
|
Username = r.GetString("Username"),
|
||||||
|
PasswordHash = r.GetString("PasswordHash"),
|
||||||
|
Role = r.GetString("Role"),
|
||||||
|
Email = r.IsDBNull(r.GetOrdinal("Email")) ? "" : r.GetString("Email"),
|
||||||
|
CreatedAt = r.GetDateTime("CreatedAt")
|
||||||
|
});
|
||||||
|
|
||||||
|
public bool CreateUser(string username, string password, string role, string email)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(username) || username.Length < 3) return false;
|
||||||
|
if (string.IsNullOrWhiteSpace(password) || password.Length < 6) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_db.Execute(
|
||||||
|
"INSERT INTO Users (Username, PasswordHash, Role, Email) VALUES (@u, @p, @r, @e)",
|
||||||
|
new Dictionary<string, object?> { ["@u"] = username, ["@p"] = HashPassword(password), ["@r"] = role, ["@e"] = email });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UpdateUser(int id, string username, string role, string email)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_db.Execute("UPDATE Users SET Username=@u, Role=@r, Email=@e WHERE UserID=@id",
|
||||||
|
new Dictionary<string, object?> { ["@u"] = username, ["@r"] = role, ["@e"] = email, ["@id"] = id });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DeleteUser(int id)
|
||||||
|
{
|
||||||
|
try { _db.Execute("DELETE FROM Users WHERE UserID=@id", new Dictionary<string, object?> { ["@id"] = id }); return true; }
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== MOVIES ====================
|
||||||
|
public List<Movie> GetMovies(bool onlyActive = false)
|
||||||
|
{
|
||||||
|
var sql = onlyActive
|
||||||
|
? "SELECT * FROM Movies WHERE IsActive=1 ORDER BY Title"
|
||||||
|
: "SELECT * FROM Movies ORDER BY Title";
|
||||||
|
return _db.Query(sql, r => MapMovie(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Movie> SearchMovies(string term) => _db.Query(
|
||||||
|
"SELECT * FROM Movies WHERE Title LIKE @t OR Genre LIKE @t ORDER BY Title",
|
||||||
|
new Dictionary<string, object?> { ["@t"] = $"%{term}%" },
|
||||||
|
r => MapMovie(r));
|
||||||
|
|
||||||
|
public bool CreateMovie(Movie m)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(m.Title)) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_db.Execute(
|
||||||
|
"INSERT INTO Movies (Title, Genre, DurationMinutes, Description, Rating, IsActive) VALUES (@t,@g,@d,@desc,@r,1)",
|
||||||
|
new Dictionary<string, object?> { ["@t"] = m.Title, ["@g"] = m.Genre, ["@d"] = m.DurationMinutes, ["@desc"] = m.Description, ["@r"] = m.Rating });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UpdateMovie(Movie m)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_db.Execute("UPDATE Movies SET Title=@t, Genre=@g, DurationMinutes=@d, Description=@desc, Rating=@r, IsActive=@a WHERE MovieID=@id",
|
||||||
|
new Dictionary<string, object?> { ["@t"] = m.Title, ["@g"] = m.Genre, ["@d"] = m.DurationMinutes, ["@desc"] = m.Description, ["@r"] = m.Rating, ["@a"] = m.IsActive ? 1 : 0, ["@id"] = m.MovieID });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DeleteMovie(int id)
|
||||||
|
{
|
||||||
|
try { _db.Execute("DELETE FROM Movies WHERE MovieID=@id", new Dictionary<string, object?> { ["@id"] = id }); return true; }
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private Movie MapMovie(MySqlDataReader r) => new Movie
|
||||||
|
{
|
||||||
|
MovieID = r.GetInt32("MovieID"),
|
||||||
|
Title = r.GetString("Title"),
|
||||||
|
Genre = r.IsDBNull(r.GetOrdinal("Genre")) ? "" : r.GetString("Genre"),
|
||||||
|
DurationMinutes = r.GetInt32("DurationMinutes"),
|
||||||
|
Description = r.IsDBNull(r.GetOrdinal("Description")) ? "" : r.GetString("Description"),
|
||||||
|
Rating = r.IsDBNull(r.GetOrdinal("Rating")) ? 0 : r.GetDecimal("Rating"),
|
||||||
|
IsActive = r.GetBoolean("IsActive"),
|
||||||
|
CreatedAt = r.GetDateTime("CreatedAt")
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== SCREENINGS ====================
|
||||||
|
public List<Screening> GetScreenings() => _db.Query(
|
||||||
|
@"SELECT s.*, m.Title as MovieTitle FROM Screenings s
|
||||||
|
JOIN Movies m ON s.MovieID=m.MovieID
|
||||||
|
ORDER BY s.ScreeningDate, s.StartTime",
|
||||||
|
r => MapScreening(r));
|
||||||
|
|
||||||
|
public List<Screening> GetScreeningsByMovie(int movieId) => _db.Query(
|
||||||
|
@"SELECT s.*, m.Title as MovieTitle FROM Screenings s
|
||||||
|
JOIN Movies m ON s.MovieID=m.MovieID
|
||||||
|
WHERE s.MovieID=@id ORDER BY s.ScreeningDate, s.StartTime",
|
||||||
|
new Dictionary<string, object?> { ["@id"] = movieId },
|
||||||
|
r => MapScreening(r));
|
||||||
|
|
||||||
|
public bool CreateScreening(Screening s)
|
||||||
|
{
|
||||||
|
if (s.MovieID <= 0) return false;
|
||||||
|
if (s.ScreeningDate == default) return false;
|
||||||
|
if (s.TotalSeats <= 0) return false;
|
||||||
|
if (s.PricePerSeat <= 0) return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Quelle/Hilfe: https://mysqlconnector.net/transactions/ (abgerufen: 2026-03-05)
|
||||||
|
// Zweck: Screening + Seats atomar anlegen, damit Ticketbuchung immer Seat-Daten vorfindet.
|
||||||
|
return _db.InTransaction((con, tx) =>
|
||||||
|
{
|
||||||
|
// Screening anlegen
|
||||||
|
using var cmd = new MySqlCommand(
|
||||||
|
"INSERT INTO Screenings (MovieID, ScreeningDate, StartTime, Hall, TotalSeats, AvailableSeats, PricePerSeat) " +
|
||||||
|
"VALUES (@m,@d,@t,@h,@ts,@as,@p);",
|
||||||
|
con, tx);
|
||||||
|
cmd.Parameters.AddWithValue("@m", s.MovieID);
|
||||||
|
cmd.Parameters.AddWithValue("@d", s.ScreeningDate.Date);
|
||||||
|
cmd.Parameters.AddWithValue("@t", s.StartTime.ToString(@"hh\:mm\:ss"));
|
||||||
|
cmd.Parameters.AddWithValue("@h", s.Hall);
|
||||||
|
cmd.Parameters.AddWithValue("@ts", s.TotalSeats);
|
||||||
|
cmd.Parameters.AddWithValue("@as", s.TotalSeats);
|
||||||
|
cmd.Parameters.AddWithValue("@p", s.PricePerSeat);
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
var screeningId = (int)cmd.LastInsertedId;
|
||||||
|
|
||||||
|
// Seats generieren (einfaches Kino-Layout: Reihen A.., Sitznummer 1..10)
|
||||||
|
GenerateSeats(con, tx, screeningId, s.TotalSeats);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UpdateScreening(Screening s)
|
||||||
|
{
|
||||||
|
if (s.ScreeningID <= 0) return false;
|
||||||
|
if (s.MovieID <= 0) return false;
|
||||||
|
if (s.ScreeningDate == default) return false;
|
||||||
|
if (s.PricePerSeat <= 0) return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// AvailableSeats wird NICHT blind überschrieben, damit bestehende Buchungen nicht zerstört werden.
|
||||||
|
_db.Execute(
|
||||||
|
"UPDATE Screenings SET MovieID=@m, ScreeningDate=@d, StartTime=@t, Hall=@h, PricePerSeat=@p WHERE ScreeningID=@id",
|
||||||
|
new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["@m"] = s.MovieID,
|
||||||
|
["@d"] = s.ScreeningDate.Date,
|
||||||
|
["@t"] = s.StartTime.ToString(@"hh\:mm\:ss"),
|
||||||
|
["@h"] = s.Hall,
|
||||||
|
["@p"] = s.PricePerSeat,
|
||||||
|
["@id"] = s.ScreeningID
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DeleteScreening(int id)
|
||||||
|
{
|
||||||
|
try { _db.Execute("DELETE FROM Screenings WHERE ScreeningID=@id", new Dictionary<string, object?> { ["@id"] = id }); return true; }
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private Screening MapScreening(MySqlDataReader r) => new Screening
|
||||||
|
{
|
||||||
|
ScreeningID = r.GetInt32("ScreeningID"),
|
||||||
|
MovieID = r.GetInt32("MovieID"),
|
||||||
|
MovieTitle = r.IsDBNull(r.GetOrdinal("MovieTitle")) ? "" : r.GetString("MovieTitle"),
|
||||||
|
ScreeningDate = r.GetDateTime("ScreeningDate"),
|
||||||
|
StartTime = r.GetTimeSpan("StartTime"),
|
||||||
|
Hall = r.GetString("Hall"),
|
||||||
|
TotalSeats = r.GetInt32("TotalSeats"),
|
||||||
|
AvailableSeats = r.GetInt32("AvailableSeats"),
|
||||||
|
PricePerSeat = r.GetDecimal("PricePerSeat")
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== SEATS ====================
|
||||||
|
public List<Seat> GetSeatsByScreening(int screeningId) => _db.Query(
|
||||||
|
"SELECT * FROM Seats WHERE ScreeningID=@id ORDER BY Row, SeatNumber",
|
||||||
|
new Dictionary<string, object?> { ["@id"] = screeningId },
|
||||||
|
r => new Seat
|
||||||
|
{
|
||||||
|
SeatID = r.GetInt32("SeatID"),
|
||||||
|
ScreeningID = r.GetInt32("ScreeningID"),
|
||||||
|
Row = r.GetString("Row"),
|
||||||
|
SeatNumber = r.GetInt32("SeatNumber"),
|
||||||
|
IsBooked = r.GetBoolean("IsBooked"),
|
||||||
|
Category = r.GetString("Category")
|
||||||
|
});
|
||||||
|
|
||||||
|
public bool EnsureSeatsForScreening(int screeningId, int totalSeats)
|
||||||
|
{
|
||||||
|
if (screeningId <= 0 || totalSeats <= 0) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _db.InTransaction((con, tx) =>
|
||||||
|
{
|
||||||
|
var existing = _db.Query(con, tx,
|
||||||
|
"SELECT COUNT(*) FROM Seats WHERE ScreeningID=@id",
|
||||||
|
new Dictionary<string, object?> { ["@id"] = screeningId },
|
||||||
|
r => r.GetInt32(0));
|
||||||
|
if (existing.Count > 0 && existing[0] > 0) return true;
|
||||||
|
GenerateSeats(con, tx, screeningId, totalSeats);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== BOOKINGS ====================
|
||||||
|
public List<Booking> GetAllBookings() => _db.Query(
|
||||||
|
@"SELECT b.*, u.Username, m.Title as MovieTitle, s.ScreeningDate, s.StartTime,
|
||||||
|
CONCAT(se.Row, se.SeatNumber) as SeatDisplay
|
||||||
|
FROM Bookings b
|
||||||
|
JOIN Users u ON b.UserID=u.UserID
|
||||||
|
JOIN Screenings s ON b.ScreeningID=s.ScreeningID
|
||||||
|
JOIN Movies m ON s.MovieID=m.MovieID
|
||||||
|
JOIN Seats se ON b.SeatID=se.SeatID
|
||||||
|
ORDER BY b.BookingDate DESC",
|
||||||
|
r => MapBooking(r));
|
||||||
|
|
||||||
|
public List<Booking> GetBookingsByUser(int userId) => _db.Query(
|
||||||
|
@"SELECT b.*, u.Username, m.Title as MovieTitle, s.ScreeningDate, s.StartTime,
|
||||||
|
CONCAT(se.Row, se.SeatNumber) as SeatDisplay
|
||||||
|
FROM Bookings b
|
||||||
|
JOIN Users u ON b.UserID=u.UserID
|
||||||
|
JOIN Screenings s ON b.ScreeningID=s.ScreeningID
|
||||||
|
JOIN Movies m ON s.MovieID=m.MovieID
|
||||||
|
JOIN Seats se ON b.SeatID=se.SeatID
|
||||||
|
WHERE b.UserID=@uid ORDER BY b.BookingDate DESC",
|
||||||
|
new Dictionary<string, object?> { ["@uid"] = userId },
|
||||||
|
r => MapBooking(r));
|
||||||
|
|
||||||
|
public bool CreateBooking(int userId, int screeningId, int seatId, decimal price)
|
||||||
|
{
|
||||||
|
if (userId <= 0 || screeningId <= 0 || seatId <= 0) return false;
|
||||||
|
if (price <= 0) return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Quelle/Hilfe: https://mysqlconnector.net/transactions/ (abgerufen: 2026-03-05)
|
||||||
|
// Zweck: Seat sperren + AvailableSeats updaten + Booking anlegen atomar.
|
||||||
|
return _db.InTransaction((con, tx) =>
|
||||||
|
{
|
||||||
|
var code = "CB-" + DateTime.Now.ToString("yyyyMMddHHmmss");
|
||||||
|
|
||||||
|
// Seat nur buchen, wenn er noch frei ist
|
||||||
|
var seatUpdated = _db.Execute(con, tx,
|
||||||
|
"UPDATE Seats SET IsBooked=1 WHERE SeatID=@sid AND ScreeningID=@sc AND IsBooked=0",
|
||||||
|
new Dictionary<string, object?> { ["@sid"] = seatId, ["@sc"] = screeningId });
|
||||||
|
if (seatUpdated != 1) return false;
|
||||||
|
|
||||||
|
// AvailableSeats nur verringern, wenn noch > 0
|
||||||
|
var seatsCounterUpdated = _db.Execute(con, tx,
|
||||||
|
"UPDATE Screenings SET AvailableSeats=AvailableSeats-1 WHERE ScreeningID=@id AND AvailableSeats > 0",
|
||||||
|
new Dictionary<string, object?> { ["@id"] = screeningId });
|
||||||
|
if (seatsCounterUpdated != 1) return false;
|
||||||
|
|
||||||
|
_db.Execute(con, tx,
|
||||||
|
"INSERT INTO Bookings (UserID, ScreeningID, SeatID, TotalPrice, Status, BookingCode) VALUES (@u,@s,@seat,@p,'Confirmed',@c)",
|
||||||
|
new Dictionary<string, object?> { ["@u"] = userId, ["@s"] = screeningId, ["@seat"] = seatId, ["@p"] = price, ["@c"] = code });
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CancelBooking(int bookingId, int seatId, int screeningId)
|
||||||
|
{
|
||||||
|
if (bookingId <= 0 || seatId <= 0 || screeningId <= 0) return false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _db.InTransaction((con, tx) =>
|
||||||
|
{
|
||||||
|
var b = _db.Execute(con, tx,
|
||||||
|
"UPDATE Bookings SET Status='Cancelled' WHERE BookingID=@id AND Status='Confirmed'",
|
||||||
|
new Dictionary<string, object?> { ["@id"] = bookingId });
|
||||||
|
if (b != 1) return false;
|
||||||
|
|
||||||
|
_db.Execute(con, tx,
|
||||||
|
"UPDATE Seats SET IsBooked=0 WHERE SeatID=@sid AND ScreeningID=@sc",
|
||||||
|
new Dictionary<string, object?> { ["@sid"] = seatId, ["@sc"] = screeningId });
|
||||||
|
|
||||||
|
_db.Execute(con, tx,
|
||||||
|
"UPDATE Screenings SET AvailableSeats=AvailableSeats+1 WHERE ScreeningID=@id",
|
||||||
|
new Dictionary<string, object?> { ["@id"] = screeningId });
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void GenerateSeats(MySqlConnection con, MySqlTransaction tx, int screeningId, int totalSeats)
|
||||||
|
{
|
||||||
|
// Quelle/Hilfe: Eigene Implementierung (kein Unterrichtsthema), Layout-Logik dokumentiert.
|
||||||
|
// Layout:
|
||||||
|
// - Standardmäßig 10 Sitze pro Reihe.
|
||||||
|
// - Reihen A, B, C, ...
|
||||||
|
// - VIP: Reihen C und D (falls vorhanden)
|
||||||
|
var seatsPerRow = 10;
|
||||||
|
var rowsNeeded = (int)Math.Ceiling(totalSeats / (double)seatsPerRow);
|
||||||
|
var created = 0;
|
||||||
|
for (var i = 0; i < rowsNeeded; i++)
|
||||||
|
{
|
||||||
|
var rowChar = (char)('A' + i);
|
||||||
|
for (var seat = 1; seat <= seatsPerRow && created < totalSeats; seat++)
|
||||||
|
{
|
||||||
|
var category = (rowChar == 'C' || rowChar == 'D') ? "VIP" : "Standard";
|
||||||
|
using var cmd = new MySqlCommand(
|
||||||
|
"INSERT INTO Seats (ScreeningID, Row, SeatNumber, IsBooked, Category) VALUES (@sid,@r,@n,0,@c)",
|
||||||
|
con, tx);
|
||||||
|
cmd.Parameters.AddWithValue("@sid", screeningId);
|
||||||
|
cmd.Parameters.AddWithValue("@r", rowChar.ToString());
|
||||||
|
cmd.Parameters.AddWithValue("@n", seat);
|
||||||
|
cmd.Parameters.AddWithValue("@c", category);
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Booking MapBooking(MySqlDataReader r) => new Booking
|
||||||
|
{
|
||||||
|
BookingID = r.GetInt32("BookingID"),
|
||||||
|
UserID = r.GetInt32("UserID"),
|
||||||
|
Username = r.GetString("Username"),
|
||||||
|
ScreeningID = r.GetInt32("ScreeningID"),
|
||||||
|
MovieTitle = r.GetString("MovieTitle"),
|
||||||
|
ScreeningDate = r.GetDateTime("ScreeningDate"),
|
||||||
|
StartTime = r.GetTimeSpan("StartTime"),
|
||||||
|
SeatID = r.GetInt32("SeatID"),
|
||||||
|
SeatDisplay = r.GetString("SeatDisplay"),
|
||||||
|
BookingDate = r.GetDateTime("BookingDate"),
|
||||||
|
TotalPrice = r.GetDecimal("TotalPrice"),
|
||||||
|
Status = r.GetString("Status"),
|
||||||
|
BookingCode = r.IsDBNull(r.GetOrdinal("BookingCode")) ? "" : r.GetString("BookingCode")
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================== STATS ====================
|
||||||
|
public (int movies, int screenings, int bookings, int users) GetStats()
|
||||||
|
{
|
||||||
|
var m = _db.Query("SELECT COUNT(*) FROM Movies WHERE IsActive=1", r => r.GetInt32(0));
|
||||||
|
var s = _db.Query("SELECT COUNT(*) FROM Screenings WHERE ScreeningDate >= CURDATE()", r => r.GetInt32(0));
|
||||||
|
var b = _db.Query("SELECT COUNT(*) FROM Bookings WHERE Status='Confirmed'", r => r.GetInt32(0));
|
||||||
|
var u = _db.Query("SELECT COUNT(*) FROM Users", r => r.GetInt32(0));
|
||||||
|
return (m[0], s[0], b[0], u[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string HashPassword(string password)
|
||||||
|
{
|
||||||
|
// Quelle: https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.sha256 (abgerufen: 2026-02-01)
|
||||||
|
using var sha = SHA256.Create();
|
||||||
|
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(password));
|
||||||
|
return Convert.ToHexString(bytes).ToLower();
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Helpers/Session.cs
Normal file
11
Helpers/Session.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using CineBook.Models;
|
||||||
|
|
||||||
|
namespace CineBook.Helpers;
|
||||||
|
|
||||||
|
public static class Session
|
||||||
|
{
|
||||||
|
public static User? CurrentUser { get; set; }
|
||||||
|
public static bool IsLoggedIn => CurrentUser != null;
|
||||||
|
public static bool IsAdmin => CurrentUser?.IsAdmin == true;
|
||||||
|
public static void Logout() => CurrentUser = null;
|
||||||
|
}
|
||||||
76
Models/Models.cs
Normal file
76
Models/Models.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CineBook.Models;
|
||||||
|
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public int UserID { get; set; }
|
||||||
|
public string Username { get; set; } = "";
|
||||||
|
public string PasswordHash { get; set; } = "";
|
||||||
|
public string Role { get; set; } = "User";
|
||||||
|
public string Email { get; set; } = "";
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public bool IsAdmin => Role == "Admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Movie
|
||||||
|
{
|
||||||
|
public int MovieID { get; set; }
|
||||||
|
public string Title { get; set; } = "";
|
||||||
|
public string Genre { get; set; } = "";
|
||||||
|
public int DurationMinutes { get; set; }
|
||||||
|
public string Description { get; set; } = "";
|
||||||
|
public decimal Rating { get; set; }
|
||||||
|
public string PosterUrl { get; set; } = "";
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public string DurationDisplay => $"{DurationMinutes} Min.";
|
||||||
|
public string RatingDisplay => $"★ {Rating:F1}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Screening
|
||||||
|
{
|
||||||
|
public int ScreeningID { get; set; }
|
||||||
|
public int MovieID { get; set; }
|
||||||
|
public string MovieTitle { get; set; } = "";
|
||||||
|
public DateTime ScreeningDate { get; set; }
|
||||||
|
public TimeSpan StartTime { get; set; }
|
||||||
|
public string Hall { get; set; } = "";
|
||||||
|
public int TotalSeats { get; set; }
|
||||||
|
public int AvailableSeats { get; set; }
|
||||||
|
public decimal PricePerSeat { get; set; }
|
||||||
|
public string DateTimeDisplay => $"{ScreeningDate:dd.MM.yyyy} {StartTime:hh\\:mm}";
|
||||||
|
public string SeatsDisplay => $"{AvailableSeats}/{TotalSeats} frei";
|
||||||
|
public string PriceDisplay => $"{PricePerSeat:F2} €";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Seat
|
||||||
|
{
|
||||||
|
public int SeatID { get; set; }
|
||||||
|
public int ScreeningID { get; set; }
|
||||||
|
public string Row { get; set; } = "";
|
||||||
|
public int SeatNumber { get; set; }
|
||||||
|
public bool IsBooked { get; set; }
|
||||||
|
public string Category { get; set; } = "Standard";
|
||||||
|
public string Display => $"{Row}{SeatNumber}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Booking
|
||||||
|
{
|
||||||
|
public int BookingID { get; set; }
|
||||||
|
public int UserID { get; set; }
|
||||||
|
public string Username { get; set; } = "";
|
||||||
|
public int ScreeningID { get; set; }
|
||||||
|
public string MovieTitle { get; set; } = "";
|
||||||
|
public DateTime ScreeningDate { get; set; }
|
||||||
|
public TimeSpan StartTime { get; set; }
|
||||||
|
public int SeatID { get; set; }
|
||||||
|
public string SeatDisplay { get; set; } = "";
|
||||||
|
public DateTime BookingDate { get; set; }
|
||||||
|
public decimal TotalPrice { get; set; }
|
||||||
|
public string Status { get; set; } = "Confirmed";
|
||||||
|
public string BookingCode { get; set; } = "";
|
||||||
|
public string PriceDisplay => $"{TotalPrice:F2} €";
|
||||||
|
public string DateDisplay => BookingDate.ToString("dd.MM.yyyy HH:mm");
|
||||||
|
public string ScreeningDisplay => $"{ScreeningDate:dd.MM.yyyy} {StartTime:hh\\:mm}";
|
||||||
|
}
|
||||||
71
Views/AdminPanelView.xaml
Normal file
71
Views/AdminPanelView.xaml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<UserControl x:Class="CineBook.Views.AdminPanelView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Background="#0A0A0A">
|
||||||
|
<Grid Margin="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Text="🛠 ADMIN-PANEL — BENUTZERVERWALTUNG"
|
||||||
|
Style="{StaticResource RetroHeader}" Margin="0,0,0,16"/>
|
||||||
|
|
||||||
|
<!-- TOOLBAR -->
|
||||||
|
<Border Grid.Row="1" Background="#111" BorderBrush="#222" BorderThickness="1" Padding="12" Margin="0,0,0,12">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock Text="BENUTZERLISTE" Style="{StaticResource RetroLabel}" VerticalAlignment="Center"/>
|
||||||
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Content="[ + BENUTZER ANLEGEN ]" Style="{StaticResource RetroButtonGreen}"
|
||||||
|
Height="30" Click="AddUserClick"/>
|
||||||
|
<Button x:Name="BtnEditUser" Content="[ ✎ BEARBEITEN ]" Style="{StaticResource RetroButton}"
|
||||||
|
Margin="6,0" Height="30" Click="EditUserClick" IsEnabled="False"/>
|
||||||
|
<Button x:Name="BtnDeleteUser" Content="[ ✕ LÖSCHEN ]" Style="{StaticResource RetroButtonRed}"
|
||||||
|
Height="30" Click="DeleteUserClick" IsEnabled="False"/>
|
||||||
|
<Button Content="[ ↺ AKTUALISIEREN ]" Style="{StaticResource RetroButtonGray}"
|
||||||
|
Margin="6,0,0,0" Height="30" Click="RefreshClick"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<DataGrid x:Name="UsersGrid" Grid.Row="2" Style="{StaticResource RetroDataGrid}"
|
||||||
|
SelectionChanged="GridSelectionChanged">
|
||||||
|
<DataGrid.ColumnHeaderStyle>
|
||||||
|
<Style TargetType="DataGridColumnHeader">
|
||||||
|
<Setter Property="Background" Value="#1a1a1a"/>
|
||||||
|
<Setter Property="Foreground" Value="#F5C518"/>
|
||||||
|
<Setter Property="FontFamily" Value="Courier New"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter Property="FontSize" Value="11"/>
|
||||||
|
<Setter Property="Padding" Value="8,6"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#333"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0,0,1,1"/>
|
||||||
|
</Style>
|
||||||
|
</DataGrid.ColumnHeaderStyle>
|
||||||
|
<DataGrid.RowStyle>
|
||||||
|
<Style TargetType="DataGridRow">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="#CCC"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter Property="Background" Value="#1E1A00"/>
|
||||||
|
<Setter Property="Foreground" Value="#F5C518"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</DataGrid.RowStyle>
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="ID" Binding="{Binding UserID}" Width="50"/>
|
||||||
|
<DataGridTextColumn Header="BENUTZERNAME" Binding="{Binding Username}" Width="160"/>
|
||||||
|
<DataGridTextColumn Header="ROLLE" Binding="{Binding Role}" Width="100"/>
|
||||||
|
<DataGridTextColumn Header="E-MAIL" Binding="{Binding Email}" Width="*"/>
|
||||||
|
<DataGridTextColumn Header="ERSTELLT AM" Binding="{Binding CreatedAt, StringFormat='dd.MM.yyyy'}" Width="120"/>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
|
||||||
|
<TextBlock x:Name="StatusText" Grid.Row="3" Style="{StaticResource RetroLabel}"
|
||||||
|
Foreground="#666" Margin="0,8,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
92
Views/AdminPanelView.xaml.cs
Normal file
92
Views/AdminPanelView.xaml.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
using CineBook.Data;
|
||||||
|
using CineBook.Helpers;
|
||||||
|
using CineBook.Models;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace CineBook.Views;
|
||||||
|
|
||||||
|
public partial class AdminPanelView : UserControl
|
||||||
|
{
|
||||||
|
private readonly Repository _repo;
|
||||||
|
private User? _selected;
|
||||||
|
|
||||||
|
public AdminPanelView(Repository repo)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_repo = repo;
|
||||||
|
if (!Session.IsAdmin)
|
||||||
|
{
|
||||||
|
StatusText.Text = "⚠ Kein Zugriff. Nur für Administratoren.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LoadUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadUsers()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
UsersGrid.ItemsSource = _repo.GetAllUsers();
|
||||||
|
StatusText.Text = $"● {_repo.GetAllUsers().Count} Benutzer geladen.";
|
||||||
|
}
|
||||||
|
catch (System.Exception ex) { StatusText.Text = "⚠ Fehler: " + ex.Message; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GridSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_selected = UsersGrid.SelectedItem as User;
|
||||||
|
BtnEditUser.IsEnabled = _selected != null;
|
||||||
|
BtnDeleteUser.IsEnabled = _selected != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshClick(object sender, RoutedEventArgs e) => LoadUsers();
|
||||||
|
|
||||||
|
private void AddUserClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dlg = new UserEditDialog(null);
|
||||||
|
if (dlg.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_repo.CreateUser(dlg.Username, dlg.Password, dlg.Role, dlg.Email))
|
||||||
|
LoadUsers();
|
||||||
|
else
|
||||||
|
MessageBox.Show("Fehler beim Anlegen. Benutzername bereits vergeben?", "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
catch (System.Exception ex) { MessageBox.Show("DB-Fehler: " + ex.Message); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EditUserClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selected == null) return;
|
||||||
|
var dlg = new UserEditDialog(_selected);
|
||||||
|
if (dlg.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_repo.UpdateUser(_selected.UserID, dlg.Username, dlg.Role, dlg.Email);
|
||||||
|
LoadUsers();
|
||||||
|
}
|
||||||
|
catch (System.Exception ex) { MessageBox.Show("DB-Fehler: " + ex.Message); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteUserClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selected == null) return;
|
||||||
|
if (_selected.UserID == Session.CurrentUser?.UserID)
|
||||||
|
{
|
||||||
|
MessageBox.Show("Sie können sich nicht selbst löschen!", "Fehler", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var r = MessageBox.Show($"Benutzer \"{_selected.Username}\" wirklich löschen?",
|
||||||
|
"Bestätigung", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
|
if (r == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
try { _repo.DeleteUser(_selected.UserID); LoadUsers(); }
|
||||||
|
catch (System.Exception ex) { MessageBox.Show("DB-Fehler: " + ex.Message); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
Views/BookingsView.xaml
Normal file
77
Views/BookingsView.xaml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<UserControl x:Class="CineBook.Views.BookingsView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Background="#0A0A0A">
|
||||||
|
<Grid Margin="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Text="🎟 BUCHUNGSVERWALTUNG" Style="{StaticResource RetroHeader}" Margin="0,0,0,16"/>
|
||||||
|
|
||||||
|
<Border Grid.Row="1" Background="#111" BorderBrush="#222" BorderThickness="1" Padding="12" Margin="0,0,0,12">
|
||||||
|
<DockPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="STATUS:" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
|
<ComboBox x:Name="StatusFilter" Width="160" Height="30"
|
||||||
|
Background="#111" Foreground="#F5C518" FontFamily="Courier New"
|
||||||
|
SelectionChanged="FilterChanged">
|
||||||
|
<ComboBoxItem>Alle</ComboBoxItem>
|
||||||
|
<ComboBoxItem>Confirmed</ComboBoxItem>
|
||||||
|
<ComboBoxItem>Cancelled</ComboBoxItem>
|
||||||
|
</ComboBox>
|
||||||
|
<Button Content="[ AKTUALISIEREN ]" Style="{StaticResource RetroButtonGray}"
|
||||||
|
Margin="8,0,0,0" Height="30" Click="RefreshClick"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button x:Name="BtnCancel" Content="[ ✕ BUCHUNG STORNIEREN ]" Style="{StaticResource RetroButtonRed}"
|
||||||
|
Height="30" Click="CancelBookingClick" IsEnabled="False"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<DataGrid x:Name="BookingsGrid" Grid.Row="2" Style="{StaticResource RetroDataGrid}"
|
||||||
|
SelectionChanged="GridSelectionChanged">
|
||||||
|
<DataGrid.ColumnHeaderStyle>
|
||||||
|
<Style TargetType="DataGridColumnHeader">
|
||||||
|
<Setter Property="Background" Value="#1a1a1a"/>
|
||||||
|
<Setter Property="Foreground" Value="#F5C518"/>
|
||||||
|
<Setter Property="FontFamily" Value="Courier New"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter Property="FontSize" Value="11"/>
|
||||||
|
<Setter Property="Padding" Value="8,6"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#333"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0,0,1,1"/>
|
||||||
|
</Style>
|
||||||
|
</DataGrid.ColumnHeaderStyle>
|
||||||
|
<DataGrid.RowStyle>
|
||||||
|
<Style TargetType="DataGridRow">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="#CCC"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter Property="Background" Value="#1E1A00"/>
|
||||||
|
<Setter Property="Foreground" Value="#F5C518"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</DataGrid.RowStyle>
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="CODE" Binding="{Binding BookingCode}" Width="130"/>
|
||||||
|
<DataGridTextColumn Header="BENUTZER" Binding="{Binding Username}" Width="100"/>
|
||||||
|
<DataGridTextColumn Header="FILM" Binding="{Binding MovieTitle}" Width="*"/>
|
||||||
|
<DataGridTextColumn Header="VORFÜHRUNG" Binding="{Binding ScreeningDisplay}" Width="150"/>
|
||||||
|
<DataGridTextColumn Header="SITZ" Binding="{Binding SeatDisplay}" Width="70"/>
|
||||||
|
<DataGridTextColumn Header="PREIS" Binding="{Binding PriceDisplay}" Width="80"/>
|
||||||
|
<DataGridTextColumn Header="STATUS" Binding="{Binding Status}" Width="90"/>
|
||||||
|
<DataGridTextColumn Header="GEBUCHT AM" Binding="{Binding DateDisplay}" Width="130"/>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
|
||||||
|
<TextBlock x:Name="StatusText" Grid.Row="3" Style="{StaticResource RetroLabel}"
|
||||||
|
Foreground="#666" Margin="0,8,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
79
Views/BookingsView.xaml.cs
Normal file
79
Views/BookingsView.xaml.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using CineBook.Data;
|
||||||
|
using CineBook.Helpers;
|
||||||
|
using CineBook.Models;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace CineBook.Views;
|
||||||
|
|
||||||
|
public partial class BookingsView : UserControl
|
||||||
|
{
|
||||||
|
private readonly Repository _repo;
|
||||||
|
private readonly User _currentUser;
|
||||||
|
private Booking? _selected;
|
||||||
|
private List<Booking> _allBookings = new();
|
||||||
|
|
||||||
|
public BookingsView(Repository repo, User user)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_repo = repo;
|
||||||
|
_currentUser = user;
|
||||||
|
|
||||||
|
if (!Session.IsAdmin)
|
||||||
|
BtnCancel.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
|
StatusFilter.SelectedIndex = 0;
|
||||||
|
LoadBookings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadBookings()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_allBookings = Session.IsAdmin
|
||||||
|
? _repo.GetAllBookings()
|
||||||
|
: _repo.GetBookingsByUser(_currentUser.UserID);
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
catch (System.Exception ex) { StatusText.Text = "⚠ Fehler: " + ex.Message; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyFilter()
|
||||||
|
{
|
||||||
|
var filter = (StatusFilter.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "Alle";
|
||||||
|
var data = filter == "Alle" ? _allBookings : _allBookings.Where(b => b.Status == filter).ToList();
|
||||||
|
BookingsGrid.ItemsSource = data;
|
||||||
|
StatusText.Text = $"● {data.Count} Buchungen angezeigt.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FilterChanged(object sender, SelectionChangedEventArgs e) => ApplyFilter();
|
||||||
|
private void RefreshClick(object sender, RoutedEventArgs e) => LoadBookings();
|
||||||
|
|
||||||
|
private void GridSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_selected = BookingsGrid.SelectedItem as Booking;
|
||||||
|
BtnCancel.IsEnabled = _selected != null && _selected.Status == "Confirmed" && Session.IsAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelBookingClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selected == null) return;
|
||||||
|
var r = MessageBox.Show($"Buchung {_selected.BookingCode} wirklich stornieren?",
|
||||||
|
"Stornierung", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
|
if (r == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_repo.CancelBooking(_selected.BookingID, _selected.SeatID, _selected.ScreeningID);
|
||||||
|
LoadBookings();
|
||||||
|
MessageBox.Show("Buchung erfolgreich storniert.", "Erfolg", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show("DB-Fehler: " + ex.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
Views/LoginWindow.xaml
Normal file
48
Views/LoginWindow.xaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<Window x:Class="CineBook.Views.LoginWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="CineBook — Anmeldung" Height="460" Width="400"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
Background="#0A0A0A">
|
||||||
|
<Grid>
|
||||||
|
<StackPanel VerticalAlignment="Center" Margin="40">
|
||||||
|
<!-- Logo -->
|
||||||
|
<Border BorderBrush="#F5C518" BorderThickness="2" Padding="20,16" Margin="0,0,0,30">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="🎬 CINEBOOK" FontFamily="Courier New" FontSize="28"
|
||||||
|
FontWeight="Bold" Foreground="#F5C518" HorizontalAlignment="Center"/>
|
||||||
|
<TextBlock Text="━━━━━━━━━━━━━━━━━━" Foreground="#444" HorizontalAlignment="Center" Margin="0,4"/>
|
||||||
|
<TextBlock Text="K I N O V E R W A L T U N G S S Y S T E M" FontFamily="Courier New" FontSize="10"
|
||||||
|
Foreground="#888" HorizontalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<TextBlock x:Name="StatusText" Text="" Foreground="#C0392B"
|
||||||
|
FontFamily="Courier New" FontSize="12" HorizontalAlignment="Center"
|
||||||
|
Margin="0,0,0,10" Visibility="Collapsed"/>
|
||||||
|
|
||||||
|
<!-- Username -->
|
||||||
|
<TextBlock Text="BENUTZERNAME" Style="{StaticResource RetroLabel}" Margin="0,0,0,4"/>
|
||||||
|
<TextBox x:Name="UsernameBox" Style="{StaticResource RetroTextBox}"
|
||||||
|
Height="34" Margin="0,0,0,14"
|
||||||
|
KeyDown="InputKeyDown"/>
|
||||||
|
|
||||||
|
<!-- Password -->
|
||||||
|
<TextBlock Text="PASSWORT" Style="{StaticResource RetroLabel}" Margin="0,0,0,4"/>
|
||||||
|
<PasswordBox x:Name="PasswordBox" Style="{StaticResource RetroPasswordBox}"
|
||||||
|
Height="34" Margin="0,0,0,24"
|
||||||
|
KeyDown="InputKeyDown"/>
|
||||||
|
|
||||||
|
<!-- Login Button -->
|
||||||
|
<Button Content="[ ANMELDEN ]" Style="{StaticResource RetroButton}"
|
||||||
|
Height="38" Click="LoginClick"/>
|
||||||
|
|
||||||
|
<!-- Hint -->
|
||||||
|
<TextBlock Text="Demo: admin / admin123 | maria / 1" Foreground="#555"
|
||||||
|
FontFamily="Courier New" FontSize="10" HorizontalAlignment="Center"
|
||||||
|
Margin="0,20,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
88
Views/LoginWindow.xaml.cs
Normal file
88
Views/LoginWindow.xaml.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
using CineBook.Data;
|
||||||
|
using CineBook.Helpers;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace CineBook.Views;
|
||||||
|
|
||||||
|
public partial class LoginWindow : Window
|
||||||
|
{
|
||||||
|
private readonly Repository _repo;
|
||||||
|
private readonly bool _dbConfigured;
|
||||||
|
|
||||||
|
public LoginWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
var cs = DbConfig.GetConnectionString();
|
||||||
|
if (string.IsNullOrWhiteSpace(cs))
|
||||||
|
{
|
||||||
|
MessageBox.Show(
|
||||||
|
"Es ist keine DB-Verbindung konfiguriert.\n\n" +
|
||||||
|
"Bitte setze die Umgebungsvariable 'CINEBOOK_CONNECTION_STRING'.\n" +
|
||||||
|
"(Siehe Kommentar in Data/DbConfig.cs)",
|
||||||
|
"Konfiguration fehlt",
|
||||||
|
MessageBoxButton.OK,
|
||||||
|
MessageBoxImage.Error);
|
||||||
|
// Window bleibt offen, damit die Info sichtbar bleibt.
|
||||||
|
_repo = new Repository(new Db(""));
|
||||||
|
_dbConfigured = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var db = new Db(cs);
|
||||||
|
_repo = new Repository(db);
|
||||||
|
_dbConfigured = true;
|
||||||
|
}
|
||||||
|
UsernameBox.Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoginClick(object sender, RoutedEventArgs e) => TryLogin();
|
||||||
|
|
||||||
|
private void InputKeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == Key.Enter) TryLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryLogin()
|
||||||
|
{
|
||||||
|
if (!_dbConfigured)
|
||||||
|
{
|
||||||
|
ShowError("DB-Verbindung nicht konfiguriert (CINEBOOK_CONNECTION_STRING). Login nicht möglich.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var user = UsernameBox.Text.Trim();
|
||||||
|
var pass = PasswordBox.Password;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(user) || string.IsNullOrEmpty(pass))
|
||||||
|
{
|
||||||
|
ShowError("Bitte Benutzername und Passwort eingeben.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var found = _repo.GetUserByCredentials(user, pass);
|
||||||
|
if (found == null)
|
||||||
|
{
|
||||||
|
ShowError("Falscher Benutzername oder Passwort.");
|
||||||
|
PasswordBox.Clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Session.CurrentUser = found;
|
||||||
|
var main = new MainWindow();
|
||||||
|
main.Show();
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
ShowError("DB-Fehler: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowError(string msg)
|
||||||
|
{
|
||||||
|
StatusText.Text = "⚠ " + msg;
|
||||||
|
StatusText.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
118
Views/MainWindow.xaml
Normal file
118
Views/MainWindow.xaml
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<Window x:Class="CineBook.Views.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="CineBook — Kino Verwaltungssystem"
|
||||||
|
Height="680" Width="1100"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
Background="#0A0A0A">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="56"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="28"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- TOP BAR -->
|
||||||
|
<Border Grid.Row="0" Background="#111" BorderBrush="#F5C518" BorderThickness="0,0,0,2">
|
||||||
|
<DockPanel Margin="16,0">
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="🎬" FontSize="22" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text=" CINEBOOK" FontFamily="Courier New" FontSize="20"
|
||||||
|
FontWeight="Bold" Foreground="#F5C518" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text=" — KINO VERWALTUNGSSYSTEM" FontFamily="Courier New"
|
||||||
|
FontSize="11" Foreground="#666" VerticalAlignment="Center" Margin="4,0,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right">
|
||||||
|
<TextBlock x:Name="UserInfoText" FontFamily="Courier New" FontSize="12"
|
||||||
|
Foreground="#888" VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||||
|
<Button Content="[ ABMELDEN ]" Style="{StaticResource RetroButtonRed}"
|
||||||
|
Height="30" Click="LogoutClick"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- MAIN CONTENT - TAB NAVIGATION -->
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="160"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- SIDEBAR NAV -->
|
||||||
|
<Border Background="#111" BorderBrush="#222" BorderThickness="0,0,2,0">
|
||||||
|
<StackPanel Margin="0,16,0,0">
|
||||||
|
<TextBlock Text="N A V I G A T I O N" FontFamily="Courier New" FontSize="9"
|
||||||
|
Foreground="#555" Margin="16,0,0,10"/>
|
||||||
|
|
||||||
|
<Button x:Name="BtnMovies" Content="🎥 FILME" Click="NavClick"
|
||||||
|
Tag="movies" Style="{StaticResource NavButton}"/>
|
||||||
|
<Button x:Name="BtnScreenings" Content="📅 VORFÜHRUNGEN" Click="NavClick"
|
||||||
|
Tag="screenings" Style="{StaticResource NavButton}"/>
|
||||||
|
<Button x:Name="BtnBookings" Content="🎟 BUCHUNGEN" Click="NavClick"
|
||||||
|
Tag="bookings" Style="{StaticResource NavButton}"/>
|
||||||
|
<Button x:Name="BtnAdmin" Content="🛠 ADMIN" Click="NavClick"
|
||||||
|
Tag="admin" Style="{StaticResource NavButton}" Visibility="Collapsed"/>
|
||||||
|
|
||||||
|
<Separator Background="#222" Margin="16,20"/>
|
||||||
|
|
||||||
|
<TextBlock Text="K O N T O" FontFamily="Courier New" FontSize="9"
|
||||||
|
Foreground="#555" Margin="16,0,0,10"/>
|
||||||
|
<Button x:Name="BtnMine" Content="👤 MEINE BUCHUNGEN" Click="NavClick"
|
||||||
|
Tag="mine" Style="{StaticResource NavButton}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- VIEW AREA -->
|
||||||
|
<ContentControl x:Name="MainContent" Grid.Column="1" Background="#0A0A0A"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- STATUS BAR -->
|
||||||
|
<Border Grid.Row="2" Background="#111" BorderBrush="#222" BorderThickness="0,1,0,0">
|
||||||
|
<DockPanel Margin="12,0">
|
||||||
|
<TextBlock x:Name="DbStatusText" FontFamily="Courier New" FontSize="10"
|
||||||
|
Foreground="#555" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="StatsText" DockPanel.Dock="Right" FontFamily="Courier New"
|
||||||
|
FontSize="10" Foreground="#555" VerticalAlignment="Center" HorizontalAlignment="Right"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<Style x:Key="NavButton" TargetType="Button">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="#888"/>
|
||||||
|
<Setter Property="FontFamily" Value="Courier New"/>
|
||||||
|
<Setter Property="FontSize" Value="11"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Left"/>
|
||||||
|
<Setter Property="Padding" Value="16,10"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="{TemplateBinding Padding}"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="#1a1a1a"/>
|
||||||
|
<Setter Property="Foreground" Value="#F5C518"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="NavButtonActive" TargetType="Button" BasedOn="{StaticResource NavButton}">
|
||||||
|
<Setter Property="Background" Value="#1E1A00"/>
|
||||||
|
<Setter Property="Foreground" Value="#F5C518"/>
|
||||||
|
<Setter Property="BorderThickness" Value="3,0,0,0"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#F5C518"/>
|
||||||
|
</Style>
|
||||||
|
</Window.Resources>
|
||||||
|
</Window>
|
||||||
96
Views/MainWindow.xaml.cs
Normal file
96
Views/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
using CineBook.Data;
|
||||||
|
using CineBook.Helpers;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace CineBook.Views;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
private readonly Repository _repo;
|
||||||
|
private Button? _activeNavBtn;
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
if (!Session.IsLoggedIn)
|
||||||
|
{
|
||||||
|
// Schutz gegen NullReference, falls MainWindow direkt gestartet wird.
|
||||||
|
new LoginWindow().Show();
|
||||||
|
Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cs = DbConfig.GetConnectionString();
|
||||||
|
if (string.IsNullOrWhiteSpace(cs))
|
||||||
|
{
|
||||||
|
MessageBox.Show(
|
||||||
|
"DB-Verbindung nicht konfiguriert. Bitte setze 'CINEBOOK_CONNECTION_STRING'.",
|
||||||
|
"Konfiguration fehlt",
|
||||||
|
MessageBoxButton.OK,
|
||||||
|
MessageBoxImage.Error);
|
||||||
|
new LoginWindow().Show();
|
||||||
|
Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var db = new Db(cs);
|
||||||
|
_repo = new Repository(db);
|
||||||
|
|
||||||
|
UserInfoText.Text = $"[ {Session.CurrentUser!.Username.ToUpper()} | {Session.CurrentUser!.Role.ToUpper()} ]";
|
||||||
|
|
||||||
|
if (Session.IsAdmin)
|
||||||
|
BtnAdmin.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
|
SetStats();
|
||||||
|
Navigate("movies", BtnMovies);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NavClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Button btn)
|
||||||
|
Navigate(btn.Tag?.ToString() ?? "movies", btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Navigate(string view, Button btn)
|
||||||
|
{
|
||||||
|
if (_activeNavBtn != null)
|
||||||
|
_activeNavBtn.Style = (Style)Resources["NavButton"];
|
||||||
|
btn.Style = (Style)Resources["NavButtonActive"];
|
||||||
|
_activeNavBtn = btn;
|
||||||
|
|
||||||
|
MainContent.Content = view switch
|
||||||
|
{
|
||||||
|
"movies" => new MovieListView(_repo),
|
||||||
|
"screenings" => new ScreeningsView(_repo),
|
||||||
|
"bookings" => new BookingsView(_repo, Session.CurrentUser!),
|
||||||
|
"admin" => new AdminPanelView(_repo),
|
||||||
|
"mine" => new MyBookingsView(_repo, Session.CurrentUser!),
|
||||||
|
_ => new MovieListView(_repo)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogoutClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Session.Logout();
|
||||||
|
new LoginWindow().Show();
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetStats()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (movies, screenings, bookings, users) = _repo.GetStats();
|
||||||
|
StatsText.Text = $"Filme: {movies} | Vorführungen: {screenings} | Buchungen: {bookings} | Benutzer: {users}";
|
||||||
|
DbStatusText.Text = "● DB VERBUNDEN";
|
||||||
|
DbStatusText.Foreground = System.Windows.Media.Brushes.LimeGreen;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DbStatusText.Text = "● DB NICHT ERREICHBAR";
|
||||||
|
DbStatusText.Foreground = System.Windows.Media.Brushes.Red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
Views/MovieEditDialog.xaml
Normal file
74
Views/MovieEditDialog.xaml
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<Window x:Class="CineBook.Views.MovieEditDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Film bearbeiten" Height="420" Width="480"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
Background="#0A0A0A">
|
||||||
|
<Grid Margin="24">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock x:Name="DialogTitle" Text="🎥 FILM BEARBEITEN" Style="{StaticResource RetroHeader}"
|
||||||
|
Margin="0,0,0,20"/>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="110"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="TITEL *" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<TextBox x:Name="TitleBox" Grid.Column="1" Style="{StaticResource RetroTextBox}" Height="30" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Text="GENRE" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<ComboBox x:Name="GenreBox" Grid.Row="1" Grid.Column="1" Style="{StaticResource RetroComboBox}"
|
||||||
|
Height="30" Margin="0,0,0,10" Background="#111" Foreground="#F5C518">
|
||||||
|
<ComboBoxItem>Action</ComboBoxItem>
|
||||||
|
<ComboBoxItem>Drama</ComboBoxItem>
|
||||||
|
<ComboBoxItem>Komödie</ComboBoxItem>
|
||||||
|
<ComboBoxItem>Sci-Fi</ComboBoxItem>
|
||||||
|
<ComboBoxItem>Thriller</ComboBoxItem>
|
||||||
|
<ComboBoxItem>Horror</ComboBoxItem>
|
||||||
|
<ComboBoxItem>Dokumentation</ComboBoxItem>
|
||||||
|
<ComboBoxItem>Animation</ComboBoxItem>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Text="DAUER (Min)" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<TextBox x:Name="DurationBox" Grid.Row="2" Grid.Column="1" Style="{StaticResource RetroTextBox}"
|
||||||
|
Height="30" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Text="BEWERTUNG" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<TextBox x:Name="RatingBox" Grid.Row="3" Grid.Column="1" Style="{StaticResource RetroTextBox}"
|
||||||
|
Height="30" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="4" Text="AKTIV" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<CheckBox x:Name="ActiveCheck" Grid.Row="4" Grid.Column="1" IsChecked="True"
|
||||||
|
Foreground="#F5C518" FontFamily="Courier New" VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="5" Text="BESCHREIBUNG" Style="{StaticResource RetroLabel}" VerticalAlignment="Top" Margin="0,4,0,0"/>
|
||||||
|
<TextBox x:Name="DescBox" Grid.Row="5" Grid.Column="1" Style="{StaticResource RetroTextBox}"
|
||||||
|
TextWrapping="Wrap" AcceptsReturn="True" VerticalScrollBarVisibility="Auto"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,16,0,0">
|
||||||
|
<TextBlock x:Name="ErrorText" Foreground="#C0392B" FontFamily="Courier New" FontSize="11"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,0"/>
|
||||||
|
<Button Content="[ ABBRECHEN ]" Style="{StaticResource RetroButtonGray}"
|
||||||
|
Height="32" Click="CancelClick" Margin="0,0,8,0"/>
|
||||||
|
<Button Content="[ SPEICHERN ]" Style="{StaticResource RetroButtonGreen}"
|
||||||
|
Height="32" Click="SaveClick"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
76
Views/MovieEditDialog.xaml.cs
Normal file
76
Views/MovieEditDialog.xaml.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using CineBook.Models;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace CineBook.Views;
|
||||||
|
|
||||||
|
public partial class MovieEditDialog : Window
|
||||||
|
{
|
||||||
|
public Movie? Result { get; private set; }
|
||||||
|
private readonly Movie? _editing;
|
||||||
|
|
||||||
|
public MovieEditDialog(Movie? movie)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_editing = movie;
|
||||||
|
|
||||||
|
if (movie != null)
|
||||||
|
{
|
||||||
|
DialogTitle.Text = "🎥 FILM BEARBEITEN";
|
||||||
|
TitleBox.Text = movie.Title;
|
||||||
|
// Set genre combobox
|
||||||
|
foreach (ComboBoxItem item in GenreBox.Items)
|
||||||
|
if (item.Content?.ToString() == movie.Genre)
|
||||||
|
GenreBox.SelectedItem = item;
|
||||||
|
DurationBox.Text = movie.DurationMinutes.ToString();
|
||||||
|
RatingBox.Text = movie.Rating.ToString("F1");
|
||||||
|
ActiveCheck.IsChecked = movie.IsActive;
|
||||||
|
DescBox.Text = movie.Description;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DialogTitle.Text = "🎥 FILM HINZUFÜGEN";
|
||||||
|
RatingBox.Text = "7.0";
|
||||||
|
ActiveCheck.IsChecked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
// Validierung
|
||||||
|
if (string.IsNullOrWhiteSpace(TitleBox.Text))
|
||||||
|
{
|
||||||
|
ErrorText.Text = "⚠ Titel ist Pflichtfeld!"; return;
|
||||||
|
}
|
||||||
|
if (!int.TryParse(DurationBox.Text, out var dur) || dur <= 0)
|
||||||
|
{
|
||||||
|
ErrorText.Text = "⚠ Ungültige Dauer!"; return;
|
||||||
|
}
|
||||||
|
if (!decimal.TryParse(RatingBox.Text.Replace(',', '.'),
|
||||||
|
System.Globalization.NumberStyles.Any,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var rating)
|
||||||
|
|| rating < 0 || rating > 10)
|
||||||
|
{
|
||||||
|
ErrorText.Text = "⚠ Bewertung muss zwischen 0-10 liegen!"; return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result = new Movie
|
||||||
|
{
|
||||||
|
MovieID = _editing?.MovieID ?? 0,
|
||||||
|
Title = TitleBox.Text.Trim(),
|
||||||
|
Genre = (GenreBox.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "",
|
||||||
|
DurationMinutes = dur,
|
||||||
|
Rating = rating,
|
||||||
|
IsActive = ActiveCheck.IsChecked == true,
|
||||||
|
Description = DescBox.Text.Trim()
|
||||||
|
};
|
||||||
|
DialogResult = true;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
DialogResult = false;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
86
Views/MovieListView.xaml
Normal file
86
Views/MovieListView.xaml
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<UserControl x:Class="CineBook.Views.MovieListView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Background="#0A0A0A">
|
||||||
|
<Grid Margin="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,16">
|
||||||
|
<TextBlock Text="🎥 FILMVERWALTUNG" Style="{StaticResource RetroHeader}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- TOOLBAR -->
|
||||||
|
<Border Grid.Row="1" Background="#111" BorderBrush="#222" BorderThickness="1" Padding="12" Margin="0,0,0,12">
|
||||||
|
<DockPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="SUCHE:" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
|
<TextBox x:Name="SearchBox" Style="{StaticResource RetroTextBox}"
|
||||||
|
Width="220" Height="30" TextChanged="SearchChanged"/>
|
||||||
|
<Button Content="[ SUCHEN ]" Style="{StaticResource RetroButton}"
|
||||||
|
Margin="8,0,0,0" Height="30" Click="SearchClick"/>
|
||||||
|
<Button Content="[ ALLE ]" Style="{StaticResource RetroButtonGray}"
|
||||||
|
Margin="4,0,0,0" Height="30" Click="LoadAllClick"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button x:Name="BtnAdd" Content="[ + HINZUFÜGEN ]" Style="{StaticResource RetroButtonGreen}"
|
||||||
|
Height="30" Click="AddClick"/>
|
||||||
|
<Button x:Name="BtnEdit" Content="[ ✎ BEARBEITEN ]" Style="{StaticResource RetroButton}"
|
||||||
|
Margin="6,0" Height="30" Click="EditClick" IsEnabled="False"/>
|
||||||
|
<Button x:Name="BtnDelete" Content="[ ✕ LÖSCHEN ]" Style="{StaticResource RetroButtonRed}"
|
||||||
|
Height="30" Click="DeleteClick" IsEnabled="False"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- DATA GRID -->
|
||||||
|
<DataGrid x:Name="MoviesGrid" Grid.Row="2" Style="{StaticResource RetroDataGrid}"
|
||||||
|
SelectionChanged="GridSelectionChanged">
|
||||||
|
<DataGrid.ColumnHeaderStyle>
|
||||||
|
<Style TargetType="DataGridColumnHeader">
|
||||||
|
<Setter Property="Background" Value="#1a1a1a"/>
|
||||||
|
<Setter Property="Foreground" Value="#F5C518"/>
|
||||||
|
<Setter Property="FontFamily" Value="Courier New"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter Property="FontSize" Value="11"/>
|
||||||
|
<Setter Property="Padding" Value="8,6"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#333"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0,0,1,1"/>
|
||||||
|
</Style>
|
||||||
|
</DataGrid.ColumnHeaderStyle>
|
||||||
|
<DataGrid.RowStyle>
|
||||||
|
<Style TargetType="DataGridRow">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="#CCC"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter Property="Background" Value="#1E1A00"/>
|
||||||
|
<Setter Property="Foreground" Value="#F5C518"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="#161200"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</DataGrid.RowStyle>
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="ID" Binding="{Binding MovieID}" Width="50"/>
|
||||||
|
<DataGridTextColumn Header="TITEL" Binding="{Binding Title}" Width="*"/>
|
||||||
|
<DataGridTextColumn Header="GENRE" Binding="{Binding Genre}" Width="100"/>
|
||||||
|
<DataGridTextColumn Header="DAUER" Binding="{Binding DurationDisplay}" Width="90"/>
|
||||||
|
<DataGridTextColumn Header="BEWERTUNG" Binding="{Binding RatingDisplay}" Width="100"/>
|
||||||
|
<DataGridCheckBoxColumn Header="AKTIV" Binding="{Binding IsActive}" Width="70"/>
|
||||||
|
<DataGridTextColumn Header="BESCHREIBUNG" Binding="{Binding Description}" Width="200"/>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
|
||||||
|
<!-- STATUS -->
|
||||||
|
<TextBlock x:Name="StatusText" Grid.Row="3" Style="{StaticResource RetroLabel}"
|
||||||
|
Foreground="#666" Margin="0,8,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
120
Views/MovieListView.xaml.cs
Normal file
120
Views/MovieListView.xaml.cs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
using CineBook.Data;
|
||||||
|
using CineBook.Helpers;
|
||||||
|
using CineBook.Models;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace CineBook.Views;
|
||||||
|
|
||||||
|
public partial class MovieListView : UserControl
|
||||||
|
{
|
||||||
|
private readonly Repository _repo;
|
||||||
|
private Movie? _selected;
|
||||||
|
|
||||||
|
public MovieListView(Repository repo)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_repo = repo;
|
||||||
|
|
||||||
|
// Nur Admins dürfen bearbeiten/löschen
|
||||||
|
if (!Session.IsAdmin)
|
||||||
|
{
|
||||||
|
BtnAdd.IsEnabled = false;
|
||||||
|
BtnAdd.Visibility = Visibility.Collapsed;
|
||||||
|
BtnDelete.Visibility = Visibility.Collapsed;
|
||||||
|
BtnEdit.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
LoadMovies();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadMovies()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MoviesGrid.ItemsSource = _repo.GetMovies();
|
||||||
|
StatusText.Text = $"● {_repo.GetMovies().Count} Filme geladen.";
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
StatusText.Text = "⚠ Fehler: " + ex.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GridSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_selected = MoviesGrid.SelectedItem as Movie;
|
||||||
|
BtnEdit.IsEnabled = _selected != null && Session.IsAdmin;
|
||||||
|
BtnDelete.IsEnabled = _selected != null && Session.IsAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SearchChanged(object sender, TextChangedEventArgs e) { }
|
||||||
|
private void SearchClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var term = SearchBox.Text.Trim();
|
||||||
|
if (string.IsNullOrEmpty(term)) { LoadMovies(); return; }
|
||||||
|
try { MoviesGrid.ItemsSource = _repo.SearchMovies(term); }
|
||||||
|
catch (System.Exception ex) { StatusText.Text = "⚠ " + ex.Message; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadAllClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
SearchBox.Text = "";
|
||||||
|
LoadMovies();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dlg = new MovieEditDialog(null);
|
||||||
|
if (dlg.ShowDialog() == true && dlg.Result != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_repo.CreateMovie(dlg.Result))
|
||||||
|
LoadMovies();
|
||||||
|
else
|
||||||
|
MessageBox.Show("Fehler beim Speichern.", "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show("DB-Fehler: " + ex.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EditClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selected == null) return;
|
||||||
|
var dlg = new MovieEditDialog(_selected);
|
||||||
|
if (dlg.ShowDialog() == true && dlg.Result != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_repo.UpdateMovie(dlg.Result))
|
||||||
|
LoadMovies();
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show("DB-Fehler: " + ex.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selected == null) return;
|
||||||
|
var r = MessageBox.Show($"Film \"{_selected.Title}\" wirklich löschen?", "Bestätigung",
|
||||||
|
MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
|
if (r == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_repo.DeleteMovie(_selected.MovieID);
|
||||||
|
LoadMovies();
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show("DB-Fehler: " + ex.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
Views/MyBookingsView.xaml
Normal file
84
Views/MyBookingsView.xaml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<UserControl x:Class="CineBook.Views.MyBookingsView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Background="#0A0A0A">
|
||||||
|
<Grid Margin="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="0" Margin="0,0,0,16">
|
||||||
|
<TextBlock Text="👤 MEINE BUCHUNGEN" Style="{StaticResource RetroHeader}"/>
|
||||||
|
<TextBlock x:Name="UserNameText" FontFamily="Courier New" FontSize="11" Foreground="#666" Margin="0,4,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- STATS PANEL -->
|
||||||
|
<Border Grid.Row="1" Background="#111" BorderBrush="#222" BorderThickness="1" Padding="16,12" Margin="0,0,0,14">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border Background="#1B2E1B" BorderBrush="#27AE60" BorderThickness="1" Padding="16,8" CornerRadius="2" Margin="0,0,12,0">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock x:Name="TotalBookingsText" Text="0" FontFamily="Courier New" FontSize="22"
|
||||||
|
FontWeight="Bold" Foreground="#27AE60" HorizontalAlignment="Center"/>
|
||||||
|
<TextBlock Text="BUCHUNGEN" FontFamily="Courier New" FontSize="9" Foreground="#555" HorizontalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Background="#2A1A00" BorderBrush="#F5C518" BorderThickness="1" Padding="16,8" CornerRadius="2" Margin="0,0,12,0">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock x:Name="TotalSpentText" Text="0 €" FontFamily="Courier New" FontSize="22"
|
||||||
|
FontWeight="Bold" Foreground="#F5C518" HorizontalAlignment="Center"/>
|
||||||
|
<TextBlock Text="AUSGEGEBEN" FontFamily="Courier New" FontSize="9" Foreground="#555" HorizontalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Background="#2A1A1A" BorderBrush="#C0392B" BorderThickness="1" Padding="16,8" CornerRadius="2">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock x:Name="ActiveBookingsText" Text="0" FontFamily="Courier New" FontSize="22"
|
||||||
|
FontWeight="Bold" Foreground="#C0392B" HorizontalAlignment="Center"/>
|
||||||
|
<TextBlock Text="AKTIV" FontFamily="Courier New" FontSize="9" Foreground="#555" HorizontalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<DataGrid x:Name="MyGrid" Grid.Row="2" Style="{StaticResource RetroDataGrid}">
|
||||||
|
<DataGrid.ColumnHeaderStyle>
|
||||||
|
<Style TargetType="DataGridColumnHeader">
|
||||||
|
<Setter Property="Background" Value="#1a1a1a"/>
|
||||||
|
<Setter Property="Foreground" Value="#F5C518"/>
|
||||||
|
<Setter Property="FontFamily" Value="Courier New"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter Property="FontSize" Value="11"/>
|
||||||
|
<Setter Property="Padding" Value="8,6"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#333"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0,0,1,1"/>
|
||||||
|
</Style>
|
||||||
|
</DataGrid.ColumnHeaderStyle>
|
||||||
|
<DataGrid.RowStyle>
|
||||||
|
<Style TargetType="DataGridRow">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="#CCC"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter Property="Background" Value="#1E1A00"/>
|
||||||
|
<Setter Property="Foreground" Value="#F5C518"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</DataGrid.RowStyle>
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="BUCHUNGSCODE" Binding="{Binding BookingCode}" Width="140"/>
|
||||||
|
<DataGridTextColumn Header="FILM" Binding="{Binding MovieTitle}" Width="*"/>
|
||||||
|
<DataGridTextColumn Header="VORFÜHRUNG" Binding="{Binding ScreeningDisplay}" Width="150"/>
|
||||||
|
<DataGridTextColumn Header="SITZ" Binding="{Binding SeatDisplay}" Width="70"/>
|
||||||
|
<DataGridTextColumn Header="PREIS" Binding="{Binding PriceDisplay}" Width="80"/>
|
||||||
|
<DataGridTextColumn Header="STATUS" Binding="{Binding Status}" Width="100"/>
|
||||||
|
<DataGridTextColumn Header="DATUM" Binding="{Binding DateDisplay}" Width="130"/>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
|
||||||
|
<TextBlock x:Name="StatusText" Grid.Row="3" Style="{StaticResource RetroLabel}"
|
||||||
|
Foreground="#666" Margin="0,8,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
36
Views/MyBookingsView.xaml.cs
Normal file
36
Views/MyBookingsView.xaml.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using CineBook.Data;
|
||||||
|
using CineBook.Models;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace CineBook.Views;
|
||||||
|
|
||||||
|
public partial class MyBookingsView : UserControl
|
||||||
|
{
|
||||||
|
private readonly Repository _repo;
|
||||||
|
private readonly User _user;
|
||||||
|
|
||||||
|
public MyBookingsView(Repository repo, User user)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_repo = repo;
|
||||||
|
_user = user;
|
||||||
|
UserNameText.Text = $"Angemeldet als: {user.Username} ({user.Role}) | {user.Email}";
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Load()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bookings = _repo.GetBookingsByUser(_user.UserID);
|
||||||
|
MyGrid.ItemsSource = bookings;
|
||||||
|
|
||||||
|
TotalBookingsText.Text = bookings.Count.ToString();
|
||||||
|
TotalSpentText.Text = bookings.Where(b => b.Status == "Confirmed").Sum(b => b.TotalPrice).ToString("F2") + " €";
|
||||||
|
ActiveBookingsText.Text = bookings.Count(b => b.Status == "Confirmed").ToString();
|
||||||
|
StatusText.Text = $"● {bookings.Count} Buchungen gefunden.";
|
||||||
|
}
|
||||||
|
catch (System.Exception ex) { StatusText.Text = "⚠ Fehler: " + ex.Message; }
|
||||||
|
}
|
||||||
|
}
|
||||||
64
Views/ScreeningEditDialog.xaml
Normal file
64
Views/ScreeningEditDialog.xaml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<Window x:Class="CineBook.Views.ScreeningEditDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Neue Vorführung" Height="360" Width="440"
|
||||||
|
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
|
||||||
|
Background="#0A0A0A">
|
||||||
|
<Grid Margin="24">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="📅 NEUE VORFÜHRUNG" Style="{StaticResource RetroHeader}" Margin="0,0,0,20"/>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="110"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="FILM *" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<ComboBox x:Name="MovieBox" Grid.Column="1" Height="30" Margin="0,0,0,10"
|
||||||
|
Background="#111" Foreground="#F5C518" FontFamily="Courier New"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Text="DATUM *" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<DatePicker x:Name="DatePicker" Grid.Row="1" Grid.Column="1" Height="30" Margin="0,0,0,10"
|
||||||
|
Background="#111" Foreground="#F5C518" FontFamily="Courier New"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Text="UHRZEIT *" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<TextBox x:Name="TimeBox" Grid.Row="2" Grid.Column="1" Style="{StaticResource RetroTextBox}"
|
||||||
|
Height="30" Margin="0,0,0,10" Text="18:00"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Text="SAAL *" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<ComboBox x:Name="HallBox" Grid.Row="3" Grid.Column="1" Height="30" Margin="0,0,0,10"
|
||||||
|
Background="#111" Foreground="#F5C518" FontFamily="Courier New">
|
||||||
|
<ComboBoxItem>Saal 1</ComboBoxItem>
|
||||||
|
<ComboBoxItem>Saal 2</ComboBoxItem>
|
||||||
|
<ComboBoxItem>Saal 3</ComboBoxItem>
|
||||||
|
<ComboBoxItem>VIP-Saal</ComboBoxItem>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="4" Text="PREIS (€) *" Style="{StaticResource RetroLabel}" VerticalAlignment="Center"/>
|
||||||
|
<TextBox x:Name="PriceBox" Grid.Row="4" Grid.Column="1" Style="{StaticResource RetroTextBox}"
|
||||||
|
Height="30" Text="12.00"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,16,0,0">
|
||||||
|
<TextBlock x:Name="ErrorText" Foreground="#C0392B" FontFamily="Courier New" FontSize="11"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,0"/>
|
||||||
|
<Button Content="[ ABBRECHEN ]" Style="{StaticResource RetroButtonGray}" Height="32"
|
||||||
|
Click="CancelClick" Margin="0,0,8,0"/>
|
||||||
|
<Button Content="[ SPEICHERN ]" Style="{StaticResource RetroButtonGreen}" Height="32"
|
||||||
|
Click="SaveClick"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
87
Views/ScreeningEditDialog.xaml.cs
Normal file
87
Views/ScreeningEditDialog.xaml.cs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
using CineBook.Models;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace CineBook.Views;
|
||||||
|
|
||||||
|
public partial class ScreeningEditDialog : Window
|
||||||
|
{
|
||||||
|
public Screening? Result { get; private set; }
|
||||||
|
private readonly List<Movie> _movies;
|
||||||
|
private readonly Screening? _edit;
|
||||||
|
|
||||||
|
public ScreeningEditDialog(List<Movie> movies, Screening? edit = null)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_movies = movies;
|
||||||
|
_edit = edit;
|
||||||
|
|
||||||
|
foreach (var m in movies)
|
||||||
|
MovieBox.Items.Add(new ComboBoxItem { Content = m.Title, Tag = m.MovieID, Foreground = System.Windows.Media.Brushes.Gold, FontFamily = new System.Windows.Media.FontFamily("Courier New") });
|
||||||
|
|
||||||
|
if (MovieBox.Items.Count > 0) MovieBox.SelectedIndex = 0;
|
||||||
|
HallBox.SelectedIndex = 0;
|
||||||
|
DatePicker.SelectedDate = DateTime.Today.AddDays(1);
|
||||||
|
|
||||||
|
if (_edit != null)
|
||||||
|
{
|
||||||
|
Title = "Vorführung bearbeiten";
|
||||||
|
// Film vorauswählen
|
||||||
|
for (var i = 0; i < MovieBox.Items.Count; i++)
|
||||||
|
{
|
||||||
|
if (MovieBox.Items[i] is ComboBoxItem ci && (int)ci.Tag == _edit.MovieID)
|
||||||
|
{
|
||||||
|
MovieBox.SelectedIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DatePicker.SelectedDate = _edit.ScreeningDate;
|
||||||
|
TimeBox.Text = _edit.StartTime.ToString(@"hh\:mm");
|
||||||
|
|
||||||
|
// Hall vorauswählen
|
||||||
|
for (var i = 0; i < HallBox.Items.Count; i++)
|
||||||
|
{
|
||||||
|
if (HallBox.Items[i] is ComboBoxItem hi && (hi.Content?.ToString() ?? "") == _edit.Hall)
|
||||||
|
{
|
||||||
|
HallBox.SelectedIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PriceBox.Text = _edit.PricePerSeat.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (MovieBox.SelectedItem is not ComboBoxItem mi) { ErrorText.Text = "⚠ Film auswählen!"; return; }
|
||||||
|
if (DatePicker.SelectedDate == null) { ErrorText.Text = "⚠ Datum auswählen!"; return; }
|
||||||
|
if (!TimeSpan.TryParse(TimeBox.Text, out var time)) { ErrorText.Text = "⚠ Uhrzeit ungültig (HH:MM)!"; return; }
|
||||||
|
if (HallBox.SelectedItem is not ComboBoxItem hi) { ErrorText.Text = "⚠ Saal auswählen!"; return; }
|
||||||
|
if (!decimal.TryParse(PriceBox.Text.Replace(',', '.'),
|
||||||
|
System.Globalization.NumberStyles.Any,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var price) || price <= 0)
|
||||||
|
{ ErrorText.Text = "⚠ Ungültiger Preis!"; return; }
|
||||||
|
|
||||||
|
Result = new Screening
|
||||||
|
{
|
||||||
|
ScreeningID = _edit?.ScreeningID ?? 0,
|
||||||
|
MovieID = (int)mi.Tag,
|
||||||
|
ScreeningDate = DatePicker.SelectedDate.Value,
|
||||||
|
StartTime = time,
|
||||||
|
Hall = hi.Content?.ToString() ?? "Saal 1",
|
||||||
|
// Seats werden beim Anlegen im Repository automatisch erzeugt.
|
||||||
|
// Beim Bearbeiten werden TotalSeats/AvailableSeats nicht verändert (damit Buchungen konsistent bleiben).
|
||||||
|
TotalSeats = _edit?.TotalSeats ?? 80,
|
||||||
|
AvailableSeats = _edit?.AvailableSeats ?? 80,
|
||||||
|
PricePerSeat = price
|
||||||
|
};
|
||||||
|
DialogResult = true;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelClick(object sender, RoutedEventArgs e) { DialogResult = false; Close(); }
|
||||||
|
}
|
||||||
73
Views/ScreeningsView.xaml
Normal file
73
Views/ScreeningsView.xaml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<UserControl x:Class="CineBook.Views.ScreeningsView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Background="#0A0A0A">
|
||||||
|
<Grid Margin="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Grid.Row="0" Text="📅 VORFÜHRUNGEN" Style="{StaticResource RetroHeader}" Margin="0,0,0,16"/>
|
||||||
|
<Border Grid.Row="1" Background="#111" BorderBrush="#222" BorderThickness="1" Padding="12" Margin="0,0,0,12">
|
||||||
|
<DockPanel>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="FILM-FILTER:" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
|
<ComboBox x:Name="MovieFilter" Width="200" Height="30"
|
||||||
|
Background="#111" Foreground="#F5C518" FontFamily="Courier New"
|
||||||
|
SelectionChanged="FilterChanged"/>
|
||||||
|
<Button Content="[ ALLE ]" Style="{StaticResource RetroButtonGray}"
|
||||||
|
Margin="8,0,0,0" Height="30" Click="LoadAllClick"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button x:Name="BtnAdd" Content="[ + NEUE VORFÜHRUNG ]" Style="{StaticResource RetroButtonGreen}"
|
||||||
|
Height="30" Click="AddClick"/>
|
||||||
|
<Button x:Name="BtnEdit" Content="[ ✎ BEARBEITEN ]" Style="{StaticResource RetroButtonGray}"
|
||||||
|
Margin="6,0,0,0" Height="30" Click="EditClick" IsEnabled="False"/>
|
||||||
|
<Button x:Name="BtnDelete" Content="[ ✕ LÖSCHEN ]" Style="{StaticResource RetroButtonRed}"
|
||||||
|
Margin="6,0,0,0" Height="30" Click="DeleteClick" IsEnabled="False"/>
|
||||||
|
<Button x:Name="BtnBookSeat" Content="[ 🎟 TICKET BUCHEN ]" Style="{StaticResource RetroButton}"
|
||||||
|
Margin="6,0,0,0" Height="30" Click="BookSeatClick" IsEnabled="False"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
<DataGrid x:Name="ScreeningsGrid" Grid.Row="2" Style="{StaticResource RetroDataGrid}"
|
||||||
|
SelectionChanged="GridSelectionChanged">
|
||||||
|
<DataGrid.ColumnHeaderStyle>
|
||||||
|
<Style TargetType="DataGridColumnHeader">
|
||||||
|
<Setter Property="Background" Value="#1a1a1a"/>
|
||||||
|
<Setter Property="Foreground" Value="#F5C518"/>
|
||||||
|
<Setter Property="FontFamily" Value="Courier New"/>
|
||||||
|
<Setter Property="FontWeight" Value="Bold"/>
|
||||||
|
<Setter Property="FontSize" Value="11"/>
|
||||||
|
<Setter Property="Padding" Value="8,6"/>
|
||||||
|
<Setter Property="BorderBrush" Value="#333"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0,0,1,1"/>
|
||||||
|
</Style>
|
||||||
|
</DataGrid.ColumnHeaderStyle>
|
||||||
|
<DataGrid.RowStyle>
|
||||||
|
<Style TargetType="DataGridRow">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="Foreground" Value="#CCC"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter Property="Background" Value="#1E1A00"/>
|
||||||
|
<Setter Property="Foreground" Value="#F5C518"/>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</DataGrid.RowStyle>
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="ID" Binding="{Binding ScreeningID}" Width="50"/>
|
||||||
|
<DataGridTextColumn Header="FILM" Binding="{Binding MovieTitle}" Width="*"/>
|
||||||
|
<DataGridTextColumn Header="DATUM/ZEIT" Binding="{Binding DateTimeDisplay}" Width="160"/>
|
||||||
|
<DataGridTextColumn Header="SAAL" Binding="{Binding Hall}" Width="80"/>
|
||||||
|
<DataGridTextColumn Header="FREIE SITZE" Binding="{Binding SeatsDisplay}" Width="110"/>
|
||||||
|
<DataGridTextColumn Header="PREIS" Binding="{Binding PriceDisplay}" Width="90"/>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
<TextBlock x:Name="StatusText" Grid.Row="3" Style="{StaticResource RetroLabel}"
|
||||||
|
Foreground="#666" Margin="0,8,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
158
Views/ScreeningsView.xaml.cs
Normal file
158
Views/ScreeningsView.xaml.cs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
using CineBook.Data;
|
||||||
|
using CineBook.Helpers;
|
||||||
|
using CineBook.Models;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace CineBook.Views;
|
||||||
|
|
||||||
|
public partial class ScreeningsView : UserControl
|
||||||
|
{
|
||||||
|
private readonly Repository _repo;
|
||||||
|
private Screening? _selected;
|
||||||
|
|
||||||
|
public ScreeningsView(Repository repo)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_repo = repo;
|
||||||
|
if (!Session.IsAdmin)
|
||||||
|
{
|
||||||
|
BtnAdd.Visibility = Visibility.Collapsed;
|
||||||
|
BtnEdit.Visibility = Visibility.Collapsed;
|
||||||
|
BtnDelete.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
LoadMovieFilter();
|
||||||
|
LoadScreenings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadMovieFilter()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var movies = _repo.GetMovies(onlyActive: true);
|
||||||
|
MovieFilter.Items.Clear();
|
||||||
|
MovieFilter.Items.Add(new ComboBoxItem { Content = "— Alle Filme —", Tag = 0, Foreground = System.Windows.Media.Brushes.Gray, FontFamily = new System.Windows.Media.FontFamily("Courier New") });
|
||||||
|
foreach (var m in movies)
|
||||||
|
MovieFilter.Items.Add(new ComboBoxItem { Content = m.Title, Tag = m.MovieID, Foreground = System.Windows.Media.Brushes.Gold, FontFamily = new System.Windows.Media.FontFamily("Courier New") });
|
||||||
|
MovieFilter.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadScreenings()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = _repo.GetScreenings();
|
||||||
|
ScreeningsGrid.ItemsSource = list;
|
||||||
|
StatusText.Text = $"● {list.Count} Vorführungen geladen.";
|
||||||
|
}
|
||||||
|
catch (System.Exception ex) { StatusText.Text = "⚠ Fehler: " + ex.Message; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FilterChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (MovieFilter.SelectedItem is ComboBoxItem item && item.Tag is int id && id > 0)
|
||||||
|
{
|
||||||
|
try { ScreeningsGrid.ItemsSource = _repo.GetScreeningsByMovie(id); }
|
||||||
|
catch (System.Exception ex) { StatusText.Text = "⚠ " + ex.Message; }
|
||||||
|
}
|
||||||
|
else LoadScreenings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GridSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_selected = ScreeningsGrid.SelectedItem as Screening;
|
||||||
|
BtnDelete.IsEnabled = _selected != null && Session.IsAdmin;
|
||||||
|
BtnEdit.IsEnabled = _selected != null && Session.IsAdmin;
|
||||||
|
BtnBookSeat.IsEnabled = _selected != null && _selected.AvailableSeats > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadAllClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
MovieFilter.SelectedIndex = 0;
|
||||||
|
LoadScreenings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var movies = _repo.GetMovies(onlyActive: true);
|
||||||
|
var dlg = new ScreeningEditDialog(movies);
|
||||||
|
if (dlg.ShowDialog() == true && dlg.Result != null)
|
||||||
|
{
|
||||||
|
try { _repo.CreateScreening(dlg.Result); LoadScreenings(); LoadMovieFilter(); }
|
||||||
|
catch (System.Exception ex) { MessageBox.Show("DB-Fehler: " + ex.Message); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EditClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selected == null) return;
|
||||||
|
var movies = _repo.GetMovies(onlyActive: true);
|
||||||
|
var dlg = new ScreeningEditDialog(movies, _selected);
|
||||||
|
if (dlg.ShowDialog() == true && dlg.Result != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_repo.UpdateScreening(dlg.Result))
|
||||||
|
LoadScreenings();
|
||||||
|
else
|
||||||
|
MessageBox.Show("Fehler beim Speichern.", "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show("DB-Fehler: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selected == null) return;
|
||||||
|
var r = MessageBox.Show($"Vorführung löschen?", "Bestätigung", MessageBoxButton.YesNo, MessageBoxImage.Warning);
|
||||||
|
if (r == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
try { _repo.DeleteScreening(_selected.ScreeningID); LoadScreenings(); }
|
||||||
|
catch (System.Exception ex) { MessageBox.Show("DB-Fehler: " + ex.Message); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BookSeatClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selected == null) return;
|
||||||
|
var seats = _repo.GetSeatsByScreening(_selected.ScreeningID);
|
||||||
|
if (seats.Count == 0)
|
||||||
|
{
|
||||||
|
if (Session.IsAdmin)
|
||||||
|
{
|
||||||
|
var r = MessageBox.Show(
|
||||||
|
"Für diese Vorführung existieren noch keine Sitzplätze.\nSoll jetzt automatisch ein Sitzplan erstellt werden?",
|
||||||
|
"Sitzplan fehlt",
|
||||||
|
MessageBoxButton.YesNo,
|
||||||
|
MessageBoxImage.Question);
|
||||||
|
if (r == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
if (_repo.EnsureSeatsForScreening(_selected.ScreeningID, _selected.TotalSeats))
|
||||||
|
seats = _repo.GetSeatsByScreening(_selected.ScreeningID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (seats.Count == 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show("Keine Sitzplatzdaten verfügbar.", "Hinweis");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var dlg = new SeatSelectionDialog(_selected, seats, Session.CurrentUser!);
|
||||||
|
if (dlg.ShowDialog() == true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_repo.CreateBooking(Session.CurrentUser!.UserID, dlg.SelectedScreeningId, dlg.SelectedSeatId, dlg.TotalPrice);
|
||||||
|
LoadScreenings();
|
||||||
|
MessageBox.Show($"✓ Buchung erfolgreich!\nSitz: {dlg.SeatDisplay}\nPreis: {dlg.TotalPrice:F2} €",
|
||||||
|
"Buchung bestätigt", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
}
|
||||||
|
catch (System.Exception ex) { MessageBox.Show("DB-Fehler: " + ex.Message); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
Views/SeatSelectionDialog.xaml
Normal file
54
Views/SeatSelectionDialog.xaml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<Window x:Class="CineBook.Views.SeatSelectionDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Sitzplatz wählen" Height="500" Width="560"
|
||||||
|
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
|
||||||
|
Background="#0A0A0A">
|
||||||
|
<Grid Margin="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Grid.Row="0" Text="🎟 SITZPLATZ AUSWAHL" Style="{StaticResource RetroHeader}" Margin="0,0,0,8"/>
|
||||||
|
<Border Grid.Row="1" Background="#111" BorderBrush="#333" BorderThickness="1" Padding="12,8" Margin="0,0,0,16">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock x:Name="MovieInfoText" FontFamily="Courier New" FontSize="13" Foreground="#F5C518"/>
|
||||||
|
<TextBlock x:Name="ScreeningInfoText" FontFamily="Courier New" FontSize="11" Foreground="#888" Margin="0,4,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
|
||||||
|
<Border Width="20" Height="20" Background="#27AE60" CornerRadius="2" Margin="0,0,4,0"/>
|
||||||
|
<TextBlock Text="Frei" FontFamily="Courier New" FontSize="11" Foreground="#888" Margin="0,0,16,0" VerticalAlignment="Center"/>
|
||||||
|
<Border Width="20" Height="20" Background="#C0392B" CornerRadius="2" Margin="0,0,4,0"/>
|
||||||
|
<TextBlock Text="Belegt" FontFamily="Courier New" FontSize="11" Foreground="#888" Margin="0,0,16,0" VerticalAlignment="Center"/>
|
||||||
|
<Border Width="20" Height="20" Background="#F5C518" CornerRadius="2" Margin="0,0,4,0"/>
|
||||||
|
<TextBlock Text="Ausgewählt" FontFamily="Courier New" FontSize="11" Foreground="#888" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Border Background="#222" Height="8" CornerRadius="4" Margin="20,0,20,16">
|
||||||
|
<TextBlock Text="L E I N W A N D" FontFamily="Courier New" FontSize="8"
|
||||||
|
Foreground="#555" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ItemsControl x:Name="SeatsPanel"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
<Border Grid.Row="3" Background="#111" BorderBrush="#333" BorderThickness="1" Padding="12,8" Margin="0,12,0,12">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="AUSGEWÄHLT: " FontFamily="Courier New" FontSize="12" Foreground="#888" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="SelectedSeatText" FontFamily="Courier New" FontSize="12" Foreground="#F5C518" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text=" | PREIS: " FontFamily="Courier New" FontSize="12" Foreground="#888" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="PriceText" FontFamily="Courier New" FontSize="12" Foreground="#27AE60" FontWeight="Bold" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Content="[ ABBRECHEN ]" Style="{StaticResource RetroButtonGray}" Height="32"
|
||||||
|
Click="CancelClick" Margin="0,0,8,0"/>
|
||||||
|
<Button x:Name="BtnConfirm" Content="[ ✓ BUCHUNG BESTÄTIGEN ]" Style="{StaticResource RetroButtonGreen}"
|
||||||
|
Height="32" Click="ConfirmClick" IsEnabled="False"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
141
Views/SeatSelectionDialog.xaml.cs
Normal file
141
Views/SeatSelectionDialog.xaml.cs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
using CineBook.Models;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace CineBook.Views;
|
||||||
|
|
||||||
|
public partial class SeatSelectionDialog : Window
|
||||||
|
{
|
||||||
|
private readonly Screening _screening;
|
||||||
|
private readonly List<Seat> _seats;
|
||||||
|
private Seat? _selectedSeat;
|
||||||
|
|
||||||
|
public int SelectedSeatId => _selectedSeat?.SeatID ?? 0;
|
||||||
|
public int SelectedScreeningId => _screening.ScreeningID;
|
||||||
|
public decimal TotalPrice => _screening.PricePerSeat;
|
||||||
|
public string SeatDisplay => _selectedSeat?.Display ?? "";
|
||||||
|
|
||||||
|
public SeatSelectionDialog(Screening screening, List<Seat> seats, User user)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_screening = screening;
|
||||||
|
_seats = seats;
|
||||||
|
|
||||||
|
MovieInfoText.Text = screening.MovieTitle;
|
||||||
|
ScreeningInfoText.Text = $"{screening.DateTimeDisplay} | {screening.Hall} | {screening.PriceDisplay} pro Sitz";
|
||||||
|
|
||||||
|
BuildSeatGrid();
|
||||||
|
UpdateSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildSeatGrid()
|
||||||
|
{
|
||||||
|
var rows = _seats.GroupBy(s => s.Row).OrderBy(g => g.Key);
|
||||||
|
var panel = new StackPanel();
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
var rowPanel = new StackPanel
|
||||||
|
{
|
||||||
|
Orientation = Orientation.Horizontal,
|
||||||
|
Margin = new Thickness(0, 2, 0, 2)
|
||||||
|
};
|
||||||
|
rowPanel.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = row.Key,
|
||||||
|
Width = 24,
|
||||||
|
Foreground = Brushes.Gray,
|
||||||
|
FontFamily = new FontFamily("Courier New"),
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var seat in row.OrderBy(s => s.SeatNumber))
|
||||||
|
{
|
||||||
|
var btn = new Button
|
||||||
|
{
|
||||||
|
Content = seat.SeatNumber.ToString(),
|
||||||
|
Width = 34,
|
||||||
|
Height = 30,
|
||||||
|
Margin = new Thickness(2),
|
||||||
|
FontFamily = new FontFamily("Courier New"),
|
||||||
|
FontSize = 11,
|
||||||
|
Tag = seat,
|
||||||
|
IsEnabled = !seat.IsBooked,
|
||||||
|
Background = seat.IsBooked
|
||||||
|
? new SolidColorBrush(Color.FromRgb(0x44, 0x22, 0x22))
|
||||||
|
: new SolidColorBrush(Color.FromRgb(0x1B, 0x4D, 0x2A)),
|
||||||
|
Foreground = seat.IsBooked ? Brushes.DarkRed : Brushes.LightGreen,
|
||||||
|
BorderBrush = seat.IsBooked
|
||||||
|
? new SolidColorBrush(Color.FromRgb(0x80, 0x20, 0x20))
|
||||||
|
: new SolidColorBrush(Color.FromRgb(0x27, 0xAE, 0x60)),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
Cursor = seat.IsBooked
|
||||||
|
? System.Windows.Input.Cursors.No
|
||||||
|
: System.Windows.Input.Cursors.Hand
|
||||||
|
};
|
||||||
|
btn.ToolTip = $"Reihe {seat.Row}, Sitz {seat.SeatNumber} ({seat.Category})";
|
||||||
|
btn.Click += SeatClick;
|
||||||
|
rowPanel.Children.Add(btn);
|
||||||
|
}
|
||||||
|
panel.Children.Add(rowPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
SeatsPanel.ItemsSource = new[] { new ContentControl { Content = panel } };
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SeatClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Button btn || btn.Tag is not Seat seat) return;
|
||||||
|
_selectedSeat = seat;
|
||||||
|
UpdateSelection();
|
||||||
|
|
||||||
|
foreach (var child in FindVisualChildren<Button>(SeatsPanel))
|
||||||
|
{
|
||||||
|
if (child.Tag is Seat s && !s.IsBooked)
|
||||||
|
{
|
||||||
|
bool isSel = s.SeatID == seat.SeatID;
|
||||||
|
child.Background = isSel
|
||||||
|
? new SolidColorBrush(Color.FromRgb(0x7A, 0x62, 0x00))
|
||||||
|
: new SolidColorBrush(Color.FromRgb(0x1B, 0x4D, 0x2A));
|
||||||
|
child.Foreground = isSel ? Brushes.Gold : Brushes.LightGreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSelection()
|
||||||
|
{
|
||||||
|
SelectedSeatText.Text = _selectedSeat != null
|
||||||
|
? $"Reihe {_selectedSeat.Row}, Sitz {_selectedSeat.SeatNumber} ({_selectedSeat.Category})"
|
||||||
|
: "— kein Sitz gewählt —";
|
||||||
|
PriceText.Text = _selectedSeat != null ? $"{_screening.PricePerSeat:F2} €" : "";
|
||||||
|
BtnConfirm.IsEnabled = _selectedSeat != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfirmClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_selectedSeat == null) return;
|
||||||
|
DialogResult = true;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
DialogResult = false;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject parent) where T : DependencyObject
|
||||||
|
{
|
||||||
|
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||||
|
{
|
||||||
|
var child = VisualTreeHelper.GetChild(parent, i);
|
||||||
|
if (child is T t) yield return t;
|
||||||
|
foreach (var sub in FindVisualChildren<T>(child))
|
||||||
|
yield return sub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
Views/UserEditDialog.xaml
Normal file
56
Views/UserEditDialog.xaml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<Window x:Class="CineBook.Views.UserEditDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Benutzer bearbeiten" Height="340" Width="420"
|
||||||
|
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
|
||||||
|
Background="#0A0A0A">
|
||||||
|
<Grid Margin="24">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock x:Name="DialogTitle" Text="👤 BENUTZER BEARBEITEN"
|
||||||
|
Style="{StaticResource RetroHeader}" Margin="0,0,0,20"/>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="110"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="BENUTZERNAME *" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<TextBox x:Name="UsernameBox" Grid.Column="1" Style="{StaticResource RetroTextBox}" Height="30" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Text="PASSWORT" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<PasswordBox x:Name="PasswordBox" Grid.Row="1" Grid.Column="1" Style="{StaticResource RetroPasswordBox}"
|
||||||
|
Height="30" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Text="ROLLE *" Style="{StaticResource RetroLabel}" VerticalAlignment="Center" Margin="0,0,0,10"/>
|
||||||
|
<ComboBox x:Name="RoleBox" Grid.Row="2" Grid.Column="1" Height="30" Margin="0,0,0,10"
|
||||||
|
Background="#111" Foreground="#F5C518" FontFamily="Courier New">
|
||||||
|
<ComboBoxItem>User</ComboBoxItem>
|
||||||
|
<ComboBoxItem>Admin</ComboBoxItem>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Text="E-MAIL" Style="{StaticResource RetroLabel}" VerticalAlignment="Center"/>
|
||||||
|
<TextBox x:Name="EmailBox" Grid.Row="3" Grid.Column="1" Style="{StaticResource RetroTextBox}" Height="30"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,16,0,0">
|
||||||
|
<TextBlock x:Name="ErrorText" Foreground="#C0392B" FontFamily="Courier New" FontSize="11"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,12,0"/>
|
||||||
|
<Button Content="[ ABBRECHEN ]" Style="{StaticResource RetroButtonGray}" Height="32"
|
||||||
|
Click="CancelClick" Margin="0,0,8,0"/>
|
||||||
|
<Button Content="[ SPEICHERN ]" Style="{StaticResource RetroButtonGreen}" Height="32"
|
||||||
|
Click="SaveClick"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
58
Views/UserEditDialog.xaml.cs
Normal file
58
Views/UserEditDialog.xaml.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using CineBook.Models;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
|
||||||
|
namespace CineBook.Views;
|
||||||
|
|
||||||
|
public partial class UserEditDialog : Window
|
||||||
|
{
|
||||||
|
public string Username { get; private set; } = "";
|
||||||
|
public string Password { get; private set; } = "";
|
||||||
|
public string Role { get; private set; } = "User";
|
||||||
|
public string Email { get; private set; } = "";
|
||||||
|
|
||||||
|
private readonly User? _editing;
|
||||||
|
|
||||||
|
public UserEditDialog(User? user)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_editing = user;
|
||||||
|
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
DialogTitle.Text = "👤 BENUTZER BEARBEITEN";
|
||||||
|
UsernameBox.Text = user.Username;
|
||||||
|
EmailBox.Text = user.Email;
|
||||||
|
foreach (ComboBoxItem item in RoleBox.Items)
|
||||||
|
if (item.Content?.ToString() == user.Role)
|
||||||
|
RoleBox.SelectedItem = item;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DialogTitle.Text = "👤 BENUTZER ANLEGEN";
|
||||||
|
RoleBox.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(UsernameBox.Text) || UsernameBox.Text.Trim().Length < 3)
|
||||||
|
{ ErrorText.Text = "⚠ Benutzername mindestens 3 Zeichen!"; return; }
|
||||||
|
|
||||||
|
if (_editing == null && (string.IsNullOrEmpty(PasswordBox.Password) || PasswordBox.Password.Length < 6))
|
||||||
|
{ ErrorText.Text = "⚠ Passwort mindestens 6 Zeichen!"; return; }
|
||||||
|
|
||||||
|
if (RoleBox.SelectedItem is not ComboBoxItem ri)
|
||||||
|
{ ErrorText.Text = "⚠ Rolle auswählen!"; return; }
|
||||||
|
|
||||||
|
Username = UsernameBox.Text.Trim();
|
||||||
|
Password = PasswordBox.Password;
|
||||||
|
Role = ri.Content?.ToString() ?? "User";
|
||||||
|
Email = EmailBox.Text.Trim();
|
||||||
|
|
||||||
|
DialogResult = true;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CancelClick(object sender, RoutedEventArgs e) { DialogResult = false; Close(); }
|
||||||
|
}
|
||||||
115
cinebook_schema.sql
Normal file
115
cinebook_schema.sql
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
-- CineBook Datenbank Schema + Testdaten
|
||||||
|
-- Erstellt: 2026-03-05
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS CineBook;
|
||||||
|
USE CineBook;
|
||||||
|
|
||||||
|
-- Tabelle: Benutzer / Rollen
|
||||||
|
CREATE TABLE IF NOT EXISTS Users (
|
||||||
|
UserID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
Username VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
PasswordHash VARCHAR(255) NOT NULL,
|
||||||
|
Role ENUM('Admin', 'User') NOT NULL DEFAULT 'User',
|
||||||
|
Email VARCHAR(100),
|
||||||
|
CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tabelle: Filme
|
||||||
|
CREATE TABLE IF NOT EXISTS Movies (
|
||||||
|
MovieID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
Title VARCHAR(200) NOT NULL,
|
||||||
|
Genre VARCHAR(50),
|
||||||
|
DurationMinutes INT NOT NULL,
|
||||||
|
Description TEXT,
|
||||||
|
Rating DECIMAL(3,1) DEFAULT 0.0,
|
||||||
|
PosterUrl VARCHAR(500),
|
||||||
|
IsActive TINYINT(1) DEFAULT 1,
|
||||||
|
CreatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tabelle: Vorführungen (Screenings)
|
||||||
|
CREATE TABLE IF NOT EXISTS Screenings (
|
||||||
|
ScreeningID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
MovieID INT NOT NULL,
|
||||||
|
ScreeningDate DATE NOT NULL,
|
||||||
|
StartTime TIME NOT NULL,
|
||||||
|
Hall VARCHAR(20) NOT NULL DEFAULT 'Saal 1',
|
||||||
|
TotalSeats INT NOT NULL DEFAULT 80,
|
||||||
|
AvailableSeats INT NOT NULL DEFAULT 80,
|
||||||
|
PricePerSeat DECIMAL(6,2) NOT NULL DEFAULT 12.00,
|
||||||
|
FOREIGN KEY (MovieID) REFERENCES Movies(MovieID) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tabelle: Sitzplätze
|
||||||
|
CREATE TABLE IF NOT EXISTS Seats (
|
||||||
|
SeatID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
ScreeningID INT NOT NULL,
|
||||||
|
Row CHAR(1) NOT NULL,
|
||||||
|
SeatNumber INT NOT NULL,
|
||||||
|
IsBooked TINYINT(1) DEFAULT 0,
|
||||||
|
Category ENUM('Standard', 'VIP', 'Loge') DEFAULT 'Standard',
|
||||||
|
FOREIGN KEY (ScreeningID) REFERENCES Screenings(ScreeningID) ON DELETE CASCADE,
|
||||||
|
UNIQUE KEY unique_seat (ScreeningID, Row, SeatNumber)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tabelle: Buchungen
|
||||||
|
CREATE TABLE IF NOT EXISTS Bookings (
|
||||||
|
BookingID INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
UserID INT NOT NULL,
|
||||||
|
ScreeningID INT NOT NULL,
|
||||||
|
SeatID INT NOT NULL,
|
||||||
|
BookingDate DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
TotalPrice DECIMAL(8,2) NOT NULL,
|
||||||
|
Status ENUM('Confirmed', 'Cancelled', 'Pending') DEFAULT 'Confirmed',
|
||||||
|
BookingCode VARCHAR(20) UNIQUE,
|
||||||
|
FOREIGN KEY (UserID) REFERENCES Users(UserID),
|
||||||
|
FOREIGN KEY (ScreeningID) REFERENCES Screenings(ScreeningID),
|
||||||
|
FOREIGN KEY (SeatID) REFERENCES Seats(SeatID)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ===================== TESTDATEN =====================
|
||||||
|
|
||||||
|
-- Admin + Benutzer (Passwort: "admin123" bzw. "user123" als SHA256)
|
||||||
|
INSERT INTO Users (Username, PasswordHash, Role, Email) VALUES
|
||||||
|
('admin', '240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9', 'Admin', 'admin@cinebook.de'),
|
||||||
|
('maria', '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', 'User', 'maria@example.de'),
|
||||||
|
('thomas', '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', 'User', 'thomas@example.de'),
|
||||||
|
('julia', '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', 'User', 'julia@example.de');
|
||||||
|
|
||||||
|
-- Filme
|
||||||
|
INSERT INTO Movies (Title, Genre, DurationMinutes, Description, Rating) VALUES
|
||||||
|
('Interstellar', 'Sci-Fi', 169, 'Eine Reise durch Raum und Zeit jenseits unserer Galaxis.', 8.6),
|
||||||
|
('The Dark Knight', 'Action', 152, 'Batman kämpft gegen den Joker in Gotham City.', 9.0),
|
||||||
|
('Inception', 'Thriller', 148, 'Ein Dieb schleust sich in Träume ein um Geheimnisse zu stehlen.', 8.8),
|
||||||
|
('Parasite', 'Drama', 132, 'Eine arme Familie infiltriert ein reiches Haushalt.', 8.5),
|
||||||
|
('Dune', 'Sci-Fi', 155, 'Ein junger Adliger reist zur gefährlichsten Welt im Universum.', 8.0),
|
||||||
|
('The Matrix', 'Action', 136, 'Ein Hacker entdeckt die erschreckende Wahrheit über seine Welt.', 8.7),
|
||||||
|
('Oppenheimer', 'Drama', 180, 'Die Geschichte des Vaters der Atombombe.', 8.9),
|
||||||
|
('Barbie', 'Komödie', 114, 'Barbie und Ken erkunden die Realität.', 6.9);
|
||||||
|
|
||||||
|
-- Vorführungen
|
||||||
|
INSERT INTO Screenings (MovieID, ScreeningDate, StartTime, Hall, TotalSeats, AvailableSeats, PricePerSeat) VALUES
|
||||||
|
(1, '2026-03-06', '18:00:00', 'Saal 1', 80, 75, 12.00),
|
||||||
|
(1, '2026-03-07', '20:30:00', 'Saal 1', 80, 60, 12.00),
|
||||||
|
(2, '2026-03-06', '20:00:00', 'Saal 2', 60, 45, 11.50),
|
||||||
|
(3, '2026-03-08', '17:30:00', 'Saal 1', 80, 80, 12.00),
|
||||||
|
(4, '2026-03-09', '19:00:00', 'Saal 3', 40, 38, 13.50),
|
||||||
|
(5, '2026-03-10', '21:00:00', 'Saal 2', 60, 55, 14.00),
|
||||||
|
(6, '2026-03-06', '15:00:00', 'Saal 1', 80, 70, 10.00),
|
||||||
|
(7, '2026-03-11', '18:00:00', 'Saal 2', 60, 50, 15.00),
|
||||||
|
(8, '2026-03-12', '16:00:00', 'Saal 3', 40, 40, 9.50);
|
||||||
|
|
||||||
|
-- Sitzplätze für Screening 1 (Saal 1 = 8 Reihen A-H, 10 Sitze)
|
||||||
|
INSERT INTO Seats (ScreeningID, Row, SeatNumber, IsBooked, Category) VALUES
|
||||||
|
(1,'A',1,0,'Standard'),(1,'A',2,0,'Standard'),(1,'A',3,0,'Standard'),(1,'A',4,0,'Standard'),(1,'A',5,0,'Standard'),
|
||||||
|
(1,'B',1,1,'Standard'),(1,'B',2,1,'Standard'),(1,'B',3,0,'Standard'),(1,'B',4,0,'Standard'),(1,'B',5,0,'Standard'),
|
||||||
|
(1,'C',1,1,'VIP'),(1,'C',2,1,'VIP'),(1,'C',3,1,'VIP'),(1,'C',4,0,'VIP'),(1,'C',5,0,'VIP'),
|
||||||
|
(1,'D',1,0,'Standard'),(1,'D',2,0,'Standard'),(1,'D',3,0,'Standard'),(1,'D',4,0,'Standard'),(1,'D',5,0,'Standard');
|
||||||
|
|
||||||
|
-- Buchungen
|
||||||
|
INSERT INTO Bookings (UserID, ScreeningID, SeatID, TotalPrice, Status, BookingCode) VALUES
|
||||||
|
(2, 1, 6, 12.00, 'Confirmed', 'CB-2026-001'),
|
||||||
|
(2, 1, 7, 12.00, 'Confirmed', 'CB-2026-002'),
|
||||||
|
(3, 1, 11, 12.00, 'Confirmed', 'CB-2026-003'),
|
||||||
|
(3, 1, 12, 12.00, 'Confirmed', 'CB-2026-004'),
|
||||||
|
(4, 1, 13, 12.00, 'Confirmed', 'CB-2026-005');
|
||||||
112
cinebook_setup.sql
Normal file
112
cinebook_setup.sql
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- CineBook — Datenbankschema + Testdaten
|
||||||
|
-- Datenbank: pbt3h24akv_Kino
|
||||||
|
-- Erstellt: 2026-03-05
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Tabellen in korrekter Reihenfolge erstellen (wegen FK-Abhängigkeiten)
|
||||||
|
|
||||||
|
-- 1. BENUTZER
|
||||||
|
CREATE TABLE IF NOT EXISTS Users (
|
||||||
|
UserID INT NOT NULL AUTO_INCREMENT,
|
||||||
|
Username VARCHAR(60) NOT NULL UNIQUE,
|
||||||
|
Email VARCHAR(120) NOT NULL,
|
||||||
|
PasswordHash VARCHAR(255) NOT NULL,
|
||||||
|
Role ENUM('admin','customer') NOT NULL DEFAULT 'customer',
|
||||||
|
IsActive TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (UserID)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 2. FILME
|
||||||
|
CREATE TABLE IF NOT EXISTS Movies (
|
||||||
|
MovieID INT NOT NULL AUTO_INCREMENT,
|
||||||
|
Title VARCHAR(200) NOT NULL,
|
||||||
|
Genre VARCHAR(60) NOT NULL DEFAULT '',
|
||||||
|
DurationMinutes INT NOT NULL,
|
||||||
|
Description TEXT,
|
||||||
|
AgeRating VARCHAR(10) NOT NULL DEFAULT 'FSK 0',
|
||||||
|
IsActive TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (MovieID)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 3. SÄLE
|
||||||
|
CREATE TABLE IF NOT EXISTS Halls (
|
||||||
|
HallID INT NOT NULL AUTO_INCREMENT,
|
||||||
|
Name VARCHAR(60) NOT NULL,
|
||||||
|
Capacity INT NOT NULL,
|
||||||
|
IsActive TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (HallID)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 4. VORSTELLUNGEN
|
||||||
|
CREATE TABLE IF NOT EXISTS Screenings (
|
||||||
|
ScreeningID INT NOT NULL AUTO_INCREMENT,
|
||||||
|
MovieID INT NOT NULL,
|
||||||
|
HallID INT NOT NULL,
|
||||||
|
StartTime DATETIME NOT NULL,
|
||||||
|
PricePerSeat DECIMAL(8,2) NOT NULL,
|
||||||
|
PRIMARY KEY (ScreeningID),
|
||||||
|
FOREIGN KEY (MovieID) REFERENCES Movies(MovieID) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (HallID) REFERENCES Halls(HallID) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- 5. BUCHUNGEN
|
||||||
|
CREATE TABLE IF NOT EXISTS Bookings (
|
||||||
|
BookingID INT NOT NULL AUTO_INCREMENT,
|
||||||
|
UserID INT NOT NULL,
|
||||||
|
ScreeningID INT NOT NULL,
|
||||||
|
BookingDate DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
SeatCount INT NOT NULL,
|
||||||
|
TotalPrice DECIMAL(10,2) NOT NULL,
|
||||||
|
Status ENUM('confirmed','cancelled') NOT NULL DEFAULT 'confirmed',
|
||||||
|
PRIMARY KEY (BookingID),
|
||||||
|
FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (ScreeningID) REFERENCES Screenings(ScreeningID) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- TESTDATEN
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Benutzer
|
||||||
|
-- Passwort für beide: 'admin123' (BCrypt-Hash)
|
||||||
|
INSERT INTO Users (Username, Email, PasswordHash, Role, IsActive) VALUES
|
||||||
|
('admin', 'admin@cinebook.de', '$2a$11$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lh7y', 'admin', 1),
|
||||||
|
-- Passwort: 'kunde123'
|
||||||
|
('max', 'max@example.de', '$2a$11$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'customer', 1),
|
||||||
|
('anna', 'anna@example.de', '$2a$11$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'customer', 1);
|
||||||
|
|
||||||
|
-- Filme
|
||||||
|
INSERT INTO Movies (Title, Genre, DurationMinutes, Description, AgeRating, IsActive) VALUES
|
||||||
|
('Interstellar', 'Sci-Fi', 169, 'Ein Team von Astronauten reist durch ein Wurmloch auf der Suche nach einem neuen Zuhause für die Menschheit.', 'FSK 12', 1),
|
||||||
|
('The Dark Knight', 'Action', 152, 'Batman kämpft gegen den Joker, der Gotham City in Chaos und Angst stürzt.', 'FSK 12', 1),
|
||||||
|
('Inception', 'Thriller', 148, 'Ein Dieb dringt in die Träume anderer Menschen ein, um Geheimnisse zu stehlen.', 'FSK 12', 1),
|
||||||
|
('The Grand Budapest Hotel', 'Komödie', 99, 'Der legendäre Concierge eines berühmten europäischen Hotels und sein Lehrling erleben Abenteuer.', 'FSK 6', 1),
|
||||||
|
('Parasite', 'Drama', 132, 'Zwei Familien unterschiedlicher sozialer Klassen verstricken sich in einem brisanten Spiel.', 'FSK 16', 1),
|
||||||
|
('Dune: Part Two', 'Sci-Fi', 166, 'Paul Atreides vereint sich mit den Fremen und kämpft um Arrakis.', 'FSK 12', 1);
|
||||||
|
|
||||||
|
-- Säle
|
||||||
|
INSERT INTO Halls (Name, Capacity, IsActive) VALUES
|
||||||
|
('Saal 1 - Classic', 80, 1),
|
||||||
|
('Saal 2 - Premium', 50, 1),
|
||||||
|
('Saal 3 - IMAX', 120, 1),
|
||||||
|
('Saal 4 - 4DX', 40, 1);
|
||||||
|
|
||||||
|
-- Vorstellungen (alle in der Zukunft)
|
||||||
|
INSERT INTO Screenings (MovieID, HallID, StartTime, PricePerSeat) VALUES
|
||||||
|
(1, 3, DATE_ADD(NOW(), INTERVAL 1 DAY), 14.50),
|
||||||
|
(2, 1, DATE_ADD(NOW(), INTERVAL 1 DAY), 10.00),
|
||||||
|
(3, 2, DATE_ADD(NOW(), INTERVAL 2 DAY), 13.00),
|
||||||
|
(4, 1, DATE_ADD(NOW(), INTERVAL 2 DAY), 9.50),
|
||||||
|
(5, 4, DATE_ADD(NOW(), INTERVAL 3 DAY), 16.00),
|
||||||
|
(6, 3, DATE_ADD(NOW(), INTERVAL 3 DAY), 15.50),
|
||||||
|
(1, 2, DATE_ADD(NOW(), INTERVAL 5 DAY), 13.00),
|
||||||
|
(2, 4, DATE_ADD(NOW(), INTERVAL 7 DAY), 12.00);
|
||||||
|
|
||||||
|
-- Buchungen (Testdaten)
|
||||||
|
INSERT INTO Bookings (UserID, ScreeningID, BookingDate, SeatCount, TotalPrice, Status) VALUES
|
||||||
|
(2, 1, DATE_SUB(NOW(), INTERVAL 2 DAY), 2, 29.00, 'confirmed'),
|
||||||
|
(3, 2, DATE_SUB(NOW(), INTERVAL 1 DAY), 3, 30.00, 'confirmed'),
|
||||||
|
(2, 3, NOW(), 1, 13.00, 'confirmed');
|
||||||
Reference in New Issue
Block a user