WishlistController.cs
8,979 bytes
| 1 | using System.Security.Claims; |
|---|---|
| 2 | using Asp.Versioning; |
| 3 | using MediatR; |
| 4 | using Microsoft.AspNetCore.Authentication.JwtBearer; |
| 5 | using Microsoft.AspNetCore.Authorization; |
| 6 | using Microsoft.AspNetCore.Mvc; |
| 7 | using SplitApp.Modules.Trips.Api.Dto.v1; |
| 8 | using SplitApp.Modules.Trips.Application.Contracts; |
| 9 | using SplitApp.Modules.Trips.Domain.Entities; |
| 10 | using SplitApp.Modules.Trips.Domain.Enums; |
| 11 | using SplitApp.Shared.Messaging.Integration.Users; |
| 12 | |
| 13 | namespace SplitApp.Modules.Trips.Api.Controllers; |
| 14 | |
| 15 | [ApiVersion("1.0")] |
| 16 | [ApiController] |
| 17 | [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] |
| 18 | [Route("api/v{version:apiVersion}/[controller]")] |
| 19 | public class WishlistController : ControllerBase |
| 20 | { |
| 21 | private readonly ITripsUnitOfWork _uow; |
| 22 | private readonly IMediator _mediator; |
| 23 | private readonly IUserLookup _users; |
| 24 | |
| 25 | public WishlistController(ITripsUnitOfWork uow, IMediator mediator, IUserLookup users) |
| 26 | { |
| 27 | _uow = uow; |
| 28 | _mediator = mediator; |
| 29 | _users = users; |
| 30 | } |
| 31 | |
| 32 | [HttpGet("trip/{tripId:guid}")] |
| 33 | public async Task<ActionResult<List<WishlistItemDto>>> GetForTrip(Guid tripId) |
| 34 | { |
| 35 | var userId = CurrentUserId(); |
| 36 | if (userId == null) return Unauthorized(); |
| 37 | |
| 38 | if (!await IsParticipantAsync(tripId, userId.Value)) return Forbid(); |
| 39 | |
| 40 | var allItems = await _uow.WishlistItems.GetAllAsync(); |
| 41 | var items = allItems.Where(i => i.TripId == tripId).OrderBy(i => i.DisplayOrder).ToList(); |
| 42 | var allVotes = await _uow.WishlistVotes.GetAllAsync(); |
| 43 | |
| 44 | var addedByIds = items.Select(i => i.AddedByUserId).Distinct().ToList(); |
| 45 | var users = await _users.GetByIdsAsync(addedByIds); |
| 46 | var nameLookup = users.ToDictionary(kv => kv.Key, kv => kv.Value.DisplayName); |
| 47 | |
| 48 | return Ok(items.Select(i => MapItem(i, allVotes, userId.Value, nameLookup)).ToList()); |
| 49 | } |
| 50 | |
| 51 | [HttpPost] |
| 52 | public async Task<ActionResult<WishlistItemDto>> Create([FromBody] WishlistItemCreateDto dto) |
| 53 | { |
| 54 | var userId = CurrentUserId(); |
| 55 | if (userId == null) return Unauthorized(); |
| 56 | |
| 57 | if (!await IsParticipantAsync(dto.TripId, userId.Value)) return Forbid(); |
| 58 | if (!Enum.TryParse<EWishlistCategory>(dto.Category, true, out var category)) |
| 59 | return BadRequest($"Unknown category '{dto.Category}'."); |
| 60 | if (!Enum.TryParse<EWishlistPriority>(dto.Priority, true, out var priority)) |
| 61 | return BadRequest($"Unknown priority '{dto.Priority}'."); |
| 62 | |
| 63 | var entity = new TripWishlistItem |
| 64 | { |
| 65 | TripId = dto.TripId, |
| 66 | AddedByUserId = userId.Value, |
| 67 | Title = dto.Title, |
| 68 | Description = dto.Description, |
| 69 | Category = category, |
| 70 | Priority = priority, |
| 71 | EstimatedCost = dto.EstimatedCost, |
| 72 | Url = dto.Url, |
| 73 | Location = dto.Location, |
| 74 | IsCompleted = false, |
| 75 | DisplayOrder = 0, |
| 76 | }; |
| 77 | _uow.WishlistItems.Add(entity); |
| 78 | await _uow.SaveChangesAsync(); |
| 79 | |
| 80 | var votes = await _uow.WishlistVotes.GetAllAsync(); |
| 81 | return CreatedAtAction(null, new { id = entity.Id }, MapItem(entity, votes, userId.Value, null)); |
| 82 | } |
| 83 | |
| 84 | [HttpPut("{id:guid}")] |
| 85 | public async Task<IActionResult> Update(Guid id, [FromBody] WishlistItemCreateDto dto) |
| 86 | { |
| 87 | var userId = CurrentUserId(); |
| 88 | if (userId == null) return Unauthorized(); |
| 89 | |
| 90 | var existing = await _uow.WishlistItems.GetByIdAsync(id); |
| 91 | if (existing == null) return NotFound(); |
| 92 | |
| 93 | var canEdit = existing.AddedByUserId == userId.Value |
| 94 | || await IsOrganizerAsync(existing.TripId, userId.Value); |
| 95 | if (!canEdit) return Forbid(); |
| 96 | |
| 97 | if (!Enum.TryParse<EWishlistCategory>(dto.Category, true, out var category)) |
| 98 | return BadRequest($"Unknown category '{dto.Category}'."); |
| 99 | if (!Enum.TryParse<EWishlistPriority>(dto.Priority, true, out var priority)) |
| 100 | return BadRequest($"Unknown priority '{dto.Priority}'."); |
| 101 | |
| 102 | existing.Title = dto.Title; |
| 103 | existing.Description = dto.Description; |
| 104 | existing.Category = category; |
| 105 | existing.Priority = priority; |
| 106 | existing.EstimatedCost = dto.EstimatedCost; |
| 107 | existing.Url = dto.Url; |
| 108 | existing.Location = dto.Location; |
| 109 | |
| 110 | _uow.WishlistItems.Update(existing); |
| 111 | await _uow.SaveChangesAsync(); |
| 112 | return NoContent(); |
| 113 | } |
| 114 | |
| 115 | [HttpDelete("{id:guid}")] |
| 116 | public async Task<IActionResult> Delete(Guid id) |
| 117 | { |
| 118 | var userId = CurrentUserId(); |
| 119 | if (userId == null) return Unauthorized(); |
| 120 | |
| 121 | var existing = await _uow.WishlistItems.GetByIdAsync(id); |
| 122 | if (existing == null) return NotFound(); |
| 123 | |
| 124 | var canDelete = existing.AddedByUserId == userId.Value |
| 125 | || await IsOrganizerAsync(existing.TripId, userId.Value); |
| 126 | if (!canDelete) return Forbid(); |
| 127 | |
| 128 | var allVotes = await _uow.WishlistVotes.GetAllAsync(); |
| 129 | foreach (var v in allVotes.Where(v => v.WishlistItemId == id)) |
| 130 | { |
| 131 | await _uow.WishlistVotes.RemoveAsync(v.Id); |
| 132 | } |
| 133 | await _uow.WishlistItems.RemoveAsync(id); |
| 134 | await _uow.SaveChangesAsync(); |
| 135 | return NoContent(); |
| 136 | } |
| 137 | |
| 138 | [HttpPost("{id:guid}/vote")] |
| 139 | public async Task<IActionResult> ToggleVote(Guid id) |
| 140 | { |
| 141 | var userId = CurrentUserId(); |
| 142 | if (userId == null) return Unauthorized(); |
| 143 | |
| 144 | var item = await _uow.WishlistItems.GetByIdAsync(id); |
| 145 | if (item == null) return NotFound(); |
| 146 | |
| 147 | if (!await IsParticipantAsync(item.TripId, userId.Value)) return Forbid(); |
| 148 | |
| 149 | var allVotes = await _uow.WishlistVotes.GetAllAsync(); |
| 150 | var existing = allVotes.FirstOrDefault(v => v.WishlistItemId == id && v.UserId == userId.Value); |
| 151 | if (existing != null) |
| 152 | { |
| 153 | await _uow.WishlistVotes.RemoveAsync(existing.Id); |
| 154 | } |
| 155 | else |
| 156 | { |
| 157 | _uow.WishlistVotes.Add(new TripWishlistVote |
| 158 | { |
| 159 | WishlistItemId = id, |
| 160 | UserId = userId.Value, |
| 161 | IsInterested = true, |
| 162 | }); |
| 163 | } |
| 164 | |
| 165 | await _uow.SaveChangesAsync(); |
| 166 | return Ok(); |
| 167 | } |
| 168 | |
| 169 | [HttpPost("{id:guid}/complete")] |
| 170 | public async Task<IActionResult> ToggleComplete(Guid id) |
| 171 | { |
| 172 | var userId = CurrentUserId(); |
| 173 | if (userId == null) return Unauthorized(); |
| 174 | |
| 175 | var item = await _uow.WishlistItems.GetByIdAsync(id); |
| 176 | if (item == null) return NotFound(); |
| 177 | |
| 178 | var canToggle = item.AddedByUserId == userId.Value |
| 179 | || await IsOrganizerAsync(item.TripId, userId.Value); |
| 180 | if (!canToggle) return Forbid(); |
| 181 | |
| 182 | item.IsCompleted = !item.IsCompleted; |
| 183 | item.CompletedAt = item.IsCompleted ? DateTime.UtcNow : null; |
| 184 | _uow.WishlistItems.Update(item); |
| 185 | await _uow.SaveChangesAsync(); |
| 186 | return Ok(); |
| 187 | } |
| 188 | |
| 189 | private Guid? CurrentUserId() |
| 190 | { |
| 191 | var raw = User.FindFirstValue(ClaimTypes.NameIdentifier); |
| 192 | return Guid.TryParse(raw, out var id) ? id : null; |
| 193 | } |
| 194 | |
| 195 | private async Task<bool> IsParticipantAsync(Guid tripId, Guid userId) |
| 196 | { |
| 197 | var trip = await _uow.Trips.GetByIdAsync(tripId); |
| 198 | if (trip == null) return false; |
| 199 | if (trip.CreatedById == userId) return true; |
| 200 | var all = await _uow.Participants.GetAllAsync(); |
| 201 | return all.Any(p => p.TripId == tripId && p.UserId == userId && p.IsActive); |
| 202 | } |
| 203 | |
| 204 | private async Task<bool> IsOrganizerAsync(Guid tripId, Guid userId) |
| 205 | { |
| 206 | var trip = await _uow.Trips.GetByIdAsync(tripId); |
| 207 | if (trip == null) return false; |
| 208 | if (trip.CreatedById == userId) return true; |
| 209 | var all = await _uow.Participants.GetAllAsync(); |
| 210 | return all.Any(p => p.TripId == tripId |
| 211 | && p.UserId == userId |
| 212 | && p.IsActive |
| 213 | && p.Role == EParticipantRole.Organizer); |
| 214 | } |
| 215 | |
| 216 | private static WishlistItemDto MapItem( |
| 217 | TripWishlistItem item, |
| 218 | IEnumerable<TripWishlistVote> allVotes, |
| 219 | Guid currentUserId, |
| 220 | IDictionary<Guid, string>? userNames) |
| 221 | { |
| 222 | var votes = allVotes.Where(v => v.WishlistItemId == item.Id).ToList(); |
| 223 | return new WishlistItemDto |
| 224 | { |
| 225 | Id = item.Id, |
| 226 | TripId = item.TripId, |
| 227 | AddedByUserId = item.AddedByUserId, |
| 228 | AddedByUserName = userNames != null && userNames.TryGetValue(item.AddedByUserId, out var name) ? name : null, |
| 229 | Title = item.Title, |
| 230 | Description = item.Description, |
| 231 | Category = item.Category.ToString(), |
| 232 | Priority = item.Priority.ToString(), |
| 233 | EstimatedCost = item.EstimatedCost, |
| 234 | Url = item.Url, |
| 235 | Location = item.Location, |
| 236 | IsCompleted = item.IsCompleted, |
| 237 | VoteCount = votes.Count, |
| 238 | UserHasVoted = votes.Any(v => v.UserId == currentUserId), |
| 239 | DisplayOrder = item.DisplayOrder, |
| 240 | }; |
| 241 | } |
| 242 | } |
| 243 | |