CineBook Projekt hinzugefügt

This commit is contained in:
2026-03-12 14:28:52 +01:00
commit b6357c9b8a
33 changed files with 2857 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.vs/
bin/
obj/
*.user
*.suo

139
App.xaml Normal file
View 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
View File

@@ -0,0 +1,5 @@
using System.Windows;
namespace CineBook;
public partial class App : Application { }

17
CineBook.csproj Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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>

View 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
View 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>

View 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
View 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
View 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
View 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
View 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;
}
}
}

View 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>

View 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
View 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
View 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
View 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>

View 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; }
}
}

View 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>

View 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
View 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>

View 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); }
}
}
}

View 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>

View 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
View 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>

View 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
View 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
View 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');