commit
979096d2c9
@ -0,0 +1,133 @@
|
|||||||
|
package online.wesal.wesal.controller;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import online.wesal.wesal.dto.AcceptInvitationRequest;
|
||||||
|
import online.wesal.wesal.dto.ApiResponse;
|
||||||
|
import online.wesal.wesal.dto.CancelInvitationRequest;
|
||||||
|
import online.wesal.wesal.dto.CategorizedInvitationsResponse;
|
||||||
|
import online.wesal.wesal.dto.CreateInvitationRequest;
|
||||||
|
import online.wesal.wesal.dto.InvitationResponse;
|
||||||
|
import online.wesal.wesal.service.InvitationService;
|
||||||
|
import org.springframework.validation.BindingResult;
|
||||||
|
import org.springframework.validation.FieldError;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/invitations")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
@Tag(name = "Invitations", description = "Invitation management endpoints")
|
||||||
|
public class InvitationController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private InvitationService invitationService;
|
||||||
|
|
||||||
|
@PostMapping(value = "/create", consumes = "application/json", produces = "application/json")
|
||||||
|
@Operation(summary = "Create invitation", description = "Create a new invitation")
|
||||||
|
public ResponseEntity<ApiResponse<InvitationResponse>> createInvitation(
|
||||||
|
@Valid @RequestBody CreateInvitationRequest request,
|
||||||
|
BindingResult bindingResult,
|
||||||
|
Authentication authentication) {
|
||||||
|
|
||||||
|
if (bindingResult.hasErrors()) {
|
||||||
|
String errorMessage = bindingResult.getFieldErrors().stream()
|
||||||
|
.map(error -> {
|
||||||
|
String field = error.getField();
|
||||||
|
if ("title".equals(field)) return "Title is required";
|
||||||
|
if ("description".equals(field)) return "Description is required";
|
||||||
|
if ("maxParticipants".equals(field)) return "Maximum participants must be a positive number";
|
||||||
|
if ("tagId".equals(field)) return "Tag ID is required";
|
||||||
|
return error.getDefaultMessage();
|
||||||
|
})
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.error(errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String userEmail = authentication.getName();
|
||||||
|
InvitationResponse response = invitationService.createInvitation(request, userEmail);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
String message;
|
||||||
|
if (e.getMessage().contains("User not found")) {
|
||||||
|
message = "Authentication error. Please log in again.";
|
||||||
|
} else if (e.getMessage().contains("Tag not found")) {
|
||||||
|
message = "The selected tag does not exist.";
|
||||||
|
} else {
|
||||||
|
message = "Something went wrong.. We're sorry but try again later";
|
||||||
|
}
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.error(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/get")
|
||||||
|
@Operation(summary = "Get invitation", description = "Get invitation by ID")
|
||||||
|
public ResponseEntity<ApiResponse<InvitationResponse>> getInvitation(@RequestParam Long id) {
|
||||||
|
if (id == null || id <= 0) {
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.error("Valid invitation ID is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return invitationService.getInvitationById(id)
|
||||||
|
.map(invitation -> ResponseEntity.ok(ApiResponse.success(invitation)))
|
||||||
|
.orElse(ResponseEntity.status(404).body(ApiResponse.error("Invitation not found")));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/all")
|
||||||
|
@Operation(summary = "Get all categorized invitations", description = "Get invitations in three categories: created, accepted, and available")
|
||||||
|
public ResponseEntity<ApiResponse<CategorizedInvitationsResponse>> getAllCategorizedInvitations(Authentication authentication) {
|
||||||
|
try {
|
||||||
|
String userEmail = authentication.getName();
|
||||||
|
CategorizedInvitationsResponse response = invitationService.getAllCategorizedInvitations(userEmail);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/accept")
|
||||||
|
@Operation(summary = "Accept invitation", description = "Accept an invitation by ID")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> acceptInvitation(
|
||||||
|
@Valid @RequestBody AcceptInvitationRequest request,
|
||||||
|
Authentication authentication) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
String userEmail = authentication.getName();
|
||||||
|
invitationService.acceptInvitation(request.getId(), userEmail);
|
||||||
|
return ResponseEntity.ok(new ApiResponse<>(true));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/cancel")
|
||||||
|
@Operation(summary = "Cancel invitation", description = "Cancel an invitation - attendee leaves or organizer deletes entire invitation")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> cancelInvitation(
|
||||||
|
@Valid @RequestBody CancelInvitationRequest request,
|
||||||
|
Authentication authentication) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
String userEmail = authentication.getName();
|
||||||
|
invitationService.cancelInvitation(request.getId(), userEmail);
|
||||||
|
return ResponseEntity.ok(new ApiResponse<>(true));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.error(e.getMessage()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package online.wesal.wesal.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
|
||||||
|
public class AcceptInvitationRequest {
|
||||||
|
|
||||||
|
@NotNull(message = "Invitation ID is required")
|
||||||
|
@Positive(message = "Invitation ID must be positive")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
public AcceptInvitationRequest() {}
|
||||||
|
|
||||||
|
public AcceptInvitationRequest(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
package online.wesal.wesal.dto;
|
||||||
|
|
||||||
|
public class ApiResponse<T> {
|
||||||
|
|
||||||
|
private boolean status;
|
||||||
|
private String message;
|
||||||
|
private T data;
|
||||||
|
|
||||||
|
public ApiResponse() {}
|
||||||
|
|
||||||
|
public ApiResponse(boolean status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiResponse(boolean status, String message) {
|
||||||
|
this.status = status;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiResponse(boolean status, T data) {
|
||||||
|
this.status = status;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiResponse(boolean status, String message, T data) {
|
||||||
|
this.status = status;
|
||||||
|
this.message = message;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> success(T data) {
|
||||||
|
return new ApiResponse<>(true, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> error(String message) {
|
||||||
|
return new ApiResponse<>(false, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(boolean status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setData(T data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package online.wesal.wesal.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
|
||||||
|
public class CancelInvitationRequest {
|
||||||
|
|
||||||
|
@NotNull(message = "Invitation ID is required")
|
||||||
|
@Positive(message = "Invitation ID must be positive")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
public CancelInvitationRequest() {}
|
||||||
|
|
||||||
|
public CancelInvitationRequest(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package online.wesal.wesal.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CategorizedInvitationsResponse {
|
||||||
|
|
||||||
|
private List<InvitationResponse> created;
|
||||||
|
private List<InvitationResponse> accepted;
|
||||||
|
private List<InvitationResponse> available;
|
||||||
|
|
||||||
|
public CategorizedInvitationsResponse() {}
|
||||||
|
|
||||||
|
public CategorizedInvitationsResponse(List<InvitationResponse> created,
|
||||||
|
List<InvitationResponse> accepted,
|
||||||
|
List<InvitationResponse> available) {
|
||||||
|
this.created = created;
|
||||||
|
this.accepted = accepted;
|
||||||
|
this.available = available;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<InvitationResponse> getCreated() {
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreated(List<InvitationResponse> created) {
|
||||||
|
this.created = created;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<InvitationResponse> getAccepted() {
|
||||||
|
return accepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccepted(List<InvitationResponse> accepted) {
|
||||||
|
this.accepted = accepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<InvitationResponse> getAvailable() {
|
||||||
|
return available;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvailable(List<InvitationResponse> available) {
|
||||||
|
this.available = available;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
package online.wesal.wesal.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class CreateInvitationRequest {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private LocalDateTime dateTime;
|
||||||
|
|
||||||
|
private String location;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Positive
|
||||||
|
private Integer maxParticipants;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long tagId;
|
||||||
|
|
||||||
|
public CreateInvitationRequest() {}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getDateTime() {
|
||||||
|
return dateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDateTime(LocalDateTime dateTime) {
|
||||||
|
this.dateTime = dateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLocation() {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLocation(String location) {
|
||||||
|
this.location = location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getMaxParticipants() {
|
||||||
|
return maxParticipants;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxParticipants(Integer maxParticipants) {
|
||||||
|
this.maxParticipants = maxParticipants;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTagId() {
|
||||||
|
return tagId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTagId(Long tagId) {
|
||||||
|
this.tagId = tagId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,222 @@
|
|||||||
|
package online.wesal.wesal.dto;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class InvitationResponse {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String title;
|
||||||
|
private String description;
|
||||||
|
private LocalDateTime dateTime;
|
||||||
|
private String location;
|
||||||
|
private Integer maxParticipants;
|
||||||
|
private Integer currentAttendees;
|
||||||
|
private TagDto tag;
|
||||||
|
private UserDto creator;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private java.util.List<AttendeeDto> attendees;
|
||||||
|
|
||||||
|
public InvitationResponse() {}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getDateTime() {
|
||||||
|
return dateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDateTime(LocalDateTime dateTime) {
|
||||||
|
this.dateTime = dateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLocation() {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLocation(String location) {
|
||||||
|
this.location = location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getMaxParticipants() {
|
||||||
|
return maxParticipants;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxParticipants(Integer maxParticipants) {
|
||||||
|
this.maxParticipants = maxParticipants;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getCurrentAttendees() {
|
||||||
|
return currentAttendees;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentAttendees(Integer currentAttendees) {
|
||||||
|
this.currentAttendees = currentAttendees;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TagDto getTag() {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTag(TagDto tag) {
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserDto getCreator() {
|
||||||
|
return creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreator(UserDto creator) {
|
||||||
|
this.creator = creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public java.util.List<AttendeeDto> getAttendees() {
|
||||||
|
return attendees;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttendees(java.util.List<AttendeeDto> attendees) {
|
||||||
|
this.attendees = attendees;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TagDto {
|
||||||
|
private Long id;
|
||||||
|
private String name;
|
||||||
|
private String colorHex;
|
||||||
|
private String iconName;
|
||||||
|
|
||||||
|
public TagDto() {}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getColorHex() {
|
||||||
|
return colorHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColorHex(String colorHex) {
|
||||||
|
this.colorHex = colorHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIconName() {
|
||||||
|
return iconName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIconName(String iconName) {
|
||||||
|
this.iconName = iconName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UserDto {
|
||||||
|
private Long id;
|
||||||
|
private String displayName;
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
public UserDto() {}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayName(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAvatar() {
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvatar(String avatar) {
|
||||||
|
this.avatar = avatar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AttendeeDto {
|
||||||
|
private Long id;
|
||||||
|
private String displayName;
|
||||||
|
private String avatar;
|
||||||
|
private LocalDateTime joinedAt;
|
||||||
|
|
||||||
|
public AttendeeDto() {}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayName(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAvatar() {
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAvatar(String avatar) {
|
||||||
|
this.avatar = avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getJoinedAt() {
|
||||||
|
return joinedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJoinedAt(LocalDateTime joinedAt) {
|
||||||
|
this.joinedAt = joinedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
package online.wesal.wesal.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "attendees",
|
||||||
|
uniqueConstraints = @UniqueConstraint(columnNames = {"invitation_id", "user_id"}))
|
||||||
|
public class Attendee {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "invitation_id", nullable = false)
|
||||||
|
@NotNull
|
||||||
|
private Invitation invitation;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "user_id", nullable = false)
|
||||||
|
@NotNull
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime joinedAt = LocalDateTime.now();
|
||||||
|
|
||||||
|
public Attendee() {}
|
||||||
|
|
||||||
|
public Attendee(Invitation invitation, User user) {
|
||||||
|
this.invitation = invitation;
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Invitation getInvitation() {
|
||||||
|
return invitation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInvitation(Invitation invitation) {
|
||||||
|
this.invitation = invitation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUser(User user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getJoinedAt() {
|
||||||
|
return joinedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setJoinedAt(LocalDateTime joinedAt) {
|
||||||
|
this.joinedAt = joinedAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
138
backend/src/main/java/online/wesal/wesal/entity/Invitation.java
Normal file
138
backend/src/main/java/online/wesal/wesal/entity/Invitation.java
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
package online.wesal.wesal.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "invitations")
|
||||||
|
public class Invitation {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@NotBlank
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Column(nullable = false, columnDefinition = "TEXT")
|
||||||
|
@NotBlank
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
private LocalDateTime dateTime;
|
||||||
|
|
||||||
|
private String location;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@Positive
|
||||||
|
private Integer maxParticipants;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "tag_id", nullable = false)
|
||||||
|
@NotNull
|
||||||
|
private Tag tag;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "creator_id", nullable = false)
|
||||||
|
@NotNull
|
||||||
|
private User creator;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Integer currentAttendees = 0;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
|
|
||||||
|
public Invitation() {}
|
||||||
|
|
||||||
|
public Invitation(String title, String description, Integer maxParticipants, Tag tag, User creator) {
|
||||||
|
this.title = title;
|
||||||
|
this.description = description;
|
||||||
|
this.maxParticipants = maxParticipants;
|
||||||
|
this.tag = tag;
|
||||||
|
this.creator = creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getDateTime() {
|
||||||
|
return dateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDateTime(LocalDateTime dateTime) {
|
||||||
|
this.dateTime = dateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLocation() {
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLocation(String location) {
|
||||||
|
this.location = location;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getMaxParticipants() {
|
||||||
|
return maxParticipants;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMaxParticipants(Integer maxParticipants) {
|
||||||
|
this.maxParticipants = maxParticipants;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Tag getTag() {
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTag(Tag tag) {
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getCreator() {
|
||||||
|
return creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreator(User creator) {
|
||||||
|
this.creator = creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getCurrentAttendees() {
|
||||||
|
return currentAttendees;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentAttendees(Integer currentAttendees) {
|
||||||
|
this.currentAttendees = currentAttendees;
|
||||||
|
}
|
||||||
|
}
|
||||||
65
backend/src/main/java/online/wesal/wesal/entity/Tag.java
Normal file
65
backend/src/main/java/online/wesal/wesal/entity/Tag.java
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package online.wesal.wesal.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "tags")
|
||||||
|
public class Tag {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true)
|
||||||
|
@NotBlank
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@NotBlank
|
||||||
|
private String colorHex;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@NotBlank
|
||||||
|
private String iconName;
|
||||||
|
|
||||||
|
public Tag() {}
|
||||||
|
|
||||||
|
public Tag(String name, String colorHex, String iconName) {
|
||||||
|
this.name = name;
|
||||||
|
this.colorHex = colorHex;
|
||||||
|
this.iconName = iconName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getColorHex() {
|
||||||
|
return colorHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColorHex(String colorHex) {
|
||||||
|
this.colorHex = colorHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIconName() {
|
||||||
|
return iconName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIconName(String iconName) {
|
||||||
|
this.iconName = iconName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package online.wesal.wesal.repository;
|
||||||
|
|
||||||
|
import online.wesal.wesal.entity.Attendee;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface AttendeeRepository extends JpaRepository<Attendee, Long> {
|
||||||
|
|
||||||
|
boolean existsByInvitationIdAndUserId(Long invitationId, Long userId);
|
||||||
|
|
||||||
|
List<Attendee> findByInvitationId(Long invitationId);
|
||||||
|
|
||||||
|
void deleteByInvitationId(Long invitationId);
|
||||||
|
|
||||||
|
void deleteByInvitationIdAndUserId(Long invitationId, Long userId);
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package online.wesal.wesal.repository;
|
||||||
|
|
||||||
|
import online.wesal.wesal.entity.Invitation;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface InvitationRepository extends JpaRepository<Invitation, Long> {
|
||||||
|
|
||||||
|
@Query("SELECT i FROM Invitation i WHERE i.currentAttendees < i.maxParticipants AND (i.dateTime IS NULL OR i.dateTime > CURRENT_TIMESTAMP) ORDER BY i.id ASC")
|
||||||
|
List<Invitation> findAvailableInvitationsOrderByCreationDate();
|
||||||
|
|
||||||
|
@Query("SELECT i FROM Invitation i WHERE i.creator.id = :userId ORDER BY i.id DESC")
|
||||||
|
List<Invitation> findByCreatorIdOrderByCreationDateDesc(Long userId);
|
||||||
|
|
||||||
|
@Query("SELECT i FROM Invitation i JOIN Attendee a ON a.invitation.id = i.id WHERE a.user.id = :userId ORDER BY i.id DESC")
|
||||||
|
List<Invitation> findAcceptedInvitationsByUserIdOrderByCreationDateDesc(Long userId);
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package online.wesal.wesal.repository;
|
||||||
|
|
||||||
|
import online.wesal.wesal.entity.Tag;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TagRepository extends JpaRepository<Tag, Long> {
|
||||||
|
}
|
||||||
@ -0,0 +1,202 @@
|
|||||||
|
package online.wesal.wesal.service;
|
||||||
|
|
||||||
|
import online.wesal.wesal.dto.CategorizedInvitationsResponse;
|
||||||
|
import online.wesal.wesal.dto.CreateInvitationRequest;
|
||||||
|
import online.wesal.wesal.dto.InvitationResponse;
|
||||||
|
import online.wesal.wesal.entity.Attendee;
|
||||||
|
import online.wesal.wesal.entity.Invitation;
|
||||||
|
import online.wesal.wesal.entity.Tag;
|
||||||
|
import online.wesal.wesal.entity.User;
|
||||||
|
import online.wesal.wesal.repository.AttendeeRepository;
|
||||||
|
import online.wesal.wesal.repository.InvitationRepository;
|
||||||
|
import online.wesal.wesal.repository.TagRepository;
|
||||||
|
import online.wesal.wesal.repository.UserRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class InvitationService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private InvitationRepository invitationRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TagRepository tagRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AttendeeRepository attendeeRepository;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public InvitationResponse createInvitation(CreateInvitationRequest request, String userEmail) {
|
||||||
|
User creator = userRepository.findByEmail(userEmail)
|
||||||
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
|
Tag tag = tagRepository.findById(request.getTagId())
|
||||||
|
.orElseThrow(() -> new RuntimeException("Tag not found"));
|
||||||
|
|
||||||
|
Invitation invitation = new Invitation(
|
||||||
|
request.getTitle(),
|
||||||
|
request.getDescription(),
|
||||||
|
request.getMaxParticipants(),
|
||||||
|
tag,
|
||||||
|
creator
|
||||||
|
);
|
||||||
|
|
||||||
|
invitation.setDateTime(request.getDateTime());
|
||||||
|
invitation.setLocation(request.getLocation());
|
||||||
|
|
||||||
|
Invitation saved = invitationRepository.save(invitation);
|
||||||
|
return mapToResponse(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<InvitationResponse> getInvitationById(Long id) {
|
||||||
|
return invitationRepository.findById(id)
|
||||||
|
.map(this::mapToResponseWithAttendees);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<InvitationResponse> getAvailableInvitations() {
|
||||||
|
return invitationRepository.findAvailableInvitationsOrderByCreationDate()
|
||||||
|
.stream()
|
||||||
|
.map(this::mapToResponse)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public CategorizedInvitationsResponse getAllCategorizedInvitations(String userEmail) {
|
||||||
|
User user = userRepository.findByEmail(userEmail)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
|
||||||
|
|
||||||
|
List<InvitationResponse> created = invitationRepository.findByCreatorIdOrderByCreationDateDesc(user.getId())
|
||||||
|
.stream()
|
||||||
|
.map(this::mapToResponse)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<InvitationResponse> accepted = invitationRepository.findAcceptedInvitationsByUserIdOrderByCreationDateDesc(user.getId())
|
||||||
|
.stream()
|
||||||
|
.map(this::mapToResponse)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<InvitationResponse> available = invitationRepository.findAvailableInvitationsOrderByCreationDate()
|
||||||
|
.stream()
|
||||||
|
.filter(invitation -> !invitation.getCreator().getId().equals(user.getId()))
|
||||||
|
.filter(invitation -> !attendeeRepository.existsByInvitationIdAndUserId(invitation.getId(), user.getId()))
|
||||||
|
.map(this::mapToResponse)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
return new CategorizedInvitationsResponse(created, accepted, available);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void acceptInvitation(Long invitationId, String userEmail) {
|
||||||
|
User user = userRepository.findByEmail(userEmail)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
|
||||||
|
|
||||||
|
Invitation invitation = invitationRepository.findById(invitationId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("This invitation does not exist"));
|
||||||
|
|
||||||
|
if (invitation.getCreator().getId().equals(user.getId())) {
|
||||||
|
throw new RuntimeException("You cannot accept your own invitation");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invitation.getCurrentAttendees() >= invitation.getMaxParticipants()) {
|
||||||
|
throw new RuntimeException("This invitation is already full");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attendeeRepository.existsByInvitationIdAndUserId(invitationId, user.getId())) {
|
||||||
|
throw new RuntimeException("You have already accepted this invitation");
|
||||||
|
}
|
||||||
|
|
||||||
|
Attendee attendee = new Attendee(invitation, user);
|
||||||
|
attendeeRepository.save(attendee);
|
||||||
|
|
||||||
|
invitation.setCurrentAttendees(invitation.getCurrentAttendees() + 1);
|
||||||
|
invitationRepository.save(invitation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void cancelInvitation(Long invitationId, String userEmail) {
|
||||||
|
User user = userRepository.findByEmail(userEmail)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Authentication error. Please log in again"));
|
||||||
|
|
||||||
|
Invitation invitation = invitationRepository.findById(invitationId)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
|
if (invitation == null) {
|
||||||
|
throw new RuntimeException("Invitation not found or already cancelled by organizer");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invitation.getDateTime() != null && invitation.getDateTime().isBefore(LocalDateTime.now())) {
|
||||||
|
throw new RuntimeException("Cannot cancel expired invitation");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isOrganizer = invitation.getCreator().getId().equals(user.getId());
|
||||||
|
boolean isAttendee = attendeeRepository.existsByInvitationIdAndUserId(invitationId, user.getId());
|
||||||
|
|
||||||
|
if (!isOrganizer && !isAttendee) {
|
||||||
|
throw new RuntimeException("You are not associated with this invitation");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOrganizer) {
|
||||||
|
attendeeRepository.deleteByInvitationId(invitationId);
|
||||||
|
invitationRepository.deleteById(invitationId);
|
||||||
|
} else {
|
||||||
|
attendeeRepository.deleteByInvitationIdAndUserId(invitationId, user.getId());
|
||||||
|
invitation.setCurrentAttendees(invitation.getCurrentAttendees() - 1);
|
||||||
|
invitationRepository.save(invitation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvitationResponse mapToResponse(Invitation invitation) {
|
||||||
|
InvitationResponse response = new InvitationResponse();
|
||||||
|
response.setId(invitation.getId());
|
||||||
|
response.setTitle(invitation.getTitle());
|
||||||
|
response.setDescription(invitation.getDescription());
|
||||||
|
response.setDateTime(invitation.getDateTime());
|
||||||
|
response.setLocation(invitation.getLocation());
|
||||||
|
response.setMaxParticipants(invitation.getMaxParticipants());
|
||||||
|
response.setCurrentAttendees(invitation.getCurrentAttendees());
|
||||||
|
response.setCreatedAt(invitation.getCreatedAt());
|
||||||
|
|
||||||
|
InvitationResponse.TagDto tagDto = new InvitationResponse.TagDto();
|
||||||
|
tagDto.setId(invitation.getTag().getId());
|
||||||
|
tagDto.setName(invitation.getTag().getName());
|
||||||
|
tagDto.setColorHex(invitation.getTag().getColorHex());
|
||||||
|
tagDto.setIconName(invitation.getTag().getIconName());
|
||||||
|
response.setTag(tagDto);
|
||||||
|
|
||||||
|
InvitationResponse.UserDto userDto = new InvitationResponse.UserDto();
|
||||||
|
userDto.setId(invitation.getCreator().getId());
|
||||||
|
userDto.setDisplayName(invitation.getCreator().getDisplayName());
|
||||||
|
userDto.setAvatar(invitation.getCreator().getAvatar());
|
||||||
|
response.setCreator(userDto);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private InvitationResponse mapToResponseWithAttendees(Invitation invitation) {
|
||||||
|
InvitationResponse response = mapToResponse(invitation);
|
||||||
|
|
||||||
|
List<Attendee> attendees = attendeeRepository.findByInvitationId(invitation.getId());
|
||||||
|
List<InvitationResponse.AttendeeDto> attendeeDtos = attendees.stream()
|
||||||
|
.map(attendee -> {
|
||||||
|
InvitationResponse.AttendeeDto dto = new InvitationResponse.AttendeeDto();
|
||||||
|
dto.setId(attendee.getUser().getId());
|
||||||
|
dto.setDisplayName(attendee.getUser().getDisplayName());
|
||||||
|
dto.setAvatar(attendee.getUser().getAvatar());
|
||||||
|
dto.setJoinedAt(attendee.getJoinedAt());
|
||||||
|
return dto;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
response.setAttendees(attendeeDtos);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,4 +7,10 @@ class ApiConstants {
|
|||||||
// User endpoints
|
// User endpoints
|
||||||
static const String getUserEndpoint = '/getUser';
|
static const String getUserEndpoint = '/getUser';
|
||||||
static const String updateUserEndpoint = '/updateUser';
|
static const String updateUserEndpoint = '/updateUser';
|
||||||
|
|
||||||
|
// Invitation endpoints
|
||||||
|
static const String invitationsEndpoint = '/invitations';
|
||||||
|
static const String getAllInvitationsEndpoint = '/invitations/all';
|
||||||
|
static const String acceptInvitationEndpoint = '/invitations/accept';
|
||||||
|
static const String createInvitationEndpoint = '/invitations/create';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,7 +42,7 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
await Future.delayed(Duration(milliseconds: 500));
|
await Future.delayed(Duration(milliseconds: 500));
|
||||||
|
|
||||||
final isLoggedIn = await AuthService.isLoggedIn();
|
final isLoggedIn = await AuthService.isLoggedIn();
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
final userResult = await UserService.getCurrentUser();
|
final userResult = await UserService.getCurrentUser();
|
||||||
if (userResult['success'] == true) {
|
if (userResult['success'] == true) {
|
||||||
@ -56,9 +56,9 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(
|
||||||
MaterialPageRoute(builder: (context) => LandingPage()),
|
context,
|
||||||
);
|
).pushReplacement(MaterialPageRoute(builder: (context) => LandingPage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,10 +70,7 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: [
|
colors: [Color(0xFF32B0A5), Color(0xFF4600B9)],
|
||||||
Color(0xFF32B0A5),
|
|
||||||
Color(0xFF4600B9),
|
|
||||||
],
|
|
||||||
stops: [0.0, 0.5],
|
stops: [0.0, 0.5],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -334,9 +331,11 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
|
|
||||||
if (result['success'] == true) {
|
if (result['success'] == true) {
|
||||||
final userResult = await UserService.getCurrentUser(forceRefresh: true);
|
final userResult = await UserService.getCurrentUser(forceRefresh: true);
|
||||||
|
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(builder: (context) => NotificationPermissionScreen()),
|
MaterialPageRoute(
|
||||||
|
builder: (context) => NotificationPermissionScreen(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_showErrorAlert(result['message'] ?? 'Login failed');
|
_showErrorAlert(result['message'] ?? 'Login failed');
|
||||||
@ -353,10 +352,7 @@ class _SignInPageState extends State<SignInPage> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
child: Text(
|
child: Text('OK', style: TextStyle(color: Color(0xFF6A4C93))),
|
||||||
'OK',
|
|
||||||
style: TextStyle(color: Color(0xFF6A4C93)),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
207
frontend/lib/models/invitation_models.dart
Normal file
207
frontend/lib/models/invitation_models.dart
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
class InvitationTag {
|
||||||
|
final int id;
|
||||||
|
final String name;
|
||||||
|
final String colorHex;
|
||||||
|
final String iconName;
|
||||||
|
|
||||||
|
InvitationTag({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.colorHex,
|
||||||
|
required this.iconName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory InvitationTag.fromJson(Map<String, dynamic> json) {
|
||||||
|
return InvitationTag(
|
||||||
|
id: json['id'],
|
||||||
|
name: json['name'],
|
||||||
|
colorHex: json['colorHex'],
|
||||||
|
iconName: json['iconName'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvitationCreator {
|
||||||
|
final int id;
|
||||||
|
final String displayName;
|
||||||
|
final String? avatar;
|
||||||
|
|
||||||
|
InvitationCreator({
|
||||||
|
required this.id,
|
||||||
|
required this.displayName,
|
||||||
|
this.avatar,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory InvitationCreator.fromJson(Map<String, dynamic> json) {
|
||||||
|
return InvitationCreator(
|
||||||
|
id: json['id'],
|
||||||
|
displayName: json['displayName'],
|
||||||
|
avatar: json['avatar'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvitationAttendee {
|
||||||
|
final int id;
|
||||||
|
final String displayName;
|
||||||
|
final String? avatar;
|
||||||
|
final DateTime joinedAt;
|
||||||
|
|
||||||
|
InvitationAttendee({
|
||||||
|
required this.id,
|
||||||
|
required this.displayName,
|
||||||
|
this.avatar,
|
||||||
|
required this.joinedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory InvitationAttendee.fromJson(Map<String, dynamic> json) {
|
||||||
|
return InvitationAttendee(
|
||||||
|
id: json['id'],
|
||||||
|
displayName: json['displayName'],
|
||||||
|
avatar: json['avatar'],
|
||||||
|
joinedAt: DateTime.parse(json['joinedAt']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Invitation {
|
||||||
|
final int id;
|
||||||
|
final String title;
|
||||||
|
final String? description;
|
||||||
|
final DateTime? dateTime;
|
||||||
|
final String? location;
|
||||||
|
final int maxParticipants;
|
||||||
|
final int currentAttendees;
|
||||||
|
final InvitationTag tag;
|
||||||
|
final InvitationCreator creator;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
Invitation({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
this.description,
|
||||||
|
this.dateTime,
|
||||||
|
this.location,
|
||||||
|
required this.maxParticipants,
|
||||||
|
required this.currentAttendees,
|
||||||
|
required this.tag,
|
||||||
|
required this.creator,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Invitation.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Invitation(
|
||||||
|
id: json['id'],
|
||||||
|
title: json['title'],
|
||||||
|
description: json['description'],
|
||||||
|
dateTime: json['dateTime'] != null ? DateTime.parse(json['dateTime']) : null,
|
||||||
|
location: json['location'],
|
||||||
|
maxParticipants: json['maxParticipants'],
|
||||||
|
currentAttendees: json['currentAttendees'],
|
||||||
|
tag: InvitationTag.fromJson(json['tag']),
|
||||||
|
creator: InvitationCreator.fromJson(json['creator']),
|
||||||
|
createdAt: DateTime.parse(json['createdAt']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvitationsResponse {
|
||||||
|
final bool status;
|
||||||
|
final String? message;
|
||||||
|
final InvitationsData? data;
|
||||||
|
|
||||||
|
InvitationsResponse({
|
||||||
|
required this.status,
|
||||||
|
this.message,
|
||||||
|
this.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory InvitationsResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return InvitationsResponse(
|
||||||
|
status: json['status'],
|
||||||
|
message: json['message'],
|
||||||
|
data: json['data'] != null ? InvitationsData.fromJson(json['data']) : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvitationsData {
|
||||||
|
final List<Invitation> created;
|
||||||
|
final List<Invitation> accepted;
|
||||||
|
final List<Invitation> available;
|
||||||
|
|
||||||
|
InvitationsData({
|
||||||
|
required this.created,
|
||||||
|
required this.accepted,
|
||||||
|
required this.available,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory InvitationsData.fromJson(Map<String, dynamic> json) {
|
||||||
|
return InvitationsData(
|
||||||
|
created: (json['created'] as List)
|
||||||
|
.map((item) => Invitation.fromJson(item))
|
||||||
|
.toList(),
|
||||||
|
accepted: (json['accepted'] as List)
|
||||||
|
.map((item) => Invitation.fromJson(item))
|
||||||
|
.toList(),
|
||||||
|
available: (json['available'] as List)
|
||||||
|
.map((item) => Invitation.fromJson(item))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isEmpty => created.isEmpty && accepted.isEmpty && available.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvitationDetails {
|
||||||
|
final int id;
|
||||||
|
final String title;
|
||||||
|
final String? description;
|
||||||
|
final DateTime? dateTime;
|
||||||
|
final String? location;
|
||||||
|
final int maxParticipants;
|
||||||
|
final int currentAttendees;
|
||||||
|
final InvitationTag tag;
|
||||||
|
final InvitationCreator creator;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final List<InvitationAttendee> attendees;
|
||||||
|
|
||||||
|
InvitationDetails({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
this.description,
|
||||||
|
this.dateTime,
|
||||||
|
this.location,
|
||||||
|
required this.maxParticipants,
|
||||||
|
required this.currentAttendees,
|
||||||
|
required this.tag,
|
||||||
|
required this.creator,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.attendees,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory InvitationDetails.fromJson(Map<String, dynamic> json) {
|
||||||
|
return InvitationDetails(
|
||||||
|
id: json['id'],
|
||||||
|
title: json['title'],
|
||||||
|
description: json['description'],
|
||||||
|
dateTime: json['dateTime'] != null ? DateTime.parse(json['dateTime']) : null,
|
||||||
|
location: json['location'],
|
||||||
|
maxParticipants: json['maxParticipants'],
|
||||||
|
currentAttendees: json['currentAttendees'],
|
||||||
|
tag: InvitationTag.fromJson(json['tag']),
|
||||||
|
creator: InvitationCreator.fromJson(json['creator']),
|
||||||
|
createdAt: DateTime.parse(json['createdAt']),
|
||||||
|
attendees: (json['attendees'] as List)
|
||||||
|
.map((item) => InvitationAttendee.fromJson(item))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get status {
|
||||||
|
if (currentAttendees >= maxParticipants) {
|
||||||
|
return 'Full';
|
||||||
|
}
|
||||||
|
return 'Available';
|
||||||
|
}
|
||||||
|
}
|
||||||
550
frontend/lib/screens/pages/invitation_details_page.dart
Normal file
550
frontend/lib/screens/pages/invitation_details_page.dart
Normal file
@ -0,0 +1,550 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../models/invitation_models.dart';
|
||||||
|
import '../../services/invitations_service.dart';
|
||||||
|
import '../../utils/invitation_utils.dart';
|
||||||
|
|
||||||
|
class InvitationDetailsPage extends StatefulWidget {
|
||||||
|
final int invitationId;
|
||||||
|
final bool isOwner;
|
||||||
|
final bool isParticipant;
|
||||||
|
|
||||||
|
const InvitationDetailsPage({
|
||||||
|
Key? key,
|
||||||
|
required this.invitationId,
|
||||||
|
required this.isOwner,
|
||||||
|
this.isParticipant = true,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_InvitationDetailsPageState createState() => _InvitationDetailsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
||||||
|
InvitationDetails? _invitationDetails;
|
||||||
|
bool _isLoading = true;
|
||||||
|
bool _isCancelling = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadInvitationDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadInvitationDetails() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await InvitationsService.getInvitationDetails(widget.invitationId);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
if (result['success']) {
|
||||||
|
_invitationDetails = result['data'];
|
||||||
|
} else {
|
||||||
|
_errorMessage = result['message'];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAvatarOrInitial(String displayName, String? avatar) {
|
||||||
|
return Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Color(0xFF6A4C93).withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: avatar != null
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Image.network(
|
||||||
|
avatar,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
displayName.isNotEmpty ? displayName[0].toUpperCase() : '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Center(
|
||||||
|
child: Text(
|
||||||
|
displayName.isNotEmpty ? displayName[0].toUpperCase() : '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _cancelInvitation() async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(widget.isOwner ? 'Cancel Invitation' : 'Leave Invitation'),
|
||||||
|
content: Text(
|
||||||
|
widget.isOwner
|
||||||
|
? 'Are you sure you want to cancel this invitation? This action cannot be undone.'
|
||||||
|
: 'Are you sure you want to leave this invitation?',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: Text('No'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
child: Text('Yes'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed == true) {
|
||||||
|
setState(() {
|
||||||
|
_isCancelling = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await InvitationsService.cancelInvitation(widget.invitationId);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isCancelling = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result['success']) {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(result['message'] ?? 'Action completed successfully'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(result['message'] ?? 'Failed to complete action'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.grey[50],
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
'Invitation Details',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Color(0xFF6A4C93),
|
||||||
|
elevation: 0,
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: Size.fromHeight(1),
|
||||||
|
child: Container(height: 1, color: Colors.grey[200]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: _isLoading
|
||||||
|
? Center(
|
||||||
|
child: CircularProgressIndicator(color: Color(0xFF6A4C93)),
|
||||||
|
)
|
||||||
|
: _errorMessage != null
|
||||||
|
? Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline, size: 80, color: Colors.red[400]),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
_errorMessage!,
|
||||||
|
style: TextStyle(fontSize: 16, color: Colors.red[600]),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _loadInvitationDetails,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: Text('Retry'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.08),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: InvitationUtils.getColorFromHex(
|
||||||
|
_invitationDetails!.tag.colorHex,
|
||||||
|
).withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
InvitationUtils.getIconFromName(
|
||||||
|
_invitationDetails!.tag.iconName,
|
||||||
|
),
|
||||||
|
color: InvitationUtils.getColorFromHex(
|
||||||
|
_invitationDetails!.tag.colorHex,
|
||||||
|
),
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_invitationDetails!.title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(top: 8),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: InvitationUtils.getColorFromHex(
|
||||||
|
_invitationDetails!.tag.colorHex,
|
||||||
|
).withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_invitationDetails!.tag.name,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: InvitationUtils.getColorFromHex(
|
||||||
|
_invitationDetails!.tag.colorHex,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _invitationDetails!.status == 'Available'
|
||||||
|
? Colors.green.withOpacity(0.1)
|
||||||
|
: Colors.orange.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
_invitationDetails!.status,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: _invitationDetails!.status == 'Available'
|
||||||
|
? Colors.green[700]
|
||||||
|
: Colors.orange[700],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_invitationDetails!.description != null &&
|
||||||
|
_invitationDetails!.description!.isNotEmpty) ...[
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
'Description',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
_invitationDetails!.description!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (_invitationDetails!.location != null) ...[
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.location_on, size: 20, color: Colors.grey[600]),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_invitationDetails!.location!,
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (_invitationDetails!.dateTime != null) ...[
|
||||||
|
if (_invitationDetails!.location != null) SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.schedule, size: 20, color: Colors.grey[600]),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
InvitationUtils.getRelativeDateTime(
|
||||||
|
_invitationDetails!.dateTime!,
|
||||||
|
),
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.people, size: 20, color: Colors.grey[600]),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'${_invitationDetails!.currentAttendees} / ${_invitationDetails!.maxParticipants} participants',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.08),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Organizer',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Color(0xFF6A4C93).withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'ORGANIZER',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildAvatarOrInitial(
|
||||||
|
_invitationDetails!.creator.displayName,
|
||||||
|
_invitationDetails!.creator.avatar,
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
_invitationDetails!.creator.displayName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_invitationDetails!.attendees.isNotEmpty) ...[
|
||||||
|
SizedBox(height: 24),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.08),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Attendees (${_invitationDetails!.attendees.length})',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
...List.generate(_invitationDetails!.attendees.length, (index) {
|
||||||
|
final attendee = _invitationDetails!.attendees[index];
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.only(bottom: index < _invitationDetails!.attendees.length - 1 ? 12 : 0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_buildAvatarOrInitial(
|
||||||
|
attendee.displayName,
|
||||||
|
attendee.avatar,
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
attendee.displayName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Joined ${InvitationUtils.getRelativeTime(attendee.joinedAt)}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (widget.isParticipant) ...[
|
||||||
|
SizedBox(height: 32),
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 56,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _isCancelling ? null : _cancelInvitation,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
elevation: 2,
|
||||||
|
),
|
||||||
|
child: _isCancelling
|
||||||
|
? SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
widget.isOwner ? 'Cancel Invitation' : 'Leave Invitation',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
SizedBox(height: 32),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -493,7 +493,8 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
bottom: PreferredSize(
|
bottom: PreferredSize(
|
||||||
preferredSize: Size.fromHeight(1),
|
preferredSize: Size.fromHeight(1),
|
||||||
child: Container(height: 1, color: Colors.grey[200]),
|
child: Container(height: 1, color: Colors.grey[200]),
|
||||||
),
|
),
|
||||||
|
automaticallyImplyLeading: true,
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Padding(
|
||||||
padding: EdgeInsets.all(16),
|
padding: EdgeInsets.all(16),
|
||||||
|
|||||||
217
frontend/lib/services/invitations_service.dart
Normal file
217
frontend/lib/services/invitations_service.dart
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import '../constants/api_constants.dart';
|
||||||
|
import '../models/invitation_models.dart';
|
||||||
|
import 'http_service.dart';
|
||||||
|
|
||||||
|
class InvitationsService {
|
||||||
|
static Future<Map<String, dynamic>> getAllInvitations() async {
|
||||||
|
try {
|
||||||
|
final response = await HttpService.get(
|
||||||
|
ApiConstants.getAllInvitationsEndpoint,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
final invitationsResponse = InvitationsResponse.fromJson(responseData);
|
||||||
|
|
||||||
|
if (invitationsResponse.status) {
|
||||||
|
return {'success': true, 'data': invitationsResponse.data};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message':
|
||||||
|
invitationsResponse.message ?? 'Failed to fetch invitations',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (response.statusCode == 401) {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Session expired. Please login again.',
|
||||||
|
};
|
||||||
|
} else if (response.statusCode == 403) {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Access denied. Invalid credentials.',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Server error (${response.statusCode})',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching invitations: $e');
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Network error. Please check your connection.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> acceptInvitation(int invitationId) async {
|
||||||
|
try {
|
||||||
|
final response = await HttpService.post(
|
||||||
|
ApiConstants.acceptInvitationEndpoint,
|
||||||
|
{'id': invitationId},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
return {
|
||||||
|
'success': responseData['status'] ?? false,
|
||||||
|
'message': responseData['message'] ?? 'Request completed',
|
||||||
|
};
|
||||||
|
} else if (response.statusCode == 401) {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Session expired. Please login again.',
|
||||||
|
};
|
||||||
|
} else if (response.statusCode == 403) {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Access denied. Invalid credentials.',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Server error (${response.statusCode})',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error accepting invitation: $e');
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Network error. Please check your connection.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> createInvitation(Map<String, dynamic> invitationData) async {
|
||||||
|
try {
|
||||||
|
final response = await HttpService.post(
|
||||||
|
ApiConstants.createInvitationEndpoint,
|
||||||
|
invitationData,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
if (responseData['status'] == true) {
|
||||||
|
return {
|
||||||
|
'success': true,
|
||||||
|
'data': responseData['data'],
|
||||||
|
'message': responseData['message'],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': responseData['message'] ?? 'Failed to create invitation',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (response.statusCode == 401) {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Session expired. Please login again.',
|
||||||
|
};
|
||||||
|
} else if (response.statusCode == 403) {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Access denied. Invalid credentials.',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Server error (${response.statusCode})',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error creating invitation: $e');
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Network error. Please check your connection.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> getInvitationDetails(int invitationId) async {
|
||||||
|
try {
|
||||||
|
final response = await HttpService.get(
|
||||||
|
'${ApiConstants.invitationsEndpoint}/get?id=$invitationId',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
if (responseData['status'] == true) {
|
||||||
|
final invitationDetails = InvitationDetails.fromJson(responseData['data']);
|
||||||
|
return {
|
||||||
|
'success': true,
|
||||||
|
'data': invitationDetails,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': responseData['message'] ?? 'Failed to get invitation details',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (response.statusCode == 401) {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Session expired. Please login again.',
|
||||||
|
};
|
||||||
|
} else if (response.statusCode == 403) {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Access denied. Invalid credentials.',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Server error (${response.statusCode})',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error getting invitation details: $e');
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Network error. Please check your connection.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> cancelInvitation(int invitationId) async {
|
||||||
|
try {
|
||||||
|
final response = await HttpService.post(
|
||||||
|
'${ApiConstants.invitationsEndpoint}/cancel',
|
||||||
|
{'id': invitationId},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
return {
|
||||||
|
'success': responseData['status'] ?? false,
|
||||||
|
'message': responseData['message'] ?? 'Request completed',
|
||||||
|
};
|
||||||
|
} else if (response.statusCode == 401) {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Session expired. Please login again.',
|
||||||
|
};
|
||||||
|
} else if (response.statusCode == 403) {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Access denied. Invalid credentials.',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Server error (${response.statusCode})',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error canceling invitation: $e');
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Network error. Please check your connection.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
frontend/lib/utils/invitation_utils.dart
Normal file
138
frontend/lib/utils/invitation_utils.dart
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class InvitationUtils {
|
||||||
|
// Centralized tag configuration
|
||||||
|
static const List<Map<String, dynamic>> availableTags = [
|
||||||
|
{"id": 1, "name": "Coffee", "color_hex": "#8B4513", "icon_name": "coffee"},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Lunch",
|
||||||
|
"color_hex": "#FF6347",
|
||||||
|
"icon_name": "restaurant",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Exercise",
|
||||||
|
"color_hex": "#32CD32",
|
||||||
|
"icon_name": "fitness_center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "Sports",
|
||||||
|
"color_hex": "#FF4500",
|
||||||
|
"icon_name": "sports_soccer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "Gaming",
|
||||||
|
"color_hex": "#9932CC",
|
||||||
|
"icon_name": "sports_esports",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"name": "Learning/Networking",
|
||||||
|
"color_hex": "#4169E1",
|
||||||
|
"icon_name": "school",
|
||||||
|
},
|
||||||
|
{"id": 7, "name": "Other", "color_hex": "#708090", "icon_name": "category"},
|
||||||
|
];
|
||||||
|
|
||||||
|
static IconData getIconFromName(String iconName) {
|
||||||
|
switch (iconName) {
|
||||||
|
// Primary supported icons (from availableTags)
|
||||||
|
case 'coffee':
|
||||||
|
return Icons.coffee;
|
||||||
|
case 'restaurant':
|
||||||
|
return Icons.restaurant;
|
||||||
|
case 'fitness_center':
|
||||||
|
return Icons.fitness_center;
|
||||||
|
case 'sports_soccer':
|
||||||
|
return Icons.sports_soccer;
|
||||||
|
case 'sports_esports':
|
||||||
|
return Icons.sports_esports;
|
||||||
|
case 'school':
|
||||||
|
return Icons.school;
|
||||||
|
case 'category':
|
||||||
|
return Icons.category;
|
||||||
|
default:
|
||||||
|
return Icons.category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Color getColorFromHex(String hexColor) {
|
||||||
|
String cleanHex = hexColor.replaceAll('#', '');
|
||||||
|
if (cleanHex.length == 6) {
|
||||||
|
cleanHex = 'FF' + cleanHex;
|
||||||
|
}
|
||||||
|
return Color(int.parse(cleanHex, radix: 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getRelativeTime(DateTime dateTime) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final difference = now.difference(dateTime);
|
||||||
|
|
||||||
|
if (difference.inMinutes < 1) {
|
||||||
|
return 'just now';
|
||||||
|
} else if (difference.inMinutes < 60) {
|
||||||
|
return '${difference.inMinutes}m ago';
|
||||||
|
} else if (difference.inHours < 24) {
|
||||||
|
return '${difference.inHours}h ago';
|
||||||
|
} else if (difference.inDays == 1) {
|
||||||
|
return 'yesterday';
|
||||||
|
} else if (difference.inDays < 7) {
|
||||||
|
return '${difference.inDays}d ago';
|
||||||
|
} else {
|
||||||
|
return '${dateTime.day}/${dateTime.month}/${dateTime.year}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getRelativeDateTime(DateTime dateTime) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
final tomorrow = today.add(Duration(days: 1));
|
||||||
|
final eventDate = DateTime(dateTime.year, dateTime.month, dateTime.day);
|
||||||
|
|
||||||
|
String timeString = _formatTime(dateTime);
|
||||||
|
|
||||||
|
if (eventDate == today) {
|
||||||
|
return 'today at $timeString';
|
||||||
|
} else if (eventDate == tomorrow) {
|
||||||
|
return 'tomorrow at $timeString';
|
||||||
|
} else if (eventDate.isAfter(today) &&
|
||||||
|
eventDate.isBefore(today.add(Duration(days: 7)))) {
|
||||||
|
List<String> weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||||
|
return '${weekdays[eventDate.weekday - 1]} at $timeString';
|
||||||
|
} else {
|
||||||
|
return '${eventDate.day}/${eventDate.month} at $timeString';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _formatTime(DateTime dateTime) {
|
||||||
|
int hour = dateTime.hour;
|
||||||
|
String period = hour >= 12 ? 'PM' : 'AM';
|
||||||
|
hour = hour > 12 ? hour - 12 : (hour == 0 ? 12 : hour);
|
||||||
|
String minute = dateTime.minute.toString().padLeft(2, '0');
|
||||||
|
return '$hour:$minute $period';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String truncateDescription(String? description, {int maxLength = 80}) {
|
||||||
|
if (description == null || description.isEmpty) return '';
|
||||||
|
if (description.length <= maxLength) return description;
|
||||||
|
return '${description.substring(0, maxLength)}...';
|
||||||
|
}
|
||||||
|
|
||||||
|
static String getParticipantsStatus(int current, int max) {
|
||||||
|
return '$current/$max';
|
||||||
|
}
|
||||||
|
|
||||||
|
static Color getParticipantsStatusColor(int current, int max) {
|
||||||
|
int needed = max - current;
|
||||||
|
if (needed <= 2 && needed > 0) {
|
||||||
|
return Colors.orange;
|
||||||
|
} else if (current == max) {
|
||||||
|
return Colors.green;
|
||||||
|
} else {
|
||||||
|
return Colors.blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user