Skip to content

Commit 5897972

Browse files
Zeff01claude
andcommitted
Add attendance warning system for 3+ absences
- Display warning icon (⚠️) next to members with 3 or more absences - Show red background in attendance summary column for at-risk members - Send notification to members when they reach 3 absences warning threshold - Send notification to team lead about at-risk members - Add AttendanceWarningBanner component to show summary for team leads - Check and send warnings automatically when attendance is saved - Warnings appear with animated pulse effect to draw attention - Tooltip shows "Warning: X absences - Account at risk of deactivation" 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e18efad commit 5897972

5 files changed

Lines changed: 423 additions & 6 deletions

File tree

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

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { useState, useEffect, useCallback, forwardRef, useImperativeHandle } from "react";
4-
import { ChevronLeft, ChevronRight, Circle, Save, Trophy } from "lucide-react";
4+
import { ChevronLeft, ChevronRight, Circle, Save, Trophy, AlertTriangle } from "lucide-react";
55
import { Button } from "@/components/ui/button";
66
import { SimpleMemberData } from "@/app/home/projects/actions";
77
import {
@@ -13,6 +13,8 @@ import {
1313
} from "@codevs/ui/select";
1414
import { toast } from "react-hot-toast";
1515
import { saveAttendance, getMonthlyAttendance, bulkSaveAttendance } from "../actions";
16+
import { checkAttendanceWarnings } from "../actions/attendance-warnings";
17+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@codevs/ui/tooltip";
1618

1719
const ATTENDANCE_POINTS_PER_DAY = 2;
1820

@@ -179,6 +181,15 @@ const AttendanceGrid = forwardRef<any, AttendanceGridProps>(({ teamMembers, team
179181
if (result.success) {
180182
toast.success("Attendance saved successfully! Points have been updated.");
181183
setHasUnsavedChanges(false);
184+
185+
// Check for attendance warnings after saving
186+
const warningResult = await checkAttendanceWarnings(projectId, selectedYear, selectedMonth);
187+
if (warningResult.success && warningResult.warnings && warningResult.warnings.length > 0) {
188+
const warningCount = warningResult.warnings.filter(w => w.notificationSent).length;
189+
if (warningCount > 0) {
190+
toast.warning(`${warningCount} member${warningCount > 1 ? 's' : ''} received attendance warnings`);
191+
}
192+
}
182193
} else {
183194
toast.error(result.error || "Failed to save attendance");
184195
}
@@ -212,6 +223,21 @@ const AttendanceGrid = forwardRef<any, AttendanceGridProps>(({ teamMembers, team
212223
return count;
213224
};
214225

226+
// Count absent days for a member
227+
const countAbsentDays = (memberId: string) => {
228+
let count = 0;
229+
monthDays.forEach(day => {
230+
const dateKey = `${memberId}-${selectedYear}-${String(selectedMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
231+
// Only count absences on weekdays unless weekend meetings are allowed
232+
const dayOfWeek = getDayOfWeek(selectedYear, selectedMonth, day);
233+
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
234+
if ((!isWeekend || allowWeekendMeetings) && attendanceData[dateKey] === "absent") {
235+
count++;
236+
}
237+
});
238+
return count;
239+
};
240+
215241
// Get status icon
216242
const getStatusIcon = (status: AttendanceStatus) => {
217243
switch (status) {
@@ -365,6 +391,8 @@ const AttendanceGrid = forwardRef<any, AttendanceGridProps>(({ teamMembers, team
365391
<tbody>
366392
{allMembers.map((member, index) => {
367393
const isLead = teamLead?.id === member.id;
394+
const absentDays = countAbsentDays(member.id);
395+
const hasWarning = absentDays >= 3;
368396
return (
369397
<tr
370398
key={member.id}
@@ -384,6 +412,20 @@ const AttendanceGrid = forwardRef<any, AttendanceGridProps>(({ teamMembers, team
384412
TL
385413
</span>
386414
)}
415+
{hasWarning && (
416+
<TooltipProvider>
417+
<Tooltip>
418+
<TooltipTrigger>
419+
<AlertTriangle className="h-3 w-3 text-red-500 animate-pulse" />
420+
</TooltipTrigger>
421+
<TooltipContent>
422+
<p className="text-xs">
423+
Warning: {absentDays} absences - Account at risk of deactivation
424+
</p>
425+
</TooltipContent>
426+
</Tooltip>
427+
</TooltipProvider>
428+
)}
387429
</div>
388430
<div className="text-[10px] text-gray-500 dark:text-gray-400 hidden sm:block truncate">
389431
{member.display_position || "Member"}
@@ -420,14 +462,19 @@ const AttendanceGrid = forwardRef<any, AttendanceGridProps>(({ teamMembers, team
420462
</td>
421463
);
422464
})}
423-
<td className="sticky right-0 z-10 bg-green-50 dark:bg-green-900/20 text-center p-0.5">
465+
<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'}`}>
424466
<div className="space-y-0">
425-
<div className="font-semibold text-[10px] sm:text-xs text-green-700 dark:text-green-400">
467+
<div className={`font-semibold text-[10px] sm:text-xs ${hasWarning ? 'text-red-700 dark:text-red-400' : 'text-green-700 dark:text-green-400'}`}>
426468
{countPresentDays(member.id)}
427469
</div>
428470
<div className="text-[8px] sm:text-[10px] text-gray-600 dark:text-gray-400 leading-tight">
429471
+{countPresentDays(member.id) * ATTENDANCE_POINTS_PER_DAY}
430472
</div>
473+
{hasWarning && (
474+
<div className="text-[7px] text-red-600 dark:text-red-400 font-medium">
475+
{absentDays} absent
476+
</div>
477+
)}
431478
</div>
432479
</td>
433480
</tr>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import { AlertTriangle, X } from "lucide-react";
5+
import { Button } from "@codevs/ui/button";
6+
import { getAttendanceWarningStatus } from "../actions/attendance-warnings";
7+
8+
interface AttendanceWarningBannerProps {
9+
projectId: string;
10+
isTeamLead: boolean;
11+
}
12+
13+
export default function AttendanceWarningBanner({ projectId, isTeamLead }: AttendanceWarningBannerProps) {
14+
const [warningData, setWarningData] = useState<any>(null);
15+
const [isLoading, setIsLoading] = useState(true);
16+
const [isVisible, setIsVisible] = useState(true);
17+
18+
useEffect(() => {
19+
const loadWarnings = async () => {
20+
if (!isTeamLead) {
21+
setIsLoading(false);
22+
return;
23+
}
24+
25+
const currentDate = new Date();
26+
const result = await getAttendanceWarningStatus(
27+
projectId,
28+
currentDate.getFullYear(),
29+
currentDate.getMonth()
30+
);
31+
32+
if (result.success && result.summary?.membersWithWarnings > 0) {
33+
setWarningData(result);
34+
}
35+
setIsLoading(false);
36+
};
37+
38+
loadWarnings();
39+
}, [projectId, isTeamLead]);
40+
41+
if (!isTeamLead || isLoading || !warningData || !isVisible) {
42+
return null;
43+
}
44+
45+
const { summary, data } = warningData;
46+
const membersWithWarnings = data.filter((m: any) => m.hasWarning);
47+
48+
return (
49+
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/20">
50+
<div className="flex items-start justify-between">
51+
<div className="flex items-start gap-3">
52+
<AlertTriangle className="h-5 w-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
53+
<div className="space-y-2">
54+
<div>
55+
<h3 className="font-semibold text-red-800 dark:text-red-200">
56+
Attendance Warning: {summary.membersWithWarnings} member{summary.membersWithWarnings > 1 ? 's' : ''} at risk
57+
</h3>
58+
<p className="text-sm text-red-700 dark:text-red-300 mt-1">
59+
The following members have 3 or more absences this month:
60+
</p>
61+
</div>
62+
63+
<div className="space-y-1">
64+
{membersWithWarnings.map((member: any) => (
65+
<div key={member.codevId} className="text-sm">
66+
<span className="font-medium text-red-800 dark:text-red-200">
67+
{member.name}
68+
</span>
69+
<span className="text-red-600 dark:text-red-400 ml-2">
70+
- {member.absences} absences ({member.attendancePercentage}% attendance)
71+
</span>
72+
</div>
73+
))}
74+
</div>
75+
76+
<p className="text-xs text-red-600 dark:text-red-400 mt-2">
77+
Members with 3+ absences have been notified about potential account deactivation.
78+
Please follow up with them to understand any issues.
79+
</p>
80+
</div>
81+
</div>
82+
83+
<Button
84+
variant="ghost"
85+
size="sm"
86+
onClick={() => setIsVisible(false)}
87+
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 p-1 h-auto"
88+
>
89+
<X className="h-4 w-4" />
90+
</Button>
91+
</div>
92+
</div>
93+
);
94+
}

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

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { useState, useEffect, useCallback, forwardRef, useImperativeHandle } from "react";
4-
import { ChevronLeft, ChevronRight, Circle, Calendar, Clock, Info } from "lucide-react";
4+
import { ChevronLeft, ChevronRight, Circle, Calendar, Clock, Info, AlertTriangle } from "lucide-react";
55
import { Button } from "@/components/ui/button";
66
import { SimpleMemberData } from "@/app/home/projects/actions";
77
import {
@@ -14,6 +14,7 @@ import {
1414
import { toast } from "react-hot-toast";
1515
import { saveAttendance, getMonthlyAttendance, bulkSaveAttendance } from "../actions";
1616
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@codevs/ui/tooltip";
17+
import { checkAttendanceWarnings } from "../actions/attendance-warnings";
1718

1819
const ATTENDANCE_POINTS_PER_MEETING = 2;
1920

@@ -202,6 +203,15 @@ const MeetingBasedAttendance = forwardRef<any, MeetingBasedAttendanceProps>(({
202203
if (result.success) {
203204
toast.success("Meeting attendance saved successfully!");
204205
setHasUnsavedChanges(false);
206+
207+
// Check for attendance warnings after saving
208+
const warningResult = await checkAttendanceWarnings(projectId, selectedYear, selectedMonth);
209+
if (warningResult.success && warningResult.warnings && warningResult.warnings.length > 0) {
210+
const warningCount = warningResult.warnings.filter(w => w.notificationSent).length;
211+
if (warningCount > 0) {
212+
toast.warning(`${warningCount} member${warningCount > 1 ? 's' : ''} received attendance warnings`);
213+
}
214+
}
205215
} else {
206216
toast.error(result.error || "Failed to save attendance");
207217
}
@@ -240,6 +250,21 @@ const MeetingBasedAttendance = forwardRef<any, MeetingBasedAttendanceProps>(({
240250
return count;
241251
};
242252

253+
// Count absences for a member
254+
const countAbsences = (memberId: string) => {
255+
let count = 0;
256+
monthDays.forEach(day => {
257+
const scheduledMeeting = hasScheduledMeeting(day);
258+
if (!scheduledMeeting) return;
259+
260+
const dateKey = `${memberId}-${selectedYear}-${String(selectedMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
261+
if (attendanceData[dateKey] === "absent") {
262+
count++;
263+
}
264+
});
265+
return count;
266+
};
267+
243268
// Count total scheduled meetings in the month
244269
const countTotalScheduledMeetings = () => {
245270
let count = 0;
@@ -458,6 +483,8 @@ const MeetingBasedAttendance = forwardRef<any, MeetingBasedAttendanceProps>(({
458483
{allMembers.map((member, index) => {
459484
const isLead = teamLead?.id === member.id;
460485
const meetingsAttended = countMeetingsAttended(member.id);
486+
const absences = countAbsences(member.id);
487+
const hasWarning = absences >= 3;
461488

462489
return (
463490
<tr
@@ -478,6 +505,18 @@ const MeetingBasedAttendance = forwardRef<any, MeetingBasedAttendanceProps>(({
478505
TL
479506
</span>
480507
)}
508+
{hasWarning && (
509+
<Tooltip>
510+
<TooltipTrigger>
511+
<AlertTriangle className="h-3 w-3 text-red-500 animate-pulse" />
512+
</TooltipTrigger>
513+
<TooltipContent>
514+
<p className="text-xs">
515+
Warning: {absences} absences - Account at risk of deactivation
516+
</p>
517+
</TooltipContent>
518+
</Tooltip>
519+
)}
481520
</div>
482521
</div>
483522
</td>
@@ -511,14 +550,19 @@ const MeetingBasedAttendance = forwardRef<any, MeetingBasedAttendanceProps>(({
511550
</td>
512551
);
513552
})}
514-
<td className="sticky right-0 z-10 bg-green-50 dark:bg-green-900/20 text-center p-0.5">
553+
<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'}`}>
515554
<div className="space-y-0">
516-
<div className="font-semibold text-[10px] sm:text-xs text-green-700 dark:text-green-400">
555+
<div className={`font-semibold text-[10px] sm:text-xs ${hasWarning ? 'text-red-700 dark:text-red-400' : 'text-green-700 dark:text-green-400'}`}>
517556
{meetingsAttended}/{totalScheduledMeetings}
518557
</div>
519558
<div className="text-[8px] sm:text-[10px] text-gray-600 dark:text-gray-400 leading-tight">
520559
+{meetingsAttended * ATTENDANCE_POINTS_PER_MEETING}
521560
</div>
561+
{hasWarning && (
562+
<div className="text-[7px] text-red-600 dark:text-red-400 font-medium">
563+
{absences} absent
564+
</div>
565+
)}
522566
</div>
523567
</td>
524568
</tr>

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,14 @@ const TeamDetailView = ({ projectData }: TeamDetailViewProps) => {
228228
return (
229229
<>
230230
<div className="space-y-6">
231+
{/* Attendance Warning Banner */}
232+
{viewMode === "attendance" && (
233+
<AttendanceWarningBanner
234+
projectId={projectInfo.id}
235+
isTeamLead={isTeamLead || !currentUserId}
236+
/>
237+
)}
238+
231239
{/* Meeting Schedule Banner */}
232240
{formatSchedule() && (
233241
<div className="rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 p-4">

0 commit comments

Comments
 (0)