diff --git a/.gitignore b/.gitignore index bf7c8b0..ac9ff9d 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ Thumbs.db .idea/ *.swp *.swo +client_secret.json diff --git a/backend/TRFSAE.MemberPortal.API/Controllers/PurchaseRequestController.cs b/backend/TRFSAE.MemberPortal.API/Controllers/PurchaseRequestController.cs new file mode 100644 index 0000000..efc91f0 --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/Controllers/PurchaseRequestController.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Mvc; +using TRFSAE.MemberPortal.API.DTOs; +using TRFSAE.MemberPortal.API.Interfaces; +using Supabase; + +namespace TRFSAE.MemberPortal.API.Controllers +{ + [ApiController] + [Route("api/purchase-request")] + public class PurchaseRequestController : ControllerBase + { + private readonly IPurchaseRequestService _purchaseRequestService; + + public PurchaseRequestController(IPurchaseRequestService purchaseRequestService) + { + _purchaseRequestService = purchaseRequestService; + } + + [HttpGet] + public async Task GetAllPurchaseAsync([FromQuery] PurchaseRequestSearchDto? dto) + { + var result = await _purchaseRequestService.GetAllPurchaseRequestsAsync(dto ?? new PurchaseRequestSearchDto()); + return Ok(result); + } + + [HttpGet("{id:guid}", Name = "GetPurchaseRequestById")] + public async Task GetPurchaseRequestByIDAsync(Guid id) + { + var item = await _purchaseRequestService.GetPurchaseRequestByIDAsync(id); + return item is null ? NotFound() : Ok(item); + } + + [HttpPost] + public async Task CreatePurchaseRequestAsync(PurchaseRequestCreateDto dto) + { + var created = await _purchaseRequestService.CreatePurchaseRequestAsync(dto); + return CreatedAtRoute("GetPurchaseRequestById", new { id = created.Id }, created); + } + + [HttpPut("{id:guid}")] + public async Task UpdatePurchaseRequestByIDAsync(Guid id, PurchaseRequestUpdateDto dto) + { + var updated = await _purchaseRequestService.UpdatePurchaseRequestByIDAsync(id, dto); + return updated is null ? NotFound() : Ok(updated); + } + + [HttpDelete("{id:guid}")] + public async Task DeletePurchaseRequestAsync(Guid id, string confirmationString) + { + var deleted = await _purchaseRequestService.DeletePurchaseRequestAsync(id, confirmationString); + return deleted ? NoContent() : NotFound(); + } + } +} diff --git a/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestCreateDto.cs b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestCreateDto.cs new file mode 100644 index 0000000..7ade908 --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestCreateDto.cs @@ -0,0 +1,8 @@ +namespace TRFSAE.MemberPortal.API.DTOs +{ + public class PurchaseRequestCreateDto + { + public string Status { get; set; } = string.Empty; + public Guid Requester { get; set; } + } +} diff --git a/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestResponseDto.cs b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestResponseDto.cs new file mode 100644 index 0000000..e2f5d5c --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestResponseDto.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace TRFSAE.MemberPortal.API.DTOs +{ + public class PurchaseRequestResponseDto + { + [JsonPropertyName("id")] + public Guid Id { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } = string.Empty; + + [JsonPropertyName("requester")] + public Guid? Requester { get; set; } + + [JsonPropertyName("created_at")] + public DateTimeOffset CreatedAt { get; set; } + + [JsonPropertyName("updated_at")] + public DateTime UpdatedAt { get; set; } + } +} diff --git a/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestSearchDto.cs b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestSearchDto.cs new file mode 100644 index 0000000..8554c1b --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestSearchDto.cs @@ -0,0 +1,8 @@ +namespace TRFSAE.MemberPortal.API.DTOs +{ + public class PurchaseRequestSearchDto + { + public string Status { get; set; } = string.Empty; + public Guid? Requester { get; set; } + } +} diff --git a/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestUpdateDto.cs b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestUpdateDto.cs new file mode 100644 index 0000000..58a634e --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/DTOs/PurchaseRequest/PurchaseRequestUpdateDto.cs @@ -0,0 +1,8 @@ +namespace TRFSAE.MemberPortal.API.DTOs +{ + public class PurchaseRequestUpdateDto + { + public string Status { get; set; } = string.Empty; + public Guid Requester { get; set; } + } +} diff --git a/backend/TRFSAE.MemberPortal.API/Interfaces/IPurchaseRequestService.cs b/backend/TRFSAE.MemberPortal.API/Interfaces/IPurchaseRequestService.cs new file mode 100644 index 0000000..40b6833 --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/Interfaces/IPurchaseRequestService.cs @@ -0,0 +1,13 @@ +using TRFSAE.MemberPortal.API.DTOs; + +namespace TRFSAE.MemberPortal.API.Interfaces +{ + public interface IPurchaseRequestService + { + Task> GetAllPurchaseRequestsAsync(PurchaseRequestSearchDto dto); + Task GetPurchaseRequestByIDAsync(Guid id); + Task CreatePurchaseRequestAsync(PurchaseRequestCreateDto dto); + Task UpdatePurchaseRequestByIDAsync(Guid id, PurchaseRequestUpdateDto dto); + Task DeletePurchaseRequestAsync(Guid id, string confirmationString); + } +} diff --git a/backend/TRFSAE.MemberPortal.API/Models/PurchaseRequestModel.cs b/backend/TRFSAE.MemberPortal.API/Models/PurchaseRequestModel.cs new file mode 100644 index 0000000..61d8e19 --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/Models/PurchaseRequestModel.cs @@ -0,0 +1,24 @@ +using Supabase.Postgrest.Attributes; +using Supabase.Postgrest.Models; + +namespace TRFSAE.MemberPortal.API.Models +{ + [Table("purchase_request")] + public class PurchaseRequestModel: BaseModel + { + [PrimaryKey("id", false)] + public Guid Id { get; set; } + + [Column("status")] + public string Status { get; set; } = string.Empty; + + [Column("requester")] + public Guid Requester { get; set; } + + [Column("created_at")] + public DateTimeOffset CreatedAt { get; set; } + + [Column("updated_at")] + public DateTime UpdatedAt { get; set; } + } +} \ No newline at end of file diff --git a/backend/TRFSAE.MemberPortal.API/Program.cs b/backend/TRFSAE.MemberPortal.API/Program.cs index 3df84b0..04e68aa 100644 --- a/backend/TRFSAE.MemberPortal.API/Program.cs +++ b/backend/TRFSAE.MemberPortal.API/Program.cs @@ -12,6 +12,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); } @@ -35,20 +37,6 @@ return client; }); -// register Supabase client as a singleton for reuse across project -builder.Services.AddScoped(provider => -{ - var options = new SupabaseOptions - { - AutoConnectRealtime = true, - AutoRefreshToken = true, - }; - - var url = builder.Configuration["SupabaseUrl"] ?? throw new InvalidOperationException("Supabase URL is not configured."); - var key = builder.Configuration["SupabaseKey"] ?? throw new InvalidOperationException("Supabase Key is not configured."); - return new Client(url, key, options); -}); - // Add services to the container. // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddEndpointsApiExplorer(); @@ -81,12 +69,22 @@ app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); -using (var scope = app.Services.CreateScope()) -{ - var googleSheetsService = scope.ServiceProvider.GetRequiredService(); - // Initialize Google Sheets API once - await googleSheetsService.ListenToSupabaseChangesAsync(); -} +// Initialize Google Sheets listener after app is built +app.Lifetime.ApplicationStarted.Register(async () => +{ + using (var scope = app.Services.CreateScope()) + { + var googleSheetsService = scope.ServiceProvider.GetRequiredService(); + try + { + await googleSheetsService.ListenToSupabaseChangesAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"Error initializing Google Sheets listener: {ex.Message}"); + } + } +}); app.Run(); diff --git a/backend/TRFSAE.MemberPortal.API/Services/GoogleSheetsService.cs b/backend/TRFSAE.MemberPortal.API/Services/GoogleSheetsService.cs index dabc834..e98055a 100644 --- a/backend/TRFSAE.MemberPortal.API/Services/GoogleSheetsService.cs +++ b/backend/TRFSAE.MemberPortal.API/Services/GoogleSheetsService.cs @@ -84,22 +84,30 @@ public async Task> GetSupabase() public async Task ListenToSupabaseChangesAsync() { - var channel = await _supabaseClient - .From() - .On(Supabase.Realtime.PostgresChanges.PostgresChangesOptions.ListenType.Inserts, async (sender, change) => - { - var items = await GetSupabase(); - if (items != null && items.Count > 0) - { - CreateEntry(items); - } - else + try + { + var channel = await _supabaseClient + .From() + .On(Supabase.Realtime.PostgresChanges.PostgresChangesOptions.ListenType.Inserts, async (sender, change) => { - Console.WriteLine(" No item found to write to Google Sheets"); - } - }); + var items = await GetSupabase(); + if (items != null && items.Count > 0) + { + CreateEntry(items); + } + else + { + Console.WriteLine("No item found to write to Google Sheets"); + } + }); - await channel.Subscribe(); + await channel.Subscribe(); + } + catch (Exception ex) + { + Console.WriteLine($"Error in ListenToSupabaseChangesAsync: {ex.Message}"); + throw; + } } } diff --git a/backend/TRFSAE.MemberPortal.API/Services/PurchaseRequestService.cs b/backend/TRFSAE.MemberPortal.API/Services/PurchaseRequestService.cs new file mode 100644 index 0000000..774db7d --- /dev/null +++ b/backend/TRFSAE.MemberPortal.API/Services/PurchaseRequestService.cs @@ -0,0 +1,140 @@ +using TRFSAE.MemberPortal.API.DTOs; +using TRFSAE.MemberPortal.API.Interfaces; +using TRFSAE.MemberPortal.API.Models; +using System.Text.Json; +using Supabase; + +namespace TRFSAE.MemberPortal.API.Services +{ + public class PurchaseRequestService : IPurchaseRequestService + { + private readonly Client _supabaseClient; + + public PurchaseRequestService(Client supabaseClient) + { + _supabaseClient = supabaseClient; + } + + private PurchaseRequestResponseDto MapToDto(PurchaseRequestModel model) + { + return new PurchaseRequestResponseDto + { + Id = model.Id, + Status = model.Status, + Requester = model.Requester, + CreatedAt = model.CreatedAt, + UpdatedAt = model.UpdatedAt + }; + } + + private PurchaseRequestModel MapToModel(PurchaseRequestCreateDto dto) + { + return new PurchaseRequestModel + { + Status = dto.Status, + Requester = dto.Requester + }; + } + + public async Task> GetAllPurchaseRequestsAsync(PurchaseRequestSearchDto dto) + { + var response = await _supabaseClient + .From() + .Get(); + + var results = response.Models.AsEnumerable(); + + if (!string.IsNullOrWhiteSpace(dto.Status)) + { + results = results.Where(x => x.Status == dto.Status); + } + + if (dto.Requester.HasValue && dto.Requester != Guid.Empty) + { + results = results.Where(x => x.Requester == dto.Requester); + } + + return results.Select(MapToDto).ToList(); + } + + public async Task GetPurchaseRequestByIDAsync(Guid id) + { + var response = await _supabaseClient + .From() + .Where(x => x.Id == id) + .Single(); + + if (response == null) + { + throw new Exception("Purchase request not found"); + } + + return MapToDto(response); + } + + public async Task CreatePurchaseRequestAsync(PurchaseRequestCreateDto dto) + { + var newModel = MapToModel(dto); + if (newModel.Id == Guid.Empty) + newModel.Id = Guid.NewGuid(); + if (newModel.CreatedAt == default) + newModel.CreatedAt = DateTimeOffset.UtcNow; + + var response = await _supabaseClient + .From() + .Insert(new List { newModel }); + + if (response.Models is null || response.Models.Count == 0) + throw new Exception("Failed to create purchase request"); + + return MapToDto(response.Models.First()); + } + + public async Task UpdatePurchaseRequestByIDAsync(Guid id, PurchaseRequestUpdateDto dto) + { + var currentRequest = await _supabaseClient + .From() + .Where(x => x.Id == id) + .Single(); + + if (currentRequest == null) + throw new Exception("Purchase request not found"); + + if (!string.IsNullOrWhiteSpace(dto.Status)) + currentRequest.Status = dto.Status; + + if (dto.Requester != Guid.Empty) + currentRequest.Requester = dto.Requester; + + currentRequest.UpdatedAt = DateTime.UtcNow; + + var response = await _supabaseClient + .From() + .Update(currentRequest); + + if (response.Models is null || response.Models.Count == 0) + throw new Exception("Failed to update purchase request"); + + return MapToDto(response.Models.First()); + } + + public async Task DeletePurchaseRequestAsync(Guid id, string confirmationString) + { + if (confirmationString != "confirm") + return false; + + try + { + await _supabaseClient + .From() + .Where(x => x.Id == id) + .Delete(); + return true; + } + catch + { + return false; + } + } + } +} \ No newline at end of file