Compare commits

..

3 Commits

Author SHA1 Message Date
a98b05f14b Delete source/ticketAPI/api/appsettings.json 2024-12-11 22:26:47 -05:00
903e562e42 Delete source/ticketAPI/api/appsettings.Development.json 2024-12-11 22:26:39 -05:00
Tara Wilson
50efe0f310 2024.12.11 2024-12-11 15:42:31 -05:00
50 changed files with 174 additions and 656 deletions

View File

@ -1,31 +0,0 @@
name: Build
run-name: ${{gitea.actor}} is building 🚀
on: [push]
jobs:
BuildUI:
runs-on: osx
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Restoring Node Packages
run: |
cd source
cd ticketUI
yarn install --frozen-lockfile
- name: Build
run: |
cd source
cd ticketUI
yarn run build
BuildAPI:
runs-on: osx
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Setting up .Net CLI
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Build API
run: dotnet build ./source/ticketAPI/ticketAPI.sln

View File

@ -1,100 +0,0 @@
name: Deploy
run-name: ${{gitea.actor}} is deploying 🚀
on:
release:
types: [published]
jobs:
BuildUI:
runs-on: osx
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Restoring Node Packages
run: |
cd source
cd ticketUI
yarn install --frozen-lockfile
- name: Build
run: |
cd source
cd ticketUI
yarn run build
- name: Saving build artifact
uses: actions/upload-artifact@v3
with:
name: ticketUI
path: source/ticketUI/dist/ticket-ui
BuildAPI:
runs-on: osx
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Setting up .Net CLI
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Build API
run: dotnet publish -c release -o artifacts/api /p:DebugType=None /p:DebugSymbols=false ./source/ticketAPI/ticketAPI.sln
- name: Saving build artifact
uses: actions/upload-artifact@v3
with:
name: ticketAPI
path: artifacts/api
PublishUI:
runs-on: osx
needs: [BuildUI, BuildAPI]
name: Publish UI Docker Image
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Getting Artifact
uses: actions/download-artifact@v3
with:
name: ticketUI
path: source/ticketUI/dist/ticket-ui
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: source/ticketUI/Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ticket-ui:${{ github.ref_name }}
PublishAPI:
runs-on: osx
needs: [BuildAPI, PublishUI]
name: Publish API Docker Image
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Getting Artifact
uses: actions/download-artifact@v3
with:
name: ticketAPI
path: artifacts/api
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: source/ticketAPI/Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/ticket-api:${{ github.ref_name }}

2
.gitignore vendored
View File

@ -47,5 +47,3 @@ testem.log
# System files
.DS_Store
Thumbs.db
source/ticketAPI/api/appsettings.Development.json
source/ticketAPI/api/appsettings.json

View File

@ -2,61 +2,38 @@
```mermaid
classDiagram
Ticket
Event
Patron
Season
Talent
Ticket
Venue
Event : Guid Id
Event : Guid SeasonId
Event : Date Date
Event : String Name
Event : String? Description
Event : Object Venue
Event : Object Talent
Event : Array TicketIds
Patron : String FirstName
Patron : String? MiddleName
Patron : String LastName
Patron : String Email
Patron : String? PhoneNumber
Patron : String AddressOne
Patron : String? AddressTwo
Patron : String City
Patron : String State
Patron : String Zip
Season : Guid Id
Season : String Name
Season : String? Description
Season : Date StartDate
Season : Date EndDate
Season : Array EventIds
Talent : String Name
Talent : String? Description
Address
Order
Ticket : Guid Id
Ticket : Guid? SeasonId
Ticket : Guid EventId
Ticket : TicketType Type
Ticket : String QrCode
Ticket : Object Patron
Ticket : int Seats
Ticket : DateTime Date
Ticket : Patron Patron
Venue : String Name
Venue : String? Description
Venue : String AddressOne
Venue : String? AddressTwo
Venue : String City
Venue : String State
Venue : String Zip
Event : Guid Id
Event : String Title
Event : String Description
Event : DateTime Date
Event : Array Tickets
Patron --* Ticket
Patron : Guid Id
Patron : String EmailAddress
Patron : String PhoneNumber
Patron : Address Address
Venue --* Event
Talent --* Event
Address : Guid Id
Address : String StreetAddressOne
Address : String StreetAddressTwo
Address : String City
Address : String State
Address : String ZipCode
Order : Guid Id
Order : Patron Patron
Order : Array Tickets
Event o-- Ticket : X Tickets
```

12
diagrams/order-lookup.md Normal file
View File

@ -0,0 +1,12 @@
# Order Lookup
```mermaid
sequenceDiagram
participant UI
participant API
participant DB
UI->>API: Order Id
API->>DB: Order Id
DB->>API: Order Returned
API->>UI: Order Details Returned
```

View File

@ -1,16 +0,0 @@
# Ticket Generation
```mermaid
sequenceDiagram
participant UI
participant API
participant DB
UI ->> API: Print Ticket Request
API ->> API: Generate Ticket Id
API ->> API: Generate Qr Code
API ->> DB: Save Ticket
API ->> DB: Associate Ticket with Event
API ->> API: Build Ticket Data for UI
API ->> UI: Respond with Ticket
UI ->> UI: Builds Visual Ticket
```

View File

@ -1,34 +0,0 @@
# Ticket Lookup
## Happy Path
```mermaid
sequenceDiagram
participant UI
participant API
participant DB
UI ->> UI: Decode QrCode
UI ->> API: Send TicketId from QrCode
API ->> DB: Query Ticket
DB -->> API: Ticket Returned, if found
API ->> DB: Query Event from Ticket
DB -->> API: Event Information Returned
API ->> API: Check for Ticket Validity
API ->> UI: Return Ticket Validity
```
## No Ticket Found
```mermaid
sequenceDiagram
participant UI
participant API
participant DB
UI ->> UI: Decode QrCode
UI ->> API: Send TicketId from QrCode
API ->> DB: Query Ticket
DB -->> API: None Found
API ->> UI: Ticket Not Found
```

View File

@ -0,0 +1,14 @@
# Ticket Minting
```mermaid
sequenceDiagram
participant UI
participant API
participant DB
UI->>API: Mint Ticket(s) Request
API->>DB: Get Ticket Ids
DB->>DB: IDs Assigned
DB->>API: IDs Returned
API->>API: QR Codes Generated
API->>UI: Code Returned, Email Sent
```

View File

@ -0,0 +1,29 @@
# Ticket Verification
## Happy Path
```mermaid
sequenceDiagram
participant UI
participant API
participant DB
UI ->> API: Decoded Ticket Id from QR Code
API ->> DB: Query DB with Id
DB ->> API: Found Ticket Info
API ->> UI: Tickets Found, Admit X Seats
```
## Negative Path
```mermaid
sequenceDiagram
participant UI
participant API
participant DB
UI ->> API: Decoded Ticket Id from QR Code
API ->> DB: Query DB with Id
DB ->> API: None Found
API ->> UI: Ticket Not Found
```

View File

@ -1,26 +0,0 @@
# Ticket Validity Check
```mermaid
flowchart TD
start([Ticket Found, Event Found])
eventDateIsToday(Checks if Event Date is Today)
todayIsAfterEvent(Checks if Event was before Today)
twoHoursEarly(Checks if its less than 2 hours to event start)
fiveHoursAgo(Checks if Event started less than 5 hours ago)
returnsInvalid([Ticket is Invalid])
returnsExpired([Ticket is Expired])
returnsExpired2([Ticket is Expired])
returnsEarly([Ticket is Early])
returnsValid([Ticket is Valid])
start --> eventDateIsToday -- yes --- todayIsAfterEvent
eventDateIsToday -- no --- returnsInvalid
todayIsAfterEvent -- yes --- returnsExpired2
todayIsAfterEvent --> twoHoursEarly -- no --- returnsEarly
twoHoursEarly -- yes --- fiveHoursAgo
fiveHoursAgo -- yes --- returnsValid
fiveHoursAgo -- no --- returnsExpired
```

View File

@ -1,5 +1,5 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0
COPY ../../artifacts/api /app
COPY artifacts /app
WORKDIR /app
EXPOSE 80
ENTRYPOINT ["dotnet", "api.dll"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@ -56,16 +56,15 @@ public class EventController(IEventManager eventManager) : ControllerBase
/// </summary>
/// <param name="startDate">Start date for the event search</param>
/// <param name="endDate">End date for the event search</param>
/// <param name="seasonId">Season to find events for</param>
/// <returns></returns>
[HttpGet]
public ActionResult<List<Event>> Get(DateTime? startDate, DateTime? endDate, string? seasonId)
public ActionResult<List<Event>> Get(DateTime? startDate, DateTime? endDate)
{
try
{
if (startDate == null && endDate == null)
{
return Ok(seasonId == null ? eventManager.GetAllEvents() : eventManager.GetBySeason(new Guid(seasonId)));
return Ok(eventManager.GetAllEvents());
}
return Ok(eventManager.GetEvents(startDate, endDate));
}
@ -74,24 +73,4 @@ public class EventController(IEventManager eventManager) : ControllerBase
return BadRequest(e.Message);
}
}
/// <summary>
/// Deletes an event
/// </summary>
/// <param name="eventId">Event to delete</param>
/// <returns></returns>
[HttpDelete]
public IActionResult Delete(Guid eventId)
{
// TODO: Protect Endpoint
try
{
eventManager.DeleteEvent(eventId);
return Ok();
}
catch (Exception e)
{
return BadRequest(e.Message);
}
}
}

View File

@ -53,39 +53,18 @@ public class SeasonController(ISeasonManager seasonManager) : ControllerBase
}
/// <summary>
/// Updates a season
/// Adds an event to a season
/// </summary>
/// <param name="request">Updated season information</param>
/// <param name="seasonId">Season Id</param>
/// <param name="eventId">Event Id</param>
/// <returns></returns>
[HttpPatch]
public IActionResult Patch(PatchSeason request)
[HttpPut]
public IActionResult Put(Guid eventId, Guid seasonId)
{
//TODO: Protect Endpoint
try
{
seasonManager.PatchSeason(request);
return Ok();
}
catch (Exception e)
{
return BadRequest(e.Message);
}
}
/// <summary>
/// Deletes a season
/// </summary>
/// <param name="seasonId">SeasonId to delete</param>
/// <returns></returns>
[HttpDelete]
public IActionResult Delete(Guid seasonId)
{
//TODO: Protect Endpoint
try
{
seasonManager.DeleteSeason(seasonId);
seasonManager.AddEventToSeason(eventId, seasonId);
return Ok();
}
catch (Exception e)

View File

@ -56,11 +56,6 @@ public class TicketController(
Details = eventManager.GetEvent(ticket.EventId)
};
if (!string.IsNullOrEmpty(ticket.Patron.Email))
{
//TODO: Hookup Email Service
}
return Ok(response);
}
catch (Exception e)

View File

@ -11,6 +11,5 @@ public static class ServiceCollectionExtensions
services.AddScoped<ITicketManager, TicketManager>();
services.AddScoped<IEventManager, EventManager>();
services.AddScoped<ISeasonManager, SeasonManager>();
services.AddScoped<IEmailService, EmailService>();
}
}

View File

@ -1,9 +0,0 @@
using models.Core;
using models.Response;
namespace api.Interfaces;
public interface IEmailService
{
void SendEmail(Ticket ticket, EventDetails details);
}

View File

@ -11,6 +11,4 @@ public interface IEventManager
List<Event> GetEvents(DateTime? startDate, DateTime? endDate);
List<Event> GetAllEvents();
EventDetails GetEvent(Guid id);
void DeleteEvent(Guid eventId);
List<Event> GetBySeason(Guid seasonId);
}

View File

@ -7,6 +7,5 @@ public interface ISeasonManager
{
void AddSeason(AddSeason season);
List<Season> GetSeasons();
void PatchSeason(PatchSeason request);
void DeleteSeason(Guid seasonId);
void AddEventToSeason(Guid eventId, Guid seasonId);
}

View File

@ -62,10 +62,4 @@ Content-Type: application/json
GET {{api_HostAddress}}/event
Accept: application/json
Content-Type: application/json
###
GET {{api_HostAddress}}/event?seasonId={{seasonId}}
Accept: application/json
Content-Type: application/json

View File

@ -1,77 +0,0 @@
using System.Net;
using System.Net.Mail;
using System.Text;
using api.Interfaces;
using models.Core;
using models.Response;
namespace api.Services;
public class EmailService(IConfiguration config) : IEmailService
{
public void SendEmail(Ticket ticket, EventDetails @event)
{
using var client = new SmtpClient(config.GetSection("Email:Server").Value);
client.UseDefaultCredentials = false;
var auth = new NetworkCredential(config.GetSection("Email:UserName").Value,
config.GetSection("Email:Password").Value);
client.Credentials = auth;
var from = new MailAddress(config.GetSection("Email:From").Value ?? string.Empty);
if (from.Address != config.GetSection("Email:From").Value)
{
throw new Exception("Invalid email address");
}
var to = new MailAddress(ticket.Patron.Email);
var emailMessage = new MailMessage(from, to);
emailMessage.ReplyToList.Add(from);
emailMessage.Subject = $"Tickets - {@event.Name}";
emailMessage.SubjectEncoding = Encoding.UTF8;
emailMessage.IsBodyHtml = true;
emailMessage.Body = GenerateTicketBody(ticket, @event);
client.Send(emailMessage);
}
private string GenerateTicketBody(Ticket ticket, EventDetails @event)
{
var sb = new StringBuilder();
const string cardStyle = "margin: 15px; padding: 5px; backdrop-filter: blur(25px) saturate(112%); -webkit-backdrop-filter: blur(25px) saturate(112%); background-color: rgba(255, 255, 255, 0.11); border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.125); width: fit-content;";
const string columnStyle = "display: flex; flex-direction: column; justify-content: center;";
const string rowStyle = "display: flex; flex-direction: row; justify-content: center; align-items: baseline;";
const string qrCodeStyle = "height: 150px; width: 150px; padding: 10px;";
const string imagePath = "Assets/pso-logo.png";
sb.AppendLine($"<div #ticket style=\"{cardStyle} {columnStyle}\">");
sb.AppendLine($"<div #talentLogo style=\"{cardStyle} {rowStyle}\">");
sb.AppendLine($"<img src=\"{imagePath}\" width=\"250\" height=\"100\" alt=\"Parma Symphony Orchestra Logo\"/>");
sb.AppendLine("</div>"); //closes #talentLogo
sb.AppendLine($"<div #qrCode style=\"{rowStyle}\">");
sb.AppendLine($"<img style=\"{qrCodeStyle}\" alt=\"QR Code\" src=\"data:image/png;base64,{ticket.QrCode}\"/>");
sb.AppendLine("</div>"); //closes #qrCode
sb.AppendLine($"<div #venue style=\"{rowStyle}\">");
sb.AppendLine($"<div style=\"{cardStyle}\">");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Talent.Name}</span>");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Name}</span>");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Description}</span>");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Date.ToString("F")}</span>");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Venue.Name}</span>");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Venue.Description}</span>");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Venue.AddressOne}</span>");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Venue.City} {@event.Venue.State} {@event.Venue.Zip}</span>");
sb.AppendLine("</div>"); //closes #venue nested card
sb.AppendLine("</div>"); //closes #venue
sb.AppendLine("</div>"); //closes #ticket
return sb.ToString();
}
}

View File

@ -22,6 +22,7 @@ public class EventManager : IEventManager
};
new Save().Execute(@event);
new data.Seasons.AddEvent().Execute(@event.Id, request.SeasonId);
}
public void PatchEvent(PatchEvent request)
@ -43,14 +44,4 @@ public class EventManager : IEventManager
{
return new GetDetails().Execute(id);
}
public void DeleteEvent(Guid eventId)
{
new Delete().Execute(eventId);
}
public List<Event> GetBySeason(Guid seasonId)
{
return new GetBySeason().Execute(seasonId);
}
}

View File

@ -2,6 +2,7 @@ using api.Interfaces;
using data.Seasons;
using models.Core;
using models.Request;
using AddEvent = data.Seasons.AddEvent;
namespace api.Services;
@ -26,13 +27,8 @@ public class SeasonManager : ISeasonManager
return new GetAll().Execute();
}
public void PatchSeason(PatchSeason request)
public void AddEventToSeason(Guid eventId, Guid seasonId)
{
new Update().Execute(request);
}
public void DeleteSeason(Guid seasonId)
{
new Delete().Execute(seasonId);
new AddEvent().Execute(eventId, seasonId);
}
}

View File

@ -10,6 +10,7 @@ public class TicketManager : ITicketManager
public void SaveMintedTicket(Ticket ticket)
{
new data.Tickets.Save().Execute(ticket);
new data.Events.AddTicket().Execute(ticket.Id, ticket.EventId);
}
public TicketSearch SearchTicket(Guid ticketId)

View File

@ -21,8 +21,4 @@
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\" />
</ItemGroup>
</Project>

View File

@ -3,14 +3,16 @@ using MongoDB.Driver;
namespace data.Events;
public class Delete
public class AddTicket
{
public void Execute(Guid eventId)
public void Execute(Guid ticketId, Guid eventId)
{
var database = MongoFactory.GetDatabase();
var filter = Builders<Event>.Filter.Eq(e => e.Id, eventId);
var collection = database.GetCollection<Event>("events");
var filter = Builders<Event>.Filter.Eq(e => e.Id, eventId);
collection.DeleteOne(filter);
var update = Builders<Event>.Update.Push("TicketIds", ticketId);
collection.FindOneAndUpdate(filter, update);
}
}

View File

@ -9,7 +9,7 @@ public class Find
{
var database = MongoFactory.GetDatabase();
var collection = database.GetCollection<Event>("events");
var filter = Builders<Event>.Filter.Eq(e => e.Id, eventId);
var filter = Builders<Event>.Filter.Eq("Id", eventId);
return collection.Find(filter).FirstOrDefault();
}
}

View File

@ -1,16 +0,0 @@
using models.Core;
using MongoDB.Driver;
namespace data.Events;
public class GetBySeason
{
public List<Event> Execute(Guid seasonId)
{
var database = MongoFactory.GetDatabase();
var collection = database.GetCollection<Event>("events");
var filter = Builders<Event>.Filter.Eq(e => e.SeasonId, seasonId);
return collection.Find(filter).ToList();
}
}

View File

@ -6,24 +6,23 @@ namespace data.Events;
public class Update
{
public void Execute(PatchEvent request)
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 filter = Builders<Event>.Filter.Eq("_id", request.Id);
var newEvent = new Event
{
Id = request.Id,
SeasonId = request.SeasonId,
Date = request.Date,
Name = request.Name,
Description = request.Description,
Venue = request.Venue,
Talent = request.Talent,
};
collection.ReplaceOne(filter, newEvent);
return collection.ReplaceOne(filter, newEvent).IsAcknowledged;
}
}

View File

@ -3,14 +3,16 @@ using MongoDB.Driver;
namespace data.Seasons;
public class Delete
public class AddEvent
{
public void Execute(Guid seasonId)
public void Execute(Guid eventId, Guid seasonId)
{
var database = MongoFactory.GetDatabase();
var filter = Builders<Season>.Filter.Eq(s => s.Id, seasonId);
var collection = database.GetCollection<Season>("seasons");
var filter = Builders<Season>.Filter.Eq(s => s.Id, seasonId);
collection.DeleteOne(filter);
var update = Builders<Season>.Update.Push("EventIds", eventId);
collection.FindOneAndUpdate(filter, update);
}
}

View File

@ -1,27 +0,0 @@
using models.Core;
using models.Request;
using MongoDB.Driver;
namespace data.Seasons;
public class Update
{
public void Execute(PatchSeason request)
{
var database = MongoFactory.GetDatabase();
var collection = database.GetCollection<Season>("seasons");
var filter = Builders<Season>.Filter.Eq(s => s.Id, request.Id);
var newSeason = new Season
{
Id = request.Id,
Name = request.Name,
Description = request.Description,
StartDate = request.StartDate,
EndDate = request.EndDate,
};
collection.ReplaceOne(filter, newSeason);
}
}

View File

@ -9,4 +9,5 @@ public class Event
public string? Description { get; set; }
public required Venue Venue { get; set; }
public required Talent Talent { get; set; }
public List<Guid> TicketIds { get; set; } = [];
}

View File

@ -7,4 +7,5 @@ public class Season
public string? Description { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public List<Guid> EventIds { get; set; } = [];
}

View File

@ -5,7 +5,6 @@ namespace models.Request;
public class PatchEvent
{
public Guid Id { get; set; }
public Guid SeasonId { get; set; }
public DateTime Date { get; set; }
public required string Name { get; set; }
public string? Description { get; set; }

View File

@ -1,10 +0,0 @@
namespace models.Request;
public class PatchSeason
{
public Guid Id { get; set; }
public required string Name { get; set; }
public string? Description { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}

View File

@ -1,5 +1,5 @@
FROM nginx:latest
COPY source/ticketUI/nginx.conf /etc/nginx/conf.d/default.conf
COPY source/ticketUI/dist/ticket-ui /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
COPY dist/ticket-ui /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,6 +1,6 @@
{
"name": "ticket-ui",
"version": "0.0.0",
"version": "2024.12.11",
"scripts": {
"ng": "ng",
"start": "ng serve",

View File

@ -17,7 +17,7 @@ export class EventBrowserComponent implements OnInit {
public selectedEventId$: WritableSignal<string> = signal('');
public ngOnInit() {
this.eventService.searchEvents(undefined, undefined, undefined);
this.eventService.searchAllEvents();
}
public click(eventId: string): void {

View File

@ -15,8 +15,7 @@
</div>
<div class="row row-space-between row-bottom-margin">
<label class="label" for="email">Email</label>
<input id="email" [formControl]="email" type="email" class="input"
[ngClass]="{'invalid': email.invalid && email.dirty}"/>
<input id="email" [formControl]="email" type="text" class="input"/>
</div>
<div class="row row-space-between row-bottom-margin">
<label class="label" for="phoneNumber">Phone</label>

View File

@ -1,13 +1,11 @@
import {Component} from '@angular/core';
import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms';
import {Patron} from '../../../models/core/patron';
import {NgClass} from '@angular/common';
@Component({
selector: 'app-patron-info',
imports: [
ReactiveFormsModule,
NgClass
ReactiveFormsModule
],
templateUrl: './patron-info.component.html',
styleUrl: './patron-info.component.scss'
@ -16,9 +14,7 @@ export class PatronInfoComponent {
public firstName = new FormControl('', {nonNullable: true, validators: [Validators.required]});
public middleName = new FormControl('');
public lastName = new FormControl('', {nonNullable: true, validators: [Validators.required]});
public email = new FormControl('', {
nonNullable: true, validators: [Validators.required, Validators.email]
});
public email = new FormControl('', {nonNullable: true, validators: [Validators.required]});
public phoneNumber = new FormControl('');
public addressOne = new FormControl('', {nonNullable: true, validators: [Validators.required]});
public addressTwo = new FormControl('');

View File

@ -1,18 +1,16 @@
<div [ngClass]="{'row': ticket$() !== null}">
<div class="card">
<h3>Generate Tickets</h3>
<div class="column">
<app-ticket-type-selector #ticketType></app-ticket-type-selector>
<app-event-browser />
@if(IsSeasonTicket(ticketType.selectedTicketType$())) {
<app-season-browser />
}
<app-patron-info />
</div>
<button class="button" (click)="saveTicket()">Save Ticket</button>
<div class="card">
<h3>Generate Tickets</h3>
<div class="column">
<app-ticket-type-selector #ticketType></app-ticket-type-selector>
<app-event-browser />
@if(IsSeasonTicket(ticketType.selectedTicketType$())) {
<app-season-browser />
}
<app-patron-info />
</div>
@if(ticket$() !== null) {
<app-generated-ticket />
}
<button class="button" (click)="saveTicket()">Save Ticket</button>
</div>
@if(ticket$() !== null) {
<app-generated-ticket />
}

View File

@ -8,8 +8,6 @@ import {SeasonBrowserComponent} from '../../components/season-browser/season-bro
import {IsSeasonTicket} from '../../../models/enums/ticket-type.enum';
import {GeneratedTicketComponent} from '../../components/generated-ticket/generated-ticket.component';
import {Guid} from 'guid-typescript';
import {SnackbarService} from '../../services/snackbar.service';
import {NgClass} from '@angular/common';
@Component({
selector: 'app-ticket',
@ -18,8 +16,7 @@ import {NgClass} from '@angular/common';
TicketTypeSelectorComponent,
PatronInfoComponent,
SeasonBrowserComponent,
GeneratedTicketComponent,
NgClass
GeneratedTicketComponent
],
templateUrl: './ticket.component.html',
styleUrl: './ticket.component.scss'
@ -31,15 +28,9 @@ export class TicketComponent {
@ViewChild(SeasonBrowserComponent) seasonBrowserComponent!: SeasonBrowserComponent;
public ticketService = inject(TicketService);
public snackBar = inject(SnackbarService);
public ticket$ = this.ticketService.mintResponse$;
public saveTicket(): void {
if(this.patronInfoComponent.email.invalid && this.patronInfoComponent.email.dirty) {
this.snackBar.showMessage("Please enter a valid email address");
return;
}
const addTicket: AddTicket = {
eventId: this.eventBrowserComponent.selectedEventId,
type: this.ticketTypeSelector.selectedTicketType,

View File

@ -1,13 +1,31 @@
import {HttpHeaders} from '@angular/common/http';
import {HttpHeaders, HttpParams} from '@angular/common/http';
export class ApiUtils {
protected setHttpRequestOptions(): any {
protected setHttpRequestOptions(params?: any): any {
let headers: HttpHeaders = new HttpHeaders();
headers = headers.set('Content-Type', 'application/json');
return {
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

@ -7,7 +7,6 @@ import {Endpoints} from '../../models/endpoints';
import {catchError, map, of} from 'rxjs';
import {SnackbarService} from './snackbar.service';
import {Event} from '../../models/core/event';
import {PatchEvent} from '../../models/request/patch-event';
@Injectable({
providedIn: 'root'
@ -22,6 +21,7 @@ export class EventService extends ApiUtils {
}
public addEvent(request: AddEventRequest): void {
//TODO: Remove hard coded venue and talent information
const options = this.setHttpRequestOptions();
const url = environment.apiBase + Endpoints.EVENT;
@ -34,30 +34,14 @@ export class EventService extends ApiUtils {
).subscribe(res => {
if(res !== null) {
this.snackbar.showMessage('Event added successfully.');
this.searchEvents(undefined, undefined, undefined);
this.searchAllEvents();
}
});
}
/**
* Searches for events, returns all events if parameters are not provided
* @param startDate If provided with an end date, will search in the date range
* @param endDate If provided with a start date, will search in the date range
* @param seasonId If provided, returns all events for a season
*/
public searchEvents(startDate: Date | undefined,
endDate: Date | undefined,
seasonId: string | undefined): void {
public searchAllEvents(): void {
const options = this.setHttpRequestOptions();
let url: string = '';
if (startDate && endDate) {
url = environment.apiBase + Endpoints.EVENT_DATE_SEARCH(startDate, endDate);
} else if (seasonId) {
url = environment.apiBase + Endpoints.EVENT_SEASON_SEARCH(seasonId);
} else {
url = environment.apiBase + Endpoints.EVENT;
}
const url = environment.apiBase + Endpoints.EVENT;
this.httpClient.get(url, options)
.pipe(
@ -67,47 +51,23 @@ export class EventService extends ApiUtils {
return of(undefined);
})
).subscribe(res => {
if(res !== null) {
this.events$.set(res);
} else {
this.snackbar.showError('No events found.');
}
this.events$.set(res);
});
}
public updateEvent(request: PatchEvent): void {
public searchEvents(startDate: Date, endDate: Date): void {
const options = this.setHttpRequestOptions();
const url = environment.apiBase + Endpoints.EVENT;
const url = environment.apiBase + Endpoints.EVENT_DATE_SEARCH(startDate, endDate);
this.httpClient.patch(url, JSON.stringify(request), options)
this.httpClient.get(url, options)
.pipe(
map((response: any) => response.body),
catchError(error => {
this.snackbar.showError(error.error);
return of(undefined);
})
).subscribe(res => {
if(res !== null) {
this.snackbar.showMessage('Event updated successfully.');
this.searchEvents(undefined, undefined, undefined);
}
})
}
public deleteEvent(eventId: string): void {
const options = this.setHttpRequestOptions();
const url = environment.apiBase + Endpoints.EVENT_DELETE(eventId);
this.httpClient.delete(url, options)
.pipe(
catchError(error => {
this.snackbar.showError(error.error);
return of(undefined);
})
).subscribe(res => {
if(res !== null) {
this.snackbar.showMessage('Event deleted successfully.');
this.searchEvents(undefined, undefined, undefined);
}
})
this.events$.set(res);
});
}
}

View File

@ -7,7 +7,6 @@ import {environment} from '../../environments/environment';
import {Endpoints} from '../../models/endpoints';
import {catchError, map, of} from 'rxjs';
import {AddSeason} from '../../models/request/add-season';
import {PatchSeason} from '../../models/request/patch-season';
@Injectable({
providedIn: 'root'
@ -40,7 +39,7 @@ export class SeasonService extends ApiUtils {
}
public saveSeason(request: AddSeason): void {
const options = this.setHttpRequestOptions();
const options = this.setHttpRequestOptions(request);
const url = environment.apiBase + Endpoints.SEASON;
this.httpClient.post(url, JSON.stringify(request), options)
@ -69,40 +68,4 @@ export class SeasonService extends ApiUtils {
})
).subscribe();
}
public updateSeason(request: PatchSeason): void {
const options = this.setHttpRequestOptions();
const url = environment.apiBase + Endpoints.SEASON;
this.httpClient.patch(url, JSON.stringify(request), options)
.pipe(
catchError(error => {
this.snackbar.showError(error.error);
return of(undefined);
})
).subscribe(res => {
if (res !== null) {
this.snackbar.showMessage('Season updated successfully.');
this.getSeasons();
}
});
}
public deleteSeason(seasonId: string): void {
const options = this.setHttpRequestOptions();
const url = environment.apiBase + Endpoints.SEASON_DELETE(seasonId);
this.httpClient.delete(url, options)
.pipe(
catchError(error => {
this.snackbar.showError(error.error);
return of(undefined);
})
).subscribe(res => {
if (res !== null) {
this.snackbar.showMessage('Season deleted successfully.');
this.getSeasons();
}
});
}
}

View File

@ -1,4 +1,4 @@
export const environment = {
production: false,
apiBase: 'http://localhost:5168/api/',
apiBase: 'http://localhost:5168/',
};

View File

@ -8,5 +8,6 @@ export interface Event {
name: string,
description: string | null,
venue: Venue,
talent: Talent
talent: Talent,
ticketIds: Array<string>
}

View File

@ -22,14 +22,6 @@ export class Endpoints {
return `${Endpoints.EVENT}?startDate=${startDate}&endDate=${endDate}`;
}
public static EVENT_SEASON_SEARCH(seasonId: string): string {
return `${Endpoints.EVENT}?seasonId=${seasonId}`;
}
public static EVENT_DELETE(eventId: string): string {
return `${Endpoints.EVENT}?eventId=${eventId}`;
}
/* Season Routes */
public static readonly SEASON: string = 'season';
@ -37,8 +29,4 @@ export class Endpoints {
public static SEASON_ADD_EVENT(eventId: string, seasonId: string): string {
return `${Endpoints.SEASON}?eventId=${eventId}&seasonId=${seasonId}`;
}
public static SEASON_DELETE(seasonId: string): string {
return `${Endpoints.SEASON}?seasonId=${seasonId}`;
}
}

View File

@ -1,7 +0,0 @@
export interface PatchSeason {
id: string,
name: string,
description: string | null,
startDate: Date,
endDate: Date
}

View File

@ -58,7 +58,7 @@ body {
}
.input {
padding: 7px;
padding: 5px;
backdrop-filter: blur(25px) saturate(112%);
-webkit-backdrop-filter: blur(25px) saturate(112%);
background-color: rgba(255, 255, 255, 0.11);
@ -99,7 +99,3 @@ body {
justify-content: space-between;
}
}
.invalid {
border: 2px solid rgba(255, 43, 43, 0.5);
}