Add Announcements and Dashboard controllers with all DTOs
This commit is contained in:
@@ -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,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;
|
||||
}
|
||||
@@ -18,20 +18,20 @@
|
||||
|
||||
## 3. User Authentication (user-auth)
|
||||
|
||||
- [ ] 3.1 Implement user registration endpoint
|
||||
- [ ] 3.2 Implement user login endpoint with JWT
|
||||
- [ ] 3.3 Create authentication middleware
|
||||
- [ ] 3.4 Implement role-based access control middleware
|
||||
- [x] 3.1 Implement user registration endpoint
|
||||
- [x] 3.2 Implement user login endpoint with JWT
|
||||
- [x] 3.3 Create authentication middleware
|
||||
- [x] 3.4 Implement role-based access control middleware
|
||||
- [ ] 3.5 Create registration form component
|
||||
- [ ] 3.6 Create login form component
|
||||
- [ ] 3.7 Implement logout functionality
|
||||
|
||||
## 4. Event Management (event-management)
|
||||
|
||||
- [ ] 4.1 Implement create event endpoint
|
||||
- [ ] 4.2 Implement update event endpoint
|
||||
- [ ] 4.3 Implement list events endpoint with filtering
|
||||
- [ ] 4.4 Implement get event details endpoint
|
||||
- [x] 4.1 Implement create event endpoint
|
||||
- [x] 4.2 Implement update event endpoint
|
||||
- [x] 4.3 Implement list events endpoint with filtering
|
||||
- [x] 4.4 Implement get event details endpoint
|
||||
- [ ] 4.5 Create event creation form component
|
||||
- [ ] 4.6 Create event editing form component
|
||||
- [ ] 4.7 Create event list view with filters
|
||||
@@ -39,36 +39,36 @@
|
||||
|
||||
## 5. Registration System (registration-system)
|
||||
|
||||
- [ ] 5.1 Implement registration endpoint
|
||||
- [ ] 5.2 Implement registration status update endpoint
|
||||
- [ ] 5.3 Implement cancel registration endpoint
|
||||
- [x] 5.1 Implement registration endpoint
|
||||
- [x] 5.2 Implement registration status update endpoint
|
||||
- [x] 5.3 Implement cancel registration endpoint
|
||||
- [ ] 5.4 Create registration form component
|
||||
- [ ] 5.5 Create my registrations list component
|
||||
- [ ] 5.6 Implement registration status display
|
||||
|
||||
## 6. Payment Tracking (payment-tracking)
|
||||
|
||||
- [ ] 6.1 Implement record payment endpoint
|
||||
- [ ] 6.2 Implement payment status endpoint
|
||||
- [ ] 6.3 Implement payment report endpoint
|
||||
- [x] 6.1 Implement record payment endpoint
|
||||
- [x] 6.2 Implement payment status endpoint
|
||||
- [x] 6.3 Implement payment report endpoint
|
||||
- [ ] 6.4 Create payment recording form component
|
||||
- [ ] 6.5 Create payment status display component
|
||||
- [ ] 6.6 Create payment report view
|
||||
|
||||
## 7. Announcements (announcements)
|
||||
|
||||
- [ ] 7.1 Implement create announcement endpoint
|
||||
- [ ] 7.2 Implement edit announcement endpoint
|
||||
- [ ] 7.3 Implement delete announcement endpoint
|
||||
- [ ] 7.4 Implement list announcements endpoint
|
||||
- [x] 7.1 Implement create announcement endpoint
|
||||
- [x] 7.2 Implement edit announcement endpoint
|
||||
- [x] 7.3 Implement delete announcement endpoint
|
||||
- [x] 7.4 Implement list announcements endpoint
|
||||
- [ ] 7.5 Create announcement creation form
|
||||
- [ ] 7.6 Create announcement list component
|
||||
- [ ] 7.7 Implement notification system
|
||||
|
||||
## 8. Dashboard (dashboard)
|
||||
|
||||
- [ ] 8.1 Implement organizer metrics endpoint
|
||||
- [ ] 8.2 Implement participant registrations endpoint
|
||||
- [x] 8.1 Implement organizer metrics endpoint
|
||||
- [x] 8.2 Implement participant registrations endpoint
|
||||
- [ ] 8.3 Create organizer dashboard component
|
||||
- [ ] 8.4 Create participant dashboard component
|
||||
- [ ] 8.5 Add quick action buttons
|
||||
|
||||
Reference in New Issue
Block a user