|
|
| import React, { useState } from 'react'; |
| import { AttendanceRecord } from '../types'; |
| import { UserCheck, Search, Filter, Calendar, Clock, MapPin, MoreVertical, Loader2 } from 'lucide-react'; |
|
|
| interface AttendanceManagerProps { |
| attendance: AttendanceRecord[]; |
| } |
|
|
| const AttendanceManager: React.FC<AttendanceManagerProps> = ({ attendance }) => { |
| const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); |
| const [isCheckingIn, setIsCheckingIn] = useState(false); |
|
|
| const dailyAttendance = attendance.filter(a => a.date === selectedDate); |
|
|
| const stats = { |
| present: dailyAttendance.filter(a => a.status === 'PRESENT').length, |
| absent: dailyAttendance.filter(a => a.status === 'ABSENT').length, |
| total: dailyAttendance.length |
| }; |
|
|
| const handleGpsCheckIn = () => { |
| setIsCheckingIn(true); |
| if ('geolocation' in navigator) { |
| navigator.geolocation.getCurrentPosition( |
| (position) => { |
| setIsCheckingIn(false); |
| const { latitude, longitude } = position.coords; |
| alert(`GPS Check-in Successful!\nLat: ${latitude.toFixed(6)}\nLng: ${longitude.toFixed(6)}\nLocation verified with site geofence.`); |
| }, |
| (error) => { |
| setIsCheckingIn(false); |
| alert(`Check-in failed: ${error.message}. Please ensure GPS is enabled.`); |
| }, |
| { enableHighAccuracy: true, timeout: 5000, maximumAge: 0 } |
| ); |
| } else { |
| setIsCheckingIn(false); |
| alert('Geolocation is not supported by your browser.'); |
| } |
| }; |
|
|
| return ( |
| <div className="space-y-6"> |
| <div className="flex justify-between items-center bg-white p-4 rounded-2xl border border-slate-200 shadow-sm"> |
| <div> |
| <h2 className="text-xl font-bold text-slate-800">Attendance & Labor</h2> |
| <p className="text-sm text-slate-500">Track workforce productivity and geofenced attendance.</p> |
| </div> |
| <button |
| onClick={handleGpsCheckIn} |
| disabled={isCheckingIn} |
| className="flex items-center gap-2 bg-slate-800 text-white px-4 py-2 rounded-xl text-sm font-bold shadow-lg hover:bg-slate-900 transition-all disabled:opacity-50" |
| > |
| {isCheckingIn ? <Loader2 className="w-4 h-4 animate-spin" /> : <MapPin className="w-4 h-4" />} |
| {isCheckingIn ? 'Locating...' : 'GPS Check-In'} |
| </button> |
| </div> |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> |
| <div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm"> |
| <div className="flex items-center gap-3 mb-4"> |
| <div className="w-10 h-10 bg-emerald-50 text-emerald-600 rounded-xl flex items-center justify-center"> |
| <UserCheck className="w-5 h-5" /> |
| </div> |
| <div> |
| <p className="text-xs font-bold text-slate-400 uppercase">Present Today</p> |
| <p className="text-2xl font-black text-slate-800">{stats.present}</p> |
| </div> |
| </div> |
| <div className="w-full bg-slate-100 h-2 rounded-full overflow-hidden"> |
| <div |
| className="h-full bg-emerald-500" |
| style={{ width: `${(stats.present / (stats.total || 1)) * 100}%` }} |
| /> |
| </div> |
| </div> |
| |
| <div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm"> |
| <div className="flex items-center gap-3 mb-4"> |
| <div className="w-10 h-10 bg-blue-50 text-blue-600 rounded-xl flex items-center justify-center"> |
| <Calendar className="w-5 h-5" /> |
| </div> |
| <div> |
| <p className="text-xs font-bold text-slate-400 uppercase">Total Workforce</p> |
| <p className="text-2xl font-black text-slate-800">{stats.total}</p> |
| </div> |
| </div> |
| <p className="text-xs text-slate-500 font-medium">Across all categories</p> |
| </div> |
| |
| <div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm"> |
| <div className="flex items-center gap-3 mb-4"> |
| <div className="w-10 h-10 bg-amber-50 text-amber-600 rounded-xl flex items-center justify-center"> |
| <Clock className="w-5 h-5" /> |
| </div> |
| <div> |
| <p className="text-xs font-bold text-slate-400 uppercase">Avg. Productivity</p> |
| <p className="text-2xl font-black text-slate-800">84%</p> |
| </div> |
| </div> |
| <p className="text-xs text-slate-500 font-medium">Based on DPR work achieved</p> |
| </div> |
| </div> |
| |
| <div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden"> |
| <div className="p-6 border-b border-slate-100 flex flex-wrap items-center justify-between gap-4"> |
| <h3 className="font-bold text-slate-800">Daily Attendance Log</h3> |
| <div className="flex items-center gap-3"> |
| <input |
| type="date" |
| value={selectedDate} |
| onChange={(e) => setSelectedDate(e.target.value)} |
| className="px-3 py-2 bg-slate-50 border border-slate-200 rounded-xl text-sm font-medium outline-none focus:ring-2 focus:ring-blue-500" |
| /> |
| <button className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-lg transition-all"> |
| <Filter className="w-5 h-5" /> |
| </button> |
| </div> |
| </div> |
| |
| <div className="overflow-x-auto"> |
| <table className="w-full text-left border-collapse"> |
| <thead> |
| <tr className="bg-slate-50/50"> |
| <th className="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-wider">Worker Name</th> |
| <th className="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-wider">Category</th> |
| <th className="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-wider">Check In</th> |
| <th className="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-wider">Check Out</th> |
| <th className="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-wider">Status</th> |
| <th className="px-6 py-4 text-[10px] font-bold text-slate-400 uppercase tracking-wider"></th> |
| </tr> |
| </thead> |
| <tbody className="divide-y divide-slate-100"> |
| {dailyAttendance.length === 0 ? ( |
| <tr> |
| <td colSpan={6} className="px-6 py-12 text-center text-slate-500 text-sm italic"> |
| No attendance records for this date |
| </td> |
| </tr> |
| ) : ( |
| dailyAttendance.map(record => ( |
| <tr key={record.id} className="hover:bg-slate-50/50 transition-colors"> |
| <td className="px-6 py-4"> |
| <div className="flex items-center gap-3"> |
| <div className="w-8 h-8 bg-slate-100 rounded-full flex items-center justify-center text-xs font-bold text-slate-600"> |
| {record.workerName.charAt(0)} |
| </div> |
| <span className="text-sm font-bold text-slate-800">{record.workerName}</span> |
| </div> |
| </td> |
| <td className="px-6 py-4"> |
| <span className="text-xs font-medium text-slate-500">{record.category}</span> |
| </td> |
| <td className="px-6 py-4 text-sm text-slate-600">{record.checkIn}</td> |
| <td className="px-6 py-4 text-sm text-slate-600">{record.checkOut || '--:--'}</td> |
| <td className="px-6 py-4"> |
| <span className={`px-2 py-1 rounded-full text-[10px] font-bold uppercase ${ |
| record.status === 'PRESENT' ? 'text-emerald-600 bg-emerald-50' : |
| record.status === 'ABSENT' ? 'text-red-600 bg-red-50' : 'text-amber-600 bg-amber-50' |
| }`}> |
| {record.status} |
| </span> |
| </td> |
| <td className="px-6 py-4 text-right"> |
| <button className="p-1 text-slate-400 hover:text-slate-600"> |
| <MoreVertical className="w-4 h-4" /> |
| </button> |
| </td> |
| </tr> |
| )) |
| )} |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default AttendanceManager; |
|
|