Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4438bc5a93 | |||
| 30d573d1f8 |
@@ -0,0 +1,252 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using RacePlannerApi.Data;
|
||||||
|
using RacePlannerApi.DTOs;
|
||||||
|
using RacePlannerApi.Models;
|
||||||
|
|
||||||
|
namespace RacePlannerApi.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class AnnouncementsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly RacePlannerDbContext _context;
|
||||||
|
|
||||||
|
public AnnouncementsController(RacePlannerDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Roles = "Organizer")]
|
||||||
|
public async Task<ActionResult<AnnouncementDto>> CreateAnnouncement(CreateAnnouncementRequest request)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify event exists and user is organizer
|
||||||
|
var eventEntity = await _context.Events
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == request.EventId);
|
||||||
|
|
||||||
|
if (eventEntity == null)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = "Event not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventEntity.OrganizerId != userId.Value)
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
var announcement = new Announcement
|
||||||
|
{
|
||||||
|
EventId = request.EventId,
|
||||||
|
Title = request.Title,
|
||||||
|
Content = request.Content,
|
||||||
|
AuthorId = userId.Value,
|
||||||
|
IsPublished = true
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Announcements.Add(announcement);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Load related data
|
||||||
|
await _context.Entry(announcement)
|
||||||
|
.Reference(a => a.Event)
|
||||||
|
.LoadAsync();
|
||||||
|
await _context.Entry(announcement)
|
||||||
|
.Reference(a => a.Author)
|
||||||
|
.LoadAsync();
|
||||||
|
|
||||||
|
// TODO: Send notifications to registered participants (Task 7.7)
|
||||||
|
|
||||||
|
return CreatedAtAction(
|
||||||
|
nameof(GetAnnouncement),
|
||||||
|
new { id = announcement.Id },
|
||||||
|
MapToAnnouncementDto(announcement));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<ActionResult<AnnouncementDto>> GetAnnouncement(Guid id)
|
||||||
|
{
|
||||||
|
var announcement = await _context.Announcements
|
||||||
|
.Include(a => a.Event)
|
||||||
|
.Include(a => a.Author)
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == id);
|
||||||
|
|
||||||
|
if (announcement == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show published announcements to participants
|
||||||
|
if (!announcement.IsPublished)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null || announcement.Event.OrganizerId != userId.Value)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(MapToAnnouncementDto(announcement));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("event/{eventId}")]
|
||||||
|
public async Task<ActionResult<IEnumerable<AnnouncementDto>>> GetEventAnnouncements(Guid eventId)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
var isOrganizer = false;
|
||||||
|
|
||||||
|
if (userId != null)
|
||||||
|
{
|
||||||
|
isOrganizer = await _context.Events
|
||||||
|
.AnyAsync(e => e.Id == eventId && e.OrganizerId == userId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = _context.Announcements
|
||||||
|
.Include(a => a.Event)
|
||||||
|
.Include(a => a.Author)
|
||||||
|
.Where(a => a.EventId == eventId)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
// Only show published announcements to non-organizers
|
||||||
|
if (!isOrganizer)
|
||||||
|
{
|
||||||
|
query = query.Where(a => a.IsPublished);
|
||||||
|
}
|
||||||
|
|
||||||
|
var announcements = await query
|
||||||
|
.OrderByDescending(a => a.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(announcements.Select(MapToAnnouncementDto));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
[Authorize(Roles = "Organizer")]
|
||||||
|
public async Task<ActionResult<AnnouncementDto>> UpdateAnnouncement(Guid id, UpdateAnnouncementRequest request)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var announcement = await _context.Announcements
|
||||||
|
.Include(a => a.Event)
|
||||||
|
.Include(a => a.Author)
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == id);
|
||||||
|
|
||||||
|
if (announcement == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only organizer can update
|
||||||
|
if (announcement.Event.OrganizerId != userId.Value)
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields
|
||||||
|
if (request.Title != null) announcement.Title = request.Title;
|
||||||
|
if (request.Content != null) announcement.Content = request.Content;
|
||||||
|
if (request.IsPublished.HasValue) announcement.IsPublished = request.IsPublished.Value;
|
||||||
|
|
||||||
|
announcement.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(MapToAnnouncementDto(announcement));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
[Authorize(Roles = "Organizer")]
|
||||||
|
public async Task<IActionResult> DeleteAnnouncement(Guid id)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var announcement = await _context.Announcements
|
||||||
|
.Include(a => a.Event)
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == id);
|
||||||
|
|
||||||
|
if (announcement == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only organizer can delete
|
||||||
|
if (announcement.Event.OrganizerId != userId.Value)
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.Announcements.Remove(announcement);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("my-announcements")]
|
||||||
|
public async Task<ActionResult<IEnumerable<AnnouncementDto>>> GetMyAnnouncements()
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user registrations
|
||||||
|
var userRegistrationEventIds = await _context.Registrations
|
||||||
|
.Where(r => r.ParticipantId == userId.Value && r.Status != RegistrationStatus.Cancelled)
|
||||||
|
.Select(r => r.EventId)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Get announcements for those events
|
||||||
|
var announcements = await _context.Announcements
|
||||||
|
.Include(a => a.Event)
|
||||||
|
.Include(a => a.Author)
|
||||||
|
.Where(a => userRegistrationEventIds.Contains(a.EventId) && a.IsPublished)
|
||||||
|
.OrderByDescending(a => a.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(announcements.Select(MapToAnnouncementDto));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid? GetCurrentUserId()
|
||||||
|
{
|
||||||
|
var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (Guid.TryParse(userIdClaim, out var userId))
|
||||||
|
{
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnnouncementDto MapToAnnouncementDto(Announcement announcement)
|
||||||
|
{
|
||||||
|
return new AnnouncementDto
|
||||||
|
{
|
||||||
|
Id = announcement.Id,
|
||||||
|
EventId = announcement.EventId,
|
||||||
|
EventName = announcement.Event?.Name ?? string.Empty,
|
||||||
|
Title = announcement.Title,
|
||||||
|
Content = announcement.Content,
|
||||||
|
AuthorId = announcement.AuthorId,
|
||||||
|
AuthorName = announcement.Author?.Name ?? string.Empty,
|
||||||
|
CreatedAt = announcement.CreatedAt,
|
||||||
|
UpdatedAt = announcement.UpdatedAt,
|
||||||
|
IsPublished = announcement.IsPublished
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using RacePlannerApi.Data;
|
||||||
|
using RacePlannerApi.DTOs;
|
||||||
|
using RacePlannerApi.Models;
|
||||||
|
|
||||||
|
namespace RacePlannerApi.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class DashboardController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly RacePlannerDbContext _context;
|
||||||
|
|
||||||
|
public DashboardController(RacePlannerDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("organizer")]
|
||||||
|
[Authorize(Roles = "Organizer")]
|
||||||
|
public async Task<ActionResult<OrganizerDashboardDto>> GetOrganizerDashboard()
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var events = await _context.Events
|
||||||
|
.Include(e => e.Registrations)
|
||||||
|
.ThenInclude(r => r.Payments)
|
||||||
|
.Where(e => e.OrganizerId == userId.Value)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var totalEvents = events.Count;
|
||||||
|
var publishedEvents = events.Count(e => e.Status == EventStatus.Published);
|
||||||
|
var draftEvents = events.Count(e => e.Status == EventStatus.Draft);
|
||||||
|
|
||||||
|
var allRegistrations = events.SelectMany(e => e.Registrations).ToList();
|
||||||
|
var totalRegistrations = allRegistrations.Count;
|
||||||
|
var pendingRegistrations = allRegistrations.Count(r => r.Status == RegistrationStatus.Pending);
|
||||||
|
var confirmedRegistrations = allRegistrations.Count(r => r.Status == RegistrationStatus.Confirmed);
|
||||||
|
var cancelledRegistrations = allRegistrations.Count(r => r.Status == RegistrationStatus.Cancelled);
|
||||||
|
|
||||||
|
var totalRevenue = allRegistrations
|
||||||
|
.SelectMany(r => r.Payments)
|
||||||
|
.Sum(p => p.Amount);
|
||||||
|
|
||||||
|
var upcomingEvents = events
|
||||||
|
.Where(e => e.EventDate >= DateTime.UtcNow && e.EventDate <= DateTime.UtcNow.AddDays(30))
|
||||||
|
.OrderBy(e => e.EventDate)
|
||||||
|
.Take(5)
|
||||||
|
.Select(e => new EventSummaryDto
|
||||||
|
{
|
||||||
|
Id = e.Id,
|
||||||
|
Name = e.Name,
|
||||||
|
EventDate = e.EventDate,
|
||||||
|
Location = e.Location,
|
||||||
|
RegistrationCount = e.Registrations.Count(r => r.Status != RegistrationStatus.Cancelled),
|
||||||
|
MaxParticipants = e.MaxParticipants
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var eventsNearCapacity = events
|
||||||
|
.Where(e => e.MaxParticipants.HasValue && e.Status == EventStatus.Published)
|
||||||
|
.Select(e => new EventCapacityDto
|
||||||
|
{
|
||||||
|
Id = e.Id,
|
||||||
|
Name = e.Name,
|
||||||
|
EventDate = e.EventDate,
|
||||||
|
RegistrationCount = e.Registrations.Count(r => r.Status != RegistrationStatus.Cancelled),
|
||||||
|
MaxParticipants = e.MaxParticipants
|
||||||
|
})
|
||||||
|
.Where(e => e.CapacityPercentage >= 80)
|
||||||
|
.OrderByDescending(e => e.CapacityPercentage)
|
||||||
|
.Take(5)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var dashboard = new OrganizerDashboardDto
|
||||||
|
{
|
||||||
|
TotalEvents = totalEvents,
|
||||||
|
PublishedEvents = publishedEvents,
|
||||||
|
DraftEvents = draftEvents,
|
||||||
|
TotalRegistrations = totalRegistrations,
|
||||||
|
PendingRegistrations = pendingRegistrations,
|
||||||
|
ConfirmedRegistrations = confirmedRegistrations,
|
||||||
|
CancelledRegistrations = cancelledRegistrations,
|
||||||
|
TotalRevenue = totalRevenue,
|
||||||
|
UpcomingEvents = upcomingEvents,
|
||||||
|
EventsNearCapacity = eventsNearCapacity
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(dashboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("participant")]
|
||||||
|
public async Task<ActionResult<ParticipantDashboardDto>> GetParticipantDashboard()
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var registrations = await _context.Registrations
|
||||||
|
.Include(r => r.Event)
|
||||||
|
.Where(r => r.ParticipantId == userId.Value)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var totalRegistrations = registrations.Count;
|
||||||
|
var upcomingEvents = registrations.Count(r => r.Event.EventDate >= DateTime.UtcNow && r.Status != RegistrationStatus.Cancelled);
|
||||||
|
var completedEvents = registrations.Count(r => r.Event.EventDate < DateTime.UtcNow && r.Status == RegistrationStatus.Completed);
|
||||||
|
var cancelledRegistrations = registrations.Count(r => r.Status == RegistrationStatus.Cancelled);
|
||||||
|
|
||||||
|
var registrationSummaries = registrations
|
||||||
|
.OrderByDescending(r => r.Event.EventDate)
|
||||||
|
.Take(10)
|
||||||
|
.Select(r => new RegistrationSummaryDto
|
||||||
|
{
|
||||||
|
Id = r.Id,
|
||||||
|
EventId = r.EventId,
|
||||||
|
EventName = r.Event.Name,
|
||||||
|
EventDate = r.Event.EventDate,
|
||||||
|
Status = r.Status.ToString(),
|
||||||
|
RegisteredAt = r.CreatedAt
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var upcomingEventList = registrations
|
||||||
|
.Where(r => r.Event.EventDate >= DateTime.UtcNow && r.Status != RegistrationStatus.Cancelled)
|
||||||
|
.OrderBy(r => r.Event.EventDate)
|
||||||
|
.Take(5)
|
||||||
|
.Select(r => new UpcomingEventDto
|
||||||
|
{
|
||||||
|
Id = r.EventId,
|
||||||
|
Name = r.Event.Name,
|
||||||
|
EventDate = r.Event.EventDate,
|
||||||
|
Location = r.Event.Location
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var dashboard = new ParticipantDashboardDto
|
||||||
|
{
|
||||||
|
TotalRegistrations = totalRegistrations,
|
||||||
|
UpcomingEvents = upcomingEvents,
|
||||||
|
CompletedEvents = completedEvents,
|
||||||
|
CancelledRegistrations = cancelledRegistrations,
|
||||||
|
MyRegistrations = registrationSummaries,
|
||||||
|
UpcomingEventList = upcomingEventList
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(dashboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid? GetCurrentUserId()
|
||||||
|
{
|
||||||
|
var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (Guid.TryParse(userIdClaim, out var userId))
|
||||||
|
{
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using RacePlannerApi.Data;
|
||||||
|
using RacePlannerApi.DTOs;
|
||||||
|
using RacePlannerApi.Models;
|
||||||
|
|
||||||
|
namespace RacePlannerApi.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class EventsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly RacePlannerDbContext _context;
|
||||||
|
|
||||||
|
public EventsController(RacePlannerDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Roles = "Organizer")]
|
||||||
|
public async Task<ActionResult<EventDto>> CreateEvent(CreateEventRequest request)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventEntity = new Event
|
||||||
|
{
|
||||||
|
Name = request.Name,
|
||||||
|
Description = request.Description,
|
||||||
|
EventDate = request.EventDate,
|
||||||
|
Location = request.Location,
|
||||||
|
Category = request.Category,
|
||||||
|
Tags = request.Tags ?? new List<string>(),
|
||||||
|
MaxParticipants = request.MaxParticipants,
|
||||||
|
OrganizerId = userId.Value,
|
||||||
|
Status = EventStatus.Draft
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Events.Add(eventEntity);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Load organizer for response
|
||||||
|
await _context.Entry(eventEntity).Reference(e => e.Organizer).LoadAsync();
|
||||||
|
|
||||||
|
return CreatedAtAction(
|
||||||
|
nameof(GetEvent),
|
||||||
|
new { id = eventEntity.Id },
|
||||||
|
MapToEventDto(eventEntity));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<ActionResult<IEnumerable<EventDto>>> GetEvents(
|
||||||
|
[FromQuery] EventFilterRequest? filter = null)
|
||||||
|
{
|
||||||
|
var query = _context.Events
|
||||||
|
.Include(e => e.Organizer)
|
||||||
|
.Include(e => e.Registrations)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (filter?.Category != null)
|
||||||
|
{
|
||||||
|
query = query.Where(e => e.Category == filter.Category);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter?.Tags != null && filter.Tags.Any())
|
||||||
|
{
|
||||||
|
query = query.Where(e => e.Tags.Any(t => filter.Tags.Contains(t)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter?.FromDate != null)
|
||||||
|
{
|
||||||
|
query = query.Where(e => e.EventDate >= filter.FromDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter?.ToDate != null)
|
||||||
|
{
|
||||||
|
query = query.Where(e => e.EventDate <= filter.ToDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter?.Status != null && Enum.TryParse<EventStatus>(filter.Status, out var status))
|
||||||
|
{
|
||||||
|
query = query.Where(e => e.Status == status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter?.OrganizerId != null)
|
||||||
|
{
|
||||||
|
query = query.Where(e => e.OrganizerId == filter.OrganizerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to showing published events only for anonymous users
|
||||||
|
if (!User.Identity?.IsAuthenticated ?? true)
|
||||||
|
{
|
||||||
|
query = query.Where(e => e.Status == EventStatus.Published);
|
||||||
|
}
|
||||||
|
|
||||||
|
var events = await query
|
||||||
|
.OrderBy(e => e.EventDate)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(events.Select(MapToEventDto));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<ActionResult<EventDto>> GetEvent(Guid id)
|
||||||
|
{
|
||||||
|
var eventEntity = await _context.Events
|
||||||
|
.Include(e => e.Organizer)
|
||||||
|
.Include(e => e.Registrations)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == id);
|
||||||
|
|
||||||
|
if (eventEntity == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draft events only visible to organizers
|
||||||
|
if (eventEntity.Status == EventStatus.Draft)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null || eventEntity.OrganizerId != userId)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(MapToEventDto(eventEntity));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
[Authorize(Roles = "Organizer")]
|
||||||
|
public async Task<ActionResult<EventDto>> UpdateEvent(Guid id, UpdateEventRequest request)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventEntity = await _context.Events
|
||||||
|
.Include(e => e.Organizer)
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == id);
|
||||||
|
|
||||||
|
if (eventEntity == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only organizer can update their event
|
||||||
|
if (eventEntity.OrganizerId != userId)
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields
|
||||||
|
if (request.Name != null) eventEntity.Name = request.Name;
|
||||||
|
if (request.Description != null) eventEntity.Description = request.Description;
|
||||||
|
if (request.EventDate != null) eventEntity.EventDate = request.EventDate.Value;
|
||||||
|
if (request.Location != null) eventEntity.Location = request.Location;
|
||||||
|
if (request.Status != null) eventEntity.Status = request.Status.Value;
|
||||||
|
if (request.Category != null) eventEntity.Category = request.Category;
|
||||||
|
if (request.Tags != null) eventEntity.Tags = request.Tags;
|
||||||
|
if (request.MaxParticipants != null) eventEntity.MaxParticipants = request.MaxParticipants;
|
||||||
|
|
||||||
|
eventEntity.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(MapToEventDto(eventEntity));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
[Authorize(Roles = "Organizer")]
|
||||||
|
public async Task<IActionResult> DeleteEvent(Guid id)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventEntity = await _context.Events.FindAsync(id);
|
||||||
|
|
||||||
|
if (eventEntity == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventEntity.OrganizerId != userId)
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
_context.Events.Remove(eventEntity);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid? GetCurrentUserId()
|
||||||
|
{
|
||||||
|
var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (Guid.TryParse(userIdClaim, out var userId))
|
||||||
|
{
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EventDto MapToEventDto(Event eventEntity)
|
||||||
|
{
|
||||||
|
return new EventDto
|
||||||
|
{
|
||||||
|
Id = eventEntity.Id,
|
||||||
|
Name = eventEntity.Name,
|
||||||
|
Description = eventEntity.Description,
|
||||||
|
EventDate = eventEntity.EventDate,
|
||||||
|
Location = eventEntity.Location,
|
||||||
|
Status = eventEntity.Status.ToString(),
|
||||||
|
Category = eventEntity.Category,
|
||||||
|
Tags = eventEntity.Tags,
|
||||||
|
MaxParticipants = eventEntity.MaxParticipants,
|
||||||
|
CurrentRegistrations = eventEntity.Registrations?.Count ?? 0,
|
||||||
|
CreatedAt = eventEntity.CreatedAt,
|
||||||
|
UpdatedAt = eventEntity.UpdatedAt,
|
||||||
|
Organizer = new UserSummaryDto
|
||||||
|
{
|
||||||
|
Id = eventEntity.Organizer.Id,
|
||||||
|
Name = eventEntity.Organizer.Name,
|
||||||
|
Email = eventEntity.Organizer.Email
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using RacePlannerApi.Data;
|
||||||
|
using RacePlannerApi.DTOs;
|
||||||
|
using RacePlannerApi.Models;
|
||||||
|
|
||||||
|
namespace RacePlannerApi.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class PaymentsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly RacePlannerDbContext _context;
|
||||||
|
|
||||||
|
public PaymentsController(RacePlannerDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Roles = "Organizer")]
|
||||||
|
public async Task<ActionResult<PaymentDto>> RecordPayment(CreatePaymentRequest request)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load registration with event
|
||||||
|
var registration = await _context.Registrations
|
||||||
|
.Include(r => r.Event)
|
||||||
|
.FirstOrDefaultAsync(r => r.Id == request.RegistrationId);
|
||||||
|
|
||||||
|
if (registration == null)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = "Registration not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user is organizer of the event
|
||||||
|
if (registration.Event.OrganizerId != userId.Value)
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check registration is not cancelled
|
||||||
|
if (registration.Status == RegistrationStatus.Cancelled)
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = "Cannot record payment for cancelled registration" });
|
||||||
|
}
|
||||||
|
|
||||||
|
var payment = new Payment
|
||||||
|
{
|
||||||
|
RegistrationId = request.RegistrationId,
|
||||||
|
Amount = request.Amount,
|
||||||
|
Method = request.Method,
|
||||||
|
TransactionId = request.TransactionId,
|
||||||
|
Notes = request.Notes,
|
||||||
|
PaymentDate = request.PaymentDate ?? DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Payments.Add(payment);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Update registration status if fully paid (assuming a fixed fee amount would be known)
|
||||||
|
// For now, we'll just record the payment
|
||||||
|
|
||||||
|
return CreatedAtAction(
|
||||||
|
nameof(GetPayment),
|
||||||
|
new { id = payment.Id },
|
||||||
|
MapToPaymentDto(payment));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
[Authorize(Roles = "Organizer")]
|
||||||
|
public async Task<ActionResult<PaymentDto>> GetPayment(Guid id)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var payment = await _context.Payments
|
||||||
|
.Include(p => p.Registration)
|
||||||
|
.ThenInclude(r => r.Event)
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == id);
|
||||||
|
|
||||||
|
if (payment == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user is organizer
|
||||||
|
if (payment.Registration.Event.OrganizerId != userId.Value)
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(MapToPaymentDto(payment));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("registration/{registrationId}")]
|
||||||
|
[Authorize(Roles = "Organizer")]
|
||||||
|
public async Task<ActionResult<IEnumerable<PaymentDto>>> GetRegistrationPayments(Guid registrationId)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify organizer has access to this registration
|
||||||
|
var registration = await _context.Registrations
|
||||||
|
.Include(r => r.Event)
|
||||||
|
.FirstOrDefaultAsync(r => r.Id == registrationId);
|
||||||
|
|
||||||
|
if (registration == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registration.Event.OrganizerId != userId.Value)
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
var payments = await _context.Payments
|
||||||
|
.Where(p => p.RegistrationId == registrationId)
|
||||||
|
.OrderByDescending(p => p.PaymentDate)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(payments.Select(MapToPaymentDto));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("registration/{registrationId}/status")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<object>> GetPaymentStatus(Guid registrationId)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var registration = await _context.Registrations
|
||||||
|
.Include(r => r.Event)
|
||||||
|
.Include(r => r.Payments)
|
||||||
|
.FirstOrDefaultAsync(r => r.Id == registrationId);
|
||||||
|
|
||||||
|
if (registration == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Participant can view their own, organizer can view any
|
||||||
|
var isOrganizer = registration.Event.OrganizerId == userId.Value;
|
||||||
|
|
||||||
|
if (registration.ParticipantId != userId.Value && !isOrganizer)
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalPaid = registration.Payments.Sum(p => p.Amount);
|
||||||
|
var status = totalPaid > 0 ? "paid" : "unpaid";
|
||||||
|
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
RegistrationId = registrationId,
|
||||||
|
TotalPaid = totalPaid,
|
||||||
|
PaymentCount = registration.Payments.Count,
|
||||||
|
Status = status,
|
||||||
|
Payments = registration.Payments.Select(MapToPaymentDto)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("event/{eventId}/report")]
|
||||||
|
[Authorize(Roles = "Organizer")]
|
||||||
|
public async Task<ActionResult<PaymentReportDto>> GetPaymentReport(Guid eventId)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventEntity = await _context.Events
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == eventId && e.OrganizerId == userId.Value);
|
||||||
|
|
||||||
|
if (eventEntity == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var registrations = await _context.Registrations
|
||||||
|
.Include(r => r.Participant)
|
||||||
|
.Include(r => r.Payments)
|
||||||
|
.Where(r => r.EventId == eventId && r.Status != RegistrationStatus.Cancelled)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var totalCollected = registrations.Sum(r => r.Payments.Sum(p => p.Amount));
|
||||||
|
var totalRegistrations = registrations.Count;
|
||||||
|
var paidRegistrations = registrations.Count(r => r.Payments.Any());
|
||||||
|
var unpaidRegistrations = registrations.Count(r => !r.Payments.Any());
|
||||||
|
|
||||||
|
var report = new PaymentReportDto
|
||||||
|
{
|
||||||
|
EventId = eventId,
|
||||||
|
EventName = eventEntity.Name,
|
||||||
|
TotalCollected = totalCollected,
|
||||||
|
TotalPending = 0, // Would require event fee structure
|
||||||
|
TotalOutstanding = 0, // Would require event fee structure
|
||||||
|
TotalRegistrations = totalRegistrations,
|
||||||
|
PaidRegistrations = paidRegistrations,
|
||||||
|
PartialRegistrations = 0, // Would require partial payment logic
|
||||||
|
UnpaidRegistrations = unpaidRegistrations,
|
||||||
|
Registrations = registrations.Select(r => new RegistrationPaymentDto
|
||||||
|
{
|
||||||
|
RegistrationId = r.Id,
|
||||||
|
ParticipantName = r.Participant.Name,
|
||||||
|
Status = r.Status.ToString(),
|
||||||
|
TotalPaid = r.Payments.Sum(p => p.Amount),
|
||||||
|
AmountDue = 0, // Would require event fee structure
|
||||||
|
PaymentStatus = r.Payments.Any() ? "paid" : "unpaid",
|
||||||
|
Payments = r.Payments.Select(MapToPaymentDto).ToList()
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid? GetCurrentUserId()
|
||||||
|
{
|
||||||
|
var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (Guid.TryParse(userIdClaim, out var userId))
|
||||||
|
{
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PaymentDto MapToPaymentDto(Payment payment)
|
||||||
|
{
|
||||||
|
return new PaymentDto
|
||||||
|
{
|
||||||
|
Id = payment.Id,
|
||||||
|
RegistrationId = payment.RegistrationId,
|
||||||
|
Amount = payment.Amount,
|
||||||
|
Method = payment.Method.ToString(),
|
||||||
|
TransactionId = payment.TransactionId,
|
||||||
|
Notes = payment.Notes,
|
||||||
|
PaymentDate = payment.PaymentDate,
|
||||||
|
CreatedAt = payment.CreatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using RacePlannerApi.Data;
|
||||||
|
using RacePlannerApi.DTOs;
|
||||||
|
using RacePlannerApi.Models;
|
||||||
|
|
||||||
|
namespace RacePlannerApi.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
[Authorize]
|
||||||
|
public class RegistrationsController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly RacePlannerDbContext _context;
|
||||||
|
|
||||||
|
public RegistrationsController(RacePlannerDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<ActionResult<RegistrationDto>> CreateRegistration(CreateRegistrationRequest request)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if event exists and is published
|
||||||
|
var eventEntity = await _context.Events
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == request.EventId);
|
||||||
|
|
||||||
|
if (eventEntity == null)
|
||||||
|
{
|
||||||
|
return NotFound(new { error = "Event not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventEntity.Status != EventStatus.Published)
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = "Event is not open for registration" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already registered
|
||||||
|
var existingRegistration = await _context.Registrations
|
||||||
|
.FirstOrDefaultAsync(r => r.EventId == request.EventId && r.ParticipantId == userId.Value);
|
||||||
|
|
||||||
|
if (existingRegistration != null)
|
||||||
|
{
|
||||||
|
return Conflict(new { error = "Already registered for this event" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if event is full
|
||||||
|
if (eventEntity.MaxParticipants.HasValue)
|
||||||
|
{
|
||||||
|
var registrationCount = await _context.Registrations
|
||||||
|
.CountAsync(r => r.EventId == request.EventId && r.Status != RegistrationStatus.Cancelled);
|
||||||
|
|
||||||
|
if (registrationCount >= eventEntity.MaxParticipants.Value)
|
||||||
|
{
|
||||||
|
return BadRequest(new { error = "Event is full" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var registration = new Registration
|
||||||
|
{
|
||||||
|
EventId = request.EventId,
|
||||||
|
ParticipantId = userId.Value,
|
||||||
|
Status = RegistrationStatus.Pending,
|
||||||
|
Category = request.Category,
|
||||||
|
EmergencyContact = request.EmergencyContact
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.Registrations.Add(registration);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Load related data
|
||||||
|
await _context.Entry(registration)
|
||||||
|
.Reference(r => r.Event)
|
||||||
|
.LoadAsync();
|
||||||
|
await _context.Entry(registration)
|
||||||
|
.Reference(r => r.Participant)
|
||||||
|
.LoadAsync();
|
||||||
|
await _context.Entry(registration)
|
||||||
|
.Collection(r => r.Payments)
|
||||||
|
.LoadAsync();
|
||||||
|
|
||||||
|
return CreatedAtAction(
|
||||||
|
nameof(GetRegistration),
|
||||||
|
new { id = registration.Id },
|
||||||
|
MapToRegistrationDto(registration));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<ActionResult<RegistrationDto>> GetRegistration(Guid id)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var registration = await _context.Registrations
|
||||||
|
.Include(r => r.Event)
|
||||||
|
.Include(r => r.Participant)
|
||||||
|
.Include(r => r.Payments)
|
||||||
|
.FirstOrDefaultAsync(r => r.Id == id);
|
||||||
|
|
||||||
|
if (registration == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only participant or event organizer can view
|
||||||
|
var isOrganizer = await _context.Events
|
||||||
|
.AnyAsync(e => e.Id == registration.EventId && e.OrganizerId == userId.Value);
|
||||||
|
|
||||||
|
if (registration.ParticipantId != userId.Value && !isOrganizer)
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(MapToRegistrationDto(registration));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("my-registrations")]
|
||||||
|
public async Task<ActionResult<IEnumerable<RegistrationDto>>> GetMyRegistrations()
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var registrations = await _context.Registrations
|
||||||
|
.Include(r => r.Event)
|
||||||
|
.Include(r => r.Participant)
|
||||||
|
.Include(r => r.Payments)
|
||||||
|
.Where(r => r.ParticipantId == userId.Value)
|
||||||
|
.OrderBy(r => r.Event.EventDate)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(registrations.Select(MapToRegistrationDto));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("event/{eventId}")]
|
||||||
|
[Authorize(Roles = "Organizer")]
|
||||||
|
public async Task<ActionResult<IEnumerable<RegistrationDto>>> GetEventRegistrations(Guid eventId)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user is organizer of this event
|
||||||
|
var eventEntity = await _context.Events
|
||||||
|
.FirstOrDefaultAsync(e => e.Id == eventId && e.OrganizerId == userId.Value);
|
||||||
|
|
||||||
|
if (eventEntity == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var registrations = await _context.Registrations
|
||||||
|
.Include(r => r.Event)
|
||||||
|
.Include(r => r.Participant)
|
||||||
|
.Include(r => r.Payments)
|
||||||
|
.Where(r => r.EventId == eventId)
|
||||||
|
.OrderBy(r => r.CreatedAt)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Ok(registrations.Select(MapToRegistrationDto));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
public async Task<ActionResult<RegistrationDto>> UpdateRegistration(Guid id, UpdateRegistrationRequest request)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var registration = await _context.Registrations
|
||||||
|
.Include(r => r.Event)
|
||||||
|
.FirstOrDefaultAsync(r => r.Id == id);
|
||||||
|
|
||||||
|
if (registration == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Participants can only update their own registrations
|
||||||
|
// Organizers can update any registration for their events
|
||||||
|
var isOrganizer = registration.Event.OrganizerId == userId.Value;
|
||||||
|
|
||||||
|
if (registration.ParticipantId != userId.Value && !isOrganizer)
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update fields
|
||||||
|
if (request.Status.HasValue && isOrganizer)
|
||||||
|
{
|
||||||
|
registration.Status = request.Status.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Category != null)
|
||||||
|
{
|
||||||
|
registration.Category = request.Category;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.EmergencyContact != null)
|
||||||
|
{
|
||||||
|
registration.EmergencyContact = request.EmergencyContact;
|
||||||
|
}
|
||||||
|
|
||||||
|
registration.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Reload related data
|
||||||
|
await _context.Entry(registration)
|
||||||
|
.Reference(r => r.Participant)
|
||||||
|
.LoadAsync();
|
||||||
|
await _context.Entry(registration)
|
||||||
|
.Collection(r => r.Payments)
|
||||||
|
.LoadAsync();
|
||||||
|
|
||||||
|
return Ok(MapToRegistrationDto(registration));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("{id}/cancel")]
|
||||||
|
public async Task<ActionResult<RegistrationDto>> CancelRegistration(Guid id)
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
return Unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
var registration = await _context.Registrations
|
||||||
|
.Include(r => r.Event)
|
||||||
|
.Include(r => r.Participant)
|
||||||
|
.Include(r => r.Payments)
|
||||||
|
.FirstOrDefaultAsync(r => r.Id == id);
|
||||||
|
|
||||||
|
if (registration == null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only participant or event organizer can cancel
|
||||||
|
var isOrganizer = registration.Event.OrganizerId == userId.Value;
|
||||||
|
|
||||||
|
if (registration.ParticipantId != userId.Value && !isOrganizer)
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
registration.Status = RegistrationStatus.Cancelled;
|
||||||
|
registration.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(MapToRegistrationDto(registration));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid? GetCurrentUserId()
|
||||||
|
{
|
||||||
|
var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
if (Guid.TryParse(userIdClaim, out var userId))
|
||||||
|
{
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RegistrationDto MapToRegistrationDto(Registration registration)
|
||||||
|
{
|
||||||
|
var totalPaid = registration.Payments?.Sum(p => p.Amount) ?? 0;
|
||||||
|
|
||||||
|
return new RegistrationDto
|
||||||
|
{
|
||||||
|
Id = registration.Id,
|
||||||
|
EventId = registration.EventId,
|
||||||
|
EventName = registration.Event?.Name ?? string.Empty,
|
||||||
|
EventDate = registration.Event?.EventDate ?? DateTime.MinValue,
|
||||||
|
ParticipantId = registration.ParticipantId,
|
||||||
|
ParticipantName = registration.Participant?.Name ?? string.Empty,
|
||||||
|
ParticipantEmail = registration.Participant?.Email ?? string.Empty,
|
||||||
|
Status = registration.Status.ToString(),
|
||||||
|
Category = registration.Category,
|
||||||
|
EmergencyContact = registration.EmergencyContact,
|
||||||
|
CreatedAt = registration.CreatedAt,
|
||||||
|
UpdatedAt = registration.UpdatedAt,
|
||||||
|
TotalPaid = totalPaid,
|
||||||
|
AmountDue = 0 // Will be calculated based on event fee if needed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace RacePlannerApi.DTOs;
|
||||||
|
|
||||||
|
public class CreateAnnouncementRequest
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public Guid EventId { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(5000)]
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateAnnouncementRequest
|
||||||
|
{
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string? Title { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(5000)]
|
||||||
|
public string? Content { get; set; }
|
||||||
|
|
||||||
|
public bool? IsPublished { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AnnouncementDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid EventId { get; set; }
|
||||||
|
public string EventName { get; set; } = string.Empty;
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Content { get; set; } = string.Empty;
|
||||||
|
public Guid AuthorId { get; set; }
|
||||||
|
public string AuthorName { get; set; } = string.Empty;
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
public bool IsPublished { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
namespace RacePlannerApi.DTOs;
|
||||||
|
|
||||||
|
public class OrganizerDashboardDto
|
||||||
|
{
|
||||||
|
public int TotalEvents { get; set; }
|
||||||
|
public int PublishedEvents { get; set; }
|
||||||
|
public int DraftEvents { get; set; }
|
||||||
|
public int TotalRegistrations { get; set; }
|
||||||
|
public int PendingRegistrations { get; set; }
|
||||||
|
public int ConfirmedRegistrations { get; set; }
|
||||||
|
public int CancelledRegistrations { get; set; }
|
||||||
|
public decimal TotalRevenue { get; set; }
|
||||||
|
public List<EventSummaryDto> UpcomingEvents { get; set; } = new();
|
||||||
|
public List<EventCapacityDto> EventsNearCapacity { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EventSummaryDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public DateTime EventDate { get; set; }
|
||||||
|
public string Location { get; set; } = string.Empty;
|
||||||
|
public int RegistrationCount { get; set; }
|
||||||
|
public int? MaxParticipants { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EventCapacityDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public DateTime EventDate { get; set; }
|
||||||
|
public int RegistrationCount { get; set; }
|
||||||
|
public int? MaxParticipants { get; set; }
|
||||||
|
public double CapacityPercentage => MaxParticipants.HasValue && MaxParticipants.Value > 0
|
||||||
|
? (double)RegistrationCount / MaxParticipants.Value * 100
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ParticipantDashboardDto
|
||||||
|
{
|
||||||
|
public int TotalRegistrations { get; set; }
|
||||||
|
public int UpcomingEvents { get; set; }
|
||||||
|
public int CompletedEvents { get; set; }
|
||||||
|
public int CancelledRegistrations { get; set; }
|
||||||
|
public List<RegistrationSummaryDto> MyRegistrations { get; set; } = new();
|
||||||
|
public List<UpcomingEventDto> UpcomingEventList { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RegistrationSummaryDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid EventId { get; set; }
|
||||||
|
public string EventName { get; set; } = string.Empty;
|
||||||
|
public DateTime EventDate { get; set; }
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public DateTime RegisteredAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpcomingEventDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public DateTime EventDate { get; set; }
|
||||||
|
public string Location { get; set; } = string.Empty;
|
||||||
|
public int DaysUntil => (EventDate - DateTime.UtcNow).Days;
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using RacePlannerApi.Models;
|
||||||
|
|
||||||
|
namespace RacePlannerApi.DTOs;
|
||||||
|
|
||||||
|
public class CreateEventRequest
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[MaxLength(2000)]
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public DateTime EventDate { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string Location { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string? Category { get; set; }
|
||||||
|
|
||||||
|
public List<string> Tags { get; set; } = new();
|
||||||
|
|
||||||
|
public int? MaxParticipants { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateEventRequest
|
||||||
|
{
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(2000)]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
public DateTime? EventDate { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string? Location { get; set; }
|
||||||
|
|
||||||
|
public EventStatus? Status { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string? Category { get; set; }
|
||||||
|
|
||||||
|
public List<string>? Tags { get; set; }
|
||||||
|
|
||||||
|
public int? MaxParticipants { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EventDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public DateTime EventDate { get; set; }
|
||||||
|
public string Location { get; set; } = string.Empty;
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public string? Category { get; set; }
|
||||||
|
public List<string> Tags { get; set; } = new();
|
||||||
|
public int? MaxParticipants { get; set; }
|
||||||
|
public int CurrentRegistrations { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public UserSummaryDto Organizer { get; set; } = null!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EventFilterRequest
|
||||||
|
{
|
||||||
|
public string? Category { get; set; }
|
||||||
|
public List<string>? Tags { get; set; }
|
||||||
|
public DateTime? FromDate { get; set; }
|
||||||
|
public DateTime? ToDate { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public Guid? OrganizerId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserSummaryDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using RacePlannerApi.Models;
|
||||||
|
|
||||||
|
namespace RacePlannerApi.DTOs;
|
||||||
|
|
||||||
|
public class CreatePaymentRequest
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public Guid RegistrationId { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[Range(0.01, double.MaxValue)]
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public PaymentMethod Method { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string? TransactionId { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(500)]
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
|
||||||
|
public DateTime? PaymentDate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PaymentDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid RegistrationId { get; set; }
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
public string Method { get; set; } = string.Empty;
|
||||||
|
public string? TransactionId { get; set; }
|
||||||
|
public string? Notes { get; set; }
|
||||||
|
public DateTime PaymentDate { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PaymentReportDto
|
||||||
|
{
|
||||||
|
public Guid EventId { get; set; }
|
||||||
|
public string EventName { get; set; } = string.Empty;
|
||||||
|
public decimal TotalCollected { get; set; }
|
||||||
|
public decimal TotalPending { get; set; }
|
||||||
|
public decimal TotalOutstanding { get; set; }
|
||||||
|
public int TotalRegistrations { get; set; }
|
||||||
|
public int PaidRegistrations { get; set; }
|
||||||
|
public int PartialRegistrations { get; set; }
|
||||||
|
public int UnpaidRegistrations { get; set; }
|
||||||
|
public List<RegistrationPaymentDto> Registrations { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RegistrationPaymentDto
|
||||||
|
{
|
||||||
|
public Guid RegistrationId { get; set; }
|
||||||
|
public string ParticipantName { get; set; } = string.Empty;
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public decimal TotalPaid { get; set; }
|
||||||
|
public decimal AmountDue { get; set; }
|
||||||
|
public string PaymentStatus { get; set; } = string.Empty;
|
||||||
|
public List<PaymentDto> Payments { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using RacePlannerApi.Models;
|
||||||
|
|
||||||
|
namespace RacePlannerApi.DTOs;
|
||||||
|
|
||||||
|
public class CreateRegistrationRequest
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public Guid EventId { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string? Category { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string? EmergencyContact { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateRegistrationRequest
|
||||||
|
{
|
||||||
|
public RegistrationStatus? Status { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(100)]
|
||||||
|
public string? Category { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string? EmergencyContact { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RegistrationDto
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid EventId { get; set; }
|
||||||
|
public string EventName { get; set; } = string.Empty;
|
||||||
|
public DateTime EventDate { get; set; }
|
||||||
|
public Guid ParticipantId { get; set; }
|
||||||
|
public string ParticipantName { get; set; } = string.Empty;
|
||||||
|
public string ParticipantEmail { get; set; } = string.Empty;
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
public string? Category { get; set; }
|
||||||
|
public string? EmergencyContact { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
public decimal TotalPaid { get; set; }
|
||||||
|
public decimal AmountDue { get; set; }
|
||||||
|
}
|
||||||
@@ -18,20 +18,20 @@
|
|||||||
|
|
||||||
## 3. User Authentication (user-auth)
|
## 3. User Authentication (user-auth)
|
||||||
|
|
||||||
- [ ] 3.1 Implement user registration endpoint
|
- [x] 3.1 Implement user registration endpoint
|
||||||
- [ ] 3.2 Implement user login endpoint with JWT
|
- [x] 3.2 Implement user login endpoint with JWT
|
||||||
- [ ] 3.3 Create authentication middleware
|
- [x] 3.3 Create authentication middleware
|
||||||
- [ ] 3.4 Implement role-based access control middleware
|
- [x] 3.4 Implement role-based access control middleware
|
||||||
- [ ] 3.5 Create registration form component
|
- [ ] 3.5 Create registration form component
|
||||||
- [ ] 3.6 Create login form component
|
- [ ] 3.6 Create login form component
|
||||||
- [ ] 3.7 Implement logout functionality
|
- [ ] 3.7 Implement logout functionality
|
||||||
|
|
||||||
## 4. Event Management (event-management)
|
## 4. Event Management (event-management)
|
||||||
|
|
||||||
- [ ] 4.1 Implement create event endpoint
|
- [x] 4.1 Implement create event endpoint
|
||||||
- [ ] 4.2 Implement update event endpoint
|
- [x] 4.2 Implement update event endpoint
|
||||||
- [ ] 4.3 Implement list events endpoint with filtering
|
- [x] 4.3 Implement list events endpoint with filtering
|
||||||
- [ ] 4.4 Implement get event details endpoint
|
- [x] 4.4 Implement get event details endpoint
|
||||||
- [ ] 4.5 Create event creation form component
|
- [ ] 4.5 Create event creation form component
|
||||||
- [ ] 4.6 Create event editing form component
|
- [ ] 4.6 Create event editing form component
|
||||||
- [ ] 4.7 Create event list view with filters
|
- [ ] 4.7 Create event list view with filters
|
||||||
@@ -39,36 +39,36 @@
|
|||||||
|
|
||||||
## 5. Registration System (registration-system)
|
## 5. Registration System (registration-system)
|
||||||
|
|
||||||
- [ ] 5.1 Implement registration endpoint
|
- [x] 5.1 Implement registration endpoint
|
||||||
- [ ] 5.2 Implement registration status update endpoint
|
- [x] 5.2 Implement registration status update endpoint
|
||||||
- [ ] 5.3 Implement cancel registration endpoint
|
- [x] 5.3 Implement cancel registration endpoint
|
||||||
- [ ] 5.4 Create registration form component
|
- [ ] 5.4 Create registration form component
|
||||||
- [ ] 5.5 Create my registrations list component
|
- [ ] 5.5 Create my registrations list component
|
||||||
- [ ] 5.6 Implement registration status display
|
- [ ] 5.6 Implement registration status display
|
||||||
|
|
||||||
## 6. Payment Tracking (payment-tracking)
|
## 6. Payment Tracking (payment-tracking)
|
||||||
|
|
||||||
- [ ] 6.1 Implement record payment endpoint
|
- [x] 6.1 Implement record payment endpoint
|
||||||
- [ ] 6.2 Implement payment status endpoint
|
- [x] 6.2 Implement payment status endpoint
|
||||||
- [ ] 6.3 Implement payment report endpoint
|
- [x] 6.3 Implement payment report endpoint
|
||||||
- [ ] 6.4 Create payment recording form component
|
- [ ] 6.4 Create payment recording form component
|
||||||
- [ ] 6.5 Create payment status display component
|
- [ ] 6.5 Create payment status display component
|
||||||
- [ ] 6.6 Create payment report view
|
- [ ] 6.6 Create payment report view
|
||||||
|
|
||||||
## 7. Announcements (announcements)
|
## 7. Announcements (announcements)
|
||||||
|
|
||||||
- [ ] 7.1 Implement create announcement endpoint
|
- [x] 7.1 Implement create announcement endpoint
|
||||||
- [ ] 7.2 Implement edit announcement endpoint
|
- [x] 7.2 Implement edit announcement endpoint
|
||||||
- [ ] 7.3 Implement delete announcement endpoint
|
- [x] 7.3 Implement delete announcement endpoint
|
||||||
- [ ] 7.4 Implement list announcements endpoint
|
- [x] 7.4 Implement list announcements endpoint
|
||||||
- [ ] 7.5 Create announcement creation form
|
- [ ] 7.5 Create announcement creation form
|
||||||
- [ ] 7.6 Create announcement list component
|
- [ ] 7.6 Create announcement list component
|
||||||
- [ ] 7.7 Implement notification system
|
- [ ] 7.7 Implement notification system
|
||||||
|
|
||||||
## 8. Dashboard (dashboard)
|
## 8. Dashboard (dashboard)
|
||||||
|
|
||||||
- [ ] 8.1 Implement organizer metrics endpoint
|
- [x] 8.1 Implement organizer metrics endpoint
|
||||||
- [ ] 8.2 Implement participant registrations endpoint
|
- [x] 8.2 Implement participant registrations endpoint
|
||||||
- [ ] 8.3 Create organizer dashboard component
|
- [ ] 8.3 Create organizer dashboard component
|
||||||
- [ ] 8.4 Create participant dashboard component
|
- [ ] 8.4 Create participant dashboard component
|
||||||
- [ ] 8.5 Add quick action buttons
|
- [ ] 8.5 Add quick action buttons
|
||||||
|
|||||||
Reference in New Issue
Block a user