import React, { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { AlertCircle, Check, Loader2, Save, X } from 'lucide-react'; import { attendanceApi } from '@/services/attendanceApi'; import { toast } from 'sonner'; import * as XLSX from 'xlsx'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { AttendanceRecord } from '@/types'; interface ParsedAttendance { employeeId: string; employeeName: string; date: string; inTime: string | null; outTime: string | null; } interface PunchRecord { employeeId: string; firstName: string; date: string; punchTime: string; punchState: string; deviceName: string; isInRecord: boolean; } interface ProcessedAttendance { employeeId: string; firstName: string; date: string; firstIn: string | null; lastOut: string | null; totalHours: string; } interface AttendanceParserProps { file: File; onComplete: () => void; onCancel: () => void; } const AttendanceParser: React.FC = ({ file, onComplete, onCancel }) => { const [parsedData, setParsedData] = useState([]); const [processedData, setProcessedData] = useState([]); const [errors, setErrors] = useState([]); const [saving, setSaving] = useState(false); const [parsing, setParsing] = useState(true); const [showAllRecords, setShowAllRecords] = useState(false); useEffect(() => { parseExcelFile(); }, [file]); const parseExcelFile = async () => { setParsing(true); try { const reader = new FileReader(); reader.onload = (e) => { try { const data = new Uint8Array(e.target?.result as ArrayBuffer); const workbook = XLSX.read(data, { type: 'array' }); // Get the first sheet const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; // Convert to JSON const jsonData = XLSX.utils.sheet_to_json(worksheet); const validationErrors: string[] = []; const punchRecords: PunchRecord[] = []; // First, parse all punch records jsonData.forEach((row: any, index) => { try { if (!row['Employee ID'] || !row['Date'] || !row['Punch Time']) { validationErrors.push(`Row ${index + 2}: Missing required fields (Employee ID, Date, or Punch Time)`); return; } // Handle Employee ID as string or number const employeeId = String(row['Employee ID']).trim(); if (!employeeId) { validationErrors.push(`Row ${index + 2}: Empty Employee ID`); return; } // Parse date let date: string; try { if (typeof row['Date'] === 'string') { // Try to parse string date const dateParts = row['Date'].split(/[-\/]/); if (dateParts.length === 3) { // Assuming format is DD-MM-YYYY or MM-DD-YYYY or YYYY-MM-DD const dateObj = new Date(row['Date']); if (isNaN(dateObj.getTime())) { throw new Error('Invalid date format'); } date = dateObj.toISOString().split('T')[0]; } else { throw new Error('Invalid date format'); } } else if (typeof row['Date'] === 'number') { // Excel date (serial number) const excelDateValue = row['Date']; const dateObj = XLSX.SSF.parse_date_code(excelDateValue); const jsDate = new Date(dateObj.y, dateObj.m - 1, dateObj.d); date = jsDate.toISOString().split('T')[0]; } else { throw new Error('Invalid date format'); } } catch (error) { validationErrors.push(`Row ${index + 2}: Invalid date format`); return; } // Determine if this is an IN or OUT record based on Device Name or Punch State const deviceName = row['Device Name'] || ''; const punchState = row['Punch State'] || ''; let isInRecord = false; if (deviceName.includes('Syns-IN') || deviceName.includes('Syn-IN')) { isInRecord = true; } else if (deviceName.includes('Syns-OUT') || deviceName.includes('Syn-OUT')) { isInRecord = false; } else if (punchState === 'Check Out') { isInRecord = false; } else { // If no clear indicator, assume it's an IN record isInRecord = true; } // Create punch record const record: PunchRecord = { employeeId, firstName: row['First Name'] || '', date, punchTime: row['Punch Time'] || '', punchState, deviceName, isInRecord }; punchRecords.push(record); } catch (error) { validationErrors.push(`Row ${index + 2}: ${(error as Error).message}`); } }); // Process punch records to get first IN and last OUT for each employee by date const processedRecords = processAttendanceRecords(punchRecords); setProcessedData(processedRecords); // Convert to AttendanceRecord format for API const attendanceRecords: AttendanceRecord[] = processedRecords.map(record => ({ id: 0, // ID will be assigned by the server employeeId: parseInt(record.employeeId) || 0, // Convert back to number for API date: record.date, inTime: record.firstIn, outTime: record.lastOut, status: 'Present', // Default to 'Present' if we have punch records comments: `Total Hours: ${record.totalHours}` })); setParsedData(attendanceRecords); setErrors(validationErrors); } catch (error) { console.error('Error parsing Excel file:', error); setErrors(['Could not parse Excel file. Please check the file format.']); } finally { setParsing(false); } }; reader.onerror = () => { setErrors(['Error reading file']); setParsing(false); }; reader.readAsArrayBuffer(file); } catch (error) { console.error('Error processing file:', error); setErrors(['Error processing file']); setParsing(false); } }; // Calculate hours between two time strings const calculateHours = (inTime: string | null, outTime: string | null): string => { if (!inTime || !outTime) return '-'; try { // Parse time strings (assuming format like "09:30:00") const [inHours, inMinutes, inSeconds] = inTime.split(':').map(Number); const [outHours, outMinutes, outSeconds] = outTime.split(':').map(Number); // Convert to minutes for easier calculation const inTotalMinutes = inHours * 60 + inMinutes + (inSeconds || 0) / 60; const outTotalMinutes = outHours * 60 + outMinutes + (outSeconds || 0) / 60; // Calculate difference let diffMinutes = outTotalMinutes - inTotalMinutes; // Handle case where employee left next day (after midnight) if (diffMinutes < 0) { diffMinutes += 24 * 60; // Add 24 hours } // Convert back to hours and minutes const hours = Math.floor(diffMinutes / 60); const minutes = Math.round(diffMinutes % 60); return `${hours}h ${minutes}m`; } catch (error) { console.error('Error calculating hours:', error); return '-'; } }; // Process attendance records to get first IN and last OUT for each employee by date const processAttendanceRecords = (punchRecords: PunchRecord[]): ProcessedAttendance[] => { // Group records by employeeId and date const groupedRecords: Record = {}; punchRecords.forEach(record => { const key = `${record.employeeId}-${record.date}`; if (!groupedRecords[key]) { groupedRecords[key] = []; } groupedRecords[key].push(record); }); // Process each group to find first IN and last OUT const result: ProcessedAttendance[] = []; Object.keys(groupedRecords).forEach(key => { const records = groupedRecords[key]; // Sort records by punch time records.sort((a, b) => { return new Date(a.date + 'T' + a.punchTime).getTime() - new Date(b.date + 'T' + b.punchTime).getTime(); }); // Find first IN const inRecords = records.filter(r => r.isInRecord); const firstInRecord = inRecords.length > 0 ? inRecords[0] : null; // Find last OUT const outRecords = records.filter(r => !r.isInRecord); const lastOutRecord = outRecords.length > 0 ? outRecords[outRecords.length - 1] : null; // Calculate total hours const totalHours = calculateHours( firstInRecord?.punchTime || null, lastOutRecord?.punchTime || null ); // Add to result result.push({ employeeId: records[0].employeeId, firstName: records[0].firstName, date: records[0].date, firstIn: firstInRecord ? firstInRecord.punchTime : null, lastOut: lastOutRecord ? lastOutRecord.punchTime : null, totalHours }); }); // Sort by date and then by employee ID result.sort((a, b) => { const dateComparison = a.date.localeCompare(b.date); if (dateComparison !== 0) return dateComparison; // Try to sort by employee ID numerically if possible const empIdA = parseInt(a.employeeId); const empIdB = parseInt(b.employeeId); if (!isNaN(empIdA) && !isNaN(empIdB)) { return empIdA - empIdB; } // Fall back to string comparison return a.employeeId.localeCompare(b.employeeId); }); return result; }; const handleSave = async () => { if (parsedData.length === 0) { toast.error('No valid data to save'); return; } setSaving(true); try { await attendanceApi.uploadBulk({ month: 'Auto-detected', year: new Date().getFullYear(), records: parsedData }); toast.success(`Successfully imported ${parsedData.length} attendance records`); onComplete(); } catch (error) { console.error('Error saving attendance data:', error); toast.error('Failed to save attendance data'); } finally { setSaving(false); } }; // Export to Excel const handleExport = () => { try { // Create workbook and worksheet const wb = XLSX.utils.book_new(); // Format data for export const exportData = processedData.map(record => ({ 'Date': record.date, 'Employee ID': record.employeeId, 'Name': record.firstName, 'First IN': record.firstIn || '-', 'Last OUT': record.lastOut || '-', 'Total Hours': record.totalHours })); // Create worksheet from data const ws = XLSX.utils.json_to_sheet(exportData); // Add worksheet to workbook XLSX.utils.book_append_sheet(wb, ws, 'Attendance'); // Generate Excel file and trigger download XLSX.writeFile(wb, 'Attendance_Summary.xlsx'); toast.success('Excel file exported successfully'); } catch (error) { console.error('Error exporting to Excel:', error); toast.error('Failed to export Excel file'); } }; // Get status badge const getStatusBadge = (status: string) => { const variants: Record = { "Present": "bg-green-100 text-green-700 border-green-200", "Absent": "bg-red-100 text-red-700 border-red-200", "Half Day": "bg-yellow-100 text-yellow-700 border-yellow-200", "Leave": "bg-blue-100 text-blue-700 border-blue-200" }; return ( {status} ); }; // Toggle between showing all records and only 100 const toggleShowAllRecords = () => { setShowAllRecords(prev => !prev); }; // Get records to display based on showAllRecords flag const getDisplayRecords = () => { return showAllRecords ? processedData : processedData.slice(0, 100); }; return ( {parsing && } {!parsing && processedData.length > 0 && errors.length === 0 && } {!parsing && errors.length > 0 && } Attendance File: {file.name} {parsing ? ( 'Parsing file, please wait...' ) : ( <> {processedData.length} records found {errors.length > 0 && ` with ${errors.length} validation errors`} )} {parsing ? (

Processing file... This may take a moment.

) : ( <> {/* Display validation errors */} {errors.length > 0 && ( Validation Errors
    {errors.map((error, index) => (
  • {error}
  • ))}
)} {/* Button controls */} {processedData.length > 0 && (
{processedData.length > 100 && ( )}
Showing {getDisplayRecords().length} of {processedData.length} records
)} {/* Preview table */} {processedData.length > 0 && (
Date Employee ID Name First IN Last OUT Total Hours {getDisplayRecords().map((record, index) => ( {record.date} {record.employeeId} {record.firstName} {record.firstIn || '-'} {record.lastOut || '-'} {record.totalHours} ))}
)} {processedData.length === 0 && !parsing && (

No valid data found

The file doesn't contain any valid attendance records. Please check the file format and try again.

)} )}
); }; export default AttendanceParser;