fitaiProto/apps/mobile/src/components/AttendanceCalendar.tsx
2026-03-11 02:38:43 +01:00

304 lines
7.8 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { Ionicons } from "@expo/vector-icons";
import { theme } from "../styles/theme";
interface AttendanceRecord {
id: string;
checkInTime: string;
checkOutTime?: string | null;
duration?: number | null;
}
interface AttendanceCalendarProps {
attendanceRecords: AttendanceRecord[];
}
export function AttendanceCalendar({
attendanceRecords,
}: AttendanceCalendarProps) {
const [currentMonth, setCurrentMonth] = useState(new Date());
const [calendarDays, setCalendarDays] = useState<
Array<{ date: Date | null; hasAttendance: boolean; isToday: boolean }>
>([]);
useEffect(() => {
generateCalendar(currentMonth);
}, [currentMonth, attendanceRecords]);
const generateCalendar = (month: Date) => {
const year = month.getFullYear();
const monthIndex = month.getMonth();
// Get first day of month and number of days
const firstDay = new Date(year, monthIndex, 1);
const lastDay = new Date(year, monthIndex + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDayOfWeek = firstDay.getDay();
// Create attendance lookup set
const attendanceDates = new Set(
attendanceRecords.map((record) =>
new Date(record.checkInTime).toDateString(),
),
);
const today = new Date().toDateString();
// Build calendar array
const days: Array<{
date: Date | null;
hasAttendance: boolean;
isToday: boolean;
}> = [];
// Add empty cells for days before month starts
for (let i = 0; i < startingDayOfWeek; i++) {
days.push({ date: null, hasAttendance: false, isToday: false });
}
// Add days of the month
for (let day = 1; day <= daysInMonth; day++) {
const date = new Date(year, monthIndex, day);
const dateString = date.toDateString();
days.push({
date,
hasAttendance: attendanceDates.has(dateString),
isToday: dateString === today,
});
}
setCalendarDays(days);
};
const goToPreviousMonth = () => {
setCurrentMonth(
new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1),
);
};
const goToNextMonth = () => {
setCurrentMonth(
new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1),
);
};
const monthName = currentMonth.toLocaleDateString("en-US", {
month: "long",
year: "numeric",
});
return (
<View style={styles.container}>
<LinearGradient
colors={[theme.colors.white, theme.colors.gray50]}
style={[styles.card, theme.shadows.medium]}
>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Attendance Calendar</Text>
</View>
{/* Month Navigation */}
<View style={styles.monthNav}>
<TouchableOpacity
onPress={goToPreviousMonth}
style={styles.navButton}
>
<Ionicons
name="chevron-back"
size={20}
color={theme.colors.primary}
/>
</TouchableOpacity>
<Text style={styles.monthText}>{monthName}</Text>
<TouchableOpacity onPress={goToNextMonth} style={styles.navButton}>
<Ionicons
name="chevron-forward"
size={20}
color={theme.colors.primary}
/>
</TouchableOpacity>
</View>
{/* Day Headers */}
<View style={styles.dayHeaders}>
{["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].map((day) => (
<Text key={day} style={styles.dayHeader}>
{day}
</Text>
))}
</View>
{/* Calendar Grid */}
<View style={styles.calendarGrid}>
{calendarDays.map((day, index) => (
<View key={index} style={styles.dayCell}>
{day.date ? (
<View
style={[
styles.dayContent,
day.isToday && styles.todayContent,
day.hasAttendance && styles.attendanceContent,
]}
>
<Text
style={[
styles.dayText,
day.isToday && styles.todayText,
day.hasAttendance && styles.attendanceText,
]}
>
{day.date.getDate()}
</Text>
{day.hasAttendance && <View style={styles.attendanceDot} />}
</View>
) : (
<View style={styles.emptyCell} />
)}
</View>
))}
</View>
{/* Legend */}
<View style={styles.legend}>
<View style={styles.legendItem}>
<View style={styles.legendDotAttendance} />
<Text style={styles.legendText}>Attended</Text>
</View>
<View style={styles.legendItem}>
<View style={styles.legendDotToday} />
<Text style={styles.legendText}>Today</Text>
</View>
</View>
</LinearGradient>
</View>
);
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 20,
marginBottom: 20,
},
card: {
borderRadius: theme.borderRadius["2xl"],
padding: 20,
},
header: {
marginBottom: 16,
},
title: {
fontSize: theme.typography.fontSize.xl,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.gray800,
},
monthNav: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 16,
paddingHorizontal: 8,
},
navButton: {
padding: 8,
},
monthText: {
fontSize: theme.typography.fontSize.lg,
fontWeight: theme.typography.fontWeight.semibold,
color: theme.colors.gray700,
},
dayHeaders: {
flexDirection: "row",
marginBottom: 8,
},
dayHeader: {
flex: 1,
textAlign: "center",
fontSize: theme.typography.fontSize.xs,
fontWeight: theme.typography.fontWeight.semibold,
color: theme.colors.gray500,
},
calendarGrid: {
flexDirection: "row",
flexWrap: "wrap",
},
dayCell: {
width: `${100 / 7}%`,
aspectRatio: 1,
padding: 2,
},
dayContent: {
flex: 1,
justifyContent: "center",
alignItems: "center",
borderRadius: theme.borderRadius.md,
position: "relative",
},
todayContent: {
backgroundColor: theme.colors.primaryLight,
borderWidth: 1,
borderColor: theme.colors.primary,
},
attendanceContent: {
backgroundColor: theme.colors.successLight,
},
emptyCell: {
flex: 1,
},
dayText: {
fontSize: theme.typography.fontSize.sm,
color: theme.colors.gray700,
fontWeight: theme.typography.fontWeight.medium,
},
todayText: {
color: theme.colors.white,
fontWeight: theme.typography.fontWeight.bold,
},
attendanceText: {
color: theme.colors.white,
fontWeight: theme.typography.fontWeight.bold,
},
attendanceDot: {
position: "absolute",
bottom: 2,
width: 4,
height: 4,
borderRadius: 2,
backgroundColor: theme.colors.white,
},
legend: {
flexDirection: "row",
justifyContent: "center",
gap: 20,
marginTop: 16,
paddingTop: 16,
borderTopWidth: 1,
borderTopColor: theme.colors.gray200,
},
legendItem: {
flexDirection: "row",
alignItems: "center",
gap: 6,
},
legendDotAttendance: {
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: theme.colors.successLight,
},
legendDotToday: {
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: theme.colors.primaryLight,
borderWidth: 1,
borderColor: theme.colors.primary,
},
legendText: {
fontSize: theme.typography.fontSize.xs,
color: theme.colors.gray600,
},
});