Skip to content

Commit 242c451

Browse files
Merge pull request #524 from CodebilityDev/meet-schedule/notification-reminder
Meeting Schedule Notification Reminder
2 parents 37cec45 + 2bce922 commit 242c451

4 files changed

Lines changed: 340 additions & 52 deletions

File tree

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// app/api/cron/meeting-reminders/route.ts
2+
import { createNotificationAction } from "@/lib/actions/notification.actions";
3+
import { createClient } from "@supabase/supabase-js";
4+
import { NextResponse } from "next/server";
5+
6+
export async function GET(request: Request) {
7+
// Verify cron secret for security
8+
const authHeader = request.headers.get("authorization");
9+
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
10+
return new Response("Unauthorized", { status: 401 });
11+
}
12+
13+
try {
14+
// ✅ Create Supabase client with service role for server-side operations
15+
const supabase = createClient(
16+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
17+
process.env.SUPABASE_SERVICE_ROLE_KEY!,
18+
);
19+
20+
// ✅ FIX: Use Manila timezone (Asia/Manila = UTC+8)
21+
const nowUTC = new Date();
22+
const nowManila = new Date(
23+
nowUTC.toLocaleString("en-US", { timeZone: "Asia/Manila" })
24+
);
25+
26+
const currentDay = nowManila
27+
.toLocaleDateString("en-US", {
28+
weekday: "long",
29+
timeZone: "Asia/Manila"
30+
})
31+
.toLowerCase();
32+
33+
const currentHour = nowManila.getHours();
34+
const currentMinute = nowManila.getMinutes();
35+
36+
console.log(`[Cron] Manila time: ${nowManila.toLocaleString("en-US", { timeZone: "Asia/Manila" })}`);
37+
console.log(`[Cron] Current day: ${currentDay}, Hour: ${currentHour}, Minute: ${currentMinute}`);
38+
39+
// ✅ Query projects with Supabase syntax
40+
const { data: projectsWithMeetings, error: projectsError } = await supabase
41+
.from("project")
42+
.select(
43+
`
44+
*,
45+
teamLead:team_lead_id(*),
46+
teamMembers:project_members(user:user_id(*))
47+
`,
48+
)
49+
.not("meeting_schedule", "is", null);
50+
51+
if (projectsError) {
52+
console.error("Error fetching projects:", projectsError);
53+
return NextResponse.json(
54+
{ success: false, error: "Failed to fetch projects" },
55+
{ status: 500 },
56+
);
57+
}
58+
59+
let notificationsSent = 0;
60+
61+
for (const project of projectsWithMeetings || []) {
62+
// ✅ Validate project has required fields
63+
if (!project.id || !project.name) {
64+
console.warn("Skipping project with missing id or name");
65+
continue;
66+
}
67+
68+
const projectId: string = project.id;
69+
const projectName: string = project.name;
70+
71+
const schedule = project.meeting_schedule as {
72+
selectedDays: string[];
73+
time: string;
74+
meetingLink?: string;
75+
} | null;
76+
77+
// Check if today is a meeting day
78+
if (
79+
!schedule?.time ||
80+
!schedule?.selectedDays ||
81+
!schedule.selectedDays.includes(currentDay)
82+
) {
83+
continue;
84+
}
85+
86+
// Parse and validate time
87+
const timeParts = schedule.time.split(":");
88+
if (timeParts.length !== 2 || !timeParts[0] || !timeParts[1]) continue;
89+
90+
const meetingHour = parseInt(timeParts[0]);
91+
const meetingMinute = parseInt(timeParts[1]);
92+
93+
if (isNaN(meetingHour) || isNaN(meetingMinute)) continue;
94+
95+
const meetingTimeInMinutes = meetingHour * 60 + meetingMinute;
96+
const currentTimeInMinutes = currentHour * 60 + currentMinute;
97+
const timeDifference = meetingTimeInMinutes - currentTimeInMinutes;
98+
99+
console.log(`[Project: ${projectName}] Meeting at ${schedule.time}, Time difference: ${timeDifference} minutes`);
100+
101+
// Build team members array
102+
const teamMembers = project.teamMembers
103+
? project.teamMembers.map((pm: any) => pm.user).filter(Boolean)
104+
: [];
105+
const allMembers = project.teamLead
106+
? [project.teamLead, ...teamMembers]
107+
: teamMembers;
108+
109+
if (allMembers.length === 0) continue;
110+
111+
// Helper to format time
112+
const formatTime = (time24: string): string => {
113+
const parts = time24.split(":");
114+
if (parts.length !== 2 || !parts[0] || !parts[1]) return time24;
115+
116+
const hour = parseInt(parts[0]);
117+
const minutes = parts[1];
118+
119+
if (isNaN(hour)) return time24;
120+
121+
const ampm = hour >= 12 ? "PM" : "AM";
122+
const hour12 = hour % 12 || 12;
123+
return `${hour12}:${minutes} ${ampm}`;
124+
};
125+
126+
const formattedTime = formatTime(schedule.time);
127+
128+
// ✅ MORNING REMINDER (8:00-8:15 AM Manila time for meetings today)
129+
if (currentHour === 8 && currentMinute < 15) {
130+
console.log(`[${projectName}] Sending MORNING reminder`);
131+
132+
const notificationPromises = allMembers.map((member) =>
133+
createNotificationAction({
134+
recipientId: member.id,
135+
title: "📅 Meeting Scheduled Today",
136+
message: `Reminder: "${projectName}" team meeting today at ${formattedTime}\n\nPlatform: Discord\n\nMake sure you're available!`,
137+
type: "event",
138+
priority: "medium",
139+
actionUrl: `/home/my-team/${projectId}`,
140+
metadata: {
141+
projectId,
142+
projectName,
143+
meetingTime: schedule.time,
144+
meetingTimeFormatted: formattedTime,
145+
platform: "Discord",
146+
reminderType: "morning",
147+
timestamp: nowManila.toISOString(),
148+
},
149+
}),
150+
);
151+
152+
await Promise.all(notificationPromises);
153+
notificationsSent += notificationPromises.length;
154+
}
155+
156+
// ✅ 30-MINUTE REMINDER (25-35 minutes before meeting)
157+
if (timeDifference >= 25 && timeDifference <= 35) {
158+
console.log(`[${projectName}] Sending 30-MINUTE reminder`);
159+
160+
const notificationPromises = allMembers.map((member) =>
161+
createNotificationAction({
162+
recipientId: member.id,
163+
title: "⏰ Meeting Starting Soon",
164+
message: `"${projectName}" team meeting starts in 30 minutes (${formattedTime})\n\nJoin the Discord voice channel`,
165+
type: "event",
166+
priority: "high",
167+
actionUrl: `/home/my-team/${projectId}`,
168+
metadata: {
169+
projectId,
170+
projectName,
171+
meetingTime: schedule.time,
172+
meetingTimeFormatted: formattedTime,
173+
platform: "Discord",
174+
reminderType: "30min",
175+
timestamp: nowManila.toISOString(),
176+
},
177+
}),
178+
);
179+
180+
await Promise.all(notificationPromises);
181+
notificationsSent += notificationPromises.length;
182+
}
183+
184+
// ✅ MEETING START NOTIFICATION (at meeting time, -5 to +5 min window)
185+
if (timeDifference >= -5 && timeDifference <= 5) {
186+
console.log(`[${projectName}] Sending START notification`);
187+
188+
const notificationPromises = allMembers.map((member) =>
189+
createNotificationAction({
190+
recipientId: member.id,
191+
title: "🔴 Meeting Starting Now!",
192+
message: `"${projectName}" team meeting is starting now!\n\nJoin the Discord voice channel now!`,
193+
type: "event",
194+
priority: "urgent",
195+
actionUrl: `/home/my-team/${projectId}`,
196+
metadata: {
197+
projectId,
198+
projectName,
199+
meetingTime: schedule.time,
200+
meetingTimeFormatted: formattedTime,
201+
platform: "Discord",
202+
reminderType: "start",
203+
timestamp: nowManila.toISOString(),
204+
},
205+
}),
206+
);
207+
208+
await Promise.all(notificationPromises);
209+
notificationsSent += notificationPromises.length;
210+
}
211+
}
212+
213+
return NextResponse.json({
214+
success: true,
215+
notificationsSent,
216+
manilaTime: nowManila.toLocaleString("en-US", { timeZone: "Asia/Manila" }),
217+
currentDay,
218+
timestamp: nowManila.toISOString(),
219+
projectsChecked: projectsWithMeetings?.length || 0,
220+
});
221+
} catch (error) {
222+
console.error("Error sending meeting reminders:", error);
223+
return NextResponse.json(
224+
{ success: false, error: "Internal server error" },
225+
{ status: 500 },
226+
);
227+
}
228+
}

0 commit comments

Comments
 (0)