Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions client/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ export const NOTIFICATION_CONFIG: Record<NotificationType, NotificationConfig> =
iconColor: "text-emerald-600",
cardBg: "bg-emerald-50/40",
cardBorder: "border-emerald-200/60",
actionLabel: "View Collaboration",
actionLabel: "View Chat",
getRedirectPath: (referenceId) =>
`/dashboard/collaborations/${referenceId}`,
`/dashboard/chats/chat/${referenceId}`,
},
JOIN_REJECTED: {
icon: XCircle,
Expand Down
27 changes: 17 additions & 10 deletions server/src/controllers/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,9 @@ const approveJoinRequestController = async (
},
});
}
await createNotification(tx,joinRequest.userId,NotificationType.JOIN_APPROVED,"Join Request Approved","Yay! Your request to join a collaboration has been approved",false,collaboration.chatRoom?.id || null)
});

return res.status(200).json({
success: true,
message: "Request approved successfully",
Expand Down Expand Up @@ -378,6 +379,7 @@ const rejectJoinRequestController = async (
creatorId: true,
status: true,
deletedAt: true,
title: true,
},
},
},
Expand Down Expand Up @@ -419,15 +421,18 @@ const rejectJoinRequestController = async (
});
}

await prisma.collaborationMember.update({
where: {
id: requestId,
},
data: {
joinStatus: JoinStatus.REJECTED,
},
await prisma.$transaction(async(tx)=>{
await tx.collaborationMember.update({
where: {
id: requestId,
},
data: {
joinStatus: JoinStatus.REJECTED,
},
});
await createNotification(tx,joinRequest.userId,NotificationType.JOIN_REJECTED,"Request Rejected",`Join request for ${joinRequest.collaboration.title} has been rejected.`,false,null);
});

return res.status(200).json({
success: true,
message: "Request rejected successfully",
Expand Down Expand Up @@ -723,4 +728,6 @@ const removeMemberController = async (
}
};

export { getRequestsController, createJoinRequestController, approveJoinRequestController, rejectJoinRequestController, leaveRequestController, removeMemberController };
export { getRequestsController, createJoinRequestController, approveJoinRequestController, rejectJoinRequestController, leaveRequestController, removeMemberController };


218 changes: 133 additions & 85 deletions server/src/controllers/update-collaboration.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Request, Response } from 'express';
import logger from '../utils/logger';
import prisma from '../models';
import { JoinStatus, CollaborationStatus } from '@prisma/client';
import { JoinStatus, CollaborationStatus, NotificationType } from '@prisma/client';
import createNotification from '../utils/createNotification';

const updateCollaborationController = async (req: Request, res: Response) => {
try {
Expand Down Expand Up @@ -179,7 +180,15 @@ const deleteCollaborationController = async (req: Request, res: Response) => {

const collaboration = await prisma.collaboration.findUnique({
where: { id },
select: { creatorId: true, status: true },
select: { creatorId: true, status: true, title: true },
});

// Get current approved members count
const approvedMembers = await prisma.collaborationMember.findMany({
where: {
collaborationId: id,
joinStatus: JoinStatus.APPROVED,
},
});

if (!collaboration) {
Expand Down Expand Up @@ -207,14 +216,31 @@ const deleteCollaborationController = async (req: Request, res: Response) => {
});
}


// Soft-delete: update status to CANCELLED and set deletedAt
await prisma.collaboration.update({
where: { id },
data: {
deletedAt: new Date(),
status: CollaborationStatus.CANCELLED,
},
});
await prisma.$transaction(async(tx)=>{
await tx.collaboration.update({
where: { id },
data: {
deletedAt: new Date(),
status: CollaborationStatus.CANCELLED,
},
});
await Promise.all(
approvedMembers.map((member) =>
createNotification(
tx,
member.id,
NotificationType.COLLABORATION_COMPLETED,
"Collaboration Cancelled",
`Collaboration "${collaboration.title}" has been cancelled.`,
false,
null
)
)
);
})


// TODO:
// When collaboration is cancelled (deleted):
Expand Down Expand Up @@ -269,6 +295,11 @@ const completeCollaborationController = async (req: Request, res: Response) => {
bio: true,
},
},
chatRoom: {
select:{
id: true,
}
}
},
});

Expand Down Expand Up @@ -341,88 +372,105 @@ const completeCollaborationController = async (req: Request, res: Response) => {
}

// Update status to COMPLETED
const updated = await prisma.collaboration.update({
where: { id },
data: {
status: CollaborationStatus.COMPLETED,
completedAt: new Date(),
},
include: {
creator: {
select: {
id: true,
email: true,
name: true,
onboardingCompleted: true,
username: true,
avatarUrl: true,
bio: true,
},
await prisma.$transaction(async(tx)=>{
const updated = await tx.collaboration.update({
where: { id },
data: {
status: CollaborationStatus.COMPLETED,
completedAt: new Date(),
},
members: {
where: { joinStatus: JoinStatus.APPROVED },
include: {
user: {
select: {
id: true,
name: true,
username: true,
avatarUrl: true,
include: {
creator: {
select: {
id: true,
email: true,
name: true,
onboardingCompleted: true,
username: true,
avatarUrl: true,
bio: true,
},
},
members: {
where: { joinStatus: JoinStatus.APPROVED },
include: {
user: {
select: {
id: true,
name: true,
username: true,
avatarUrl: true,
},
},
},
},
},
},
});

// TODO:
// Ratings are available for 7 days after completedAt.
// Rating availability is computed dynamically.
// No database state is required.

// TODO:
// Create COLLABORATION_COMPLETED notifications
// for all approved members except the creator.

// Chat cleanup is handled by the hourly cron.
// Delete chat room, members and messages
// after completedAt + 7 days.

// Format members to match CollaborationResponse structure
const formattedMembers = updated.members.map((m: any) => ({
id: m.user.id,
name: m.user.name,
username: m.user.username,
avatarUrl: m.user.avatarUrl,
}));
});

return res.status(200).json({
success: true,
collaboration: {
id: updated.id,
title: updated.title,
description: updated.description,
fromLocation: {
name: updated.fromLocationName,
lat: updated.fromLat,
lng: updated.fromLng,
},
toLocation: {
name: updated.toLocationName,
lat: updated.toLat,
lng: updated.toLng,
},
scheduledAt: updated.scheduledAt.toISOString(),
maxMembers: updated.maxMembers,
status: updated.status,
creator: updated.creator,
category: updated.category,
},
members: formattedMembers,
currentMembers: approvedMembersCount,
isCreator: true,
myJoinStatus: JoinStatus.APPROVED,
});
// TODO:
// Ratings are available for 7 days after completedAt.
// Rating availability is computed dynamically.
// No database state is required.

// TODO:
// Create COLLABORATION_COMPLETED notifications
// for all approved members except the creator.

// Chat cleanup is handled by the hourly cron.
// Delete chat room, members and messages
// after completedAt + 7 days.

// Format members to match CollaborationResponse structure
const formattedMembers = updated.members.map((m: any) => ({
id: m.user.id,
name: m.user.name,
username: m.user.username,
avatarUrl: m.user.avatarUrl,
}));

await Promise.all(
formattedMembers.map((member) =>
createNotification(
tx,
member.id,
NotificationType.COLLABORATION_COMPLETED,
"Collaboration Completed",
`Yo! Your collaboration "${collaboration.title}" has been completed.`,
false,
collaboration.chatRoom?.id || null
)
)
);

return res.status(200).json({
success: true,
collaboration: {
id: updated.id,
title: updated.title,
description: updated.description,
fromLocation: {
name: updated.fromLocationName,
lat: updated.fromLat,
lng: updated.fromLng,
},
toLocation: {
name: updated.toLocationName,
lat: updated.toLat,
lng: updated.toLng,
},
scheduledAt: updated.scheduledAt.toISOString(),
maxMembers: updated.maxMembers,
status: updated.status,
creator: updated.creator,
category: updated.category,
},
members: formattedMembers,
currentMembers: approvedMembersCount,
isCreator: true,
myJoinStatus: JoinStatus.APPROVED,
});
})

} catch (error) {
logger.error('Error completing collaboration', { error });
return res
Expand Down
Loading