diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml deleted file mode 100644 index 18b32b3..0000000 --- a/.github/workflows/pull-requests.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: pull requests - -on: - pull_request_target: - types: [opened] - -permissions: - pull-requests: write - -jobs: - uneditable: - uses: laravel/.github/.github/workflows/pull-requests.yml@main diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 7766203..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Tests - -on: - push: - branches: - - master - - '*.x' - pull_request: - schedule: - - cron: '0 0 * * *' - -permissions: - contents: read - -jobs: - tests: - runs-on: ubuntu-latest - - strategy: - fail-fast: true - matrix: - php: ['8.0', 8.1, 8.2] - - name: PHP ${{ matrix.php }} - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite - coverage: none - - - name: Install Composer dependencies - run: composer install --prefer-dist --no-interaction --no-progress - - - name: Copy environment file - run: cp .env.example .env - - - name: Generate app key - run: php artisan key:generate - - - name: Execute tests - run: vendor/bin/phpunit diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml deleted file mode 100644 index ebda620..0000000 --- a/.github/workflows/update-changelog.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: update changelog - -on: - release: - types: [released] - -permissions: {} - -jobs: - update: - permissions: - contents: write - uses: laravel/.github/.github/workflows/update-changelog.yml@main diff --git a/app/Http/Controllers/Main/CinemaController.php b/app/Http/Controllers/Main/CinemaController.php new file mode 100644 index 0000000..3c17865 --- /dev/null +++ b/app/Http/Controllers/Main/CinemaController.php @@ -0,0 +1,19 @@ + "Cinemas", 'cinemas' => \App\Models\Cinema::all()]); + } + + public function show($id) + { + return view('main.cinemas.cinema', ['title' => "Cinema", 'cinema' => \App\Models\Cinema::findOrfail($id)]); + } +} diff --git a/app/Http/Controllers/Main/GenreController.php b/app/Http/Controllers/Main/GenreController.php new file mode 100644 index 0000000..a3a8e07 --- /dev/null +++ b/app/Http/Controllers/Main/GenreController.php @@ -0,0 +1,19 @@ + "Genre", 'genre' => \App\Models\Genre::findOrfail($id)]); + } + + public function showAllGenres() + { + return view('main.genres.index', ['title' => "Genres", 'genres' => \App\Models\Genre::all()]); + } +} diff --git a/app/Http/Controllers/Main/MovieController.php b/app/Http/Controllers/Main/MovieController.php new file mode 100644 index 0000000..e30500c --- /dev/null +++ b/app/Http/Controllers/Main/MovieController.php @@ -0,0 +1,35 @@ + "Movies", 'movies' => \App\Models\Movie::all(), 'genres' => \App\Models\Genre::all(), 'showings' => \App\Models\Showing::all()]); + } + + public function moviesNowShowing() + { + // map showings that are in the future to movies + $showings = \App\Models\Showing::all()->filter(function ($showing) { + return $showing->showing_start > now(); + }); + // $movies must be a collection of unique movies + $movies = collect(); + foreach ($showings as $showing) { + if (!$movies->contains($showing->movie)) { + $movies->push($showing->movie); + } + } + return view('main.movies.index', ['title' => "Movies Now Showing", 'movies' => $movies, 'genres' => \App\Models\Genre::all(), 'showings' => \App\Models\Showing::all()]); + } + + public function show($id) + { + return view('main.movies.movie', ['title' => "Movie", 'movie' => \App\Models\Movie::findOrfail($id)]); + } +} diff --git a/app/Http/Controllers/Main/ShowingController.php b/app/Http/Controllers/Main/ShowingController.php new file mode 100644 index 0000000..df75349 --- /dev/null +++ b/app/Http/Controllers/Main/ShowingController.php @@ -0,0 +1,24 @@ + "Showing", 'showing' => \App\Models\Showing::findOrfail($id)]); + } + + public function showAllShowings() + { + return view('main.showings.index', ['title' => "Showings", 'showings' => \App\Models\Showing::all()]); + } + + public function order($id) + { + return view('main.order', ['title' => "Order Tickets", 'showing' => \App\Models\Showing::findOrfail($id)]); + } +} diff --git a/app/Http/Controllers/Managing/CinemaController.php b/app/Http/Controllers/Managing/CinemaController.php index 4fcb3b2..89c6b28 100644 --- a/app/Http/Controllers/Managing/CinemaController.php +++ b/app/Http/Controllers/Managing/CinemaController.php @@ -8,12 +8,15 @@ public function __construct() { $this->middleware('auth'); $this->middleware('atleast:employee'); - $this->middleware('permission:manage_cinemas')->only(['create', 'store', 'edit', 'update', 'destroy']); + $this->middleware('permission:READ_CINEMAS')->only('index', 'show'); + $this->middleware('permission:CREATE_CINEMAS')->only('create', 'store'); + $this->middleware('permission:UPDATE_CINEMAS')->only('edit', 'update'); + $this->middleware('permission:DELETE_CINEMAS')->only('destroy'); } - public function showAllCinemas() + public function index() { - return view('manage.cinemas.index', ['title' => "Manage Cinemas", 'cinemas' => \App\Models\Cinema::all()]); + return view('manage.cinemas.index', ['title' => "Manage Cinemas", 'cinemas' => \App\Models\Cinema::all(), 'users' => \App\Models\User::all()]); } public function edit($id) diff --git a/app/Http/Controllers/Managing/GenreController.php b/app/Http/Controllers/Managing/GenreController.php index d1da3c0..30c6155 100644 --- a/app/Http/Controllers/Managing/GenreController.php +++ b/app/Http/Controllers/Managing/GenreController.php @@ -10,10 +10,13 @@ class GenreController extends Controller public function __construct() { $this->middleware('auth'); $this->middleware('atleast:employee'); - $this->middleware('permission:manage_genres')->only(['create', 'store', 'edit', 'update', 'destroy']); + $this->middleware('permission:READ_GENRES')->only('index', 'show'); + $this->middleware('permission:CREATE_GENRES')->only('create', 'store'); + $this->middleware('permission:UPDATE_GENRES')->only('edit', 'update'); + $this->middleware('permission:DELETE_GENRES')->only('destroy'); } - public function showAllGenres() { + public function index() { return view('manage.genres.index', ['title' => "Manage Genres", 'genres' => \App\Models\Genre::all()]); } diff --git a/app/Http/Controllers/Managing/MovieController.php b/app/Http/Controllers/Managing/MovieController.php index d4117ba..c3a30e9 100644 --- a/app/Http/Controllers/Managing/MovieController.php +++ b/app/Http/Controllers/Managing/MovieController.php @@ -11,14 +11,13 @@ class MovieController extends Controller public function __construct() { $this->middleware('auth'); $this->middleware('atleast:employee'); - $this->middleware('permission:manage_movies')->only(['create', 'store', 'edit', 'update', 'destroy']); + $this->middleware('permission:READ_MOVIES')->only('index', 'show'); + $this->middleware('permission:CREATE_MOVIES')->only('create', 'store'); + $this->middleware('permission:UPDATE_MOVIES')->only('edit', 'update'); + $this->middleware('permission:DELETE_MOVIES')->only('destroy'); } - public function showAllMovies() { - // has permission READ_MOVIES ? - if(!Auth::user()->allowedTo('READ_MOVIES')) { - abort(403); - } + public function index() { return view('manage.movies.index', ['title' => "Manage Movies", 'movies' => \App\Models\Movie::all()]); } @@ -37,6 +36,8 @@ public function store() { $movie->movie_description = request('movie_description'); $movie->movie_year = request('movie_year'); $movie->movie_image = request('movie_image'); + $movie->movie_length = request('movie_length'); + $movie->movie_age_limit = request('movie_age_limit'); // $movie->user_id = auth()->user()->user_id; $movie->genre_id = request('genre_id'); $movie->save(); diff --git a/app/Http/Controllers/Managing/ShowingsController.php b/app/Http/Controllers/Managing/ShowingsController.php new file mode 100644 index 0000000..97eeb92 --- /dev/null +++ b/app/Http/Controllers/Managing/ShowingsController.php @@ -0,0 +1,66 @@ +middleware('auth'); + $this->middleware('atleast:employee'); + $this->middleware('permission:READ_SHOWINGS')->only('index', 'show'); + $this->middleware('permission:CREATE_SHOWINGS')->only('create', 'store'); + $this->middleware('permission:UPDATE_SHOWINGS')->only('edit', 'update'); + $this->middleware('permission:DELETE_SHOWINGS')->only('destroy'); + } + + public function index() + { + return view('manage.showings.index', ['title' => "Manage Showings", 'showings' => \App\Models\Showing::all()]); + } + + public function edit($id) + { + $s = \App\Models\Showing::findOrfail($id); + return view('manage.showings.showing', ['title' => "Manage Showing", 'showing' => $s, 'movies' => \App\Models\Movie::all(), 'rooms' => \App\Models\Room::all(), 'cinemas' => \App\Models\Cinema::all()]); + } + + public function createShowing() + { + return view('manage.showings.create', ['title' => "Create Showing", 'movies' => \App\Models\Movie::all(), 'rooms' => \App\Models\Room::all()]); + } + + public function store() + { + $showing = new \App\Models\Showing(); + $showing->movie_id = request('movie_id'); + $showing->room_id = request('room_id'); + $showing->start_time = request('start_time'); + $showing->save(); + return redirect()->route('manage.showings'); + } + + public function show($id) + { + return view('main.showings.showing', ['title' => "Edit Showing", 'showing' => \App\Models\Showing::findOrfail($id)]); + } + + public function update($id) + { + $showing = \App\Models\Showing::findOrfail($id); + $showing->movie_id = request('movie_id'); + $showing->room_id = request('room_id'); + $showing->showing_start = request('showing_start'); + $showing->save(); + return redirect()->route('manage.showings'); + } + + public function destroy($id) + { + $showing = \App\Models\Showing::findOrfail($id); + $showing->delete(); + return redirect()->route('manage.showings'); + } +} diff --git a/app/Http/Controllers/Managing/UsersController.php b/app/Http/Controllers/Managing/UsersController.php new file mode 100644 index 0000000..ffad832 --- /dev/null +++ b/app/Http/Controllers/Managing/UsersController.php @@ -0,0 +1,81 @@ +middleware('auth'); + $this->middleware('atleast:manage'); + $this->middleware('permission:READ_USERS')->only('index', 'show'); + $this->middleware('permission:CREATE_USERS')->only('create', 'store'); + $this->middleware('permission:UPDATE_USERS')->only('edit', 'update'); + $this->middleware('permission:DELETE_USERS')->only('destroy'); + + } + + public function index() + { + return view('manage.users.index', ['title' => "Manage Users", 'users' => \App\Models\User::all()]); + } + + public function create() + { + return view('manage.users.create'); + } + + public function store() + { + $user = new \App\Models\User(); + $user->name = request('name'); + $user->email = request('email'); + $user->password = request('password'); + $user->role = request('role'); + $user->save(); + + // user assignments (user_assignments.user_id, user_assignments.cinema_id) + $cinemas = request('cinemas'); + if ($cinemas) { + foreach ($cinemas as $cinema) { + $user->cinemas()->attach($cinema); + } + } + + return redirect()->route('manage.users'); + } + + public function show($id) + { + return view('manage.users.user', ['user' => \App\Models\User::findOrfail($id)]); + } + + public function update($id) + { + $user = \App\Models\User::findOrfail($id); + $user->name = request('name'); + $user->email = request('email'); + $user->password = request('password'); + $user->role = request('role'); + $user->save(); + + // user assignments (user_assignments.user_id, user_assignments.cinema_id) + $cinemas = request('cinemas'); + if ($cinemas) { + foreach ($cinemas as $cinema) { + $user->cinemas()->attach($cinema); + } + } + + return redirect()->route('manage.users'); + } + + public function destroy($id) + { + $user = \App\Models\User::findOrfail($id); + $user->delete(); + return redirect()->route('manage.users'); + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 2da7d06..82d11f5 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -55,6 +55,7 @@ class Kernel extends HttpKernel protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'atleast' => \App\Http\Middleware\AtleastRole::class, + 'permission' => \App\Http\Middleware\Permission::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, diff --git a/app/Http/Middleware/CinemaAccess.php b/app/Http/Middleware/CinemaAccess.php new file mode 100644 index 0000000..422d12e --- /dev/null +++ b/app/Http/Middleware/CinemaAccess.php @@ -0,0 +1,33 @@ +user()->atleast('admin')) { + return $next($request); + } + + if (auth()->user()->cinemas->contains($cinema_id)) { + return $next($request); + } + + + + return $next($request); + } +} diff --git a/app/Http/Middleware/Permission.php b/app/Http/Middleware/Permission.php new file mode 100644 index 0000000..4d2dce4 --- /dev/null +++ b/app/Http/Middleware/Permission.php @@ -0,0 +1,28 @@ +user()->role == 'admin') { + return $next($request); + } + if (auth()->user()->hasPermission($permission)) { + return $next($request); + } + abort(403, "You need \"$permission\" permission"); + } +} diff --git a/app/Models/Cinema.php b/app/Models/Cinema.php new file mode 100644 index 0000000..9bf5bd3 --- /dev/null +++ b/app/Models/Cinema.php @@ -0,0 +1,53 @@ +belongsTo(Address::class, 'address_id', 'address_id'); + } + + public function rooms() + { + return $this->hasMany(Room::class, 'cinema_id', 'cinema_id'); + } + + public function showings() + { + return $this->hasManyThrough(Showing::class, Room::class, 'cinema_id', 'room_id', 'cinema_id', 'room_id'); + } + + public function find($id) + { + return $this->where('cinema_id', $id)->first(); + } + + public function users() { + //users associated + return $this->belongsToMany('App\Models\User', 'user_assignments', 'cinema_id', 'user_id'); + } + +} diff --git a/app/Models/Permission.php b/app/Models/Permission.php index fb14ecc..71a052d 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -22,4 +22,16 @@ public function users() return $this->belongsToMany('App\Models\User', 'user_permissions', 'permission_id', 'user_id'); } + public function find(mixed $permission_id) + { + return $this->where('permission_id', $permission_id)->first(); + } + + public function create(array $array) + { + $this->permission_name = $array['permission_name']; + $this->save(); + return $this; + } + } diff --git a/app/Models/Showing.php b/app/Models/Showing.php index 0c1d074..8b510b5 100644 --- a/app/Models/Showing.php +++ b/app/Models/Showing.php @@ -41,12 +41,17 @@ public function movie() return $this->belongsTo(Movie::class, 'movie_id', 'movie_id'); } + public function prices() + { + return $this->hasMany(Price::class, 'showing_id', 'showing_id'); + } + public function nowPlaying() { return $this->where('showing_start', '>=', now())->get(); } - public function end_time() { + public function showing_end() { $date = new Carbon($this->showing_start); $date->addMinutes($this->movie->movie_length); return $date; diff --git a/app/Models/User.php b/app/Models/User.php index 25df236..0977180 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -54,14 +54,17 @@ public function permissions() // the permissions are in the permissions table // only return valid permissions return $this->belongsToMany('App\Models\Permission', 'user_permissions', 'user_id', 'permission_id')->where(function ($query) { - $query->where('user_permission_start', '<=', now())->where(function ($query) { - $query->where('user_permission_end', '>=', now())->orWhereNull('user_permission_end'); - }); +// $query->where('user_permission_start', '<=', now())->where(function ($query) { +// $query->where('user_permission_end', '>=', now())->orWhereNull('user_permission_end'); +// }); }); } - public function allowedTo($permission): bool + public function hasPermission($permission): bool { + if ($this->role == 'admin') { + return true; + } if ($this->permissions()->where('permission_name', $permission)->first()) { return true; } @@ -73,7 +76,8 @@ public function atleast($role): bool $hierarchy = [ 'default' => 0, 'employee' => 1, - 'manage' => 2 + 'manage' => 2, + 'admin' => 3 ]; return $hierarchy[$this->role] >= $hierarchy[$role]; } @@ -83,4 +87,21 @@ public static function find($id) return User::all()->where('user_id', $id)->first(); } + public function cinemas() + { + // check user_assignments table for all assignments for this user + if ($this->atleast('admin')) { + // if user is a manager or admin, return all cinemas + return Cinema::all(); + } else { + // if user is an employee, return only the cinemas they are assigned to + return $this->belongsToMany('App\Models\Cinema', 'user_assignments', 'user_id', 'cinema_id')->get(); + } + } + + public function orders() + { + return $this->hasMany('App\Models\Order', 'user_id', 'user_id'); + } + } diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php index b5b9869..bbd9d2e 100644 --- a/database/migrations/2014_10_12_000000_create_users_table.php +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -19,7 +19,7 @@ public function up() $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); - $table->enum('role', ['default', 'employee', 'manage'])->default('default'); + $table->enum('role', ['default', 'employee', 'manage', 'admin'])->default('default'); $table->rememberToken(); $table->timestamps(); }); diff --git a/database/migrations/2022_11_22_000009_create_user_permissions_table.php b/database/migrations/2022_11_22_000009_create_user_permissions_table.php index 33250ca..5181ce3 100644 --- a/database/migrations/2022_11_22_000009_create_user_permissions_table.php +++ b/database/migrations/2022_11_22_000009_create_user_permissions_table.php @@ -19,8 +19,8 @@ public function up() $table->foreignId('permission_id')->constrained('permissions', 'permission_id'); $table->foreignId('user_id')->constrained('users', 'user_id'); // end can be null to mark indefinitely - $table->date('user_permission_start')->useCurrent(); - $table->date('user_permission_end')->nullable(); +// $table->date('user_permission_start')->useCurrent(); +// $table->date('user_permission_end')->nullable(); }); } diff --git a/database/migrations/2022_11_22_000013_create_user_assignment.php b/database/migrations/2022_11_22_000013_create_user_assignment.php new file mode 100644 index 0000000..43b1d7d --- /dev/null +++ b/database/migrations/2022_11_22_000013_create_user_assignment.php @@ -0,0 +1,34 @@ +id('user_assignment_id'); + $table->timestamps(); + $table->foreignId('cinema_id')->constrained('cinemas', 'cinema_id'); + $table->foreignId('user_id')->constrained('users', 'user_id'); + $table->unique(['cinema_id', 'user_id']); // only one user per cinema + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_assignments'); + } +}; diff --git a/database/seeders/RoomSeeder.php b/database/seeders/RoomSeeder.php index b3f329a..f9b1cc3 100644 --- a/database/seeders/RoomSeeder.php +++ b/database/seeders/RoomSeeder.php @@ -23,17 +23,14 @@ public function run() ], [ 'room_name' => 'Room 2', - 'room_rows' => 10, - 'room_columns' => 10, + 'room_rows' => 5, + 'room_columns' => 5, ], - [ - 'room_name' => 'Room 3', - 'room_rows' => 10, - 'room_columns' => 10, - ], - ]; + ]; foreach ($rooms as $room) { + $this->command->info("Creating room {$room['room_name']} for cinema {$cinema->cinema_name}"); + $r = new \App\Models\Room(); $r->room_name = $room['room_name']; $r->room_rows = $room['room_rows']; @@ -43,6 +40,7 @@ public function run() $r->save(); for ($row = 1; $row <= $r->room_rows; $row++) { + $this->command->info("Creating row {$row} for room {$r->room_name}"); for ($column = 1; $column <= $r->room_columns; $column++) { $s = new \App\Models\Seat(); $s->seat_row = $row; diff --git a/database/seeders/ShowingSeeder.php b/database/seeders/ShowingSeeder.php index 255ce18..1f7e5c2 100644 --- a/database/seeders/ShowingSeeder.php +++ b/database/seeders/ShowingSeeder.php @@ -27,22 +27,60 @@ public function run() 'showing_end' => $showStart->add(new \DateInterval('PT2H'))->format('Y-m-d H:i:s'), 'movie_id' => 1, 'room_id' => $room->room_id, + 'pricings' => [ + [ + 'price' => 10, + 'price_type' => 'adult', + ], + [ + 'price' => 8, + 'price_type' => 'senior', + ], + [ + 'price' => 2.5, + 'price_type' => 'child', + ], + ], ], [ 'showing_start' => $showStart->add(new \DateInterval('PT2H'))->format('Y-m-d H:i:s'), 'showing_end' => $showStart->add(new \DateInterval('PT2H'))->format('Y-m-d H:i:s'), 'movie_id' => 2, 'room_id' => $room->room_id, - ], + 'pricings' => [ + [ + 'price' => 10, + 'price_type' => 'adult', + ], + [ + 'price' => 8, + 'price_type' => 'senior', + ], + [ + 'price' => 3.5, + 'price_type' => 'child', + ], + ], + ] ]; foreach ($showings as $showing) { + $this->command->info("Creating showing for movie {$showing['movie_id']} in room {$room->room_name}"); $s = new \App\Models\Showing(); $s->showing_start = $showing['showing_start']; $s->movie_id = $showing['movie_id']; $s->room_id = $showing['room_id']; $s->user_id = 1; $s->save(); + + foreach ($showing['pricings'] as $pricing) { + $p = new \App\Models\Price(); + $p->price = $pricing['price']; + $p->price_type = $pricing['price_type']; + $p->showing_id = $s->showing_id; + $p->user_id = 1; + $p->save(); + } } } diff --git a/docker-compose.yml b/docker-compose.yml index eb29913..006b328 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,17 @@ version: '3.3' # LARAVEL ENVIRONMENT COMPOSE services: + mysql: + image: mysql:5.7 + container_name: mysql + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: laravel + MYSQL_USER: laravel + MYSQL_PASSWORD: laravel + ports: + - "3306:3306" + phpmyadmin: image: phpmyadmin/phpmyadmin container_name: phpmyadmin @@ -10,9 +21,8 @@ services: ports: - "8080:80" environment: - PMA_HOST: neo.faulty.nl - PMA_PORT: 33061 - PMA_USER: laravel - PMA_PASSWORD: laravel + PMA_HOST: mysql + PMA_USER: root + PMA_PASSWORD: root PMA_ARBITRARY: 1 PMA_ABSOLUTE_URI: http://localhost:8080/ diff --git a/public/css/generic.css b/public/css/generic.css index 38e6229..2d382a9 100644 --- a/public/css/generic.css +++ b/public/css/generic.css @@ -130,6 +130,19 @@ a.button:hover { color: var(--secondary-color-text); } +button.button:active, +a.button:active { + background: var(--primary-color); + color: var(--primary-color-text); +} + +button.button:disabled, +a.button:disabled { + background: var(--inactive-item-bg); + color: var(--dim-text); + cursor: not-allowed; +} + table { border-collapse: collapse; width: 100%; @@ -175,3 +188,14 @@ .op0 { .mono { font-family: monospace; } + +input.text { + /* generic input adjustments */ + background: var(--second-bg); + color: var(--default-text); + border: none; + outline: none; + padding: 0.5rem; + border-radius: 0.5rem; + font-size: 1rem; +} diff --git a/public/css/main.css b/public/css/main.css index 2c66f8b..82405c5 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -68,9 +68,16 @@ #movies a .details { justify-content: flex-end; align-items: center; opacity: 0; + padding: 1rem; + max-width: calc(100% - 2rem); transition: opacity 0.5s; } +#movies a .details span { + overflow: hidden; + text-overflow: ellipsis; +} + #movies a:hover .details { opacity: 1; } diff --git a/public/css/manage.css b/public/css/manage.css index bbac90a..18b85d1 100644 --- a/public/css/manage.css +++ b/public/css/manage.css @@ -46,22 +46,30 @@ #sidebar a { color: var(--default-text); display: block; margin-top: 0.5rem; + transition: all 0.5s; } #sidebar a:hover { - filter: brightness(1.5); + background: var(--second-bg); + color: var(--default-text); + } #sidebar a.active { - background: var(--highlight-bg); + border-left: 0.5rem solid var(--highlight-bg); + background-color: var(--default-bg); color: var(--highlight-text); } #sidebar a.child { padding-left: 2rem; margin-top: 0; -/* nice visual*/ - border-left: 1rem solid var(--second-bg); + /* nice visual*/ + border-left: 0.5rem solid var(--second-bg); +} + +#sidebar a.child.active { + border-left: 1rem solid var(--highlight-bg); } main { @@ -135,10 +143,10 @@ .form .form-group label { margin: 0.5rem 0; } -form .form-group button, -form .form-group select, -form .form-group textarea, -form .form-group input { +.form .form-group button, +.form .form-group select, +.form .form-group textarea, +.form .form-group input { padding: 0.5rem; border-radius: 0.5rem; background: var(--default-bg); @@ -147,14 +155,14 @@ form .form-group input { border: none; } -form .form-group button:hover, -form .form-group select:hover, -form .form-group textarea:hover, -form .form-group input:hover, -form .form-group button:focus, -form .form-group select:focus, -form .form-group textarea:focus, -form .form-group input:focus { +.form .form-group button:hover, +.form .form-group select:hover, +.form .form-group textarea:hover, +.form .form-group input:hover, +.form .form-group button:focus, +.form .form-group select:focus, +.form .form-group textarea:focus, +.form .form-group input:focus { filter: brightness(1.5); } @@ -189,3 +197,15 @@ #showings .showing .actions { justify-content: center; align-items: center; } + +.warning { + background-color: rgba(255, 0, 0, 0.5); + padding: 1rem; + border-radius: 0.5rem; + margin: 1rem 0; + min-width: 25vw; +} + +.warning h3 { + margin: 0; +} diff --git a/public/css/order.css b/public/css/order.css new file mode 100644 index 0000000..fc4fab6 --- /dev/null +++ b/public/css/order.css @@ -0,0 +1,53 @@ +body { + /* center with spacing on the left and right*/ + margin: 0 auto; + width: 100%; + max-width: 1000px; + padding: 0 20px 3rem; +} + +#seat-chooser { + border: 1px solid black; + padding: 2rem; +} + +#summary { + background-color: var(--second-bg); + border-radius: 5px; + padding: 1rem; + margin: 1rem 0; +} + +#summary-dyn .seat { + background-color: var(--darkest-bg); + color: var(--primary-color-text); + border-radius: 5px; + padding: 0.5rem; + margin: 0.5rem; +} + +#summary-dyn .seat select.ticket-select { + background-color: var(--primary-color); + color: var(--primary-color-text); + border: 0; + border-radius: 5px; + padding: 0.5rem; + margin: 0.5rem; +} + +#summary-dyn .seat .seat-span { + background-color: var(--primary-color); + color: var(--primary-color-text); + border-radius: 5px; + padding: 0.5rem; + margin: 0.5rem; +} + +#summary-dyn .seat .price { + background-color: var(--secondary-color); + color: var(--darkest-bg); + border-radius: 5px; + padding: 0.5rem; + margin: 0.5rem; + float: right; +} diff --git a/public/css/seat-chooser.css b/public/css/seat-chooser.css index 860d90b..a29daa9 100644 --- a/public/css/seat-chooser.css +++ b/public/css/seat-chooser.css @@ -12,8 +12,6 @@ #seat-chooser { flex-direction: column; align-items: center; justify-content: center; - width: 100%; - height: 100%; --seat-width: 2.5rem; --seat-height: var(--seat-width); @@ -95,24 +93,7 @@ #seat-chooser .row .seat:hover { filter: brightness(0.8); } -#load-screen { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: #000; - z-index: 9999; - display: flex; - align-items: center; - justify-content: center; -} - -div.spinner { - width: 40px; - height: 40px; - border: 4px solid #f3f3f3; - border-top: 4px solid #3498db; - border-radius: 50%; - animation: spin 2s linear infinite; +#seat-chooser #seat-chooser-status { + text-align: center; + margin: 1rem; } diff --git a/public/js/freq.js b/public/js/freq.js new file mode 100644 index 0000000..7619eb0 --- /dev/null +++ b/public/js/freq.js @@ -0,0 +1,24 @@ +class FreqManager { + #store; + static getInstance() { + if (this._instance === null) { + this._instance = new FreqManager(); + } + return this._instance; + } + + constructor() { + this.#store = {}; + } + + set(key, value) { + this.#store[key] = value; + } + + get(key) { + return this.#store[key]; + } + +} + +window.freq = FreqManager.getInstance(); diff --git a/public/js/order.js b/public/js/order.js new file mode 100644 index 0000000..3bcb429 --- /dev/null +++ b/public/js/order.js @@ -0,0 +1,106 @@ +// DEPENDS ON seat-chooser.js +(async () => { + let scDiv = document.querySelector('#seat-chooser'); + let summaryDiv = document.querySelector('#summary-dyn'); + + let showingId = scDiv.getAttribute('data-showing-id'); + let ticketTypes = await (await fetch(`/api/showing/${showingId}/prices`)).json(); + + let sc = new SeatChooser( + showingId, + scDiv, + (sc, s, add) => { + // seat click callback + // sc is the seat chooser + // s is the seat // but it's better to look at sc.selectedSeats + // add is a boolean indicating whether the seat is being added or removed + + // update the summary + summaryDiv.innerHTML = ''; + sc.selectedSeats.forEach((seat, index) => { + // set seat ticket if ticket is not set + if (!seat.ticket) seat.ticket = ticketTypes[0]; + let seatDiv = document.createElement('div'); + seatDiv.classList.add('seat'); + let ticketSelect = document.createElement('select'); + ticketTypes.forEach((ticketType, index) => { + let option = document.createElement('option'); + option.value = index; + option.innerText = ticketType.price_type; + ticketSelect.appendChild(option); + }); + ticketSelect.classList.add('ticket-select'); + ticketSelect.value = ticketTypes.indexOf(seat.ticket); + ticketSelect.addEventListener('change', (e) => { + seat.ticket = ticketTypes[e.target.value]; + updateTicket(sc, seat, seatDiv); + updateTotal(sc); + }); + seatDiv.appendChild(ticketSelect); + + seatDiv.appendChild(document.createTextNode(' ')); + + let seatSpan = document.createElement('span'); + seatSpan.innerText = `Row ${seat.row}, Seat ${seat.col}`; + seatSpan.classList.add('seat-span'); + seatDiv.appendChild(seatSpan); + seatDiv.appendChild(document.createTextNode(' ')); + + let priceSpan = document.createElement('span'); + priceSpan.classList.add('price'); + seatDiv.appendChild(priceSpan); + summaryDiv.appendChild(seatDiv); + + updateTicket(sc, seat, seatDiv); + }); + + updateTotal(sc); + + if (sc.selectedSeats.length == 0) document.querySelector('button#order-button').disabled = true; + else document.querySelector('button#order-button').disabled = false; + + } + ); + + window.freq.set('seat-chooser', sc); +})(); + +let total = 0; + +function updateTicket(sc, seat, seatDiv) { + seatDiv.querySelector('span:last-child').innerText = `€${seat.ticket.price}`; +} + +function updateTotal(sc) { + total = 0; + sc.selectedSeats.forEach(_seat => { + total += Number(_seat.ticket.price); + }); + document.querySelector('#summary-total').innerText = `Total: €${total}`; +} + +document.querySelector('button#order-button').addEventListener('click', (e) => { + let seats = []; + window.sc.selectedSeats.forEach(seat => { + seats.push({ + seat: seat.id, + ticket: seat.ticket.id + }); + }); + fetch(`/api/order`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + showing: sc.showingId, + seats: seats + }) + }).then(res => { + if (res.ok) { + window.location = '/order/complete'; + } else { + alert('Error placing order'); + } + }); +}); diff --git a/public/js/seat-chooser.js b/public/js/seat-chooser.js index 4bae2c5..8a89b8c 100644 --- a/public/js/seat-chooser.js +++ b/public/js/seat-chooser.js @@ -26,19 +26,21 @@ class SeatChooser { status.style.textAlign = 'center'; this.container.appendChild(status); } - // set the message - this.container.querySelector('#seat-chooser-status-message').innerText = msg; + // set the message if it exists + if (this.container.querySelector('#seat-chooser-status-message')) + this.container.querySelector('#seat-chooser-status-message').innerText = msg; } - constructor(showingId, container, submitCallback) { + constructor(showingId, container, seatClickCallback = (sc, s) => {}, submitCallback = null) { this.showingId = showingId; // id of the showing this.container = container; // the container to render the seat chooser in - this.submitCallback = submitCallback; // callback function to call when the user submits the form this.setLoadingStatus("Initializing..."); - this.seats = []; // array of nulls and Seat objects + this.submitCallback = submitCallback; // callback function to call when the user submits the form + this.seatClickCallback = seatClickCallback; // callback function to call when the user clicks on a seat + this.seats = []; // array of nulls and Seat objects this.selectedSeats = []; // array of Seat objects this.init(); @@ -171,15 +173,17 @@ class SeatChooser { }); buttonContainer.appendChild(clearButton); - // submit button - let submitButton = document.createElement('button'); - submitButton.classList.add('button'); - submitButton.style.margin = '1rem'; - submitButton.innerText = 'Submit'; - submitButton.addEventListener('click', () => { - this.submitCallback(this); - }); - buttonContainer.appendChild(submitButton); + if (this.submitCallback !== null) { + // submit button + let submitButton = document.createElement('button'); + submitButton.classList.add('button'); + submitButton.style.margin = '1rem'; + submitButton.innerText = 'Submit'; + submitButton.addEventListener('click', () => { + this.submitCallback(this); + }); + buttonContainer.appendChild(submitButton); + } this.container.appendChild(buttonContainer); } @@ -209,6 +213,7 @@ class SeatChooser { this.selectedSeats.push(seat); } } + this.seatClickCallback(this, seat, this.selectedSeats.includes(seat)); } clearSelection() { @@ -219,21 +224,3 @@ class SeatChooser { this.selectedSeats = []; } } - -let seatChooser = new SeatChooser(1, document.getElementById('seat-chooser'), (sc) => { - // gray overlay and show selected seats - let overlay = document.createElement('div'); - overlay.style.position = 'fixed'; - overlay.style.top = '0'; - overlay.style.left = '0'; - overlay.style.width = '100vw'; - overlay.style.height = '100vh'; - overlay.style.backgroundColor = 'rgba(0,0,0,0.5)'; - overlay.style.display = 'flex'; - overlay.style.justifyContent = 'center'; - overlay.innerText = 'You selected the following seats: ' + sc.selectedSeats.map(seat => `${seat.row}-${seat.col}`).join(', '); - overlay.addEventListener('click', () => { - overlay.remove(); - }); - sc.container.appendChild(overlay); -}); diff --git a/resources/views/components/orders.blade.php b/resources/views/components/orders.blade.php new file mode 100644 index 0000000..dea2c85 --- /dev/null +++ b/resources/views/components/orders.blade.php @@ -0,0 +1,22 @@ +
+ + + + + + + + + + + @foreach ($orders as $order) + + + + + + + @endforeach + +
Order IDOrder DateOrder StatusOrder Total
{{ $order->order_id }}{{ $order->order_date }}{{ $order->order_status }}{{ $order->order_total }}
+
diff --git a/resources/views/components/seat-chooser.blade.php b/resources/views/components/seat-chooser.blade.php index da51a17..dfc3924 100644 --- a/resources/views/components/seat-chooser.blade.php +++ b/resources/views/components/seat-chooser.blade.php @@ -3,7 +3,15 @@ @push('head') - + @endpush -
+
+ +
+
+

Loading...

+ Fetching prices... +
+
+
diff --git a/resources/views/dash.blade.php b/resources/views/dash.blade.php index 88a2df6..d4528a1 100644 --- a/resources/views/dash.blade.php +++ b/resources/views/dash.blade.php @@ -14,6 +14,10 @@
@endif +

Orders

+
+ +

Permissions


diff --git a/resources/views/layout.blade.php b/resources/views/layout.blade.php index 5a65f0a..ec92381 100644 --- a/resources/views/layout.blade.php +++ b/resources/views/layout.blade.php @@ -4,6 +4,7 @@ {{ isset($title) ? $title : 'Page' }} - {{ config('app.name', 'CineFlex') }} Manage + @stack('head') diff --git a/resources/views/main/cinemas/index.blade.php b/resources/views/main/cinemas/index.blade.php index c0ff3a0..7d662ba 100644 --- a/resources/views/main/cinemas/index.blade.php +++ b/resources/views/main/cinemas/index.blade.php @@ -7,8 +7,8 @@