diff --git a/SEBS-API/Controllers/Booking/AdminBookingController.cs b/SEBS-API/Controllers/Booking/AdminBookingController.cs index 04bd743..5a771dd 100644 --- a/SEBS-API/Controllers/Booking/AdminBookingController.cs +++ b/SEBS-API/Controllers/Booking/AdminBookingController.cs @@ -76,112 +76,77 @@ public async Task> GetBookingById(int id) [HttpPatch, Route("{id}/status")] public async Task> UpdateBookingStatus(int id, [FromBody] BookingStatusUpdateDTO statusDto) { - try - { - var booking = await Context.Bookings - .Include(b => b.Event) - .ThenInclude(e => e!.EventServices) - .FirstOrDefaultAsync(b => b.BookingID == id); - - if (booking == null) - { - return NotFound("Booking not found"); - } - - if (!IsValidStatusTransition(booking.Status, statusDto.Status)) - { - return BadRequest($"Cannot change status from {booking.Status} to {statusDto.Status}"); - } + var booking = await GetBookingWithIncludesById(id); + + if (booking == null) + return NotFound("Booking not found"); - // Set admin approval info for status changes that require admin action - if (statusDto.Status == BookingStatus.Confirmed || - statusDto.Status == BookingStatus.Cancelled || - statusDto.Status == BookingStatus.Declined || - statusDto.Status == BookingStatus.Completed || - statusDto.Status == BookingStatus.NoShow) - { - var adminUserId = GetCurrentUserId(); - if (adminUserId == null) - return Unauthorized("Invalid admin token"); + if (!IsValidStatusTransition(booking.Status, statusDto.Status)) + return BadRequest($"Cannot change status from {booking.Status} to {statusDto.Status}"); - booking.ApprovedByUserId = adminUserId.Value; - booking.ApprovedDate = DateTime.UtcNow; + // Set admin approval info + var adminUserId = GetCurrentUserId(); + if (adminUserId == null) + return Unauthorized("Invalid admin token"); - // Update event status if needed - if (booking.Event != null) - { - booking.Event.Status = statusDto.Status switch - { - BookingStatus.Confirmed => EventStatus.Scheduled, - BookingStatus.Cancelled => EventStatus.Cancelled, - BookingStatus.Completed => EventStatus.Completed, - BookingStatus.NoShow => EventStatus.NoShow, - BookingStatus.Declined => EventStatus.Cancelled, - _ => booking.Event.Status - }; - booking.Event.UpdatedAt = DateTime.UtcNow; - } - } - - booking.Status = statusDto.Status; + booking.ApprovedByUserId = adminUserId.Value; + booking.ApprovedDate = DateTime.UtcNow; + booking.Status = statusDto.Status; - if (!await SaveChangesWithValidation()) + // Update event status if needed + if (booking.Event != null) + { + booking.Event.Status = statusDto.Status switch { - return BadRequest("Failed to update booking status. Please try again later."); - } + BookingStatus.Confirmed => EventStatus.Scheduled, + BookingStatus.Cancelled => EventStatus.Cancelled, + BookingStatus.Declined => EventStatus.Cancelled, + BookingStatus.Completed => EventStatus.Completed, + BookingStatus.NoShow => EventStatus.NoShow, + _ => booking.Event.Status + }; + booking.Event.UpdatedAt = DateTime.UtcNow; + } + + if (!await SaveChangesWithValidation()) + return BadRequest("Failed to update booking status"); - // Send emails (don't let email failures break the status update) + // Send email notification (don't fail if email fails) + _ = Task.Run(async () => + { try { - var adminUserId2 = GetCurrentUserId()!.Value; - var adminUser = await Context.Users.FirstAsync(u => u.UserId == adminUserId2); - var customer = new User { Name = booking.CustomerName, Email = booking.CustomerEmail }; - - // Send appropriate email based on status - switch (statusDto.Status) - { - case BookingStatus.Confirmed: - var approvalData = EmailTemplateMapper.ToApprovalTemplate(booking, customer, booking.Event!, adminUser); - await EmailService.SendBookingApprovalAsync(customer.Email, customer.Name, approvalData); - break; - - case BookingStatus.Cancelled: - case BookingStatus.Declined: - var rejectionData = EmailTemplateMapper.ToRejectionTemplate( - booking, customer, booking.Event!, adminUser, - statusDto.Notes ?? $"Booking {statusDto.Status.ToString().ToLower()} by admin" - ); - await EmailService.SendBookingRejectionAsync(customer.Email, customer.Name, rejectionData); - break; - - case BookingStatus.Completed: - case BookingStatus.NoShow: - // For completion/no-show status changes, send confirmation template - if (booking.Event?.EventServices != null) - { - var services = await Context.Services - .Where(s => booking.Event.EventServices.Select(es => es.ServiceID).Contains(s.ServiceID)) - .ToListAsync(); - var confirmationData = EmailTemplateMapper.ToConfirmationTemplate(booking, customer, booking.Event!, services); - await EmailService.SendBookingConfirmationAsync(customer.Email, customer.Name, confirmationData); - } - break; - } + await SendStatusUpdateEmail(booking, statusDto); } - catch (Exception emailEx) + catch (Exception ex) { - // Log email error but don't fail the status update - Console.WriteLine($"Email sending failed: {emailEx.Message}"); + Console.WriteLine($"Email sending failed: {ex.Message}"); } + }); - // Reload booking with all related data - booking = await GetBookingWithIncludesById(id); + return Ok(new BookingResponseDTO(booking)); + } - return Ok(new BookingResponseDTO(booking!)); - } - catch (Exception ex) + private async Task SendStatusUpdateEmail(Booking booking, BookingStatusUpdateDTO statusDto) + { + var adminUser = await Context.Users.FirstAsync(u => u.UserId == booking.ApprovedByUserId); + var customer = new User { Name = booking.CustomerName, Email = booking.CustomerEmail }; + + switch (statusDto.Status) { - return BadRequest($"Error updating booking status: {ex.Message}"); + case BookingStatus.Confirmed: + var approvalData = EmailTemplateMapper.ToApprovalTemplate(booking, customer, booking.Event!, adminUser); + await EmailService.SendBookingApprovalAsync(customer.Email, customer.Name, approvalData); + break; + + case BookingStatus.Cancelled: + case BookingStatus.Declined: + var rejectionData = EmailTemplateMapper.ToRejectionTemplate( + booking, customer, booking.Event!, adminUser, + statusDto.Notes ?? $"Booking {statusDto.Status.ToString().ToLower()} by admin" + ); + await EmailService.SendBookingRejectionAsync(customer.Email, customer.Name, rejectionData); + break; } } diff --git a/SEBS-API/Controllers/PublicController.cs b/SEBS-API/Controllers/PublicController.cs index 90d1c60..641fb6a 100644 --- a/SEBS-API/Controllers/PublicController.cs +++ b/SEBS-API/Controllers/PublicController.cs @@ -27,14 +27,15 @@ public PublicController(SEBSDbContext db, S3Service s3) /// Get all available services for display on the website. /// [HttpGet, Route("services")] - public async Task>> GetServices() + public async Task>> GetServices() { var services = await _db.Services + .Include(s => s.Image) .AsNoTracking() .OrderBy(s => s.Name) .ToListAsync(); - var result = services.Select(s => new ServiceDTO(s)).ToList(); + var result = services.Select(s => new ServiceWithImageDTO(s)).ToList(); return Ok(result); } diff --git a/SEBS-API/Controllers/ServiceController.cs b/SEBS-API/Controllers/ServiceController.cs index 5a272c8..9476560 100644 --- a/SEBS-API/Controllers/ServiceController.cs +++ b/SEBS-API/Controllers/ServiceController.cs @@ -9,62 +9,115 @@ using SEBS_API.Models; using SEBS_API.DTOs; using Microsoft.AspNetCore.Authorization; +using SEBS_API.Controllers.Base; +using SEBS_API.Services; +using SEBS_API.Utils; namespace SEBS_API.Controllers { [Route("api/[controller]")] [ApiController] [Authorize(Policy = "IsAdmin")] - public class ServiceController : ControllerBase + public class ServiceController : BaseImageController { private readonly SEBSDbContext _context; - public ServiceController(SEBSDbContext context) + public ServiceController(SEBSDbContext context, S3Service s3, UploadHelper uploadHelper) + : base(context, s3, uploadHelper) { _context = context; } // GET: api/Service [HttpGet] - public async Task>> GetServices() + public async Task>> GetServices() { - var services = await _context.Services.ToListAsync(); - return Ok(services.Select(s => new ServiceDTO(s))); + var services = await _context.Services + .Include(s => s.Image) + .ToListAsync(); + + return Ok(services.Select(s => new ServiceWithImageDTO(s))); } // GET: api/Service/5 [HttpGet("{id}")] - public async Task> GetService(int id) + public async Task> GetService(int id) { - var service = await _context.Services.FindAsync(id); + var service = await _context.Services + .Include(s => s.Image) + .FirstOrDefaultAsync(s => s.ServiceID == id); if (service == null) { return NotFound(); } - return Ok(new ServiceDTO(service)); + return Ok(new ServiceWithImageDTO(service)); } - // PUT: api/Service/5 + // PUT: api/Service/5 - Handle multipart/form-data with optional image [HttpPut("{id}")] - public async Task PutService(int id, ServiceDTO serviceDto) + public async Task PutService(int id, [FromForm] ServiceUpdateWithImageDTO dto) { - if (serviceDto.ServiceID == null || id != serviceDto.ServiceID) - { - return BadRequest(); - } - + // FormData sends ServiceID as string, need to parse or just validate against route id + // Remove the ServiceID check since we're using the route ID as source of truth var service = await _context.Services.FindAsync(id); if (service == null) { return NotFound(); } - // Update fields - service.Name = serviceDto.Name; - service.Description = serviceDto.Description; - service.BasePrice = serviceDto.BasePrice; + // Update basic fields + service.Name = dto.Name; + service.Description = dto.Description; + service.BasePrice = dto.BasePrice; + + // Handle image upload if provided + if (dto.ImageFile != null) + { + // Delete old image if exists + if (service.ImageId.HasValue) + { + var oldImage = await _context.Images.FindAsync(service.ImageId.Value); + if (oldImage != null) + { + var oldS3Key = oldImage.S3Key; + _context.Images.Remove(oldImage); + + // Background deletion from S3 + if (!string.IsNullOrEmpty(oldS3Key)) + { + _ = Task.Run(async () => await _s3.DeleteObjectAsync(oldS3Key)); + } + } + } + + // Upload new image + var imageResult = await _uh.UploadImageAsync(new ImageUploadDTO + { + File = dto.ImageFile, + Caption = dto.Caption + }, "Services", this); + + if (imageResult.Result is CreatedAtActionResult imageResponse && imageResponse.Value is ImageDTO uploadedImage) + { + service.ImageId = uploadedImage.ImageId; + } + else + { + return BadRequest("Failed to upload image"); + } + } + else if (dto.ImageId.HasValue) + { + // If no file uploaded but ImageId provided, use existing image + var imageExists = await _context.Images.AnyAsync(i => i.ImageId == dto.ImageId.Value); + if (!imageExists) + { + return BadRequest("Specified image not found"); + } + service.ImageId = dto.ImageId; + } try { @@ -85,27 +138,117 @@ public async Task PutService(int id, ServiceDTO serviceDto) return NoContent(); } - // POST: api/Service + // POST: api/Service - Create with optional image [HttpPost] - public async Task> PostService(ServiceDTO serviceDto) + public async Task> PostService([FromForm] ServiceCreateWithImageDTO dto) { - var service = serviceDto.ToServiceModel(); + var service = new Service + { + Name = dto.Name, + Description = dto.Description, + BasePrice = dto.BasePrice + }; + + // Handle image upload if provided + if (dto.ImageFile != null) + { + var imageResult = await _uh.UploadImageAsync(new ImageUploadDTO + { + File = dto.ImageFile, + Caption = dto.Caption + }, "Services", this); + + if (imageResult.Result is CreatedAtActionResult imageResponse && imageResponse.Value is ImageDTO uploadedImage) + { + service.ImageId = uploadedImage.ImageId; + } + else + { + return BadRequest("Failed to upload image"); + } + } + else if (dto.ImageId.HasValue) + { + // If no file uploaded but ImageId provided, use existing image + var imageExists = await _context.Images.AnyAsync(i => i.ImageId == dto.ImageId.Value); + if (!imageExists) + { + return BadRequest("Specified image not found"); + } + service.ImageId = dto.ImageId; + } + _context.Services.Add(service); await _context.SaveChangesAsync(); return CreatedAtAction(nameof(GetService), new { id = service.ServiceID }, new ServiceDTO(service)); } + // PUT: api/Service/5/image - Assign existing image + [HttpPut("{id}/image")] + public async Task UpdateServiceImage(int id, [FromBody] ServiceImageUpdateDTO dto) + { + var service = await _context.Services.FindAsync(id); + if (service == null) + { + return NotFound("Service not found"); + } + + // Verify image exists + var imageExists = await _context.Images.AnyAsync(i => i.ImageId == dto.ImageId); + if (!imageExists) + { + return BadRequest("Image not found"); + } + + service.ImageId = dto.ImageId; + await _context.SaveChangesAsync(); + + return NoContent(); + } + + // DELETE: api/Service/5/image + [HttpDelete("{id}/image")] + public async Task RemoveServiceImage(int id) + { + var service = await _context.Services.FindAsync(id); + if (service == null) + { + return NotFound("Service not found"); + } + + service.ImageId = null; + await _context.SaveChangesAsync(); + + return NoContent(); + } + // DELETE: api/Service/5 [HttpDelete("{id}")] public async Task DeleteService(int id) { - var service = await _context.Services.FindAsync(id); + var service = await _context.Services + .Include(s => s.Image) + .FirstOrDefaultAsync(s => s.ServiceID == id); + if (service == null) { return NotFound(); } + // Delete associated image if exists + if (service.Image != null) + { + var s3Key = service.Image.S3Key; + _context.Images.Remove(service.Image); + + // Background deletion from S3 + if (!string.IsNullOrEmpty(s3Key)) + { + _ = Task.Run(async () => await _s3.DeleteObjectAsync(s3Key)); + } + } + _context.Services.Remove(service); await _context.SaveChangesAsync(); diff --git a/SEBS-API/DTOs/ServiceDTO.cs b/SEBS-API/DTOs/ServiceDTO.cs index 0c96e2d..d5399eb 100644 --- a/SEBS-API/DTOs/ServiceDTO.cs +++ b/SEBS-API/DTOs/ServiceDTO.cs @@ -1,5 +1,6 @@ using System; using SEBS_API.Models; +using Microsoft.AspNetCore.Http; namespace SEBS_API.DTOs; @@ -9,26 +10,83 @@ public class ServiceDTO public string Name { get; set; } = default!; public string Description { get; set; } = default!; public decimal BasePrice { get; set; } = default!; + public int? ImageId { get; set; } public ServiceDTO() { } + public ServiceDTO(Service service) { - ServiceID = service.ServiceID; Name = service.Name; Description = service.Description; BasePrice = service.BasePrice; + ImageId = service.ImageId; } - public Service ToServiceModel() { return new Service { + ServiceID = this.ServiceID ?? 0, Name = Name, Description = Description, - BasePrice = BasePrice + BasePrice = BasePrice, + ImageId = ImageId }; } +} + +public class ServiceWithImageDTO +{ + public int ServiceID { get; set; } + public string Name { get; set; } = default!; + public string Description { get; set; } = default!; + public decimal BasePrice { get; set; } + public int? ImageId { get; set; } + public ImageDTO? Image { get; set; } + public string? ImageUrl { get; set; } + + public ServiceWithImageDTO() { } + public ServiceWithImageDTO(Service service) + { + ServiceID = service.ServiceID; + Name = service.Name; + Description = service.Description; + BasePrice = service.BasePrice; + ImageId = service.ImageId; + + if (service.Image != null) + { + Image = new ImageDTO(service.Image); + ImageUrl = $"/api/public/images/{Uri.EscapeDataString(service.Image.S3Key).Replace("%2F", "/")}"; + } + } +} + +public class ServiceImageUpdateDTO +{ + public required int ImageId { get; set; } +} + +// New DTOs for FormData handling +public class ServiceUpdateWithImageDTO +{ + public int? ServiceID { get; set; } + public string Name { get; set; } = default!; + public string Description { get; set; } = default!; + public decimal BasePrice { get; set; } + public int? ImageId { get; set; } + public IFormFile? ImageFile { get; set; } + public string? Caption { get; set; } +} + +public class ServiceCreateWithImageDTO +{ + public string Name { get; set; } = default!; + public string Description { get; set; } = default!; + public decimal BasePrice { get; set; } + public int? ImageId { get; set; } + public IFormFile? ImageFile { get; set; } + public string? Caption { get; set; } } diff --git a/SEBS-API/Data/SEBSDbContext.cs b/SEBS-API/Data/SEBSDbContext.cs index 3e62ab5..e04331b 100644 --- a/SEBS-API/Data/SEBSDbContext.cs +++ b/SEBS-API/Data/SEBSDbContext.cs @@ -83,7 +83,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .HasColumnType("timestamptz"); }); - + // Service to GalleryImage relationship + modelBuilder.Entity() + .HasOne(s => s.Image) + .WithMany() + .HasForeignKey(s => s.ImageId) + .OnDelete(DeleteBehavior.SetNull); modelBuilder.Entity(entity => { diff --git a/SEBS-API/Migrations/20251028010854_AddServiceImageSupport.Designer.cs b/SEBS-API/Migrations/20251028010854_AddServiceImageSupport.Designer.cs new file mode 100644 index 0000000..63635c4 --- /dev/null +++ b/SEBS-API/Migrations/20251028010854_AddServiceImageSupport.Designer.cs @@ -0,0 +1,570 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using SEBS_API.Data; + +#nullable disable + +namespace SEBS_API.Migrations +{ + [DbContext(typeof(SEBSDbContext))] + [Migration("20251028010854_AddServiceImageSupport")] + partial class AddServiceImageSupport + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("SEBS_API.Models.Booking", b => + { + b.Property("BookingID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("BookingID")); + + b.Property("ApprovedByUserId") + .HasColumnType("integer"); + + b.Property("ApprovedDate") + .HasColumnType("timestamptz"); + + b.Property("BookingDate") + .HasColumnType("timestamptz"); + + b.Property("BookingReference") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CustomerPhone") + .IsRequired() + .HasColumnType("text"); + + b.Property("EventID") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("BookingID"); + + b.HasIndex("ApprovedByUserId"); + + b.HasIndex("BookingReference") + .IsUnique(); + + b.HasIndex("EventID"); + + b.ToTable("Bookings"); + }); + + modelBuilder.Entity("SEBS_API.Models.Event", b => + { + b.Property("EventID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EventID")); + + b.Property("CreatedAt") + .HasColumnType("timestamptz"); + + b.Property("EndTime") + .HasColumnType("time"); + + b.Property("EventDate") + .HasColumnType("timestamptz"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("Location") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("StartTime") + .HasColumnType("time"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamptz"); + + b.HasKey("EventID"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("SEBS_API.Models.EventGallery", b => + { + b.Property("EventGalleryId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EventGalleryId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPublished") + .HasColumnType("boolean"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("EventGalleryId"); + + b.ToTable("EventGalleries"); + }); + + modelBuilder.Entity("SEBS_API.Models.EventImage", b => + { + b.Property("EventImageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EventImageId")); + + b.Property("DisplayOrder") + .HasColumnType("integer"); + + b.Property("EventGalleryId") + .HasColumnType("integer"); + + b.Property("ImageId") + .HasColumnType("integer"); + + b.HasKey("EventImageId"); + + b.HasIndex("EventGalleryId"); + + b.HasIndex("ImageId"); + + b.ToTable("EventImages"); + }); + + modelBuilder.Entity("SEBS_API.Models.EventService", b => + { + b.Property("EventServiceID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("EventServiceID")); + + b.Property("CustomPrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("EventID") + .HasColumnType("integer"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Quantity") + .HasColumnType("integer"); + + b.Property("ServiceID") + .HasColumnType("integer"); + + b.HasKey("EventServiceID"); + + b.HasIndex("EventID"); + + b.HasIndex("ServiceID"); + + b.ToTable("EventServices"); + }); + + modelBuilder.Entity("SEBS_API.Models.GalleryImage", b => + { + b.Property("ImageId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ImageId")); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("S3Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("ImageId"); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("SEBS_API.Models.Highlight", b => + { + b.Property("HighlightId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("HighlightId")); + + b.Property("DisplayOrder") + .HasColumnType("integer"); + + b.Property("ImageId") + .HasColumnType("integer"); + + b.HasKey("HighlightId"); + + b.HasIndex("ImageId"); + + b.ToTable("Highlights"); + }); + + modelBuilder.Entity("SEBS_API.Models.Notification", b => + { + b.Property("NotificationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("NotificationId")); + + b.Property("BookingId") + .HasColumnType("integer"); + + b.Property("CreatedAt") + .HasColumnType("timestamptz"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("character varying(120)"); + + b.Property("UserId") + .HasColumnType("integer"); + + b.HasKey("NotificationId"); + + b.HasIndex("BookingId"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("SEBS_API.Models.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("SEBS_API.Models.Service", b => + { + b.Property("ServiceID") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ServiceID")); + + b.Property("BasePrice") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImageId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ServiceID"); + + b.HasIndex("ImageId"); + + b.ToTable("Services"); + }); + + modelBuilder.Entity("SEBS_API.Models.VisitLog", b => + { + b.Property("VisitLogId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("VisitLogId")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("timestamptz"); + + b.Property("PageViews") + .HasColumnType("integer"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("UniqueVisitors") + .HasColumnType("integer"); + + b.HasKey("VisitLogId"); + + b.ToTable("VisitLogs"); + }); + + modelBuilder.Entity("User", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("UserId")); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamptz"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("character varying(15)"); + + b.Property("RoleId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamptz"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("UserId"); + + b.HasIndex("RoleId"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("SEBS_API.Models.Booking", b => + { + b.HasOne("User", "ApprovedBy") + .WithMany("Bookings") + .HasForeignKey("ApprovedByUserId"); + + b.HasOne("SEBS_API.Models.Event", "Event") + .WithMany("Bookings") + .HasForeignKey("EventID"); + + b.Navigation("ApprovedBy"); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("SEBS_API.Models.EventImage", b => + { + b.HasOne("SEBS_API.Models.EventGallery", "EventGallery") + .WithMany("EventImages") + .HasForeignKey("EventGalleryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SEBS_API.Models.GalleryImage", "Image") + .WithMany("EventImages") + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventGallery"); + + b.Navigation("Image"); + }); + + modelBuilder.Entity("SEBS_API.Models.EventService", b => + { + b.HasOne("SEBS_API.Models.Event", "Event") + .WithMany("EventServices") + .HasForeignKey("EventID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SEBS_API.Models.Service", "Service") + .WithMany("EventServices") + .HasForeignKey("ServiceID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Event"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("SEBS_API.Models.Highlight", b => + { + b.HasOne("SEBS_API.Models.GalleryImage", "Image") + .WithMany("Highlights") + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Image"); + }); + + modelBuilder.Entity("SEBS_API.Models.Notification", b => + { + b.HasOne("SEBS_API.Models.Booking", "Booking") + .WithMany() + .HasForeignKey("BookingId"); + + b.HasOne("User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Booking"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("SEBS_API.Models.Service", b => + { + b.HasOne("SEBS_API.Models.GalleryImage", "Image") + .WithMany() + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Image"); + }); + + modelBuilder.Entity("User", b => + { + b.HasOne("SEBS_API.Models.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("SEBS_API.Models.Event", b => + { + b.Navigation("Bookings"); + + b.Navigation("EventServices"); + }); + + modelBuilder.Entity("SEBS_API.Models.EventGallery", b => + { + b.Navigation("EventImages"); + }); + + modelBuilder.Entity("SEBS_API.Models.GalleryImage", b => + { + b.Navigation("EventImages"); + + b.Navigation("Highlights"); + }); + + modelBuilder.Entity("SEBS_API.Models.Role", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("SEBS_API.Models.Service", b => + { + b.Navigation("EventServices"); + }); + + modelBuilder.Entity("User", b => + { + b.Navigation("Bookings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SEBS-API/Migrations/20251028010854_AddServiceImageSupport.cs b/SEBS-API/Migrations/20251028010854_AddServiceImageSupport.cs new file mode 100644 index 0000000..39914ba --- /dev/null +++ b/SEBS-API/Migrations/20251028010854_AddServiceImageSupport.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SEBS_API.Migrations +{ + /// + public partial class AddServiceImageSupport : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ImageId", + table: "Services", + type: "integer", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Services_ImageId", + table: "Services", + column: "ImageId"); + + migrationBuilder.AddForeignKey( + name: "FK_Services_Images_ImageId", + table: "Services", + column: "ImageId", + principalTable: "Images", + principalColumn: "ImageId", + onDelete: ReferentialAction.SetNull); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Services_Images_ImageId", + table: "Services"); + + migrationBuilder.DropIndex( + name: "IX_Services_ImageId", + table: "Services"); + + migrationBuilder.DropColumn( + name: "ImageId", + table: "Services"); + } + } +} diff --git a/SEBS-API/Migrations/SEBSDbContextModelSnapshot.cs b/SEBS-API/Migrations/SEBSDbContextModelSnapshot.cs index b9b3cd3..73d947b 100644 --- a/SEBS-API/Migrations/SEBSDbContextModelSnapshot.cs +++ b/SEBS-API/Migrations/SEBSDbContextModelSnapshot.cs @@ -330,12 +330,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("ImageId") + .HasColumnType("integer"); + b.Property("Name") .IsRequired() .HasColumnType("text"); b.HasKey("ServiceID"); + b.HasIndex("ImageId"); + b.ToTable("Services"); }); @@ -502,6 +507,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("SEBS_API.Models.Service", b => + { + b.HasOne("SEBS_API.Models.GalleryImage", "Image") + .WithMany() + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Image"); + }); + modelBuilder.Entity("User", b => { b.HasOne("SEBS_API.Models.Role", "Role") diff --git a/SEBS-API/Models/Service.cs b/SEBS-API/Models/Service.cs index 1b751c1..e661488 100644 --- a/SEBS-API/Models/Service.cs +++ b/SEBS-API/Models/Service.cs @@ -13,5 +13,9 @@ public class Service public string Description { get; set; } = string.Empty; public decimal BasePrice { get; set; } + // Thumbnail image relationship + public int? ImageId { get; set; } + public GalleryImage? Image { get; set; } + public List EventServices { get; set; } = new(); } \ No newline at end of file diff --git a/SEBS-API/bin/Debug/net9.0/SEBS-API.dll b/SEBS-API/bin/Debug/net9.0/SEBS-API.dll index af06d2a..c196ccf 100644 Binary files a/SEBS-API/bin/Debug/net9.0/SEBS-API.dll and b/SEBS-API/bin/Debug/net9.0/SEBS-API.dll differ diff --git a/SEBS-API/bin/Debug/net9.0/SEBS-API.exe b/SEBS-API/bin/Debug/net9.0/SEBS-API.exe index dd02caa..1fe8b1c 100644 Binary files a/SEBS-API/bin/Debug/net9.0/SEBS-API.exe and b/SEBS-API/bin/Debug/net9.0/SEBS-API.exe differ diff --git a/SEBS-API/bin/Debug/net9.0/SEBS-API.pdb b/SEBS-API/bin/Debug/net9.0/SEBS-API.pdb index f610436..1562113 100644 Binary files a/SEBS-API/bin/Debug/net9.0/SEBS-API.pdb and b/SEBS-API/bin/Debug/net9.0/SEBS-API.pdb differ diff --git a/SEBS-API/obj/Debug/net9.0/SEBS-API.AssemblyInfo.cs b/SEBS-API/obj/Debug/net9.0/SEBS-API.AssemblyInfo.cs index 80577a9..e19e5f6 100644 --- a/SEBS-API/obj/Debug/net9.0/SEBS-API.AssemblyInfo.cs +++ b/SEBS-API/obj/Debug/net9.0/SEBS-API.AssemblyInfo.cs @@ -14,7 +14,7 @@ [assembly: System.Reflection.AssemblyCompanyAttribute("SEBS-API")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+055d1c5e5a3d267b45b5523efb390d72c7e4b433")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+7d84d22aeafb341e30ad2781370d1c1f96ddb119")] [assembly: System.Reflection.AssemblyProductAttribute("SEBS-API")] [assembly: System.Reflection.AssemblyTitleAttribute("SEBS-API")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/SEBS-API/obj/Debug/net9.0/SEBS-API.AssemblyInfoInputs.cache b/SEBS-API/obj/Debug/net9.0/SEBS-API.AssemblyInfoInputs.cache index 171e936..2ae263f 100644 --- a/SEBS-API/obj/Debug/net9.0/SEBS-API.AssemblyInfoInputs.cache +++ b/SEBS-API/obj/Debug/net9.0/SEBS-API.AssemblyInfoInputs.cache @@ -1 +1 @@ -3046b1e67650e8dc0799ee875278dc8cd710f4fc8b3c3f632aaa6f801bd27c5a +ab99aec969c761c437ed741dc329c2d608a10559cea165cd95f635c7240e884f diff --git a/SEBS-API/obj/Debug/net9.0/SEBS-API.csproj.CoreCompileInputs.cache b/SEBS-API/obj/Debug/net9.0/SEBS-API.csproj.CoreCompileInputs.cache index 87dd601..d8937ea 100644 --- a/SEBS-API/obj/Debug/net9.0/SEBS-API.csproj.CoreCompileInputs.cache +++ b/SEBS-API/obj/Debug/net9.0/SEBS-API.csproj.CoreCompileInputs.cache @@ -1 +1 @@ -7d0e71805880955ec58e88219fa3672413971c7ac6d393468b97dabb65afa98d +06f28a7bf8f90b990d347872a66e0cbc4ebb1cbaa85f607e68d403e08b8b791c diff --git a/SEBS-API/obj/Debug/net9.0/SEBS-API.dll b/SEBS-API/obj/Debug/net9.0/SEBS-API.dll index af06d2a..c196ccf 100644 Binary files a/SEBS-API/obj/Debug/net9.0/SEBS-API.dll and b/SEBS-API/obj/Debug/net9.0/SEBS-API.dll differ diff --git a/SEBS-API/obj/Debug/net9.0/SEBS-API.pdb b/SEBS-API/obj/Debug/net9.0/SEBS-API.pdb index f610436..1562113 100644 Binary files a/SEBS-API/obj/Debug/net9.0/SEBS-API.pdb and b/SEBS-API/obj/Debug/net9.0/SEBS-API.pdb differ diff --git a/SEBS-API/obj/Debug/net9.0/SEBS-API.sourcelink.json b/SEBS-API/obj/Debug/net9.0/SEBS-API.sourcelink.json index 007e14a..f6bdde1 100644 --- a/SEBS-API/obj/Debug/net9.0/SEBS-API.sourcelink.json +++ b/SEBS-API/obj/Debug/net9.0/SEBS-API.sourcelink.json @@ -1 +1 @@ -{"documents":{"C:\\Users\\jomna\\Documents\\GitHub\\SEBS-API\\*":"https://raw.githubusercontent.com/Jomar77/SEBS-API/055d1c5e5a3d267b45b5523efb390d72c7e4b433/*"}} \ No newline at end of file +{"documents":{"C:\\Users\\jomna\\Documents\\GitHub\\SEBS-API\\*":"https://raw.githubusercontent.com/Jomar77/SEBS-API/7d84d22aeafb341e30ad2781370d1c1f96ddb119/*"}} \ No newline at end of file diff --git a/SEBS-API/obj/Debug/net9.0/apphost.exe b/SEBS-API/obj/Debug/net9.0/apphost.exe index dd02caa..1fe8b1c 100644 Binary files a/SEBS-API/obj/Debug/net9.0/apphost.exe and b/SEBS-API/obj/Debug/net9.0/apphost.exe differ diff --git a/SEBS-API/obj/Debug/net9.0/ref/SEBS-API.dll b/SEBS-API/obj/Debug/net9.0/ref/SEBS-API.dll index 850ae4b..e6de88f 100644 Binary files a/SEBS-API/obj/Debug/net9.0/ref/SEBS-API.dll and b/SEBS-API/obj/Debug/net9.0/ref/SEBS-API.dll differ diff --git a/SEBS-API/obj/Debug/net9.0/refint/SEBS-API.dll b/SEBS-API/obj/Debug/net9.0/refint/SEBS-API.dll index 850ae4b..e6de88f 100644 Binary files a/SEBS-API/obj/Debug/net9.0/refint/SEBS-API.dll and b/SEBS-API/obj/Debug/net9.0/refint/SEBS-API.dll differ diff --git a/SEBS-API/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json b/SEBS-API/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json index 904bc61..b443a3d 100644 --- a/SEBS-API/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json +++ b/SEBS-API/obj/Debug/net9.0/rjsmcshtml.dswa.cache.json @@ -1 +1 @@ -{"GlobalPropertiesHash":"wT2euAHoJHEuvSkUTH5GH58Q0B+BL8BtcCkJas5cEro=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["MF1qfZNH0VmulOjXAl8dD6nJwWuZF/Ut2RRMt6exqck=","2Zqdzv6KoJ8eTi7KABZgTChmiTbl5UUVH0dgJQe5F7Y=","HvVgJem\u002B6mbGcM7FEAHMo0pZ8U5gk2MbJD9mUSFXrcA=","\u002BKtEuIF\u002B7kZ9tBo5D9zypNq7I5L3lBfxfWnn3\u002BbYC7o=","HmcYqBxCpeoHLxYb0mvME2OJU/ivJf4tRizMjGsNyDo=","rjHLhqK9BDUHy8YC4uiiBUBpW3T1wAYfHlvn/jDNst0=","IivYPJgsP9FsXdb/VH3BdumvaSxOfi66CpssYZ\u002B6aWA=","QbQyIWqocpXGqfQ1cwozhl9\u002BI8ic4P3Zl/j8IvFTSDU="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"wT2euAHoJHEuvSkUTH5GH58Q0B+BL8BtcCkJas5cEro=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["MF1qfZNH0VmulOjXAl8dD6nJwWuZF/Ut2RRMt6exqck=","2Zqdzv6KoJ8eTi7KABZgTChmiTbl5UUVH0dgJQe5F7Y=","HvVgJem\u002B6mbGcM7FEAHMo0pZ8U5gk2MbJD9mUSFXrcA=","\u002BKtEuIF\u002B7kZ9tBo5D9zypNq7I5L3lBfxfWnn3\u002BbYC7o=","HmcYqBxCpeoHLxYb0mvME2OJU/ivJf4tRizMjGsNyDo=","rjHLhqK9BDUHy8YC4uiiBUBpW3T1wAYfHlvn/jDNst0=","IivYPJgsP9FsXdb/VH3BdumvaSxOfi66CpssYZ\u002B6aWA=","xchv85UyHU63asoKYJgXqlDwwwLeb9pS/UHFhby7FHI="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/SEBS-API/obj/Debug/net9.0/rjsmrazor.dswa.cache.json b/SEBS-API/obj/Debug/net9.0/rjsmrazor.dswa.cache.json index 7ab7d83..a0ead06 100644 --- a/SEBS-API/obj/Debug/net9.0/rjsmrazor.dswa.cache.json +++ b/SEBS-API/obj/Debug/net9.0/rjsmrazor.dswa.cache.json @@ -1 +1 @@ -{"GlobalPropertiesHash":"fnfWViW9P33J+XqzCeYR9peL8Bh3YETe5+isV6xjNL4=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["MF1qfZNH0VmulOjXAl8dD6nJwWuZF/Ut2RRMt6exqck=","2Zqdzv6KoJ8eTi7KABZgTChmiTbl5UUVH0dgJQe5F7Y=","HvVgJem\u002B6mbGcM7FEAHMo0pZ8U5gk2MbJD9mUSFXrcA=","\u002BKtEuIF\u002B7kZ9tBo5D9zypNq7I5L3lBfxfWnn3\u002BbYC7o=","HmcYqBxCpeoHLxYb0mvME2OJU/ivJf4tRizMjGsNyDo=","rjHLhqK9BDUHy8YC4uiiBUBpW3T1wAYfHlvn/jDNst0=","IivYPJgsP9FsXdb/VH3BdumvaSxOfi66CpssYZ\u002B6aWA=","QbQyIWqocpXGqfQ1cwozhl9\u002BI8ic4P3Zl/j8IvFTSDU="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"fnfWViW9P33J+XqzCeYR9peL8Bh3YETe5+isV6xjNL4=","FingerprintPatternsHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["MF1qfZNH0VmulOjXAl8dD6nJwWuZF/Ut2RRMt6exqck=","2Zqdzv6KoJ8eTi7KABZgTChmiTbl5UUVH0dgJQe5F7Y=","HvVgJem\u002B6mbGcM7FEAHMo0pZ8U5gk2MbJD9mUSFXrcA=","\u002BKtEuIF\u002B7kZ9tBo5D9zypNq7I5L3lBfxfWnn3\u002BbYC7o=","HmcYqBxCpeoHLxYb0mvME2OJU/ivJf4tRizMjGsNyDo=","rjHLhqK9BDUHy8YC4uiiBUBpW3T1wAYfHlvn/jDNst0=","IivYPJgsP9FsXdb/VH3BdumvaSxOfi66CpssYZ\u002B6aWA=","xchv85UyHU63asoKYJgXqlDwwwLeb9pS/UHFhby7FHI="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file