Adding Event Management

Fixing Bugs
Building out UI
This commit is contained in:
Tara Wilson 2024-12-02 20:21:55 -05:00
parent 3bb86da5c0
commit 3a8f2949b2
44 changed files with 562 additions and 152 deletions

View File

@ -0,0 +1,59 @@
using api.Interfaces;
using Microsoft.AspNetCore.Mvc;
using models.Request;
namespace api.Controllers;
/// <summary>
/// Endpoints for Event Management
/// </summary>
/// <param name="eventManager"></param>
[ApiController]
[Route("[controller]")]
public class EventController(IEventManager eventManager) : ControllerBase
{
[HttpPost]
public ActionResult Post([FromBody] AddEvent request)
{
//TODO: Protect Endpoint
try
{
eventManager.AddEvent(request);
return Ok();
}
catch (Exception e)
{
return BadRequest(e.Message);
}
}
[HttpPatch]
public ActionResult Patch([FromBody] PatchEvent request)
{
//TODO: Protect Endpoint
try
{
eventManager.PatchEvent(request);
return Ok();
}
catch (Exception e)
{
return BadRequest(e.Message);
}
}
[HttpGet("{startDate}/{endDate}")]
public ActionResult Get([FromRoute] DateTime startDate, DateTime endDate)
{
try
{
return Ok(eventManager.GetEvents(startDate, endDate));
}
catch (Exception e)
{
return BadRequest(e.Message);
}
}
}

View File

@ -7,7 +7,7 @@ using models.Response;
namespace api.Controllers; namespace api.Controllers;
/// <summary> /// <summary>
/// Endpoints for Qr Code Generation /// Endpoints for Ticket Management
/// </summary> /// </summary>
/// <param name="qr">Injected QR Code Service</param> /// <param name="qr">Injected QR Code Service</param>
/// <param name="ticketManager">Injected Ticket Manager Service</param> /// <param name="ticketManager">Injected Ticket Manager Service</param>
@ -18,38 +18,45 @@ public class TicketController(
ITicketManager ticketManager) : ControllerBase ITicketManager ticketManager) : ControllerBase
{ {
/// <summary> /// <summary>
/// Generates a Base64 String Qr Code /// Generates a Base64 String Qr Code and Saves Qr Code and Ticket to DB
/// </summary> /// </summary>
/// <returns>Base64 String Qr Code</returns> /// <returns>Base64 String Qr Code</returns>
[HttpPost("mint")] [HttpPost]
public ActionResult<MintResponse> MintTicket([FromBody] MintTickets mintRequest) public ActionResult<MintResponse> AddTicket([FromBody] MintTickets mintRequest)
{ {
//TODO: Protect Endpoint //TODO: Protect Endpoint
//generate ticket id //generate ticket id
var ticketId = Guid.NewGuid(); var ticketId = Guid.NewGuid();
//generate the qr code try
var qrCode = qr.GenerateQrCode(ticketId.ToString());
//build the ticket
var ticket = new Ticket
{ {
Id = Guid.NewGuid(), //generate the qr code
QrCode = qrCode, var qrCode = qr.GenerateQrCode(ticketId.ToString());
Type = mintRequest.Type,
};
//save the minted ticket //build the ticket
ticketManager.SaveMintedTicket(ticket); var ticket = new Ticket
{
Id = ticketId,
QrCode = qrCode,
Type = mintRequest.Type,
};
//return //save the minted ticket
var response = new MintResponse ticketManager.SaveMintedTicket(ticket);
//return
var response = new MintResponse
{
QrCode = ticket.QrCode,
Type = ticket.Type
};
return Ok(response);
}
catch (Exception e)
{ {
QrCode = ticket.QrCode, return BadRequest(e.Message);
Type = ticket.Type }
};
return Ok(response);
} }
} }

View File

@ -9,5 +9,6 @@ public static class ServiceCollectionExtensions
{ {
services.AddScoped<IQrCodeGenerator, QrCodeGenerator>(); services.AddScoped<IQrCodeGenerator, QrCodeGenerator>();
services.AddScoped<ITicketManager, TicketManager>(); services.AddScoped<ITicketManager, TicketManager>();
services.AddScoped<IEventManager, EventManager>();
} }
} }

View File

@ -0,0 +1,11 @@
using models.Core;
using models.Request;
namespace api.Interfaces;
public interface IEventManager
{
void AddEvent(AddEvent request);
void PatchEvent(PatchEvent request);
List<Event> GetEvents(DateTime startDate, DateTime endDate);
}

View File

@ -1,4 +1,5 @@
using api; using api;
using data;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -23,4 +24,8 @@ app.UseHttpsRedirection();
app.UseCors("AllowOrigin"); app.UseCors("AllowOrigin");
app.UseAuthorization(); app.UseAuthorization();
app.MapControllers(); app.MapControllers();
//Inject config into Mongo DB Factory
MongoFactory.InitConfig(app.Configuration);
app.Run(); app.Run();

View File

@ -0,0 +1,50 @@
@api_HostAddress = http://localhost:5168
POST {{api_HostAddress}}/event
Accept: application/json
Content-Type: application/json
{
"Date": "2024-12-02T15:00:00.991Z",
"EventName": "Winter Concert",
"EventDescription": "A wintery journey of classical music",
"Venue": {
"Name": "Valley Forge High School",
"Description": "Auditorium",
"AddressOne": "9999 Independence Blvd",
"AddressTwo": null,
"City": "Parma",
"State": "Ohio",
"Zip": "44130"
},
"Talent": {
"Name": "Parma Symphony Orchestra",
"Description": "Parma Symphony Orchestra is a Northeast Ohio community orchestra with over 50 years of history bringing classical music to people of all ages, with opportunities for local students and professional guests to perform a wide ranging repertoire."
}
}
###
PATCH {{api_HostAddress}}/Event
Accept: application/json
Content-Type: application/json
{
"Id": "1a06c032-b073-4715-9b95-9f3410e7abd9",
"Date": "2024-12-03T15:00:00.991Z",
"EventName": "Winter Concert",
"EventDescription": "A wintery journey of classical music",
"Venue": {
"Name": "Valley Forge High School",
"Description": "Auditorium",
"AddressOne": "9999 Independence Blvd",
"AddressTwo": null,
"City": "Parma",
"State": "Ohio",
"Zip": "44130"
},
"Talent": {
"Name": "Parma Symphony Orchestra",
"Description": "Parma Symphony Orchestra is a Northeast Ohio community orchestra with over 50 years of history bringing classical music to people of all ages, with opportunities for local students and professional guests to perform a wide ranging repertoire."
}
}

View File

@ -1,7 +1,8 @@
@api_HostAddress = http://localhost:5168 @api_HostAddress = http://localhost:5168
POST {{api_HostAddress}}/ticket/mint POST {{api_HostAddress}}/ticket
Accept: application/json Accept: application/json
Content-Type: application/json
{ {
"ticketType": "Single" "ticketType": "Single"

View File

@ -0,0 +1,34 @@
using api.Interfaces;
using data.Events;
using models.Core;
using models.Request;
namespace api.Services;
public class EventManager : IEventManager
{
public void AddEvent(AddEvent request)
{
var @event = new Event
{
Id = Guid.NewGuid(),
EventDescription = request.EventDescription,
EventName = request.EventName,
Date = request.Date,
Talent = request.Talent,
Venue = request.Venue
};
new Save().Execute(@event);
}
public void PatchEvent(PatchEvent request)
{
new Update().Execute(request);
}
public List<Event> GetEvents(DateTime startDate, DateTime endDate)
{
return new GetInDates().Execute(startDate, endDate);
}
}

View File

@ -4,11 +4,10 @@ using models.Core;
namespace api.Services; namespace api.Services;
public class TicketManager(IConfiguration config) : ITicketManager public class TicketManager : ITicketManager
{ {
public void SaveMintedTicket(Ticket ticket) public void SaveMintedTicket(Ticket ticket)
{ {
var db = new Save(config); new Save().Execute(ticket);
db.Execute(ticket);
} }
} }

View File

@ -0,0 +1,19 @@
using models.Core;
using models.Request;
using MongoDB.Driver;
namespace data.Events;
public class GetInDates
{
public List<Event> Execute(DateTime startDate, DateTime endDate)
{
var database = MongoFactory.GetDatabase();
var collection = database.GetCollection<Event>("events");
return collection.Find(x =>
startDate <= x.Date
&& x.Date <= endDate
).ToList();
}
}

View File

@ -0,0 +1,14 @@
using models.Core;
namespace data.Events;
public class Save
{
public void Execute(Event @event)
{
var database = MongoFactory.GetDatabase();
var collection = database.GetCollection<Event>("events");
collection.InsertOne(@event);
}
}

View File

@ -0,0 +1,28 @@
using models.Core;
using models.Request;
using MongoDB.Driver;
namespace data.Events;
public class Update
{
public bool Execute(PatchEvent request)
{
var database = MongoFactory.GetDatabase();
var collection = database.GetCollection<Event>("events");
var filter = Builders<Event>.Filter.Eq(e => e.Id, request.Id);
var newEvent = new Event
{
Id = request.Id,
Date = request.Date,
EventName = request.EventName,
EventDescription = request.EventDescription,
Venue = request.Venue,
Talent = request.Talent,
};
return collection.ReplaceOne(filter, newEvent).IsAcknowledged;
}
}

View File

@ -0,0 +1,40 @@
using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
namespace data;
public static class MongoFactory
{
private static IConfiguration? _config;
/// <summary>
/// Sets up the configuration, must be called before using GetDatabase()
/// </summary>
/// <param name="config"></param>
public static void InitConfig(IConfiguration config)
{
_config = config;
}
/// <summary>
/// Sets up the connection to Mongo and returns the database from Configuration
/// </summary>
/// <returns></returns>
/// <exception cref="NullReferenceException"></exception>
/// <exception cref="Exception"></exception>
public static IMongoDatabase GetDatabase()
{
if (_config == null)
{
throw new NullReferenceException("Configuration is not set");
}
if (String.IsNullOrEmpty(_config.GetSection("Mongo:ConnectionString").Value))
{
throw new Exception("No MongoDB connection string");
}
var client = new MongoClient(_config.GetSection("Mongo:ConnectionString").Value);
return client.GetDatabase(_config.GetSection("Mongo:Database").Value);
}
}

View File

@ -0,0 +1,14 @@
using models.Core;
namespace data.Tickets;
public class Save
{
public void Execute(Ticket ticket)
{
var database = MongoFactory.GetDatabase();
var collection = database.GetCollection<Ticket>("tickets");
collection.InsertOne(ticket);
}
}

View File

@ -1,22 +0,0 @@
using Microsoft.Extensions.Configuration;
using models.Core;
using MongoDB.Driver;
namespace data.Tickets;
public class Save(IConfiguration config)
{
public void Execute(Ticket ticket)
{
if (String.IsNullOrEmpty(config.GetSection("Mongo:ConnectionString").Value))
{
throw new Exception("No MongoDB connection string");
}
var client = new MongoClient(config.GetSection("Mongo:ConnectionString").Value);
var database = client.GetDatabase(config.GetSection("Mongo:Database").Value);
var collection = database.GetCollection<Ticket>("tickets");
collection.InsertOne(ticket);
}
}

View File

@ -5,8 +5,6 @@ namespace models.Core;
public class Talent public class Talent
{ {
[BsonGuidRepresentation(GuidRepresentation.Standard)]
public Guid Id { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
} }

View File

@ -1,15 +1,11 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace models.Core; namespace models.Core;
public class Venue public class Venue
{ {
[BsonGuidRepresentation(GuidRepresentation.Standard)]
public Guid Id { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public List<string> Address { get; set; } = []; public required string AddressOne { get; set; }
public string? AddressTwo { get; set; }
public required string City { get; set; } public required string City { get; set; }
public required string State { get; set; } public required string State { get; set; }
public required string Zip { get; set; } public required string Zip { get; set; }

View File

@ -0,0 +1,12 @@
using models.Core;
namespace models.Request;
public class AddEvent
{
public DateTime Date { get; set; }
public required string EventName { get; set; }
public string? EventDescription { get; set; }
public required Venue Venue { get; set; }
public required Talent Talent { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace models.Request;
public class EventSearch
{
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}

View File

@ -0,0 +1,13 @@
using models.Core;
namespace models.Request;
public class PatchEvent
{
public Guid Id { get; set; }
public DateTime Date { get; set; }
public required string EventName { get; set; }
public string? EventDescription { get; set; }
public required Venue Venue { get; set; }
public required Talent Talent { get; set; }
}

View File

@ -1,8 +1,7 @@
<div class="row"> <div class="sidenav">
<div class="card"> <app-sidebar/>
<app-sidebar />
</div>
<div class="card">
<router-outlet />
</div>
</div> </div>
<div class="main">
<router-outlet/>
</div>

View File

@ -0,0 +1,25 @@
/* The sidebar menu */
.sidenav {
height: 100%; /* Full-height: remove this if you want "auto" height */
width: 120px; /* Set the width of the sidebar */
position: fixed; /* Fixed Sidebar (stay in place on scroll) */
z-index: 1; /* Stay on top */
top: 0; /* Stay at the top */
left: 0;
backdrop-filter: blur(25px) saturate(112%);
-webkit-backdrop-filter: blur(25px) saturate(112%);
background-color: rgba(255, 255, 255, 0.11);
overflow-x: hidden; /* Disable horizontal scroll */
}
/* Style page content */
.main {
margin-left: 120px; /* Same as the width of the sidebar */
padding: 0px 10px;
}
/* On smaller screens, where height is less than 450px, change the style of the sidebar (less padding and a smaller font size) */
@media screen and (max-height: 450px) {
.sidenav {}
.sidenav a {font-size: 18px;}
}

View File

@ -1,9 +1,24 @@
import { Routes } from '@angular/router'; import {Routes} from '@angular/router';
import {DebugComponent} from './page/debug/debug.component'; import {DebugComponent} from './page/debug/debug.component';
import {EventComponent} from './page/event/event.component';
import {TicketComponent} from './page/ticket/ticket.component';
import {ScanComponent} from './page/scan/scan.component';
export const routes: Routes = [ export const routes: Routes = [
{ {
path: 'debug', path: 'debug',
component: DebugComponent component: DebugComponent
},
{
path: 'event',
component: EventComponent
},
{
path: 'ticket',
component: TicketComponent
},
{
path: 'scan',
component: ScanComponent
} }
]; ];

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugComponent } from './debug.component';
describe('DebugComponent', () => {
let component: DebugComponent;
let fixture: ComponentFixture<DebugComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DebugComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DebugComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -20,6 +20,6 @@ export class DebugComponent {
ticketType: TicketTypeEnum.Single ticketType: TicketTypeEnum.Single
}; };
this.ticketMinter.mintTicket(mintRequest); this.ticketMinter.addTicket(mintRequest);
} }
} }

View File

@ -0,0 +1 @@
<p>event works!</p>

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-event',
imports: [],
templateUrl: './event.component.html',
styleUrl: './event.component.scss'
})
export class EventComponent {
}

View File

@ -0,0 +1 @@
<p>scan works!</p>

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-scan',
imports: [],
templateUrl: './scan.component.html',
styleUrl: './scan.component.scss'
})
export class ScanComponent {
}

View File

@ -0,0 +1 @@
<p>ticket works!</p>

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-ticket',
imports: [],
templateUrl: './ticket.component.html',
styleUrl: './ticket.component.scss'
})
export class TicketComponent {
}

View File

@ -0,0 +1,31 @@
import {HttpHeaders, HttpParams} from '@angular/common/http';
export class ApiUtils {
public setHttpRequestOptions(params?: any): any {
let headers: HttpHeaders = new HttpHeaders();
headers = headers.set('Content-Type', 'application/json');
const options: any = {
headers,
observe: 'response'
};
if (params) {
options.params = this.setHttpParams(params);
}
return options;
}
private setHttpParams(query: object): HttpParams {
const params = new HttpParams();
for (const key in query) {
// @ts-ignore
if (query[key] && query.hasOwnProperty(key)) {
// @ts-ignore
params.append(key, query[key]);
}
}
return params;
}
}

View File

@ -0,0 +1,48 @@
import {inject, Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {AddEventRequest} from '../../models/request/add-event-request';
import {ApiUtils} from './api-utils';
import {environment} from '../../environments/environment';
import {Endpoints} from '../../models/endpoints';
import {catchError, of} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class EventService extends ApiUtils {
private httpClient = inject(HttpClient);
constructor() {
super();
}
public addEvent(request: AddEventRequest): void {
//TODO: Remove hard coded venue and talent information
request.talent = {
name: 'Parma Symphony Orchestra',
description: 'Parma Symphony Orchestra is a Northeast Ohio community orchestra with over 50 years of history bringing classical music to people of all ages, with opportunities for local students and professional guests to perform a wide ranging repertoire.',
};
request.venue = {
name: 'Valley Forge High School',
description: 'Auditorium',
addressOne: '9999 Independence Blvd',
addressTwo: null,
city: 'Parma',
state: 'Ohio',
zip: '44130'
};
const options = this.setHttpRequestOptions();
const url = environment.apiBase + Endpoints.EVENT;
this.httpClient.post<AddEventRequest>(url, JSON.stringify(request), options)
.pipe(
catchError(error => {
console.log(error);
return of(undefined);
})
).subscribe();
}
}

View File

@ -1,24 +1,27 @@
import {Injectable} from '@angular/core'; import {inject, Injectable} from '@angular/core';
import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {signal} from '@angular/core'; import {signal} from '@angular/core';
import {catchError, map, of} from 'rxjs'; import {catchError, map, of} from 'rxjs';
import {environment} from '../../environments/environment'; import {environment} from '../../environments/environment';
import {MintResponse} from '../../models/response/mint-response'; import {MintResponse} from '../../models/response/mint-response';
import {Endpoints} from '../../models/endpoints'; import {Endpoints} from '../../models/endpoints';
import {MintRequest} from '../../models/request/mint-request'; import {MintRequest} from '../../models/request/mint-request';
import {ApiUtils} from './api-utils';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class TicketService { export class TicketService extends ApiUtils {
public dataSignal = signal(''); public dataSignal = signal('');
private httpClient = inject(HttpClient);
constructor(public httpClient: HttpClient) { constructor() {
super();
} }
public mintTicket(request: MintRequest): void { public addTicket(request: MintRequest): void {
const options = this.setHttpRequestOptions(); const options = this.setHttpRequestOptions();
const url = environment.apiBase + Endpoints.MINT_TICKETS; const url = environment.apiBase + Endpoints.TICKET;
this.httpClient.post<MintResponse>(url, JSON.stringify(request), options) this.httpClient.post<MintResponse>(url, JSON.stringify(request), options)
.pipe( .pipe(
@ -31,34 +34,6 @@ export class TicketService {
if (res !== undefined) { if (res !== undefined) {
this.dataSignal.set(res.qrCode); this.dataSignal.set(res.qrCode);
} }
}) });
}
private setHttpRequestOptions(params?: any): any {
let headers: HttpHeaders = new HttpHeaders();
headers = headers.set('Content-Type', 'application/json');
const options: any = {
headers,
observe: 'response'
};
if (params) {
options.params = this.setHttpParams(params);
}
return options;
}
private setHttpParams(query: object): HttpParams {
const params = new HttpParams();
for (const key in query) {
// @ts-ignore
if (query[key] && query.hasOwnProperty(key)) {
// @ts-ignore
params.append(key, query[key]);
}
}
return params;
} }
} }

View File

@ -1,8 +1,8 @@
<div class="column"> <div class="column">
<button class="button">Generate Tickets</button> <button class="sidebar-button" [routerLink]="['/ticket']">Generate Tickets</button>
<button class="button">Scan Tickets</button> <button class="sidebar-button" [routerLink]="['/scan']">Scan Tickets</button>
<button class="button">Manage Events</button> <button class="sidebar-button" [routerLink]="['/event']">Manage Events</button>
@if(!environment.production) { @if(!environment.production) {
<button class="button" [routerLink]="['/debug']">Debug</button> <button class="sidebar-button" [routerLink]="['/debug']">Debug</button>
} }
</div> </div>

View File

@ -0,0 +1,17 @@
.sidebar-button {
margin: 5px;
padding: 5px;
backdrop-filter: blur(25px) saturate(112%);
-webkit-backdrop-filter: blur(25px) saturate(112%);
background-color: rgba(255, 255, 255, 0.11);
border: 1px solid rgba(255, 255, 255, 0.125);
border-radius: 8px;
&:hover {
background-color: rgba(255, 255, 255, 0.0);
}
&:active {
background-color: rgba(255, 255, 255, 0.25);
}
}

View File

@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SidebarComponent } from './sidebar.component';
describe('SidebarComponent', () => {
let component: SidebarComponent;
let fixture: ComponentFixture<SidebarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SidebarComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SidebarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,4 @@
export interface Talent {
name: string;
description: string;
}

View File

@ -0,0 +1,9 @@
export interface Venue {
name: string,
description: string,
addressOne: string,
addressTwo: string | null,
city: string,
state: string,
zip: string,
}

View File

@ -1,4 +1,5 @@
export class Endpoints { export class Endpoints {
public static readonly TICKET = 'ticket'; public static readonly TICKET = 'ticket';
public static readonly MINT_TICKETS = Endpoints.TICKET + '/mint'
public static readonly EVENT = 'events';
} }

View File

@ -0,0 +1,10 @@
import {Venue} from '../core/venue';
import {Talent} from '../core/talent';
export interface AddEventRequest {
date: Date,
eventName: string,
eventDescription: string,
venue: Venue,
talent: Talent,
}