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
|
||||
static const String getUserEndpoint = '/getUser';
|
||||
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));
|
||||
|
||||
final isLoggedIn = await AuthService.isLoggedIn();
|
||||
|
||||
|
||||
if (isLoggedIn) {
|
||||
final userResult = await UserService.getCurrentUser();
|
||||
if (userResult['success'] == true) {
|
||||
@ -56,9 +56,9 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => LandingPage()),
|
||||
);
|
||||
Navigator.of(
|
||||
context,
|
||||
).pushReplacement(MaterialPageRoute(builder: (context) => LandingPage()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,10 +70,7 @@ class _SplashScreenState extends State<SplashScreen> {
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFF32B0A5),
|
||||
Color(0xFF4600B9),
|
||||
],
|
||||
colors: [Color(0xFF32B0A5), Color(0xFF4600B9)],
|
||||
stops: [0.0, 0.5],
|
||||
),
|
||||
),
|
||||
@ -334,9 +331,11 @@ class _SignInPageState extends State<SignInPage> {
|
||||
|
||||
if (result['success'] == true) {
|
||||
final userResult = await UserService.getCurrentUser(forceRefresh: true);
|
||||
|
||||
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => NotificationPermissionScreen()),
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NotificationPermissionScreen(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
_showErrorAlert(result['message'] ?? 'Login failed');
|
||||
@ -353,10 +352,7 @@ class _SignInPageState extends State<SignInPage> {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(
|
||||
'OK',
|
||||
style: TextStyle(color: Color(0xFF6A4C93)),
|
||||
),
|
||||
child: Text('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(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Container(height: 1, color: Colors.grey[200]),
|
||||
),
|
||||
),
|
||||
automaticallyImplyLeading: true,
|
||||
),
|
||||
body: Padding(
|
||||
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