Merge pull request #3 from sBubshait/feature/invitations

Invitations
This commit is contained in:
Saleh Bubshait 2025-07-23 09:16:56 +03:00 committed by GitHub
commit 979096d2c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 3244 additions and 224 deletions

View File

@ -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"));
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View 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;
}
}

View 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;
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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> {
}

View File

@ -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;
}
}

View File

@ -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';
}

View File

@ -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))),
),
],
),

View 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';
}
}

View 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

View File

@ -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),

View 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.',
};
}
}
}

View 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;
}
}
}