Merge branch 'main' of git.infinimotion.de:infinimotion/frontend
This commit is contained in:
@@ -25,6 +25,8 @@ import { MatStepperModule } from '@angular/material/stepper';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatBadgeModule } from '@angular/material/badge';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
|
||||
import { HeaderComponent } from './header/header.component';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
@@ -63,6 +65,7 @@ import { PurchaseSuccessComponent } from './purchase-success/purchase-success.co
|
||||
import { PurchaseFailedComponent } from './purchase-failed/purchase-failed.component';
|
||||
import { TicketSmallComponent } from './ticket-small/ticket-small.component';
|
||||
import { TicketListComponent } from './ticket-list/ticket-list.component';
|
||||
import { StatisticsComponent } from './statistics/statistics.component';
|
||||
import { ZoomWarningComponent } from './zoom-warning/zoom-warning.component';
|
||||
import { SelectionConflictInfoComponent } from './selection-conflict-info/selection-conflict-info.component';
|
||||
import { CancellationSuccessComponent } from './cancellation-success/cancellation-success.component';
|
||||
@@ -70,6 +73,7 @@ import { CancellationFailedComponent } from './cancellation-failed/cancellation-
|
||||
import { ConversionFailedComponent } from './conversion-failed/conversion-failed.component';
|
||||
import { PayForOrderComponent } from './pay-for-order/pay-for-order.component';
|
||||
import { CancelOrderDialog } from './cancel-order/cancel-order.dialog';
|
||||
import { PricelistComponent } from './pricelist/pricelist.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -112,6 +116,7 @@ import { CancelOrderDialog } from './cancel-order/cancel-order.dialog';
|
||||
PurchaseFailedComponent,
|
||||
TicketSmallComponent,
|
||||
TicketListComponent,
|
||||
StatisticsComponent,
|
||||
ZoomWarningComponent,
|
||||
SelectionConflictInfoComponent,
|
||||
CancellationSuccessComponent,
|
||||
@@ -119,6 +124,7 @@ import { CancelOrderDialog } from './cancel-order/cancel-order.dialog';
|
||||
ConversionFailedComponent,
|
||||
PayForOrderComponent,
|
||||
CancelOrderDialog,
|
||||
PricelistComponent,
|
||||
],
|
||||
imports: [
|
||||
AppRoutingModule,
|
||||
@@ -149,6 +155,8 @@ import { CancelOrderDialog } from './cancel-order/cancel-order.dialog';
|
||||
QRCodeComponent,
|
||||
MatBadgeModule,
|
||||
MatTooltipModule,
|
||||
MatPaginatorModule,
|
||||
MatTableModule,
|
||||
],
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
|
||||
@@ -9,6 +9,8 @@ import { TheaterOverlayComponent} from './theater-overlay/theater-overlay.compon
|
||||
import { MovieImporterComponent } from './movie-importer/movie-importer.component';
|
||||
import { AuthGuard } from './auth.guard';
|
||||
import { PayForOrderComponent } from './pay-for-order/pay-for-order.component';
|
||||
import { StatisticsComponent } from './statistics/statistics.component';
|
||||
import { PricelistComponent } from './pricelist/pricelist.component';
|
||||
|
||||
const routes: Routes = [
|
||||
// Seiten ohne Layout
|
||||
@@ -31,6 +33,13 @@ const routes: Routes = [
|
||||
{ path: 'checkout/performance/:performanceId', component: TheaterOverlayComponent},
|
||||
{ path: 'checkout/order/:orderId', component: TheaterOverlayComponent},
|
||||
{ path: 'checkout/order', component: PayForOrderComponent},
|
||||
{
|
||||
path: 'admin/statistics',
|
||||
component: StatisticsComponent,
|
||||
canActivate: [AuthGuard],
|
||||
data: { roles: ['admin'] }, // Array von erlaubten Rollen. Derzeit gäbe es 'admin' und 'employee'
|
||||
},
|
||||
{ path: 'prices', component: PricelistComponent },
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { Kinosaal, Sitzplatz, Vorstellung, Film, OmdbSearch, Bestellung, Eintrittskarte } from '@infinimotion/model-frontend';
|
||||
import {
|
||||
Kinosaal,
|
||||
Sitzplatz,
|
||||
Vorstellung,
|
||||
Film,
|
||||
OmdbSearch,
|
||||
Bestellung,
|
||||
Eintrittskarte,
|
||||
StatisticsFilm, StatisticsVorstellung,
|
||||
Sitzkategorie
|
||||
} from '@infinimotion/model-frontend';
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
@@ -84,8 +94,12 @@ export class HttpService {
|
||||
/* Show-Seats APIs */
|
||||
|
||||
/* GET /api/show-seats/{show} */
|
||||
getSeatsByShowId(show: number): Observable<{seats:Sitzplatz[], reserved:Sitzplatz[], booked:Sitzplatz[]}> {
|
||||
return this.http.get<{seats:Sitzplatz[], reserved:Sitzplatz[], booked:Sitzplatz[]}>(`${this.baseUrl}show-seats/${show}`);
|
||||
getSeatsByShowId(show: number): Observable<{ seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }> {
|
||||
return this.http.get<{
|
||||
seats: Sitzplatz[],
|
||||
reserved: Sitzplatz[],
|
||||
booked: Sitzplatz[]
|
||||
}>(`${this.baseUrl}show-seats/${show}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -94,12 +108,33 @@ export class HttpService {
|
||||
/* GET /api/importer/search */
|
||||
searchMovie(query: string): Observable<OmdbSearch> {
|
||||
return this.http.get<OmdbSearch>(`${this.baseUrl}importer/search`, {
|
||||
params: { title: query }
|
||||
params: {title: query}
|
||||
});
|
||||
}
|
||||
|
||||
/* POST /api/importer/import */
|
||||
importMovie(imdbId: string): Observable<Film> {
|
||||
return this.http.post<Film>(`${this.baseUrl}importer/import?id=${imdbId}`, {})
|
||||
return this.http.post<Film>(`${this.baseUrl}importer/import?id=${imdbId}`, {})
|
||||
}
|
||||
|
||||
|
||||
/* Statistics APIs */
|
||||
|
||||
/* GET /api/statistics/movies */
|
||||
getMovieStatistics(): Observable<StatisticsFilm[]> {
|
||||
return this.http.get<StatisticsFilm[]>(`${this.baseUrl}statistics/movies`)
|
||||
}
|
||||
|
||||
/* GET /api/statistics/shows */
|
||||
getShowStatistics(): Observable<StatisticsVorstellung[]> {
|
||||
return this.http.get<StatisticsVorstellung[]>(`${this.baseUrl}statistics/shows`)
|
||||
}
|
||||
|
||||
|
||||
/* Sitzkategorie APIs */
|
||||
|
||||
/* GET /api/sitzkategorie */
|
||||
getSeatCategories(): Observable<Sitzkategorie[]> {
|
||||
return this.http.get<Sitzkategorie[]>(`${this.baseUrl}sitzkategorie`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ import { Component, inject, computed, OnInit } from '@angular/core';
|
||||
export class NavbarComponent {
|
||||
navItems: { label:string, path:string }[] = [
|
||||
{label: 'Programm', path: '/schedule'},
|
||||
{label: 'Preise', path: '/prices'},
|
||||
{label: 'Bezahlen', path: '/checkout/order'},
|
||||
{label: 'Film importieren', path: '/admin/movie-importer'},
|
||||
{label: 'Statistiken', path: '/admin/statistics'},
|
||||
]
|
||||
|
||||
private auth = inject(AuthService)
|
||||
|
||||
@@ -241,7 +241,7 @@ export class OrderComponent {
|
||||
|
||||
// Tickets anlegen
|
||||
const tickets = seats.map(seat => {
|
||||
return this.generateNewTicketObject(performance, seat, order);;
|
||||
return this.generateNewTicketObject(performance, seat, order);
|
||||
});
|
||||
|
||||
// Transaktionssicher Sitzplatzbuchung
|
||||
|
||||
82
src/app/pricelist/pricelist.component.css
Normal file
82
src/app/pricelist/pricelist.component.css
Normal file
@@ -0,0 +1,82 @@
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
/* Nur 2 Spalten insgesamt */
|
||||
.menu-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 30px;
|
||||
max-width: 900px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #faf8ff;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-top: 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.5);
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
font-size: 1.8rem; /* größer */
|
||||
font-weight: 700; /* fett */
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Sitzplätze-Karte ist DOPPELT so breit */
|
||||
.seats-card {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
/* Sitzplätze→ 2 Items pro Zeile */
|
||||
.seats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.seat-item {
|
||||
background: #ffffff;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 6px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.seat-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.seat-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.seat-price {
|
||||
font-weight: bold;
|
||||
}
|
||||
62
src/app/pricelist/pricelist.component.html
Normal file
62
src/app/pricelist/pricelist.component.html
Normal file
@@ -0,0 +1,62 @@
|
||||
<app-menu-header label="Preislisten" icon="euro_symbol"></app-menu-header>
|
||||
|
||||
<div class="menu-container my-20">
|
||||
|
||||
<!-- Sitze → jetzt an erster Stelle -->
|
||||
@if (seatCategories.length > 0) {
|
||||
<div class="card seats-card">
|
||||
<h2>🪑 Sitzplätze</h2>
|
||||
|
||||
<div class="seats-grid">
|
||||
@for (seatCategory of seatCategories; track seatCategory.id) {
|
||||
<div class="seat-item">
|
||||
<span class="seat-icon">
|
||||
<mat-icon style="font-size: 26px; width: 26px; height: 26px">
|
||||
{{ seatCategory.icon }}
|
||||
</mat-icon>
|
||||
</span>
|
||||
<span class="seat-name">{{ seatCategory.name }}</span>
|
||||
<span class="seat-price">{{ getPriceDisplay(seatCategory.price) }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Popcorn -->
|
||||
<div class="card">
|
||||
<h2>🍿 Popcorn</h2>
|
||||
<div class="item"><span>Klein</span><span>3,50 €</span></div>
|
||||
<div class="item"><span>Mittel</span><span>5,00 €</span></div>
|
||||
<div class="item"><span>Groß</span><span>6,50 €</span></div>
|
||||
<div class="item"><span>Extra Butter</span><span>1,00 €</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Nachos -->
|
||||
<div class="card">
|
||||
<h2>🧀 Nachos</h2>
|
||||
<div class="item"><span>Portion</span><span>4,50 €</span></div>
|
||||
<div class="item"><span>Käse-Dip</span><span>1,00 €</span></div>
|
||||
<div class="item"><span>Salsa-Dip</span><span>1,00 €</span></div>
|
||||
<div class="item"><span>Guacamole</span><span>1,50 €</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Getränke -->
|
||||
<div class="card">
|
||||
<h2>🥤 Getränke</h2>
|
||||
<div class="item"><span>Softdrink Klein</span><span>2,80 €</span></div>
|
||||
<div class="item"><span>Softdrink Mittel</span><span>3,50 €</span></div>
|
||||
<div class="item"><span>Softdrink Groß</span><span>4,20 €</span></div>
|
||||
<div class="item"><span>Wasser</span><span>2,50 €</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Süßigkeiten -->
|
||||
<div class="card">
|
||||
<h2>🍬 Süßigkeiten</h2>
|
||||
<div class="item"><span>Schokoladentafel</span><span>2,50 €</span></div>
|
||||
<div class="item"><span>Gummibärchen</span><span>2,20 €</span></div>
|
||||
<div class="item"><span>Kindertüte</span><span>3,00 €</span></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
41
src/app/pricelist/pricelist.component.ts
Normal file
41
src/app/pricelist/pricelist.component.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { Sitzkategorie } from '@infinimotion/model-frontend';
|
||||
import { HttpService } from '../http.service';
|
||||
import { LoadingService } from '../loading.service';
|
||||
import { catchError, of, tap } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pricelist',
|
||||
standalone: false,
|
||||
templateUrl: './pricelist.component.html',
|
||||
styleUrl: './pricelist.component.css'
|
||||
})
|
||||
export class PricelistComponent implements OnInit{
|
||||
seatCategories: Sitzkategorie[] = [];
|
||||
|
||||
private http=inject(HttpService);
|
||||
private loading=inject(LoadingService);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadSeatCategories();
|
||||
}
|
||||
|
||||
private loadSeatCategories() : void {
|
||||
this.loading.show();
|
||||
this.http.getSeatCategories().pipe(
|
||||
tap(seatCategories => {
|
||||
this.seatCategories = seatCategories.sort((a, b) => a.id - b.id);
|
||||
this.loading.hide();
|
||||
}),
|
||||
catchError(err => {
|
||||
this.loading.showError(err);
|
||||
console.error('Fehler beim Laden der Sitzkategorien', err);
|
||||
return of([]);
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
getPriceDisplay(price: number): string {
|
||||
return `${(price / 100).toFixed(2)} €`;
|
||||
}
|
||||
}
|
||||
0
src/app/statistics/statistics.component.css
Normal file
0
src/app/statistics/statistics.component.css
Normal file
85
src/app/statistics/statistics.component.html
Normal file
85
src/app/statistics/statistics.component.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<app-menu-header label="Statistiken"></app-menu-header>
|
||||
|
||||
<div class="table-table-container">
|
||||
|
||||
<table mat-table [dataSource]="movies" class="example-table">
|
||||
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>ID</th>
|
||||
<td mat-cell *matCellDef="let row">{{row.movieId}}</td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container matColumnDef="title">
|
||||
<th mat-header-cell *matHeaderCellDef>Titel</th>
|
||||
<td mat-cell *matCellDef="let row">{{row.movieTitle}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="earnings">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
Umsatz
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let row">{{(row.earnings/100).toFixed(2)}} €</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="tickets">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
Gebuchte Tickets
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let row">{{row.tickets}}</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="moviesDisplayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: moviesDisplayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<mat-paginator [length]="movieResultsLength" [pageSize]="30" aria-label="Select page of GitHub search results"></mat-paginator>
|
||||
|
||||
|
||||
<div class="show-table-container">
|
||||
|
||||
<table mat-table [dataSource]="shows" class="example-table">
|
||||
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>ID</th>
|
||||
<td mat-cell *matCellDef="let row">{{row.showId}}</td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<ng-container matColumnDef="hall">
|
||||
<th mat-header-cell *matHeaderCellDef>Kinosaal</th>
|
||||
<td mat-cell *matCellDef="let row">{{row.showHallName}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="movie_title">
|
||||
<th mat-header-cell *matHeaderCellDef>Film Name</th>
|
||||
<td mat-cell *matCellDef="let row">{{row.movieTitle}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="date">
|
||||
<th mat-header-cell *matHeaderCellDef>Datum</th>
|
||||
<td mat-cell *matCellDef="let row">{{formatDate(row.showStart)}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="earnings">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
Umsatz
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let row">{{(row.earnings/100).toFixed(2)}} €</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="tickets">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
Gebuchte Tickets
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let row">{{row.tickets}}</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="showsDisplayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: showsDisplayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<mat-paginator [length]="showsResultLength" [pageSize]="30" aria-label="Select page of GitHub search results"></mat-paginator>
|
||||
|
||||
56
src/app/statistics/statistics.component.ts
Normal file
56
src/app/statistics/statistics.component.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {HttpService} from '../http.service';
|
||||
import {
|
||||
StatisticsFilm,
|
||||
StatisticsVorstellung,
|
||||
} from '@infinimotion/model-frontend';
|
||||
import {LoadingService} from '../loading.service';
|
||||
import {firstValueFrom, forkJoin} from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-statistics',
|
||||
standalone: false,
|
||||
templateUrl: './statistics.component.html',
|
||||
styleUrl: './statistics.component.css',
|
||||
})
|
||||
export class StatisticsComponent {
|
||||
private http = inject(HttpService);
|
||||
protected movies: StatisticsFilm[] = [];
|
||||
protected shows: StatisticsVorstellung[] = [];
|
||||
protected moviesDisplayedColumns: string[] = ['id', 'title', 'earnings', 'tickets'];
|
||||
protected showsDisplayedColumns: string[] = ['id', 'hall', 'movie_title', 'date', 'earnings', 'tickets'];
|
||||
protected movieResultsLength: number = 0;
|
||||
protected showsResultLength: number = 0;
|
||||
|
||||
private loading = inject(LoadingService);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading.show()
|
||||
this.loadData().then();
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
let movieRequest = this.http.getMovieStatistics();
|
||||
let showRequest = this.http.getShowStatistics();
|
||||
let movieResponse = await firstValueFrom(movieRequest);
|
||||
let showResponse = await firstValueFrom(showRequest);
|
||||
this.movies = movieResponse
|
||||
this.shows = showResponse
|
||||
if (this.movies.length / 30 < 1) {
|
||||
this.movieResultsLength = 1;
|
||||
} else {
|
||||
this.movieResultsLength = Math.ceil(this.movies.length / 30);
|
||||
}
|
||||
if (this.shows.length / 30 < 1) {
|
||||
this.showsResultLength = 1;
|
||||
} else {
|
||||
this.showsResultLength = Math.ceil(this.shows.length / 30);
|
||||
}
|
||||
this.loading.hide();
|
||||
}
|
||||
|
||||
formatDate(date: Date) {
|
||||
return new Date(date).toLocaleString("de");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user