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