erp-platform/ui/src/views/intranet/HR/OvertimeManagement.tsx
2025-10-19 23:02:09 +03:00

376 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 {
HiClock,
HiCheckCircle,
HiPlus,
HiXMark,
HiFunnel
} from 'react-icons/hi2'
import dayjs from 'dayjs'
import 'dayjs/locale/tr'
import { mockOvertimeRequests } from '../../../mocks/mockIntranetData'
import { HrOvertime, LeaveStatusEnum } from '@/types/hr'
dayjs.locale('tr')
const OvertimeManagement: React.FC = () => {
const [requests, setRequests] = useState<HrOvertime[]>(mockOvertimeRequests)
const [showCreateModal, setShowCreateModal] = useState(false)
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'approved' | 'rejected'>('all')
// Mesai istatistikleri (mock)
const overtimeStats = {
thisMonth: { total: 24, approved: 20, pending: 4, rejected: 0 },
thisYear: { total: 156, approved: 142, pending: 8, rejected: 6 },
averagePerMonth: 13
}
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]
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
<div className="max-w-7xl 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">
Mesai Yönetimi
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Fazla mesai 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 Mesai Talebi
</button>
</div>
{/* İstatistikler */}
<div className="grid grid-cols-1 md:grid-cols-4 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-2">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
Bu Ay Toplam
</h3>
<span className="text-2xl"></span>
</div>
<p className="text-3xl font-bold text-gray-900 dark:text-white">
{overtimeStats.thisMonth.total}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
saat fazla mesai
</p>
</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-2">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
Onaylanan
</h3>
<span className="text-2xl"></span>
</div>
<p className="text-3xl font-bold text-green-600 dark:text-green-400">
{overtimeStats.thisMonth.approved}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
bu ay onaylandı
</p>
</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-2">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
Bekleyen
</h3>
<span className="text-2xl"></span>
</div>
<p className="text-3xl font-bold text-yellow-600 dark:text-yellow-400">
{overtimeStats.thisMonth.pending}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
onay bekliyor
</p>
</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-2">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
Yıllık Toplam
</h3>
<span className="text-2xl">📊</span>
</div>
<p className="text-3xl font-bold text-blue-600 dark:text-blue-400">
{overtimeStats.thisYear.total}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
saat bu yıl
</p>
</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>
{/* Mesai 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">
Mesai 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">
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center">
<HiClock className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
<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">
{dayjs(request.date).format('DD MMMM YYYY dddd')}
</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">
{request.startTime}
</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">
{request.endTime}
</p>
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400">Süre</p>
<p className="text-sm font-medium text-blue-600 dark:text-blue-400">
{request.duration} saat
</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')}
</p>
</div>
</div>
{request.project && (
<div className="mb-2">
<span className="inline-flex items-center px-2.5 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded text-xs font-medium">
📁 {request.project}
</span>
</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 === LeaveStatusEnum.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">
<HiClock 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 mesai talebi bulunmuyor'
: `${filterStatus === 'pending' ? 'Bekleyen' : filterStatus === 'approved' ? 'Onaylanan' : 'Reddedilen'} mesai talebi bulunmuyor`}
</p>
</div>
)}
</div>
</div>
</div>
{/* Yeni Mesai 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 Mesai 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">
Tarih
</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 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ıç Saati
</label>
<input
type="time"
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ş Saati
</label>
<input
type="time"
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">
Proje (Opsiyonel)
</label>
<input
type="text"
placeholder="Proje adı"
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">
ı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="Mesai yapma sebebini 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 OvertimeManagement