Skip to content

Commit 7eda1fe

Browse files
Resolved attendance 'fail to save records' issues due to RLS policy and changes in tables.
1 parent be51e6c commit 7eda1fe

2 files changed

Lines changed: 371 additions & 664 deletions

File tree

‎apps/codebility/app/home/my-team/[projectId]/_components/MeetingBasedAttendance.tsx‎

Lines changed: 138 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ interface MeetingSchedule {
3232
interface MeetingBasedAttendanceProps {
3333
teamMembers: SimpleMemberData[];
3434
teamLead: SimpleMemberData | null;
35-
readOnly?: boolean; // ✅ ADDED: Controls edit access
35+
readOnly?: boolean;
3636
projectId: string;
3737
meetingSchedule: MeetingSchedule[] | null;
3838
onHasChangesUpdate?: (hasChanges: boolean) => void;
@@ -146,15 +146,12 @@ const MeetingBasedAttendance = forwardRef<any, MeetingBasedAttendanceProps>(({
146146

147147
// Toggle attendance status
148148
const toggleAttendance = (memberId: string, day: number) => {
149-
// Prevent editing in read-only mode
150149
if (readOnly) {
151150
toast.error("You don't have permission to edit attendance.");
152151
return;
153152
}
154153

155154
const scheduledMeeting = hasScheduledMeeting(day);
156-
157-
// Only allow changes for scheduled meeting days
158155
if (!scheduledMeeting) return;
159156

160157
const dateKey = `${memberId}-${selectedYear}-${String(selectedMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
@@ -164,7 +161,7 @@ const MeetingBasedAttendance = forwardRef<any, MeetingBasedAttendanceProps>(({
164161
if (currentStatus === "present") newStatus = "absent";
165162
else if (currentStatus === "absent") newStatus = "excused";
166163
else if (currentStatus === "excused") newStatus = "present";
167-
else newStatus = "absent"; // For not_scheduled, start with absent
164+
else newStatus = "absent";
168165

169166
setAttendanceData(prev => ({
170167
...prev,
@@ -175,7 +172,6 @@ const MeetingBasedAttendance = forwardRef<any, MeetingBasedAttendanceProps>(({
175172

176173
// Save all attendance changes
177174
const saveAllAttendance = useCallback(async () => {
178-
// Prevent saving in read-only mode
179175
if (readOnly) {
180176
toast.error("You don't have permission to save attendance.");
181177
return;
@@ -188,14 +184,11 @@ const MeetingBasedAttendance = forwardRef<any, MeetingBasedAttendanceProps>(({
188184
allMembers.forEach(member => {
189185
monthDays.forEach(day => {
190186
const scheduledMeeting = hasScheduledMeeting(day);
191-
192-
// Only save attendance for scheduled meeting days
193187
if (!scheduledMeeting) return;
194188

195189
const dateKey = `${member.id}-${selectedYear}-${String(selectedMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
196190
const status = attendanceData[dateKey];
197191

198-
// Skip if not_scheduled
199192
if (status === "not_scheduled") return;
200193

201194
const dateStr = `${selectedYear}-${String(selectedMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
@@ -224,7 +217,6 @@ const MeetingBasedAttendance = forwardRef<any, MeetingBasedAttendanceProps>(({
224217
toast.success("Meeting attendance saved successfully!");
225218
setHasUnsavedChanges(false);
226219

227-
// Check for attendance warnings after saving
228220
const warningResult = await checkAttendanceWarnings(projectId, selectedYear, selectedMonth);
229221
if (warningResult.success && warningResult.warnings && warningResult.warnings.length > 0) {
230222
const warningCount = warningResult.warnings.filter(w => w.notificationSent).length;
@@ -274,13 +266,12 @@ const MeetingBasedAttendance = forwardRef<any, MeetingBasedAttendanceProps>(({
274266
const countAbsences = (memberId: string) => {
275267
let count = 0;
276268
const today = new Date();
277-
today.setHours(0, 0, 0, 0); // Reset to start of day for accurate comparison
269+
today.setHours(0, 0, 0, 0);
278270

279271
monthDays.forEach(day => {
280272
const scheduledMeeting = hasScheduledMeeting(day);
281273
if (!scheduledMeeting) return;
282274

283-
// Skip future dates
284275
const currentDateToCheck = new Date(selectedYear, selectedMonth, day);
285276
if (currentDateToCheck > today) return;
286277

@@ -407,7 +398,6 @@ const MeetingBasedAttendance = forwardRef<any, MeetingBasedAttendanceProps>(({
407398
<h3 className="text-sm sm:text-base font-semibold text-gray-900 dark:text-white">
408399
Meeting Attendance Tracker
409400
</h3>
410-
{/* Read-only indicator badge */}
411401
{readOnly && (
412402
<span className="text-xs bg-amber-100 text-amber-700 dark:bg-amber-900/20 dark:text-amber-300 px-2 py-0.5 rounded-full">
413403
View Only
@@ -467,145 +457,148 @@ const MeetingBasedAttendance = forwardRef<any, MeetingBasedAttendanceProps>(({
467457

468458
{/* Attendance Grid */}
469459
<div className="overflow-x-auto max-w-full">
470-
<TooltipProvider>
471-
<table className="w-full table-auto">
472-
<thead>
473-
<tr className="border-b border-gray-200 dark:border-gray-700">
474-
<th className="sticky left-0 z-10 bg-gray-50 dark:bg-gray-900 text-left p-1 sm:p-2 text-[10px] sm:text-xs font-medium text-gray-700 dark:text-gray-300 w-28 sm:w-36">
475-
<span className="hidden sm:inline">Team Member</span>
476-
<span className="sm:hidden">Member</span>
477-
</th>
478-
{monthDays.map(day => {
479-
const scheduledMeeting = hasScheduledMeeting(day);
480-
const dayOfWeek = getDayOfWeek(selectedYear, selectedMonth, day);
481-
const dayName = getDayName(dayOfWeek);
460+
<TooltipProvider>
461+
<table className="w-full table-auto">
462+
<thead>
463+
<tr className="border-b border-gray-200 dark:border-gray-700">
464+
<th className="sticky left-0 z-10 bg-gray-50 dark:bg-gray-900 text-left p-1 sm:p-2 text-[10px] sm:text-xs font-medium text-gray-700 dark:text-gray-300 w-28 sm:w-36">
465+
<span className="hidden sm:inline">Team Member</span>
466+
<span className="sm:hidden">Member</span>
467+
</th>
468+
{/* ✅ FIXED: Tooltip is now INSIDE <th>, not wrapping it */}
469+
{monthDays.map(day => {
470+
const scheduledMeeting = hasScheduledMeeting(day);
471+
const dayOfWeek = getDayOfWeek(selectedYear, selectedMonth, day);
472+
const dayName = getDayName(dayOfWeek);
473+
474+
return (
475+
<th
476+
key={day}
477+
className={`p-0 text-center text-[9px] font-medium w-7 sm:w-8 ${
478+
scheduledMeeting
479+
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
480+
: 'bg-gray-100 dark:bg-gray-800 text-gray-400'
481+
}`}
482+
>
483+
<Tooltip>
484+
<TooltipTrigger asChild>
485+
<div>
486+
<div className="font-medium text-[8px] sm:text-[10px]">{day}</div>
487+
{scheduledMeeting && (
488+
<div className="text-[7px] text-blue-600 dark:text-blue-400">
489+
{scheduledMeeting.time}
490+
</div>
491+
)}
492+
</div>
493+
</TooltipTrigger>
494+
<TooltipContent>
495+
<p className="text-xs">
496+
{dayName.charAt(0).toUpperCase() + dayName.slice(1)}
497+
{scheduledMeeting && ` - Meeting at ${scheduledMeeting.time}`}
498+
</p>
499+
</TooltipContent>
500+
</Tooltip>
501+
</th>
502+
);
503+
})}
504+
<th className="sticky right-0 z-10 bg-green-50 dark:bg-green-900/20 text-center p-1 text-[9px] font-medium text-gray-700 dark:text-gray-300 w-14 sm:w-16">
505+
<div className="text-[8px] sm:text-[10px]">Attended</div>
506+
<div className="text-[8px] sm:text-[10px] font-normal text-gray-500">Pts</div>
507+
</th>
508+
</tr>
509+
</thead>
510+
<tbody>
511+
{allMembers.map((member, index) => {
512+
const isLead = teamLead?.id === member.id;
513+
const meetingsAttended = countMeetingsAttended(member.id);
514+
const absences = countAbsences(member.id);
515+
const hasWarning = absences >= 3;
482516

483517
return (
484-
<Tooltip key={day}>
485-
<TooltipTrigger asChild>
486-
<th
487-
className={`p-0 text-center text-[9px] font-medium w-7 sm:w-8 ${
488-
scheduledMeeting
489-
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
490-
: 'bg-gray-100 dark:bg-gray-800 text-gray-400'
491-
}`}
492-
>
493-
<div className="font-medium text-[8px] sm:text-[10px]">{day}</div>
494-
{scheduledMeeting && (
495-
<div className="text-[7px] text-blue-600 dark:text-blue-400">
496-
{scheduledMeeting.time}
497-
</div>
498-
)}
499-
</th>
500-
</TooltipTrigger>
501-
<TooltipContent>
502-
<p className="text-xs">
503-
{dayName.charAt(0).toUpperCase() + dayName.slice(1)}
504-
{scheduledMeeting && ` - Meeting at ${scheduledMeeting.time}`}
505-
</p>
506-
</TooltipContent>
507-
</Tooltip>
508-
);
509-
})}
510-
<th className="sticky right-0 z-10 bg-green-50 dark:bg-green-900/20 text-center p-1 text-[9px] font-medium text-gray-700 dark:text-gray-300 w-14 sm:w-16">
511-
<div className="text-[8px] sm:text-[10px]">Attended</div>
512-
<div className="text-[8px] sm:text-[10px] font-normal text-gray-500">Pts</div>
513-
</th>
514-
</tr>
515-
</thead>
516-
<tbody>
517-
{allMembers.map((member, index) => {
518-
const isLead = teamLead?.id === member.id;
519-
const meetingsAttended = countMeetingsAttended(member.id);
520-
const absences = countAbsences(member.id);
521-
const hasWarning = absences >= 3;
522-
523-
return (
524-
<tr
525-
key={member.id}
526-
className={`border-b border-gray-200 dark:border-gray-700 ${
527-
index % 2 === 0 ? 'bg-white dark:bg-gray-900' : 'bg-gray-50 dark:bg-gray-800/50'
528-
}`}
529-
>
530-
<td className="sticky left-0 z-10 bg-inherit p-1 sm:p-2">
531-
<div className="flex flex-col">
532-
<div className="flex items-center gap-1">
533-
<span className="font-medium text-[9px] sm:text-[11px] text-gray-900 dark:text-white truncate max-w-[50px] sm:max-w-[100px]">
534-
<span className="sm:hidden">{member.first_name.charAt(0)}. {member.last_name.charAt(0)}.</span>
535-
<span className="hidden sm:inline">{member.first_name} {member.last_name}</span>
536-
</span>
537-
{isLead && (
538-
<span className="rounded bg-blue-100 px-0.5 py-0 text-[7px] text-blue-800 dark:bg-blue-900/20 dark:text-blue-400">
539-
TL
518+
<tr
519+
key={member.id}
520+
className={`border-b border-gray-200 dark:border-gray-700 ${
521+
index % 2 === 0 ? 'bg-white dark:bg-gray-900' : 'bg-gray-50 dark:bg-gray-800/50'
522+
}`}
523+
>
524+
<td className="sticky left-0 z-10 bg-inherit p-1 sm:p-2">
525+
<div className="flex flex-col">
526+
<div className="flex items-center gap-1">
527+
<span className="font-medium text-[9px] sm:text-[11px] text-gray-900 dark:text-white truncate max-w-[50px] sm:max-w-[100px]">
528+
<span className="sm:hidden">{member.first_name.charAt(0)}. {member.last_name.charAt(0)}.</span>
529+
<span className="hidden sm:inline">{member.first_name} {member.last_name}</span>
540530
</span>
541-
)}
542-
{hasWarning && (
543-
<Tooltip>
544-
<TooltipTrigger>
545-
<AlertTriangle className="h-3 w-3 text-red-500 animate-pulse" />
546-
</TooltipTrigger>
547-
<TooltipContent>
548-
<p className="text-xs">
549-
Warning: {absences} absences - Account at risk of deactivation
550-
</p>
551-
</TooltipContent>
552-
</Tooltip>
553-
)}
554-
</div>
555-
</div>
556-
</td>
557-
{monthDays.map(day => {
558-
const scheduledMeeting = hasScheduledMeeting(day);
559-
const dateKey = `${member.id}-${selectedYear}-${String(selectedMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
560-
const status = attendanceData[dateKey] || "not_scheduled";
561-
562-
return (
563-
<td
564-
key={day}
565-
className={`p-0 sm:p-0.5 text-center w-7 sm:w-8 ${
566-
scheduledMeeting ? 'bg-blue-50 dark:bg-blue-900/20' : 'bg-gray-100 dark:bg-gray-800'
567-
}`}
568-
>
569-
<div className="flex items-center justify-center p-1">
570-
{/* Disable interaction in read-only mode */}
571-
{scheduledMeeting && !readOnly ? (
572-
<button
573-
onClick={() => toggleAttendance(member.id, day)}
574-
className="hover:scale-110 transition-transform p-1"
575-
title={`Toggle attendance for ${scheduledMeeting.day} meeting at ${scheduledMeeting.time}`}
576-
>
577-
{getStatusIcon(status, true)}
578-
</button>
579-
) : (
580-
<div className={readOnly && scheduledMeeting ? "cursor-not-allowed opacity-70" : "cursor-not-allowed"}>
581-
{getStatusIcon(scheduledMeeting ? status : "not_scheduled", !!scheduledMeeting)}
582-
</div>
531+
{isLead && (
532+
<span className="rounded bg-blue-100 px-0.5 py-0 text-[7px] text-blue-800 dark:bg-blue-900/20 dark:text-blue-400">
533+
TL
534+
</span>
535+
)}
536+
{hasWarning && (
537+
<Tooltip>
538+
<TooltipTrigger>
539+
<AlertTriangle className="h-3 w-3 text-red-500 animate-pulse" />
540+
</TooltipTrigger>
541+
<TooltipContent>
542+
<p className="text-xs">
543+
Warning: {absences} absences - Account at risk of deactivation
544+
</p>
545+
</TooltipContent>
546+
</Tooltip>
583547
)}
584548
</div>
585-
</td>
586-
);
587-
})}
588-
<td className={`sticky right-0 z-10 text-center p-0.5 ${hasWarning ? 'bg-red-50 dark:bg-red-900/20' : 'bg-green-50 dark:bg-green-900/20'}`}>
589-
<div className="space-y-0">
590-
<div className={`font-semibold text-[10px] sm:text-xs ${hasWarning ? 'text-red-700 dark:text-red-400' : 'text-green-700 dark:text-green-400'}`}>
591-
{meetingsAttended}/{totalScheduledMeetings}
592-
</div>
593-
<div className="text-[8px] sm:text-[10px] text-gray-600 dark:text-gray-400 leading-tight">
594-
+{meetingsAttended * ATTENDANCE_POINTS_PER_MEETING}
595549
</div>
596-
{hasWarning && (
597-
<div className="text-[7px] text-red-600 dark:text-red-400 font-medium">
598-
{absences} absent
550+
</td>
551+
{monthDays.map(day => {
552+
const scheduledMeeting = hasScheduledMeeting(day);
553+
const dateKey = `${member.id}-${selectedYear}-${String(selectedMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
554+
const status = attendanceData[dateKey] || "not_scheduled";
555+
556+
return (
557+
<td
558+
key={day}
559+
className={`p-0 sm:p-0.5 text-center w-7 sm:w-8 ${
560+
scheduledMeeting ? 'bg-blue-50 dark:bg-blue-900/20' : 'bg-gray-100 dark:bg-gray-800'
561+
}`}
562+
>
563+
<div className="flex items-center justify-center p-1">
564+
{scheduledMeeting && !readOnly ? (
565+
<button
566+
onClick={() => toggleAttendance(member.id, day)}
567+
className="hover:scale-110 transition-transform p-1"
568+
title={`Toggle attendance for ${scheduledMeeting.day} meeting at ${scheduledMeeting.time}`}
569+
>
570+
{getStatusIcon(status, true)}
571+
</button>
572+
) : (
573+
<div className={readOnly && scheduledMeeting ? "cursor-not-allowed opacity-70" : "cursor-not-allowed"}>
574+
{getStatusIcon(scheduledMeeting ? status : "not_scheduled", !!scheduledMeeting)}
575+
</div>
576+
)}
577+
</div>
578+
</td>
579+
);
580+
})}
581+
<td className={`sticky right-0 z-10 text-center p-0.5 ${hasWarning ? 'bg-red-50 dark:bg-red-900/20' : 'bg-green-50 dark:bg-green-900/20'}`}>
582+
<div className="space-y-0">
583+
<div className={`font-semibold text-[10px] sm:text-xs ${hasWarning ? 'text-red-700 dark:text-red-400' : 'text-green-700 dark:text-green-400'}`}>
584+
{meetingsAttended}/{totalScheduledMeetings}
585+
</div>
586+
<div className="text-[8px] sm:text-[10px] text-gray-600 dark:text-gray-400 leading-tight">
587+
+{meetingsAttended * ATTENDANCE_POINTS_PER_MEETING}
599588
</div>
600-
)}
601-
</div>
602-
</td>
603-
</tr>
604-
);
605-
})}
606-
</tbody>
607-
</table>
608-
</TooltipProvider>
589+
{hasWarning && (
590+
<div className="text-[7px] text-red-600 dark:text-red-400 font-medium">
591+
{absences} absent
592+
</div>
593+
)}
594+
</div>
595+
</td>
596+
</tr>
597+
);
598+
})}
599+
</tbody>
600+
</table>
601+
</TooltipProvider>
609602
</div>
610603

611604
{/* Legend */}

0 commit comments

Comments
 (0)