erp-platform/ui/src/views/intranet/HR/LeaveManagement.tsx
2025-10-20 14:32:41 +03:00

426 lines
19 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import {
HiCalendar,
HiClock,
HiCheckCircle,
HiXCircle,
HiPlus,
HiFunnel,
HiXMark
} from 'react-icons/hi2'
import dayjs from 'dayjs'
import 'dayjs/locale/tr'
import { mockLeaveRequests } from '../../../mocks/mockIntranetData'
import { HrLeave, LeaveStatusEnum } from '@/types/hr'
dayjs.locale('tr')
const LeaveManagement: React.FC = () => {
const [requests, setRequests] = useState<HrLeave[]>(mockLeaveRequests)
const [showCreateModal, setShowCreateModal] = useState(false)
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'approved' | 'rejected'>('all')
// İzin bakiyeleri (mock)
const leaveBalance = {
annual: { total: 20, used: 8, remaining: 12 },
sick: { total: 10, used: 2, remaining: 8 },
unpaid: { used: 0 }
}
const filteredRequests = requests.filter(req => {
if (filterStatus === 'all') return true
return req.status === filterStatus
})
const getStatusColor = (status: string) => {
const colors = {
pending: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300',
approved: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
rejected: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
}
return colors[status as keyof typeof colors]
}
const getTypeLabel = (type: string) => {
const labels = {
annual: 'Yıllık İzin',
sick: 'Hastalık İzni',
unpaid: 'Ücretsiz İzin',
maternity: 'Doğum İzni',
other: 'Diğer'
}
return labels[type as keyof typeof labels]
}
const getTypeIcon = (type: string) => {
const icons = {
annual: '🏖️',
sick: '🏥',
unpaid: '💼',
maternity: '👶',
other: '📋'
}
return icons[type as keyof typeof icons]
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
<div className="mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
İzin Yönetimi
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
İzin taleplerinizi oluşturun ve takip edin
</p>
</div>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors"
>
<HiPlus className="w-5 h-5" />
Yeni İzin Talebi
</button>
</div>
{/* İzin Bakiyeleri */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
Yıllık İzin
</h3>
<span className="text-2xl">🏖</span>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Toplam</span>
<span className="font-semibold text-gray-900 dark:text-white">
{leaveBalance.annual.total} gün
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Kullanılan</span>
<span className="font-semibold text-orange-600 dark:text-orange-400">
{leaveBalance.annual.used} gün
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Kalan</span>
<span className="font-semibold text-green-600 dark:text-green-400">
{leaveBalance.annual.remaining} gün
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mt-3">
<div
className="bg-blue-600 h-2 rounded-full transition-all"
style={{
width: `${(leaveBalance.annual.used / leaveBalance.annual.total) * 100}%`
}}
></div>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
Hastalık İzni
</h3>
<span className="text-2xl">🏥</span>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Toplam</span>
<span className="font-semibold text-gray-900 dark:text-white">
{leaveBalance.sick.total} gün
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Kullanılan</span>
<span className="font-semibold text-orange-600 dark:text-orange-400">
{leaveBalance.sick.used} gün
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Kalan</span>
<span className="font-semibold text-green-600 dark:text-green-400">
{leaveBalance.sick.remaining} gün
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mt-3">
<div
className="bg-red-600 h-2 rounded-full transition-all"
style={{
width: `${(leaveBalance.sick.used / leaveBalance.sick.total) * 100}%`
}}
></div>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
Ücretsiz İzin
</h3>
<span className="text-2xl">💼</span>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Toplam</span>
<span className="font-semibold text-gray-900 dark:text-white">
Sınırsız
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Bu yıl kullanılan</span>
<span className="font-semibold text-orange-600 dark:text-orange-400">
{leaveBalance.unpaid.used} gün
</span>
</div>
<div className="mt-8 text-center">
<p className="text-xs text-gray-500 dark:text-gray-400">
Onay sürecinden geçer
</p>
</div>
</div>
</div>
</div>
{/* Filtreler */}
<div className="flex items-center gap-2">
<HiFunnel className="w-5 h-5 text-gray-400" />
<button
onClick={() => setFilterStatus('all')}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
filterStatus === 'all'
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
>
Tümü
</button>
<button
onClick={() => setFilterStatus('pending')}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
filterStatus === 'pending'
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
>
Bekleyen
</button>
<button
onClick={() => setFilterStatus('approved')}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
filterStatus === 'approved'
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
>
Onaylanan
</button>
<button
onClick={() => setFilterStatus('rejected')}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
filterStatus === 'rejected'
? 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
>
Reddedilen
</button>
</div>
{/* İzin Talepleri Listesi */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
İzin Talepleri
</h2>
</div>
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{filteredRequests.length > 0 ? (
filteredRequests.map((request) => (
<div
key={request.id}
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
>
<div className="flex items-start gap-4">
<span className="text-3xl">{getTypeIcon(request.leaveType)}</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
{getTypeLabel(request.leaveType)}
</h3>
<span className={`px-2.5 py-1 text-xs rounded-full ${getStatusColor(request.status)}`}>
{request.status === LeaveStatusEnum.Pending && '⏳ Beklemede'}
{request.status === LeaveStatusEnum.Approved && '✅ Onaylandı'}
{request.status === LeaveStatusEnum.Rejected && '❌ Reddedildi'}
</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Başlangıç</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">
{dayjs(request.startDate).format('DD MMMM YYYY')}
</p>
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Bitiş</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">
{dayjs(request.endDate).format('DD MMMM YYYY')}
</p>
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Süre</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">
{request.days} gün
</p>
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Talep Tarihi</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">
{dayjs(request.creationTime).format('DD MMM YYYY')}
</p>
</div>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
<span className="font-medium">ıklama:</span> {request.reason}
</p>
{request.approver && (
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
<HiCheckCircle className="w-4 h-4" />
<span>
{request.approver.fullName} tarafından{' '}
{dayjs(request.approvalDate).format('DD MMMM YYYY')} tarihinde{' '}
{request.status === 'approved' ? 'onaylandı' : 'reddedildi'}
</span>
</div>
)}
{request.notes && (
<div className="mt-2 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<p className="text-xs text-gray-700 dark:text-gray-300">
<span className="font-medium">Not:</span> {request.notes}
</p>
</div>
)}
</div>
</div>
</div>
))
) : (
<div className="p-12 text-center">
<HiCalendar className="w-12 h-12 text-gray-400 mx-auto mb-3" />
<p className="text-gray-500 dark:text-gray-400">
{filterStatus === 'all'
? 'Henüz izin talebi bulunmuyor'
: `${filterStatus === 'pending' ? 'Bekleyen' : filterStatus === 'approved' ? 'Onaylanan' : 'Reddedilen'} izin talebi bulunmuyor`}
</p>
</div>
)}
</div>
</div>
</div>
{/* Yeni İzin Talebi Modal */}
<AnimatePresence>
{showCreateModal && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 z-40"
onClick={() => setShowCreateModal(false)}
/>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
>
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between sticky top-0 bg-white dark:bg-gray-800">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Yeni İzin Talebi
</h2>
<button
onClick={() => setShowCreateModal(false)}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
<HiXMark className="w-5 h-5 text-gray-500" />
</button>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
İzin Türü
</label>
<select className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
<option>Yıllık İzin</option>
<option>Hastalık İzni</option>
<option>Ücretsiz İzin</option>
<option>Doğum İzni</option>
<option>Diğer</option>
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Başlangıç Tarihi
</label>
<input
type="date"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Bitiş Tarihi
</label>
<input
type="date"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
ıklama
</label>
<textarea
rows={4}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white resize-none"
placeholder="İzin sebebinizi açıklayın..."
/>
</div>
<div className="flex gap-3 pt-4">
<button
onClick={() => setShowCreateModal(false)}
className="flex-1 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
İptal
</button>
<button
onClick={() => setShowCreateModal(false)}
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
>
Talep Oluştur
</button>
</div>
</div>
</motion.div>
</div>
</>
)}
</AnimatePresence>
</div>
)
}
export default LeaveManagement