Intranet Modulü
This commit is contained in:
parent
c78845f411
commit
ddaaa56ea0
21 changed files with 6891 additions and 4 deletions
|
|
@ -1,6 +1,35 @@
|
|||
{
|
||||
"commit": "64488c5",
|
||||
"commit": "c78845f",
|
||||
"releases": [
|
||||
{
|
||||
"version": "1.0.32",
|
||||
"buildDate": "2025-10-15",
|
||||
"commit": "8564bff367eefb62b1cfd7ac5790097fcf8feaa7",
|
||||
"changeLog": [
|
||||
"Form View ve Form Edit ekranlarında Activity özelliği eklendi. 3 farklı Activity eklenebiliyor ayrıca dosya eklenebiliyor. Bu dosyalar diskte Blob olarak kaydediliyor."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.31",
|
||||
"buildDate": "2025-10-09",
|
||||
"commit": "035366ab7020dd77bfe2b5b66ea253e743526ea6",
|
||||
"changeLog": [
|
||||
"- Grid üzerinde Mask ve Format güncellemesi",
|
||||
"- Allow Column Reordering uygulamasının çalıştırılması"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.30",
|
||||
"buildDate": "2025-10-08",
|
||||
"commit": "e45885f5693176257e12ecc05d4ed51f87ef0120",
|
||||
"changeLog": [
|
||||
"- Tenant ve Barch arasında ilişki kuruldu ve listelerde filtreli şekilde listeleniyor.",
|
||||
"- Genel seederlar düzenlendi.",
|
||||
"- Yeni tanımlamalar listeleri eklendi. Kayıt Tipi, Kayı Şekli, Program vs.",
|
||||
"- Default Helper eklendi ve tüm Application Servisler o metoda yönlendirildi.",
|
||||
"- Tanımlamalar menülere dağıtıldı."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.29",
|
||||
"buildDate": "2025-09-28",
|
||||
|
|
@ -126,7 +155,7 @@
|
|||
{
|
||||
"version": "1.0.14",
|
||||
"buildDate": "2025-09-22",
|
||||
"commit": "1c4ab4f8232b4cd2a39fa66f8101664840113ce5",
|
||||
"commit": "51208b86937484d68b699120d74872067b1c7ef6",
|
||||
"changeLog": [
|
||||
"Yeni versiyon çıktı uyarı gelecek şekilde düzenlendi.",
|
||||
"Sağ alt kısımda mesaj çıkacak ve yenile butonu ile uygulama yeni versiyona geçecektir."
|
||||
|
|
|
|||
291
ui/src/components/intranet/Announcements/index.tsx
Normal file
291
ui/src/components/intranet/Announcements/index.tsx
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
import React, { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import {
|
||||
HiBell,
|
||||
HiMagnifyingGlass,
|
||||
HiFunnel,
|
||||
HiEye,
|
||||
HiCalendar,
|
||||
HiUser
|
||||
} from 'react-icons/hi2'
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/tr'
|
||||
import { mockAnnouncements, Announcement } from '../../../mocks/mockIntranetData'
|
||||
|
||||
dayjs.locale('tr')
|
||||
|
||||
const AnnouncementsModule: React.FC = () => {
|
||||
const [announcements] = useState<Announcement[]>(mockAnnouncements)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
||||
const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null)
|
||||
|
||||
const categories = [
|
||||
{ id: 'all', label: 'Tümü', color: 'blue' },
|
||||
{ id: 'general', label: 'Genel', color: 'blue' },
|
||||
{ id: 'hr', label: 'İK', color: 'purple' },
|
||||
{ id: 'it', label: 'IT', color: 'orange' },
|
||||
{ id: 'event', label: 'Etkinlik', color: 'green' },
|
||||
{ id: 'urgent', label: 'Acil', color: 'red' }
|
||||
]
|
||||
|
||||
const filteredAnnouncements = announcements.filter(announcement => {
|
||||
const matchesSearch = announcement.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
announcement.content.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const matchesCategory = selectedCategory === 'all' || announcement.category === selectedCategory
|
||||
return matchesSearch && matchesCategory
|
||||
})
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
general: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800',
|
||||
hr: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 border-purple-200 dark:border-purple-800',
|
||||
it: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 border-orange-200 dark:border-orange-800',
|
||||
event: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 border-green-200 dark:border-green-800',
|
||||
urgent: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 border-red-200 dark:border-red-800'
|
||||
}
|
||||
return colors[category] || colors.general
|
||||
}
|
||||
|
||||
const getCategoryBadge = (category: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
general: 'Genel',
|
||||
hr: 'İK',
|
||||
it: 'IT',
|
||||
event: 'Etkinlik',
|
||||
urgent: 'Acil'
|
||||
}
|
||||
return labels[category]
|
||||
}
|
||||
|
||||
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>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Duyurular & Haberler
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Şirket duyurularını ve haberleri takip edin
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Arama ve Filtreler */}
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<HiMagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Duyuru ara..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<HiFunnel className="w-5 h-5 text-gray-400" />
|
||||
{categories.map(cat => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setSelectedCategory(cat.id)}
|
||||
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
selectedCategory === cat.id
|
||||
? `bg-${cat.color}-100 dark:bg-${cat.color}-900/30 text-${cat.color}-700 dark:text-${cat.color}-300`
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
{cat.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Duyuru Listesi */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Sol Kolon - Liste */}
|
||||
<div className="lg:col-span-1 space-y-4">
|
||||
{filteredAnnouncements.map(announcement => (
|
||||
<motion.div
|
||||
key={announcement.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className={`bg-white dark:bg-gray-800 rounded-lg border-2 shadow-sm cursor-pointer hover:shadow-md transition-all ${
|
||||
selectedAnnouncement?.id === announcement.id
|
||||
? getCategoryColor(announcement.category)
|
||||
: 'border-gray-200 dark:border-gray-700'
|
||||
}`}
|
||||
onClick={() => setSelectedAnnouncement(announcement)}
|
||||
>
|
||||
{announcement.imageUrl && (
|
||||
<div className="relative h-48 overflow-hidden rounded-t-lg">
|
||||
<img
|
||||
src={announcement.imageUrl}
|
||||
alt={announcement.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{announcement.isPinned && (
|
||||
<div className="absolute top-3 right-3 px-2.5 py-1 bg-yellow-500 text-white text-xs font-bold rounded-full shadow-lg">
|
||||
📌 SABİTLENDİ
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="p-6">
|
||||
<div className="flex items-start justify-between gap-3 mb-3">
|
||||
<h3 className="text-lg font-bold text-gray-900 dark:text-white flex-1">
|
||||
{announcement.title}
|
||||
</h3>
|
||||
<span className={`px-2.5 py-1 text-xs font-medium rounded-full whitespace-nowrap ${getCategoryColor(announcement.category)}`}>
|
||||
{getCategoryBadge(announcement.category)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
|
||||
{announcement.excerpt}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={announcement.author.avatar}
|
||||
alt={announcement.author.fullName}
|
||||
className="w-6 h-6 rounded-full"
|
||||
/>
|
||||
<span>{announcement.author.fullName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<HiCalendar className="w-4 h-4" />
|
||||
{dayjs(announcement.publishDate).format('DD MMMM YYYY')}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<HiEye className="w-4 h-4" />
|
||||
{announcement.viewCount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{filteredAnnouncements.length === 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-12 text-center border border-gray-200 dark:border-gray-700">
|
||||
<HiBell className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
{searchQuery ? 'Arama kriterlerine uygun duyuru bulunamadı' : 'Henüz duyuru bulunmuyor'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sağ Kolon - Detay */}
|
||||
<div className="lg:col-span-1">
|
||||
{selectedAnnouncement ? (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm sticky top-6">
|
||||
{selectedAnnouncement.imageUrl && (
|
||||
<div className="relative h-64 overflow-hidden rounded-t-lg">
|
||||
<img
|
||||
src={selectedAnnouncement.imageUrl}
|
||||
alt={selectedAnnouncement.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-6">
|
||||
<div className="mb-4">
|
||||
<span className={`px-3 py-1.5 text-sm font-medium rounded-full ${getCategoryColor(selectedAnnouncement.category)}`}>
|
||||
{getCategoryBadge(selectedAnnouncement.category)}
|
||||
</span>
|
||||
{selectedAnnouncement.isPinned && (
|
||||
<span className="ml-2 px-3 py-1.5 bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300 text-sm font-medium rounded-full">
|
||||
📌 Sabitlendi
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
{selectedAnnouncement.title}
|
||||
</h2>
|
||||
<div className="flex items-center gap-3 mb-4 pb-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<img
|
||||
src={selectedAnnouncement.author.avatar}
|
||||
alt={selectedAnnouncement.author.fullName}
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{selectedAnnouncement.author.fullName}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{dayjs(selectedAnnouncement.publishDate).format('DD MMMM YYYY, HH:mm')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none mb-4">
|
||||
<p className="text-gray-700 dark:text-gray-300 whitespace-pre-line">
|
||||
{selectedAnnouncement.content}
|
||||
</p>
|
||||
</div>
|
||||
{selectedAnnouncement.attachments && selectedAnnouncement.attachments.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
📎 Ekler
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{selectedAnnouncement.attachments.map((attachment, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
href={attachment.url}
|
||||
className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded flex items-center justify-center">
|
||||
<span className="text-xs">📄</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
{attachment.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{attachment.size}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedAnnouncement.departments && selectedAnnouncement.departments.length > 0 && (
|
||||
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
||||
Hedef Departmanlar:
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedAnnouncement.departments.map((dept, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded"
|
||||
>
|
||||
{dept}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 text-xs text-gray-500 dark:text-gray-400">
|
||||
<HiEye className="w-4 h-4" />
|
||||
<span>{selectedAnnouncement.viewCount} kez görüntülendi</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-12 text-center sticky top-6">
|
||||
<HiBell className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Detayları görüntülemek için bir duyuru seçin
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnnouncementsModule
|
||||
279
ui/src/components/intranet/Birthdays/index.tsx
Normal file
279
ui/src/components/intranet/Birthdays/index.tsx
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
import React, { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { HiCake, HiGift, HiCalendar } from 'react-icons/hi2'
|
||||
import dayjs from 'dayjs'
|
||||
import isBetween from 'dayjs/plugin/isBetween'
|
||||
import { mockBirthdays, mockAnniversaries, Birthday, WorkAnniversary } from '../../../mocks/mockIntranetData'
|
||||
|
||||
dayjs.extend(isBetween)
|
||||
|
||||
const BirthdaysModule: React.FC = () => {
|
||||
const [selectedMonth, setSelectedMonth] = useState(dayjs().month())
|
||||
|
||||
const months = [
|
||||
'Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran',
|
||||
'Temmuz', 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'
|
||||
]
|
||||
|
||||
// Bugünün doğum günleri
|
||||
const todayBirthdays = mockBirthdays.filter(b =>
|
||||
dayjs(b.date).month() === dayjs().month() &&
|
||||
dayjs(b.date).date() === dayjs().date()
|
||||
)
|
||||
|
||||
// Bu haftanın doğum günleri
|
||||
const thisWeekBirthdays = mockBirthdays.filter(b => {
|
||||
const birthDate = dayjs().year(dayjs().year()).month(dayjs(b.date).month()).date(dayjs(b.date).date())
|
||||
return birthDate.isBetween(dayjs().startOf('week'), dayjs().endOf('week'), null, '[]')
|
||||
})
|
||||
|
||||
// Seçilen aydaki doğum günleri
|
||||
const monthBirthdays = mockBirthdays.filter(b => dayjs(b.date).month() === selectedMonth)
|
||||
.sort((a, b) => dayjs(a.date).date() - dayjs(b.date).date())
|
||||
|
||||
// Bu ayki iş yıldönümleri
|
||||
const thisMonthAnniversaries = mockAnniversaries.filter((a: WorkAnniversary) => dayjs(a.hireDate).month() === dayjs().month())
|
||||
|
||||
const getBirthdayMessage = (birthday: Birthday) => {
|
||||
const age = birthday.age || dayjs().year() - dayjs(birthday.date).year()
|
||||
return `${age}. yaş günü kutlu olsun! 🎉`
|
||||
}
|
||||
|
||||
const getAnniversaryMessage = (anniversary: WorkAnniversary) => {
|
||||
return `${anniversary.years} yıllık iş birliğimiz için teşekkürler! 🎊`
|
||||
}
|
||||
|
||||
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>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
🎂 Doğum Günleri & Yıldönümleri
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Ekip üyelerimizin özel günlerini kutlayalım
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Bugün */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-gradient-to-br from-pink-500 to-purple-600 rounded-lg p-6 text-white"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<HiCake className="w-8 h-8" />
|
||||
<span className="text-3xl font-bold">{todayBirthdays.length}</span>
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg mb-1">Bugün Doğanlar</h3>
|
||||
<p className="text-pink-100 text-sm">
|
||||
{todayBirthdays.length > 0 ? 'Kutlama zamanı!' : 'Bugün doğum günü yok'}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Bu Hafta */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="bg-gradient-to-br from-blue-500 to-cyan-600 rounded-lg p-6 text-white"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<HiGift className="w-8 h-8" />
|
||||
<span className="text-3xl font-bold">{thisWeekBirthdays.length}</span>
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg mb-1">Bu Hafta</h3>
|
||||
<p className="text-blue-100 text-sm">
|
||||
{dayjs().startOf('week').format('DD MMM')} - {dayjs().endOf('week').format('DD MMM')}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* İş Yıldönümleri */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="bg-gradient-to-br from-orange-500 to-red-600 rounded-lg p-6 text-white"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<HiCalendar className="w-8 h-8" />
|
||||
<span className="text-3xl font-bold">{thisMonthAnniversaries.length}</span>
|
||||
</div>
|
||||
<h3 className="font-semibold text-lg mb-1">Bu Ay Yıldönümü</h3>
|
||||
<p className="text-orange-100 text-sm">
|
||||
{dayjs().format('MMMM')} ayında
|
||||
</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Bugünün Doğum Günleri */}
|
||||
{todayBirthdays.length > 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 border-2 border-pink-300 dark:border-pink-700">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
🎉 Bugün Doğum Günü Olanlar
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{todayBirthdays.map((birthday, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
className="bg-gradient-to-r from-pink-50 to-purple-50 dark:from-pink-900/20 dark:to-purple-900/20 rounded-lg p-4 border border-pink-200 dark:border-pink-800"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<img
|
||||
src={birthday.employee.avatar}
|
||||
alt={birthday.employee.fullName}
|
||||
className="w-16 h-16 rounded-full border-4 border-white dark:border-gray-800 shadow-lg"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||
{birthday.employee.fullName}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{birthday.employee.department?.name || 'Genel'}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-pink-600 dark:text-pink-400 mt-1">
|
||||
{getBirthdayMessage(birthday)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ay Seçici */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
📅 Aylık Doğum Günleri
|
||||
</h2>
|
||||
<select
|
||||
value={selectedMonth}
|
||||
onChange={(e) => setSelectedMonth(Number(e.target.value))}
|
||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
>
|
||||
{months.map((month, idx) => (
|
||||
<option key={idx} value={idx}>
|
||||
{month}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Doğum Günleri Listesi */}
|
||||
<div className="space-y-3">
|
||||
{monthBirthdays.map((birthday, idx) => {
|
||||
const isToday = dayjs(birthday.date).month() === dayjs().month() &&
|
||||
dayjs(birthday.date).date() === dayjs().date()
|
||||
return (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: idx * 0.05 }}
|
||||
className={`flex items-center gap-4 p-4 rounded-lg border ${
|
||||
isToday
|
||||
? 'bg-pink-50 dark:bg-pink-900/20 border-pink-300 dark:border-pink-700'
|
||||
: 'bg-gray-50 dark:bg-gray-700/50 border-gray-200 dark:border-gray-700'
|
||||
}`}
|
||||
>
|
||||
<div className={`w-16 h-16 rounded-lg flex flex-col items-center justify-center ${
|
||||
isToday
|
||||
? 'bg-pink-500 text-white'
|
||||
: 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300'
|
||||
}`}>
|
||||
<span className="text-2xl font-bold">{dayjs(birthday.date).date()}</span>
|
||||
<span className="text-xs">{months[selectedMonth].substring(0, 3)}</span>
|
||||
</div>
|
||||
|
||||
<img
|
||||
src={birthday.employee.avatar}
|
||||
alt={birthday.employee.fullName}
|
||||
className="w-12 h-12 rounded-full"
|
||||
/>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||
{birthday.employee.fullName}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{birthday.employee.department?.name || 'Genel'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{birthday.age || dayjs().year() - dayjs(birthday.date).year()} yaşında
|
||||
</p>
|
||||
{isToday && (
|
||||
<span className="text-xs text-pink-600 dark:text-pink-400 font-medium">
|
||||
🎂 Bugün!
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
{monthBirthdays.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||
<HiCake className="w-16 h-16 mx-auto mb-4 opacity-20" />
|
||||
<p>{months[selectedMonth]} ayında doğum günü yok</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* İş Yıldönümleri */}
|
||||
{thisMonthAnniversaries.length > 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
🎊 Bu Ayki İş Yıldönümleri
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{thisMonthAnniversaries.map((anniversary: WorkAnniversary, idx: number) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.1 }}
|
||||
className="bg-gradient-to-r from-orange-50 to-amber-50 dark:from-orange-900/20 dark:to-amber-900/20 rounded-lg p-4 border border-orange-200 dark:border-orange-800"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<img
|
||||
src={anniversary.employee.avatar}
|
||||
alt={anniversary.employee.fullName}
|
||||
className="w-16 h-16 rounded-full border-4 border-white dark:border-gray-800 shadow-lg"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||
{anniversary.employee.fullName}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{anniversary.employee.department?.name || 'Genel'}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-orange-600 dark:text-orange-400 mt-1">
|
||||
{getAnniversaryMessage(anniversary)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center bg-orange-500 text-white rounded-full w-16 h-16 flex flex-col items-center justify-center">
|
||||
<span className="text-2xl font-bold">{anniversary.years}</span>
|
||||
<span className="text-xs">YIL</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BirthdaysModule
|
||||
207
ui/src/components/intranet/Cafeteria/index.tsx
Normal file
207
ui/src/components/intranet/Cafeteria/index.tsx
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import React, { useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { HiClock, HiMapPin } from 'react-icons/hi2'
|
||||
import dayjs from 'dayjs'
|
||||
import { mockMealMenus, mockShuttleRoutes } from '../../../mocks/mockIntranetData'
|
||||
|
||||
const CafeteriaModule: React.FC = () => {
|
||||
const [selectedView, setSelectedView] = useState<'menu' | 'shuttle'>('menu')
|
||||
|
||||
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>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
🍽️ Kafeterya & Servis
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Yemek menüsü ve servis saatleri
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 border-b border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={() => setSelectedView('menu')}
|
||||
className={`px-6 py-3 font-medium transition-colors ${
|
||||
selectedView === 'menu'
|
||||
? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
📅 Haftalık Menü
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedView('shuttle')}
|
||||
className={`px-6 py-3 font-medium transition-colors ${
|
||||
selectedView === 'shuttle'
|
||||
? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
🚌 Servis Saatleri
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Menu View */}
|
||||
{selectedView === 'menu' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||
{mockMealMenus.map((menu, idx) => (
|
||||
<motion.div
|
||||
key={menu.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.1 }}
|
||||
className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="text-center mb-4">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">{dayjs(menu.date).format('DD MMM')}</div>
|
||||
<h3 className="text-lg font-bold text-gray-900 dark:text-white">{menu.dayOfWeek}</h3>
|
||||
</div>
|
||||
|
||||
{menu.meals.map((meal, mealIdx) => (
|
||||
<div key={mealIdx} className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||
<span className="font-medium">
|
||||
{meal.type === 'lunch' ? '🍽️ Öğle Yemeği' : meal.type}
|
||||
</span>
|
||||
{meal.calories && <span>{meal.calories} kcal</span>}
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{meal.items.map((item, itemIdx) => (
|
||||
<li key={itemIdx} className="text-sm text-gray-700 dark:text-gray-300 flex items-start">
|
||||
<span className="text-green-500 mr-2">•</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Shuttle View */}
|
||||
{selectedView === 'shuttle' && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">🌅 Sabah Servisleri</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{mockShuttleRoutes.filter(s => s.type === 'morning').map((shuttle, idx) => (
|
||||
<motion.div
|
||||
key={shuttle.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: idx * 0.1 }}
|
||||
className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{shuttle.name}</h3>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${
|
||||
shuttle.available > 5
|
||||
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||
: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300'
|
||||
}`}>
|
||||
{shuttle.available} Koltuk
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<HiClock className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{shuttle.departureTime} - {shuttle.arrivalTime}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<HiMapPin className="w-4 h-4 text-gray-400 mt-0.5" />
|
||||
<div className="text-gray-700 dark:text-gray-300">
|
||||
{shuttle.route.map((stop, idx) => (
|
||||
<span key={idx}>
|
||||
{stop}
|
||||
{idx < shuttle.route.length - 1 && ' → '}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full"
|
||||
style={{ width: `${((shuttle.capacity - shuttle.available) / shuttle.capacity) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||
{shuttle.capacity - shuttle.available} / {shuttle.capacity} dolu
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">🌆 Akşam Servisleri</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{mockShuttleRoutes.filter(s => s.type === 'evening').map((shuttle, idx) => (
|
||||
<motion.div
|
||||
key={shuttle.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: idx * 0.1 }}
|
||||
className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{shuttle.name}</h3>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${
|
||||
shuttle.available > 5
|
||||
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||
: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300'
|
||||
}`}>
|
||||
{shuttle.available} Koltuk
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mb-4">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<HiClock className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-gray-700 dark:text-gray-300">
|
||||
{shuttle.departureTime} - {shuttle.arrivalTime}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<HiMapPin className="w-4 h-4 text-gray-400 mt-0.5" />
|
||||
<div className="text-gray-700 dark:text-gray-300">
|
||||
{shuttle.route.map((stop, idx) => (
|
||||
<span key={idx}>
|
||||
{stop}
|
||||
{idx < shuttle.route.length - 1 && ' → '}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full"
|
||||
style={{ width: `${((shuttle.capacity - shuttle.available) / shuttle.capacity) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||
{shuttle.capacity - shuttle.available} / {shuttle.capacity} dolu
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CafeteriaModule
|
||||
458
ui/src/components/intranet/Documents/index.tsx
Normal file
458
ui/src/components/intranet/Documents/index.tsx
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
import React, { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import {
|
||||
HiMagnifyingGlass,
|
||||
HiFunnel,
|
||||
HiArrowDownTray,
|
||||
HiEye,
|
||||
HiFolder,
|
||||
HiDocument,
|
||||
HiXMark
|
||||
} from 'react-icons/hi2'
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/tr'
|
||||
import { mockDocuments, Document } from '../../../mocks/mockIntranetData'
|
||||
|
||||
dayjs.locale('tr')
|
||||
|
||||
const DocumentsModule: React.FC = () => {
|
||||
const [documents] = useState<Document[]>(mockDocuments)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
||||
const [previewDocument, setPreviewDocument] = useState<Document | null>(null)
|
||||
|
||||
const categories = [
|
||||
{ id: 'all', label: 'Tümü' },
|
||||
{ id: 'policy', label: 'Politika' },
|
||||
{ id: 'procedure', label: 'Prosedür' },
|
||||
{ id: 'form', label: 'Form' },
|
||||
{ id: 'template', label: 'Şablon' },
|
||||
{ id: 'report', label: 'Rapor' },
|
||||
{ id: 'other', label: 'Diğer' }
|
||||
]
|
||||
|
||||
const handleDownload = (doc: Document) => {
|
||||
// Gerçek indirme işlemi
|
||||
const link = document.createElement('a')
|
||||
link.href = doc.url
|
||||
link.download = doc.name
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
const filteredDocuments = documents.filter(doc => {
|
||||
const matchesSearch = doc.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
doc.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
doc.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
const matchesCategory = selectedCategory === 'all' || doc.category === selectedCategory
|
||||
return matchesSearch && matchesCategory
|
||||
})
|
||||
|
||||
const getFileIcon = (type: string) => {
|
||||
const icons = {
|
||||
pdf: '📄',
|
||||
doc: '📝',
|
||||
xls: '📊',
|
||||
ppt: '📊',
|
||||
other: '📎'
|
||||
}
|
||||
return icons[type as keyof typeof icons] || icons.other
|
||||
}
|
||||
|
||||
const getCategoryLabel = (category: string) => {
|
||||
const labels = {
|
||||
policy: 'Politika',
|
||||
procedure: 'Prosedür',
|
||||
form: 'Form',
|
||||
template: 'Şablon',
|
||||
report: 'Rapor',
|
||||
other: 'Diğer'
|
||||
}
|
||||
return labels[category as keyof typeof labels]
|
||||
}
|
||||
|
||||
const groupedDocuments = filteredDocuments.reduce((acc, doc) => {
|
||||
if (!acc[doc.category]) {
|
||||
acc[doc.category] = []
|
||||
}
|
||||
acc[doc.category].push(doc)
|
||||
return acc
|
||||
}, {} as Record<string, Document[]>)
|
||||
|
||||
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>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Doküman Yönetimi
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Şirket dokümanlarına erişin ve yönetin
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Arama ve Filtreler */}
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<HiMagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Doküman ara..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<HiFunnel className="w-5 h-5 text-gray-400" />
|
||||
{categories.map(cat => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setSelectedCategory(cat.id)}
|
||||
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
selectedCategory === cat.id
|
||||
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||
}`}
|
||||
>
|
||||
{cat.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</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">
|
||||
Toplam Doküman
|
||||
</h3>
|
||||
<span className="text-2xl">📁</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{documents.length}
|
||||
</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">
|
||||
Kategoriler
|
||||
</h3>
|
||||
<span className="text-2xl">🗂️</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{Object.keys(groupedDocuments).length}
|
||||
</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">
|
||||
Toplam İndirme
|
||||
</h3>
|
||||
<span className="text-2xl">⬇️</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{documents.reduce((sum, doc) => sum + doc.downloadCount, 0)}
|
||||
</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">
|
||||
Bu Ay Yeni
|
||||
</h3>
|
||||
<span className="text-2xl">✨</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{documents.filter(d => dayjs(d.uploadDate).isAfter(dayjs().subtract(30, 'day'))).length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Doküman Listesi */}
|
||||
{Object.entries(groupedDocuments).map(([category, categoryDocs]) => (
|
||||
<div key={category} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center gap-3">
|
||||
<HiFolder className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{getCategoryLabel(category)}
|
||||
</h2>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
({categoryDocs.length} doküman)
|
||||
</span>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{categoryDocs.map(doc => (
|
||||
<motion.div
|
||||
key={doc.id}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
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 text-2xl">
|
||||
{getFileIcon(doc.type)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-3 mb-2">
|
||||
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{doc.name}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPreviewDocument(doc)}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
title="Önizle"
|
||||
>
|
||||
<HiEye className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDownload(doc)}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
title="İndir"
|
||||
>
|
||||
<HiArrowDownTray className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
{doc.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={doc.uploadedBy.avatar}
|
||||
alt={doc.uploadedBy.fullName}
|
||||
className="w-5 h-5 rounded-full"
|
||||
/>
|
||||
<span>{doc.uploadedBy.fullName}</span>
|
||||
</div>
|
||||
<span>•</span>
|
||||
<span>{dayjs(doc.uploadDate).format('DD MMMM YYYY')}</span>
|
||||
<span>•</span>
|
||||
<span>{doc.size}</span>
|
||||
<span>•</span>
|
||||
<span>v{doc.version}</span>
|
||||
<span>•</span>
|
||||
<span>{doc.downloadCount} indirme</span>
|
||||
</div>
|
||||
{doc.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
{doc.tags.map((tag, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filteredDocuments.length === 0 && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-12 text-center border border-gray-200 dark:border-gray-700">
|
||||
<HiDocument className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
{searchQuery ? 'Arama kriterlerine uygun doküman bulunamadı' : 'Henüz doküman bulunmuyor'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Doküman Önizleme Modal */}
|
||||
<AnimatePresence>
|
||||
{previewDocument && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/50 z-40"
|
||||
onClick={() => setPreviewDocument(null)}
|
||||
/>
|
||||
<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-4xl w-full max-h-[90vh] overflow-hidden flex flex-col"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center text-2xl">
|
||||
{getFileIcon(previewDocument.type)}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{previewDocument.name}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{getCategoryLabel(previewDocument.category)} • {previewDocument.size} • v{previewDocument.version}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleDownload(previewDocument)}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<HiArrowDownTray className="w-5 h-5" />
|
||||
İndir
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPreviewDocument(null)}
|
||||
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>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="space-y-6">
|
||||
{/* Açıklama */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Açıklama
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
{previewDocument.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Dosya Önizleme Alanı */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Önizleme
|
||||
</h3>
|
||||
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-12 text-center bg-gray-50 dark:bg-gray-900/50">
|
||||
<div className="text-6xl mb-4">{getFileIcon(previewDocument.type)}</div>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||
{previewDocument.type.toUpperCase()} dosyası önizlemesi desteklenmemektedir
|
||||
</p>
|
||||
<button
|
||||
onClick={() => handleDownload(previewDocument)}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg inline-flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<HiArrowDownTray className="w-5 h-5" />
|
||||
Dosyayı İndir
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Dosya Bilgileri
|
||||
</h3>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Dosya Tipi:</span>
|
||||
<span className="text-gray-900 dark:text-white font-medium">
|
||||
{previewDocument.type.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Boyut:</span>
|
||||
<span className="text-gray-900 dark:text-white font-medium">
|
||||
{previewDocument.size}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">Versiyon:</span>
|
||||
<span className="text-gray-900 dark:text-white font-medium">
|
||||
v{previewDocument.version}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600 dark:text-gray-400">İndirme:</span>
|
||||
<span className="text-gray-900 dark:text-white font-medium">
|
||||
{previewDocument.downloadCount} kez
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Yükleyen
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<img
|
||||
src={previewDocument.uploadedBy.avatar}
|
||||
alt={previewDocument.uploadedBy.fullName}
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{previewDocument.uploadedBy.fullName}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{dayjs(previewDocument.uploadDate).format('DD MMMM YYYY, HH:mm')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Etiketler */}
|
||||
{previewDocument.tags.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Etiketler
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{previewDocument.tags.map((tag, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm rounded"
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Departmanlar */}
|
||||
{previewDocument.departments.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Yetkili Departmanlar
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{previewDocument.departments.map((dept, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-3 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm rounded"
|
||||
>
|
||||
{dept}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DocumentsModule
|
||||
460
ui/src/components/intranet/Events/index.tsx
Normal file
460
ui/src/components/intranet/Events/index.tsx
Normal file
|
|
@ -0,0 +1,460 @@
|
|||
import React, { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import {
|
||||
HiHeart,
|
||||
HiChatBubbleLeft,
|
||||
HiMapPin,
|
||||
HiUsers,
|
||||
HiCalendar,
|
||||
HiXMark,
|
||||
HiChevronLeft,
|
||||
HiChevronRight
|
||||
} from 'react-icons/hi2'
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/tr'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { mockEvents, CalendarEvent, EventComment } from '../../../mocks/mockIntranetData'
|
||||
|
||||
dayjs.locale('tr')
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const EventsModule: React.FC = () => {
|
||||
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null)
|
||||
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState(0)
|
||||
const [showPhotoModal, setShowPhotoModal] = useState(false)
|
||||
const [selectedFilter, setSelectedFilter] = useState<'all' | 'social' | 'training' | 'company' | 'sport' | 'culture'>('all')
|
||||
const [newComment, setNewComment] = useState('')
|
||||
const [events, setEvents] = useState<CalendarEvent[]>(mockEvents)
|
||||
|
||||
const filteredEvents = selectedFilter === 'all'
|
||||
? events.filter(e => e.isPublished)
|
||||
: events.filter(e => e.isPublished && e.type === selectedFilter)
|
||||
|
||||
const handleLikeEvent = (eventId: string) => {
|
||||
setEvents(prev => prev.map(e =>
|
||||
e.id === eventId ? { ...e, likes: e.likes + 1 } : e
|
||||
))
|
||||
if (selectedEvent?.id === eventId) {
|
||||
setSelectedEvent(prev => prev ? { ...prev, likes: prev.likes + 1 } : null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddComment = (eventId: string) => {
|
||||
if (!newComment.trim()) return
|
||||
|
||||
const comment: EventComment = {
|
||||
id: `c${Date.now()}`,
|
||||
author: {
|
||||
id: 'current-user',
|
||||
fullName: 'Sedat Öztürk',
|
||||
avatar: 'https://ui-avatars.com/api/?name=Sedat+Ozturk&background=3b82f6&color=fff'
|
||||
},
|
||||
content: newComment,
|
||||
createdAt: new Date(),
|
||||
likes: 0
|
||||
}
|
||||
|
||||
setEvents(prev => prev.map(e =>
|
||||
e.id === eventId ? { ...e, comments: [...e.comments, comment] } : e
|
||||
))
|
||||
|
||||
if (selectedEvent?.id === eventId) {
|
||||
setSelectedEvent(prev => prev ? { ...prev, comments: [...prev.comments, comment] } : null)
|
||||
}
|
||||
|
||||
setNewComment('')
|
||||
}
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
social: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300',
|
||||
training: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
||||
company: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||
sport: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300',
|
||||
culture: 'bg-pink-100 dark:bg-pink-900/30 text-pink-700 dark:text-pink-300'
|
||||
}
|
||||
return colors[type] || colors.social
|
||||
}
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
social: '🎉 Sosyal',
|
||||
training: '📚 Eğitim',
|
||||
company: '🏢 Kurumsal',
|
||||
sport: '⚽ Spor',
|
||||
culture: '🎨 Kültür'
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
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>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
🎊 Etkinlikler
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Şirket etkinlikleri, fotoğraflar ve anılar
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<div className="flex gap-3 overflow-x-auto pb-2">
|
||||
{[
|
||||
{ value: 'all' as const, label: '🌟 Tümü' },
|
||||
{ value: 'social' as const, label: '🎉 Sosyal' },
|
||||
{ value: 'training' as const, label: '📚 Eğitim' },
|
||||
{ value: 'company' as const, label: '🏢 Kurumsal' },
|
||||
{ value: 'sport' as const, label: '⚽ Spor' },
|
||||
{ value: 'culture' as const, label: '🎨 Kültür' }
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
onClick={() => setSelectedFilter(tab.value)}
|
||||
className={`px-4 py-2 rounded-lg border-2 transition-all whitespace-nowrap ${
|
||||
selectedFilter === tab.value
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Events Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredEvents.map((event, idx) => (
|
||||
<motion.div
|
||||
key={event.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.1 }}
|
||||
className="bg-white dark:bg-gray-800 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 hover:shadow-xl transition-all cursor-pointer"
|
||||
onClick={() => setSelectedEvent(event)}
|
||||
>
|
||||
{/* Cover Photo */}
|
||||
<div className="relative h-48 overflow-hidden">
|
||||
<img
|
||||
src={event.photos[0]}
|
||||
alt={event.title}
|
||||
className="w-full h-full object-cover hover:scale-110 transition-transform duration-300"
|
||||
/>
|
||||
<div className="absolute top-3 right-3">
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getTypeColor(event.type)}`}>
|
||||
{getTypeLabel(event.type)}
|
||||
</span>
|
||||
</div>
|
||||
{event.photos.length > 1 && (
|
||||
<div className="absolute bottom-3 right-3 bg-black/70 text-white px-2 py-1 rounded text-xs">
|
||||
+{event.photos.length - 1} fotoğraf
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4 space-y-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white line-clamp-2">
|
||||
{event.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||
{event.description}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="flex items-center gap-1">
|
||||
<HiCalendar className="w-4 h-4" />
|
||||
{dayjs(event.date).format('DD MMMM YYYY')}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<HiMapPin className="w-4 h-4" />
|
||||
{event.location}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleLikeEvent(event.id)
|
||||
}}
|
||||
className="flex items-center gap-1 text-gray-600 dark:text-gray-400 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<HiHeart className="w-5 h-5" />
|
||||
{event.likes}
|
||||
</button>
|
||||
<div className="flex items-center gap-1 text-gray-600 dark:text-gray-400">
|
||||
<HiChatBubbleLeft className="w-5 h-5" />
|
||||
{event.comments.length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
<HiUsers className="w-4 h-4" />
|
||||
{event.participants} kişi
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredEvents.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||
<p className="text-lg">Bu kategoride henüz etkinlik yok</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Event Detail Modal */}
|
||||
<AnimatePresence>
|
||||
{selectedEvent && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/50 z-40"
|
||||
onClick={() => setSelectedEvent(null)}
|
||||
/>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 overflow-y-auto">
|
||||
<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-4xl w-full max-h-[90vh] overflow-y-auto my-8"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-6 z-10">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getTypeColor(selectedEvent.type)}`}>
|
||||
{getTypeLabel(selectedEvent.type)}
|
||||
</span>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{dayjs(selectedEvent.date).format('DD MMMM YYYY')}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{selectedEvent.title}
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-2">
|
||||
{selectedEvent.description}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedEvent(null)}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
<HiXMark className="w-6 h-6 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 mt-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="flex items-center gap-2">
|
||||
<HiMapPin className="w-5 h-5" />
|
||||
{selectedEvent.location}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<HiUsers className="w-5 h-5" />
|
||||
{selectedEvent.participants} katılımcı
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={selectedEvent.organizer.avatar}
|
||||
alt={selectedEvent.organizer.fullName}
|
||||
className="w-6 h-6 rounded-full"
|
||||
/>
|
||||
<span>Düzenleyen: {selectedEvent.organizer.fullName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Photo Gallery */}
|
||||
<div className="p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
📸 Fotoğraflar ({selectedEvent.photos.length})
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{selectedEvent.photos.map((photo, idx) => (
|
||||
<motion.div
|
||||
key={idx}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className="relative aspect-square rounded-lg overflow-hidden cursor-pointer"
|
||||
onClick={() => {
|
||||
setSelectedPhotoIndex(idx)
|
||||
setShowPhotoModal(true)
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={photo}
|
||||
alt={`${selectedEvent.title} - ${idx + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Comments Section */}
|
||||
<div className="p-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
💬 Yorumlar ({selectedEvent.comments.length})
|
||||
</h3>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<button
|
||||
onClick={() => handleLikeEvent(selectedEvent.id)}
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 transition-colors"
|
||||
>
|
||||
<HiHeart className="w-5 h-5 text-red-500" />
|
||||
{selectedEvent.likes} beğeni
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Comments List */}
|
||||
<div className="space-y-4 mb-4">
|
||||
{selectedEvent.comments.map((comment) => (
|
||||
<div
|
||||
key={comment.id}
|
||||
className="flex gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg"
|
||||
>
|
||||
<img
|
||||
src={comment.author.avatar}
|
||||
alt={comment.author.fullName}
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-semibold text-gray-900 dark:text-white">
|
||||
{comment.author.fullName}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{dayjs(comment.createdAt).fromNow()}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-700 dark:text-gray-300 text-sm">
|
||||
{comment.content}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<button className="text-xs text-gray-500 hover:text-red-500 transition-colors">
|
||||
❤️ {comment.likes} beğeni
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add Comment */}
|
||||
<div className="flex gap-3">
|
||||
<img
|
||||
src="https://ui-avatars.com/api/?name=Sedat+Ozturk&background=3b82f6&color=fff"
|
||||
alt="You"
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
<div className="flex-1 flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleAddComment(selectedEvent.id)
|
||||
}
|
||||
}}
|
||||
placeholder="Yorumunuzu yazın..."
|
||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleAddComment(selectedEvent.id)}
|
||||
disabled={!newComment.trim()}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg transition-colors"
|
||||
>
|
||||
Gönder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Photo Viewer Modal */}
|
||||
<AnimatePresence>
|
||||
{showPhotoModal && selectedEvent && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/90 z-50"
|
||||
onClick={() => setShowPhotoModal(false)}
|
||||
/>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
className="relative max-w-5xl w-full"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={() => setShowPhotoModal(false)}
|
||||
className="absolute top-4 right-4 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white z-10"
|
||||
>
|
||||
<HiXMark className="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
{/* Navigation */}
|
||||
{selectedEvent.photos.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setSelectedPhotoIndex((prev) =>
|
||||
prev === 0 ? selectedEvent.photos.length - 1 : prev - 1
|
||||
)}
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white"
|
||||
>
|
||||
<HiChevronLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedPhotoIndex((prev) =>
|
||||
prev === selectedEvent.photos.length - 1 ? 0 : prev + 1
|
||||
)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white"
|
||||
>
|
||||
<HiChevronRight className="w-6 h-6" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Image */}
|
||||
<img
|
||||
src={selectedEvent.photos[selectedPhotoIndex]}
|
||||
alt={`${selectedEvent.title} - ${selectedPhotoIndex + 1}`}
|
||||
className="w-full h-auto max-h-[80vh] object-contain rounded-lg"
|
||||
/>
|
||||
|
||||
{/* Counter */}
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 px-4 py-2 bg-black/70 text-white rounded-full text-sm">
|
||||
{selectedPhotoIndex + 1} / {selectedEvent.photos.length}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EventsModule
|
||||
480
ui/src/components/intranet/HR/ExpenseManagement.tsx
Normal file
480
ui/src/components/intranet/HR/ExpenseManagement.tsx
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
import React, { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import {
|
||||
HiCurrencyDollar,
|
||||
HiCheckCircle,
|
||||
HiPlus,
|
||||
HiFunnel,
|
||||
HiXMark,
|
||||
HiPaperClip,
|
||||
HiArrowUpTray
|
||||
} from 'react-icons/hi2'
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/tr'
|
||||
import { mockExpenseRequests, ExpenseRequest } from '../../../mocks/mockIntranetData'
|
||||
|
||||
dayjs.locale('tr')
|
||||
|
||||
const ExpenseManagement: React.FC = () => {
|
||||
const [requests, setRequests] = useState<ExpenseRequest[]>(mockExpenseRequests)
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'approved' | 'rejected'>('all')
|
||||
|
||||
// Harcama istatistikleri (mock)
|
||||
const expenseStats = {
|
||||
thisMonth: { total: 2370, approved: 2050, pending: 320, rejected: 0 },
|
||||
thisYear: { total: 28450, approved: 26100, pending: 1800, rejected: 550 },
|
||||
byCategory: {
|
||||
travel: 12300,
|
||||
meal: 6800,
|
||||
accommodation: 5400,
|
||||
transport: 2950,
|
||||
other: 1000
|
||||
}
|
||||
}
|
||||
|
||||
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 getCategoryLabel = (category: string) => {
|
||||
const labels = {
|
||||
travel: 'Seyahat',
|
||||
meal: 'Yemek',
|
||||
accommodation: 'Konaklama',
|
||||
transport: 'Ulaşım',
|
||||
other: 'Diğer'
|
||||
}
|
||||
return labels[category as keyof typeof labels]
|
||||
}
|
||||
|
||||
const getCategoryIcon = (category: string) => {
|
||||
const icons = {
|
||||
travel: '✈️',
|
||||
meal: '🍽️',
|
||||
accommodation: '🏨',
|
||||
transport: '🚗',
|
||||
other: '📋'
|
||||
}
|
||||
return icons[category as keyof typeof icons]
|
||||
}
|
||||
|
||||
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">
|
||||
Harcama Yönetimi
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Harcama 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 Harcama 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">
|
||||
₺{expenseStats.thisMonth.total.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
toplam harcama
|
||||
</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">
|
||||
₺{expenseStats.thisMonth.approved.toLocaleString()}
|
||||
</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">
|
||||
₺{expenseStats.thisMonth.pending.toLocaleString()}
|
||||
</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">
|
||||
₺{expenseStats.thisYear.total.toLocaleString()}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
bu yıl toplam
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kategori Dağılımı */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Kategori Bazlı Dağılım (Yıllık)
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{Object.entries(expenseStats.byCategory).map(([category, amount]) => {
|
||||
const percentage = (amount / expenseStats.thisYear.total) * 100
|
||||
return (
|
||||
<div key={category}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
|
||||
{getCategoryIcon(category)} {getCategoryLabel(category)}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
₺{amount.toLocaleString()} ({percentage.toFixed(1)}%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all"
|
||||
style={{ width: `${percentage}%` }}
|
||||
></div>
|
||||
</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>
|
||||
|
||||
{/* Harcama 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">
|
||||
Harcama 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">{getCategoryIcon(request.category)}</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">
|
||||
{getCategoryLabel(request.category)}
|
||||
</h3>
|
||||
<span className={`px-2.5 py-1 text-xs rounded-full ${getStatusColor(request.status)}`}>
|
||||
{request.status === 'pending' && '⏳ Beklemede'}
|
||||
{request.status === 'approved' && '✅ Onaylandı'}
|
||||
{request.status === '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">Tutar</p>
|
||||
<p className="text-lg font-bold text-green-600 dark:text-green-400">
|
||||
{request.amount.toLocaleString()} {request.currency}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Tarih</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{dayjs(request.date).format('DD MMMM YYYY')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Makbuzlar</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{request.receipts.length} dosya
|
||||
</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.createdAt).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">Açıklama:</span> {request.description}
|
||||
</p>
|
||||
{request.receipts.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{request.receipts.map((receipt, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
href={receipt.url}
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<HiPaperClip className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||
<span className="text-xs text-gray-700 dark:text-gray-300">
|
||||
{receipt.name}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
({receipt.size})
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{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">
|
||||
<HiCurrencyDollar 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 harcama talebi bulunmuyor'
|
||||
: `${filterStatus === 'pending' ? 'Bekleyen' : filterStatus === 'approved' ? 'Onaylanan' : 'Reddedilen'} harcama talebi bulunmuyor`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Yeni Harcama 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 Harcama 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">
|
||||
Harcama Kategorisi
|
||||
</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>Seyahat</option>
|
||||
<option>Yemek</option>
|
||||
<option>Konaklama</option>
|
||||
<option>Ulaşım</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">
|
||||
Tutar
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="0.00"
|
||||
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">
|
||||
Para Birimi
|
||||
</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>TRY</option>
|
||||
<option>USD</option>
|
||||
<option>EUR</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Harcama 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">
|
||||
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">
|
||||
Açıklama
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
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="Harcama detaylarını açıklayın..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Makbuz / Fatura
|
||||
</label>
|
||||
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-6 text-center hover:border-blue-500 dark:hover:border-blue-500 transition-colors cursor-pointer">
|
||||
<HiArrowUpTray className="w-8 h-8 text-gray-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Dosyaları sürükle-bırak veya <span className="text-blue-600 dark:text-blue-400">tıkla</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
PDF, JPG, PNG (max 5MB)
|
||||
</p>
|
||||
</div>
|
||||
</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 ExpenseManagement
|
||||
426
ui/src/components/intranet/HR/LeaveManagement.tsx
Normal file
426
ui/src/components/intranet/HR/LeaveManagement.tsx
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
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 } 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="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">
|
||||
İ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.type)}</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.type)}
|
||||
</h3>
|
||||
<span className={`px-2.5 py-1 text-xs rounded-full ${getStatusColor(request.status)}`}>
|
||||
{request.status === 'pending' && '⏳ Beklemede'}
|
||||
{request.status === 'approved' && '✅ Onaylandı'}
|
||||
{request.status === '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.createdAt).format('DD MMM YYYY')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
<span className="font-medium">Açı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">
|
||||
Açı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
|
||||
376
ui/src/components/intranet/HR/OvertimeManagement.tsx
Normal file
376
ui/src/components/intranet/HR/OvertimeManagement.tsx
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
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 } 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 === 'pending' && '⏳ Beklemede'}
|
||||
{request.status === 'approved' && '✅ Onaylandı'}
|
||||
{request.status === '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.createdAt).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">Açı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">
|
||||
<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">
|
||||
Açı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
|
||||
539
ui/src/components/intranet/IntranetDashboard/index.tsx
Normal file
539
ui/src/components/intranet/IntranetDashboard/index.tsx
Normal file
|
|
@ -0,0 +1,539 @@
|
|||
import React, { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import {
|
||||
HiBell as BellIcon,
|
||||
HiCalendar as CalendarIcon,
|
||||
HiDocumentText as DocumentTextIcon,
|
||||
HiChartBar as ChartBarIcon,
|
||||
HiUserGroup as UserGroupIcon,
|
||||
HiClock as ClockIcon,
|
||||
HiSparkles as SparklesIcon,
|
||||
HiArrowTrendingUp as ArrowTrendingUpIcon,
|
||||
HiArrowTrendingDown as ArrowTrendingDownIcon,
|
||||
HiXMark,
|
||||
HiEye,
|
||||
HiPaperClip
|
||||
} from 'react-icons/hi2'
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/tr'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import isBetween from 'dayjs/plugin/isBetween'
|
||||
import {
|
||||
mockAnnouncements,
|
||||
mockEvents,
|
||||
mockBirthdays,
|
||||
mockAnniversaries,
|
||||
mockQuickLinks,
|
||||
mockTasks,
|
||||
Announcement
|
||||
} from '../../../mocks/mockIntranetData'
|
||||
|
||||
dayjs.locale('tr')
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(isBetween)
|
||||
|
||||
const IntranetDashboard: React.FC = () => {
|
||||
const [selectedDate] = useState(new Date())
|
||||
const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null)
|
||||
|
||||
// Bugünün etkinlikleri
|
||||
const todayEvents = mockEvents.filter(event =>
|
||||
event.isPublished && dayjs(event.date).isSame(dayjs(), 'day')
|
||||
)
|
||||
|
||||
// Yaklaşan etkinlikler (7 gün içinde)
|
||||
const upcomingEvents = mockEvents.filter(event =>
|
||||
event.isPublished &&
|
||||
dayjs(event.date).isAfter(dayjs()) &&
|
||||
dayjs(event.date).isBefore(dayjs().add(7, 'day'))
|
||||
)
|
||||
|
||||
// Bu haftaki doğum günleri
|
||||
const weekBirthdays = mockBirthdays.filter(b =>
|
||||
dayjs(b.date).isBetween(dayjs().startOf('week'), dayjs().endOf('week'))
|
||||
)
|
||||
|
||||
// Bu ayki iş yıldönümleri
|
||||
const monthAnniversaries = mockAnniversaries.filter(a =>
|
||||
dayjs(a.hireDate).month() === dayjs().month()
|
||||
)
|
||||
|
||||
// Öncelikli görevler
|
||||
const priorityTasks = mockTasks.filter(t =>
|
||||
t.priority === 'high' || t.priority === 'urgent'
|
||||
).slice(0, 3)
|
||||
|
||||
// Sabitlenmiş duyurular
|
||||
const pinnedAnnouncements = mockAnnouncements
|
||||
.filter(a => a.isPinned)
|
||||
.slice(0, 3)
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
general: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
||||
hr: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300',
|
||||
it: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300',
|
||||
event: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||
urgent: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||
}
|
||||
return colors[category] || colors.general
|
||||
}
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
low: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300',
|
||||
medium: 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-300',
|
||||
high: 'bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-300',
|
||||
urgent: 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-300'
|
||||
}
|
||||
return colors[priority]
|
||||
}
|
||||
|
||||
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>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Hoş geldiniz, {dayjs().format('DD MMMM YYYY dddd')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg relative">
|
||||
<BellIcon className="w-6 h-6 text-gray-600 dark:text-gray-400" />
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Sol Kolon - Duyurular */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Duyurular */}
|
||||
<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">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<BellIcon className="w-5 h-5" />
|
||||
Önemli Duyurular
|
||||
</h2>
|
||||
<button className="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">
|
||||
Tümünü Gör
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{pinnedAnnouncements.map((announcement) => (
|
||||
<div
|
||||
key={announcement.id}
|
||||
onClick={() => setSelectedAnnouncement(announcement)}
|
||||
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<img
|
||||
src={announcement.author.avatar}
|
||||
alt={announcement.author.fullName}
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{announcement.title}
|
||||
</h3>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${getCategoryColor(announcement.category)}`}>
|
||||
{announcement.category === 'general' && 'Genel'}
|
||||
{announcement.category === 'hr' && 'İK'}
|
||||
{announcement.category === 'it' && 'IT'}
|
||||
{announcement.category === 'event' && 'Etkinlik'}
|
||||
{announcement.category === 'urgent' && 'Acil'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||
{announcement.excerpt}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>{announcement.author.fullName}</span>
|
||||
<span>•</span>
|
||||
<span>{dayjs(announcement.publishDate).fromNow()}</span>
|
||||
<span>•</span>
|
||||
<span>{announcement.viewCount} görüntülenme</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hızlı Erişim */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2 mb-4">
|
||||
<SparklesIcon className="w-5 h-5" />
|
||||
Hızlı Erişim
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{mockQuickLinks.map((link) => (
|
||||
<motion.a
|
||||
key={link.id}
|
||||
href={link.url}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="flex flex-col items-center gap-2 p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-500 transition-colors"
|
||||
style={{ borderColor: link.color + '40' }}
|
||||
>
|
||||
<span className="text-3xl">{link.icon}</span>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white text-center">
|
||||
{link.name}
|
||||
</span>
|
||||
</motion.a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Öncelikli Görevler */}
|
||||
<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 flex items-center gap-2">
|
||||
<ChartBarIcon className="w-5 h-5" />
|
||||
Öncelikli Görevler
|
||||
</h2>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{priorityTasks.map((task) => (
|
||||
<div key={task.id} className="p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mt-1 rounded border-gray-300 dark:border-gray-600"
|
||||
checked={task.status === 'done'}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{task.title}
|
||||
</h3>
|
||||
<span className={`px-2 py-0.5 text-xs rounded ${getPriorityColor(task.priority)}`}>
|
||||
{task.priority === 'urgent' && '🔥 Acil'}
|
||||
{task.priority === 'high' && 'Yüksek'}
|
||||
{task.priority === 'medium' && 'Orta'}
|
||||
{task.priority === 'low' && 'Düşük'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mb-2">
|
||||
{task.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 text-xs text-gray-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<ClockIcon className="w-3 h-3" />
|
||||
{dayjs(task.dueDate).format('DD MMM')}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<UserGroupIcon className="w-3 h-3" />
|
||||
{task.assignedTo.length} kişi
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sağ Kolon - Etkinlikler & Kutlamalar */}
|
||||
<div className="space-y-6">
|
||||
{/* Bugünün Etkinlikleri */}
|
||||
<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 flex items-center gap-2">
|
||||
<CalendarIcon className="w-5 h-5" />
|
||||
Bugün
|
||||
</h2>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
{todayEvents.length > 0 ? (
|
||||
todayEvents.map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="p-3 rounded-lg border-l-4 bg-gray-50 dark:bg-gray-700/50 border-l-blue-500"
|
||||
>
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{event.title}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||
{dayjs(event.date).format('DD MMMM YYYY')} - {event.location}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{event.participants} katılımcı
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
|
||||
Bugün etkinlik yok
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Yaklaşan Etkinlikler */}
|
||||
<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 flex items-center gap-2">
|
||||
<CalendarIcon className="w-5 h-5" />
|
||||
Yaklaşan Etkinlikler
|
||||
</h2>
|
||||
</div>
|
||||
<div className="p-4 space-y-3">
|
||||
{upcomingEvents.slice(0, 3).map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="p-3 rounded-lg border-l-4 bg-gray-50 dark:bg-gray-700/50 border-l-green-500"
|
||||
>
|
||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{event.title}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||
{dayjs(event.date).format('DD MMMM YYYY')} - {event.location}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Doğum Günleri */}
|
||||
{weekBirthdays.length > 0 && (
|
||||
<div className="bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-lg shadow-sm border border-purple-200 dark:border-purple-800">
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2 mb-4">
|
||||
🎂 Bu Hafta Doğanlar
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{weekBirthdays.map((birthday, index) => (
|
||||
<div key={index} className="flex items-center gap-3">
|
||||
<img
|
||||
src={birthday.employee.avatar}
|
||||
alt={birthday.employee.fullName}
|
||||
className="w-10 h-10 rounded-full border-2 border-purple-200 dark:border-purple-700"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{birthday.employee.fullName}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{dayjs(birthday.date).format('DD MMMM')} • {birthday.age} yaşında
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* İş Yıldönümleri */}
|
||||
{monthAnniversaries.length > 0 && (
|
||||
<div className="bg-gradient-to-br from-blue-50 to-cyan-50 dark:from-blue-900/20 dark:to-cyan-900/20 rounded-lg shadow-sm border border-blue-200 dark:border-blue-800">
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2 mb-4">
|
||||
🎉 İş Yıldönümleri
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
{monthAnniversaries.map((anniversary, index) => (
|
||||
<div key={index} className="flex items-center gap-3">
|
||||
<img
|
||||
src={anniversary.employee.avatar}
|
||||
alt={anniversary.employee.fullName}
|
||||
className="w-10 h-10 rounded-full border-2 border-blue-200 dark:border-blue-700"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{anniversary.employee.fullName}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{anniversary.years} yıldır bizimle! 🎊
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Announcement Detail Modal */}
|
||||
<AnimatePresence>
|
||||
{selectedAnnouncement && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/50 z-40"
|
||||
onClick={() => setSelectedAnnouncement(null)}
|
||||
/>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 overflow-y-auto">
|
||||
<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-3xl w-full my-8"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className={`px-3 py-1 text-xs font-medium rounded-full ${getCategoryColor(selectedAnnouncement.category)}`}>
|
||||
{selectedAnnouncement.category === 'general' && '📢 Genel'}
|
||||
{selectedAnnouncement.category === 'hr' && '👥 İnsan Kaynakları'}
|
||||
{selectedAnnouncement.category === 'it' && '💻 Bilgi Teknolojileri'}
|
||||
{selectedAnnouncement.category === 'event' && '🎉 Etkinlik'}
|
||||
{selectedAnnouncement.category === 'urgent' && '🚨 Acil'}
|
||||
</span>
|
||||
{selectedAnnouncement.isPinned && (
|
||||
<span className="text-yellow-500 text-sm">📌 Sabitlenmiş</span>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{selectedAnnouncement.title}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedAnnouncement(null)}
|
||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||
>
|
||||
<HiXMark className="w-6 h-6 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Author Info */}
|
||||
<div className="flex items-center gap-3 mt-4">
|
||||
<img
|
||||
src={selectedAnnouncement.author.avatar}
|
||||
alt={selectedAnnouncement.author.fullName}
|
||||
className="w-12 h-12 rounded-full"
|
||||
/>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 dark:text-white">
|
||||
{selectedAnnouncement.author.fullName}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>{dayjs(selectedAnnouncement.publishDate).format('DD MMMM YYYY, HH:mm')}</span>
|
||||
<span>•</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<HiEye className="w-4 h-4" />
|
||||
{selectedAnnouncement.viewCount} görüntülenme
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
||||
{/* Image if exists */}
|
||||
{selectedAnnouncement.imageUrl && (
|
||||
<img
|
||||
src={selectedAnnouncement.imageUrl}
|
||||
alt={selectedAnnouncement.title}
|
||||
className="w-full rounded-lg mb-6"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Full Content */}
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
<p className="text-gray-700 dark:text-gray-300 whitespace-pre-line">
|
||||
{selectedAnnouncement.content}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Attachments */}
|
||||
{selectedAnnouncement.attachments && selectedAnnouncement.attachments.length > 0 && (
|
||||
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
|
||||
<HiPaperClip className="w-5 h-5" />
|
||||
Ekler ({selectedAnnouncement.attachments.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{selectedAnnouncement.attachments.map((attachment, idx) => (
|
||||
<a
|
||||
key={idx}
|
||||
href={attachment.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<HiPaperClip className="w-5 h-5 text-gray-400" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
{attachment.name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{attachment.size}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-sm text-blue-600 dark:text-blue-400">
|
||||
İndir
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Departments */}
|
||||
{selectedAnnouncement.departments && selectedAnnouncement.departments.length > 0 && (
|
||||
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Hedef Departmanlar
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedAnnouncement.departments.map((dept, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-3 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm rounded-full"
|
||||
>
|
||||
{dept}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expiry Date */}
|
||||
{selectedAnnouncement.expiryDate && (
|
||||
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span className="font-medium">Son Geçerlilik Tarihi:</span>{' '}
|
||||
{dayjs(selectedAnnouncement.expiryDate).format('DD MMMM YYYY')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-6 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<button
|
||||
onClick={() => setSelectedAnnouncement(null)}
|
||||
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
Kapat
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default IntranetDashboard
|
||||
241
ui/src/components/intranet/IntranetSidebar.tsx
Normal file
241
ui/src/components/intranet/IntranetSidebar.tsx
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
import React, { useState, useMemo } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import {
|
||||
HiHome,
|
||||
HiCalendar,
|
||||
HiFolder,
|
||||
HiClipboardDocumentList,
|
||||
HiChevronRight,
|
||||
HiChatBubbleLeftRight,
|
||||
HiCake,
|
||||
HiAcademicCap,
|
||||
HiKey,
|
||||
HiBuildingOffice2,
|
||||
HiClipboardDocumentCheck,
|
||||
HiUserPlus,
|
||||
} from 'react-icons/hi2'
|
||||
import {
|
||||
mockTasks,
|
||||
mockEvents,
|
||||
mockDocuments,
|
||||
mockBirthdays,
|
||||
mockTrainings,
|
||||
mockReservations,
|
||||
mockSurveys,
|
||||
mockVisitors,
|
||||
} from '../../mocks/mockIntranetData'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
interface MenuItem {
|
||||
id: string
|
||||
label: string
|
||||
icon: React.ElementType
|
||||
path?: string
|
||||
badge?: number
|
||||
children?: MenuItem[]
|
||||
}
|
||||
|
||||
const getMenuItems = (badgeCounts: any): MenuItem[] => [
|
||||
{
|
||||
id: 'dashboard',
|
||||
label: 'Ana Sayfa',
|
||||
icon: HiHome,
|
||||
path: '/intranet/dashboard',
|
||||
},
|
||||
{
|
||||
id: 'social',
|
||||
label: 'Sosyal Akış',
|
||||
icon: HiChatBubbleLeftRight,
|
||||
path: '/intranet/social',
|
||||
},
|
||||
{
|
||||
id: 'events',
|
||||
label: 'Etkinlikler',
|
||||
icon: HiCalendar,
|
||||
path: '/intranet/events',
|
||||
badge: badgeCounts.events || undefined,
|
||||
},
|
||||
{
|
||||
id: 'documents',
|
||||
label: 'Dokümanlar',
|
||||
icon: HiFolder,
|
||||
path: '/intranet/documents',
|
||||
badge: badgeCounts.documents || undefined,
|
||||
},
|
||||
{
|
||||
id: 'tasks',
|
||||
label: 'Görevler',
|
||||
icon: HiClipboardDocumentList,
|
||||
path: '/intranet/tasks',
|
||||
badge: badgeCounts.tasks || undefined,
|
||||
},
|
||||
{
|
||||
id: 'birthdays',
|
||||
label: 'Doğum Günleri',
|
||||
icon: HiCake,
|
||||
path: '/intranet/birthdays',
|
||||
badge: badgeCounts.birthdays || undefined,
|
||||
},
|
||||
{
|
||||
id: 'training',
|
||||
label: 'Eğitimler',
|
||||
icon: HiAcademicCap,
|
||||
path: '/intranet/training',
|
||||
badge: badgeCounts.training || undefined,
|
||||
},
|
||||
{
|
||||
id: 'reservations',
|
||||
label: 'Rezervasyonlar',
|
||||
icon: HiKey,
|
||||
path: '/intranet/reservations',
|
||||
badge: badgeCounts.reservations || undefined,
|
||||
},
|
||||
{ id: 'hr-leave', label: 'İzin Yönetimi', icon: HiCalendar, path: '/intranet/hr/leave' },
|
||||
{
|
||||
id: 'hr-overtime',
|
||||
label: 'Mesai Yönetimi',
|
||||
icon: HiClipboardDocumentList,
|
||||
path: '/intranet/hr/overtime',
|
||||
},
|
||||
{ id: 'hr-expense', label: 'Harcama Yönetimi', icon: HiFolder, path: '/intranet/hr/expense' },
|
||||
{
|
||||
id: 'cafeteria',
|
||||
label: 'Kafeterya & Servis',
|
||||
icon: HiBuildingOffice2,
|
||||
path: '/intranet/cafeteria/shuttle',
|
||||
},
|
||||
{
|
||||
id: 'surveys',
|
||||
label: 'Anketler',
|
||||
icon: HiClipboardDocumentCheck,
|
||||
path: '/intranet/surveys',
|
||||
badge: badgeCounts.surveys || undefined,
|
||||
},
|
||||
{
|
||||
id: 'visitors',
|
||||
label: 'Ziyaretçi Yönetimi',
|
||||
icon: HiUserPlus,
|
||||
path: '/intranet/visitors',
|
||||
badge: badgeCounts.visitors || undefined,
|
||||
},
|
||||
]
|
||||
|
||||
interface IntranetSidebarProps {
|
||||
activePath: string
|
||||
onNavigate: (path: string) => void
|
||||
}
|
||||
|
||||
const IntranetSidebar: React.FC<IntranetSidebarProps> = ({ activePath, onNavigate }) => {
|
||||
const [expandedMenus, setExpandedMenus] = useState<string[]>(['hr'])
|
||||
|
||||
// Dinamik badge sayılarını hesapla
|
||||
const badgeCounts = useMemo(() => {
|
||||
const today = dayjs()
|
||||
|
||||
return {
|
||||
tasks: mockTasks.filter((t) => t.status !== 'done').length,
|
||||
events: mockEvents.filter(
|
||||
(e) =>
|
||||
e.isPublished && (dayjs(e.date).isAfter(today) || dayjs(e.date).isSame(today, 'day')),
|
||||
).length,
|
||||
documents: mockDocuments.length,
|
||||
birthdays: mockBirthdays.filter((b) => {
|
||||
const birthMonth = dayjs(b.date).month()
|
||||
const currentMonth = today.month()
|
||||
return birthMonth === currentMonth
|
||||
}).length,
|
||||
training: mockTrainings.filter((t) => t.status === 'upcoming').length,
|
||||
reservations: mockReservations.filter(
|
||||
(r) => r.status === 'approved' || r.status === 'pending',
|
||||
).length,
|
||||
surveys: mockSurveys.filter((s) => s.status === 'active').length,
|
||||
visitors: mockVisitors.filter((v) => v.status === 'scheduled' || v.status === 'checked-in')
|
||||
.length,
|
||||
}
|
||||
}, [])
|
||||
|
||||
const toggleMenu = (id: string) => {
|
||||
setExpandedMenus((prev) =>
|
||||
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
|
||||
)
|
||||
}
|
||||
|
||||
const isActive = (path?: string) => {
|
||||
if (!path) return false
|
||||
return activePath === path || activePath.startsWith(path + '/')
|
||||
}
|
||||
|
||||
const menuItems = useMemo(() => getMenuItems(badgeCounts), [badgeCounts])
|
||||
|
||||
const renderMenuItem = (item: MenuItem, level: number = 0) => {
|
||||
const hasChildren = item.children && item.children.length > 0
|
||||
const isExpanded = expandedMenus.includes(item.id)
|
||||
const active = isActive(item.path)
|
||||
|
||||
return (
|
||||
<div key={item.id}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (hasChildren) {
|
||||
toggleMenu(item.id)
|
||||
} else if (item.path) {
|
||||
onNavigate(item.path)
|
||||
}
|
||||
}}
|
||||
className={`w-full flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors ${
|
||||
active
|
||||
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||
} ${level > 0 ? 'ml-6' : ''}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<item.icon className="w-5 h-5" />
|
||||
<span className="font-medium text-sm">{item.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{item.badge && item.badge > 0 && (
|
||||
<span className="px-2 py-0.5 bg-red-500 text-white text-xs rounded-full">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
{hasChildren && (
|
||||
<motion.div animate={{ rotate: isExpanded ? 90 : 0 }} transition={{ duration: 0.2 }}>
|
||||
<HiChevronRight className="w-4 h-4" />
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{hasChildren && (
|
||||
<AnimatePresence>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="mt-1 space-y-1">
|
||||
{item.children!.map((child) => renderMenuItem(child, level + 1))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 h-screen sticky top-0 overflow-y-auto">
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">İntranet Portal</h2>
|
||||
</div>
|
||||
|
||||
<nav className="p-4 space-y-1">{menuItems.map((item) => renderMenuItem(item))}</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default IntranetSidebar
|
||||
319
ui/src/components/intranet/Reservations/index.tsx
Normal file
319
ui/src/components/intranet/Reservations/index.tsx
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
import React, { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import {
|
||||
HiKey,
|
||||
HiCalendar,
|
||||
HiTruck,
|
||||
HiCog,
|
||||
HiPlus,
|
||||
HiXMark
|
||||
} from 'react-icons/hi2'
|
||||
import dayjs from 'dayjs'
|
||||
import { mockReservations, Reservation } from '../../../mocks/mockIntranetData'
|
||||
|
||||
const ReservationsModule: React.FC = () => {
|
||||
const [selectedType, setSelectedType] = useState<'all' | 'room' | 'vehicle' | 'equipment'>('all')
|
||||
const [showNewReservation, setShowNewReservation] = useState(false)
|
||||
|
||||
const filteredReservations = selectedType === 'all'
|
||||
? mockReservations
|
||||
: mockReservations.filter(r => r.type === selectedType)
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'room':
|
||||
return <HiKey className="w-5 h-5" />
|
||||
case 'vehicle':
|
||||
return <HiTruck className="w-5 h-5" />
|
||||
case 'equipment':
|
||||
return <HiCog className="w-5 h-5" />
|
||||
default:
|
||||
return <HiKey className="w-5 h-5" />
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
room: 'Toplantı Salonu',
|
||||
vehicle: 'Araç',
|
||||
equipment: 'Ekipman'
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
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',
|
||||
completed: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}
|
||||
return colors[status] || colors.pending
|
||||
}
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
pending: 'Bekliyor',
|
||||
approved: 'Onaylandı',
|
||||
rejected: 'Reddedildi',
|
||||
completed: 'Tamamlandı'
|
||||
}
|
||||
return labels[status] || status
|
||||
}
|
||||
|
||||
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">
|
||||
🔑 Rezervasyonlar
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Oda, araç ve ekipman rezervasyonları
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowNewReservation(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 Rezervasyon
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Type Filter */}
|
||||
<div className="flex gap-3">
|
||||
{[
|
||||
{ value: 'all' as const, label: 'Tümü', icon: HiCalendar },
|
||||
{ value: 'room' as const, label: 'Toplantı Salonu', icon: HiKey },
|
||||
{ value: 'vehicle' as const, label: 'Araç', icon: HiTruck },
|
||||
{ value: 'equipment' as const, label: 'Ekipman', icon: HiCog }
|
||||
].map((type) => (
|
||||
<button
|
||||
key={type.value}
|
||||
onClick={() => setSelectedType(type.value)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${
|
||||
selectedType === type.value
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
<type.icon className="w-5 h-5" />
|
||||
{type.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Reservations List */}
|
||||
<div className="space-y-4">
|
||||
{filteredReservations.map((reservation: Reservation, idx: number) => (
|
||||
<motion.div
|
||||
key={reservation.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.05 }}
|
||||
className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all"
|
||||
>
|
||||
<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 text-blue-600 dark:text-blue-400">
|
||||
{getTypeIcon(reservation.type)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{reservation.resourceName}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{getTypeLabel(reservation.type)}
|
||||
</p>
|
||||
</div>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(reservation.status)}`}>
|
||||
{getStatusLabel(reservation.status)}
|
||||
</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(reservation.startDate).format('DD MMM, HH:mm')}
|
||||
</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(reservation.endDate).format('DD MMM, HH:mm')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Rezerve Eden</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<img
|
||||
src={reservation.bookedBy.avatar}
|
||||
alt={reservation.bookedBy.fullName}
|
||||
className="w-5 h-5 rounded-full"
|
||||
/>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{reservation.bookedBy.fullName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{reservation.participants && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Katılımcı</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{reservation.participants} kişi
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Amaç: {reservation.purpose}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{reservation.notes && (
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3">
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
<strong>Not:</strong> {reservation.notes}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
{filteredReservations.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||
<HiCalendar className="w-16 h-16 mx-auto mb-4 opacity-20" />
|
||||
<p>Rezervasyon bulunamadı</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* New Reservation Modal */}
|
||||
<AnimatePresence>
|
||||
{showNewReservation && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/50 z-40"
|
||||
onClick={() => setShowNewReservation(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"
|
||||
>
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Yeni Rezervasyon Oluştur
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowNewReservation(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">
|
||||
Rezervasyon Tipi
|
||||
</label>
|
||||
<select className="w-full px-4 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>Toplantı Salonu</option>
|
||||
<option>Araç</option>
|
||||
<option>Ekipman</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Kaynak Seçin
|
||||
</label>
|
||||
<select className="w-full px-4 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>Toplantı Salonu A</option>
|
||||
<option>Toplantı Salonu B</option>
|
||||
<option>Eğitim Salonu</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="datetime-local"
|
||||
className="w-full px-4 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="datetime-local"
|
||||
className="w-full px-4 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">
|
||||
Amaç
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rezervasyon amacını yazın"
|
||||
className="w-full px-4 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">
|
||||
Notlar (Opsiyonel)
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
placeholder="Ek notlarınızı yazın"
|
||||
className="w-full px-4 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="flex gap-3 pt-4">
|
||||
<button
|
||||
onClick={() => setShowNewReservation(false)}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
||||
Rezervasyon Oluştur
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReservationsModule
|
||||
288
ui/src/components/intranet/Surveys/index.tsx
Normal file
288
ui/src/components/intranet/Surveys/index.tsx
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
import React, { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { HiClipboardDocumentCheck, HiClock, HiCheckCircle, HiXMark } from 'react-icons/hi2'
|
||||
import dayjs from 'dayjs'
|
||||
import { mockSurveys, Survey } from '../../../mocks/mockIntranetData'
|
||||
|
||||
const SurveysModule: React.FC = () => {
|
||||
const [selectedStatus, setSelectedStatus] = useState<'all' | 'active' | 'draft' | 'closed'>('all')
|
||||
const [showSurveyModal, setShowSurveyModal] = useState(false)
|
||||
const [selectedSurvey, setSelectedSurvey] = useState<Survey | null>(null)
|
||||
|
||||
const filteredSurveys = selectedStatus === 'all'
|
||||
? mockSurveys
|
||||
: mockSurveys.filter(s => s.status === selectedStatus)
|
||||
|
||||
const handleTakeSurvey = (survey: Survey) => {
|
||||
setSelectedSurvey(survey)
|
||||
setShowSurveyModal(true)
|
||||
}
|
||||
|
||||
const handleSubmitSurvey = () => {
|
||||
// Anket gönderildi
|
||||
setShowSurveyModal(false)
|
||||
setSelectedSurvey(null)
|
||||
// Başarı mesajı gösterilebilir
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
draft: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300',
|
||||
active: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||
closed: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||
}
|
||||
return colors[status] || colors.draft
|
||||
}
|
||||
|
||||
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>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
📊 Anketler & Memnuniyet Formları
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Geri bildirim ve değerlendirme anketleri
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filter */}
|
||||
<div className="flex gap-3">
|
||||
{[
|
||||
{ value: 'all' as const, label: 'Tümü' },
|
||||
{ value: 'active' as const, label: 'Aktif' },
|
||||
{ value: 'draft' as const, label: 'Taslak' },
|
||||
{ value: 'closed' as const, label: 'Kapalı' }
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
onClick={() => setSelectedStatus(tab.value)}
|
||||
className={`px-4 py-2 rounded-lg border-2 transition-all ${
|
||||
selectedStatus === tab.value
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Surveys List */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{filteredSurveys.map((survey: Survey, idx: number) => (
|
||||
<motion.div
|
||||
key={survey.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.1 }}
|
||||
className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white pr-4">
|
||||
{survey.title}
|
||||
</h3>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium whitespace-nowrap ${getStatusColor(survey.status)}`}>
|
||||
{survey.status === 'active' ? 'Aktif' : survey.status === 'draft' ? 'Taslak' : 'Kapalı'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
{survey.description}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Soru Sayısı</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{survey.totalQuestions} soru</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Yanıt</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">{survey.responses} kişi</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Son Tarih</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{dayjs(survey.deadline).format('DD MMM YYYY')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">Gizlilik</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{survey.isAnonymous ? '🔒 Anonim' : '👤 İsimli'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">Hedef Kitle</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{survey.targetAudience.map((audience, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-xs rounded"
|
||||
>
|
||||
{audience}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{survey.status === 'active' && (
|
||||
<button
|
||||
onClick={() => handleTakeSurvey(survey)}
|
||||
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
Anketi Doldur
|
||||
</button>
|
||||
)}
|
||||
|
||||
{survey.status === 'closed' && (
|
||||
<div className="flex items-center justify-center gap-2 py-2 text-green-600 dark:text-green-400">
|
||||
<HiCheckCircle className="w-5 h-5" />
|
||||
<span className="text-sm font-medium">Tamamlandı</span>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredSurveys.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||
<HiClipboardDocumentCheck className="w-16 h-16 mx-auto mb-4 opacity-20" />
|
||||
<p>Anket bulunamadı</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Survey Modal */}
|
||||
<AnimatePresence>
|
||||
{showSurveyModal && selectedSurvey && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/50 z-40"
|
||||
onClick={() => setShowSurveyModal(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"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<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 z-10">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{selectedSurvey.title}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||
{selectedSurvey.description}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowSurveyModal(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>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
handleSubmitSurvey()
|
||||
}}
|
||||
className="p-6 space-y-6"
|
||||
>
|
||||
{/* Örnek Anket Soruları */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
1. Genel memnuniyet düzeyiniz nedir? *
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{[1, 2, 3, 4, 5].map((rating) => (
|
||||
<label key={rating} className="flex items-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<input type="radio" name="rating" value={rating} required />
|
||||
<span className="text-sm text-gray-900 dark:text-white">{rating}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
2. Hangi departmanda çalışıyorsunuz? *
|
||||
</label>
|
||||
<select
|
||||
required
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Seçiniz</option>
|
||||
<option value="it">Bilgi Teknolojileri</option>
|
||||
<option value="hr">İnsan Kaynakları</option>
|
||||
<option value="finance">Finans</option>
|
||||
<option value="sales">Satış</option>
|
||||
<option value="marketing">Pazarlama</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
3. Görüş ve önerileriniz
|
||||
</label>
|
||||
<textarea
|
||||
rows={4}
|
||||
placeholder="Yorumlarınızı buraya yazabilirsiniz..."
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!selectedSurvey.isAnonymous && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3">
|
||||
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||
ℹ️ Bu anket isim belirtilerek doldurulmaktadır. Yanıtlarınız kaydedilecektir.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedSurvey.isAnonymous && (
|
||||
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-3">
|
||||
<p className="text-sm text-green-700 dark:text-green-300">
|
||||
✅ Bu anket anonimdir. Kimlik bilgileriniz kaydedilmeyecektir.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSurveyModal(false)}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
Anketi Gönder
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SurveysModule
|
||||
655
ui/src/components/intranet/Tasks/index.tsx
Normal file
655
ui/src/components/intranet/Tasks/index.tsx
Normal file
|
|
@ -0,0 +1,655 @@
|
|||
import React, { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
closestCorners,
|
||||
useDroppable
|
||||
} from '@dnd-kit/core'
|
||||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import {
|
||||
HiPlus,
|
||||
HiXMark,
|
||||
HiClock,
|
||||
HiChatBubbleLeftRight,
|
||||
HiPaperClip,
|
||||
HiTrash
|
||||
} from 'react-icons/hi2'
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/tr'
|
||||
import { mockTasks, Task } from '../../../mocks/mockIntranetData'
|
||||
import { Badge } from '@/components/ui'
|
||||
|
||||
dayjs.locale('tr')
|
||||
|
||||
type TaskStatus = 'todo' | 'in-progress' | 'review' | 'done'
|
||||
|
||||
// Droppable Column Component
|
||||
interface DroppableColumnProps {
|
||||
id: TaskStatus
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const DroppableColumn: React.FC<DroppableColumnProps> = ({ id, children }) => {
|
||||
const { setNodeRef } = useDroppable({ id })
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} >
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Sortable Task Card Component
|
||||
interface SortableTaskCardProps {
|
||||
task: Task
|
||||
onTaskClick: (task: Task) => void
|
||||
getPriorityColor: (priority: string) => string
|
||||
getPriorityLabel: (priority: string) => string
|
||||
isOverdue: (date: Date | string) => boolean
|
||||
}
|
||||
|
||||
const SortableTaskCard: React.FC<SortableTaskCardProps> = ({
|
||||
task,
|
||||
onTaskClick,
|
||||
getPriorityColor,
|
||||
getPriorityLabel,
|
||||
isOverdue
|
||||
}) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging
|
||||
} = useSortable({ id: task.id })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1
|
||||
}
|
||||
|
||||
const overdue = isOverdue(task.dueDate) && task.status !== 'done'
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={`bg-white dark:bg-gray-800 rounded-lg p-3 sm:p-4 border-2 cursor-move hover:shadow-lg transition-all ${
|
||||
overdue
|
||||
? 'border-red-300 dark:border-red-700'
|
||||
: 'border-gray-200 dark:border-gray-700'
|
||||
} ${isDragging ? 'shadow-2xl ring-4 ring-blue-500/50' : ''}`}
|
||||
onClick={(e) => {
|
||||
if (!(e.target as HTMLElement).closest('[data-no-click]')) {
|
||||
onTaskClick(task)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2 sm:mb-3">
|
||||
<span className={`px-2 py-1 text-xs font-medium rounded border ${getPriorityColor(task.priority)}`}>
|
||||
{getPriorityLabel(task.priority)}
|
||||
</span>
|
||||
{overdue && (
|
||||
<span className="text-xs text-red-600 dark:text-red-400 font-medium">
|
||||
⚠️ Gecikmiş
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
{task.title}
|
||||
</h4>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2">
|
||||
{task.description}
|
||||
</p>
|
||||
|
||||
<div className="mb-3">
|
||||
<span className="inline-flex items-center px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded text-xs">
|
||||
📁 {task.project}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{task.labels.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{task.labels.map((label, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
<div className="flex items-center gap-1">
|
||||
<HiClock className="w-4 h-4" />
|
||||
{dayjs(task.dueDate).format('DD MMM')}
|
||||
</div>
|
||||
{task.comments > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<HiChatBubbleLeftRight className="w-4 h-4" />
|
||||
{task.comments}
|
||||
</div>
|
||||
)}
|
||||
{task.attachments && task.attachments.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<HiPaperClip className="w-4 h-4" />
|
||||
{task.attachments.length}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex -space-x-2">
|
||||
{task.assignedTo.slice(0, 3).map((assignee, idx) => (
|
||||
<img
|
||||
key={idx}
|
||||
src={assignee.avatar}
|
||||
alt={assignee.fullName}
|
||||
className="w-6 h-6 rounded-full border-2 border-white dark:border-gray-800"
|
||||
title={assignee.fullName}
|
||||
/>
|
||||
))}
|
||||
{task.assignedTo.length > 3 && (
|
||||
<div className="w-6 h-6 rounded-full border-2 border-white dark:border-gray-800 bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
+{task.assignedTo.length - 3}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TasksModule: React.FC = () => {
|
||||
const [tasks, setTasks] = useState<Task[]>(mockTasks)
|
||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null)
|
||||
const [activeId, setActiveId] = useState<string | null>(null)
|
||||
const [showNewTaskModal, setShowNewTaskModal] = useState(false)
|
||||
const [newTaskColumn, setNewTaskColumn] = useState<TaskStatus>('todo')
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const columns: { id: TaskStatus; title: string; icon: string; color: string }[] = [
|
||||
{ id: 'todo', title: 'Yapılacak', icon: '📋', color: 'gray' },
|
||||
{ id: 'in-progress', title: 'Devam Ediyor', icon: '⚙️', color: 'blue' },
|
||||
{ id: 'review', title: 'İncelemede', icon: '👀', color: 'yellow' },
|
||||
{ id: 'done', title: 'Tamamlandı', icon: '✅', color: 'green' }
|
||||
]
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
setActiveId(event.active.id as string)
|
||||
}
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
setActiveId(null)
|
||||
|
||||
if (!over) return
|
||||
|
||||
const taskId = active.id as string
|
||||
|
||||
// over.id could be either a column id or a task id
|
||||
// If it's a column id (from DroppableColumn), use it directly
|
||||
// If it's a task id, find that task's column
|
||||
let newStatus: TaskStatus
|
||||
|
||||
const overColumn = columns.find(col => col.id === over.id)
|
||||
if (overColumn) {
|
||||
newStatus = overColumn.id
|
||||
} else {
|
||||
// over.id is a task, find its column
|
||||
const overTask = tasks.find(t => t.id === over.id)
|
||||
if (!overTask) return
|
||||
newStatus = overTask.status
|
||||
}
|
||||
|
||||
// Update task status
|
||||
setTasks(prevTasks =>
|
||||
prevTasks.map(task =>
|
||||
task.id === taskId ? { ...task, status: newStatus } : task
|
||||
)
|
||||
)
|
||||
|
||||
if (selectedTask?.id === taskId) {
|
||||
setSelectedTask(prev => prev ? { ...prev, status: newStatus } : null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStatusChange = (taskId: string, newStatus: TaskStatus) => {
|
||||
setTasks(prevTasks =>
|
||||
prevTasks.map(task =>
|
||||
task.id === taskId ? { ...task, status: newStatus } : task
|
||||
)
|
||||
)
|
||||
|
||||
if (selectedTask?.id === taskId) {
|
||||
setSelectedTask(prev => prev ? { ...prev, status: newStatus } : null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddTask = (status: TaskStatus) => {
|
||||
setNewTaskColumn(status)
|
||||
setShowNewTaskModal(true)
|
||||
}
|
||||
|
||||
const handleCreateTask = (title: string, description: string) => {
|
||||
const newTask: Task = {
|
||||
id: `task-${Date.now()}`,
|
||||
title,
|
||||
description,
|
||||
project: 'Genel',
|
||||
assignedTo: [mockTasks[0].assignedTo[0]], // Default assignee
|
||||
assignedBy: mockTasks[0].assignedBy,
|
||||
priority: 'medium',
|
||||
status: newTaskColumn,
|
||||
dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now
|
||||
createdAt: new Date(),
|
||||
labels: [],
|
||||
comments: 0
|
||||
}
|
||||
|
||||
setTasks(prev => [...prev, newTask])
|
||||
setShowNewTaskModal(false)
|
||||
}
|
||||
|
||||
const handleDeleteTask = (taskId: string) => {
|
||||
if (window.confirm('Bu görevi silmek istediğinizden emin misiniz?')) {
|
||||
setTasks(prevTasks => prevTasks.filter(task => task.id !== taskId))
|
||||
setSelectedTask(null)
|
||||
}
|
||||
}
|
||||
|
||||
const getTasksByStatus = (status: TaskStatus) => {
|
||||
return tasks.filter((task: Task) => task.status === status)
|
||||
}
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
const colors = {
|
||||
low: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border-gray-200 dark:border-gray-600',
|
||||
medium: 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-300 border-blue-200 dark:border-blue-700',
|
||||
high: 'bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-300 border-orange-200 dark:border-orange-700',
|
||||
urgent: 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-300 border-red-200 dark:border-red-700'
|
||||
}
|
||||
return colors[priority as keyof typeof colors]
|
||||
}
|
||||
|
||||
const getPriorityLabel = (priority: string) => {
|
||||
const labels = {
|
||||
low: 'Düşük',
|
||||
medium: 'Orta',
|
||||
high: 'Yüksek',
|
||||
urgent: '🔥 Acil'
|
||||
}
|
||||
return labels[priority as keyof typeof labels]
|
||||
}
|
||||
|
||||
const isOverdue = (dueDate: Date | string) => {
|
||||
return dayjs(dueDate).isBefore(dayjs(), 'day')
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCorners}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-3 sm:p-4 md:p-6">
|
||||
<div className="max-w-[1600px] mx-auto space-y-4 md:space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Görev & Proje Yönetimi
|
||||
</h1>
|
||||
<p className="text-sm sm:text-base text-gray-600 dark:text-gray-400 mt-1">
|
||||
Görevleri Kanban board ile yönetin
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 text-xs sm:text-sm text-gray-600 dark:text-gray-400">
|
||||
<span className="font-medium">Toplam:</span>
|
||||
<span>{tasks.length} görev</span>
|
||||
</div>
|
||||
<button className="px-3 sm:px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors text-sm sm:text-base">
|
||||
<HiPlus className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||
<span className="hidden sm:inline">Yeni Görev</span>
|
||||
<span className="sm:hidden">Yeni</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kanban Board */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-4 overflow-x-auto pb-4">
|
||||
|
||||
<div className="kanban-container sm:contents">
|
||||
{columns.map(column => {
|
||||
const columnTasks = getTasksByStatus(column.id)
|
||||
return (
|
||||
<DroppableColumn key={column.id} id={column.id}>
|
||||
<SortableContext
|
||||
id={column.id}
|
||||
items={columnTasks.map(t => t.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div
|
||||
className="kanban-column flex flex-col gap-2 sm:gap-3 bg-gray-100 dark:bg-gray-800/50 rounded-lg p-3 sm:p-4 min-h-[400px] sm:min-h-[500px] lg:min-h-[600px]"
|
||||
data-status={column.id}
|
||||
>
|
||||
{/* Column Header */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg sm:text-xl">{column.icon}</span>
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white text-sm sm:text-base">
|
||||
{column.title}
|
||||
</h3>
|
||||
</div>
|
||||
<Badge content={columnTasks.length}></Badge>
|
||||
</div>
|
||||
|
||||
{/* Tasks */}
|
||||
<div className="space-y-3 flex-1">
|
||||
{columnTasks.map(task => (
|
||||
<SortableTaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
onTaskClick={setSelectedTask}
|
||||
getPriorityColor={getPriorityColor}
|
||||
getPriorityLabel={getPriorityLabel}
|
||||
isOverdue={isOverdue}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add Task Button */}
|
||||
<button
|
||||
onClick={() => handleAddTask(column.id)}
|
||||
className="w-full p-3 sm:p-4 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-blue-500 dark:hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/10 transition-colors text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400"
|
||||
>
|
||||
<HiPlus className="w-4 h-4 sm:w-5 sm:h-5 mx-auto" />
|
||||
</button>
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DroppableColumn>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DragOverlay */}
|
||||
<DragOverlay>
|
||||
{activeId ? (
|
||||
<div className="opacity-50">
|
||||
{(() => {
|
||||
const task = tasks.find(t => t.id === activeId)
|
||||
if (!task) return null
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 border-2 border-blue-500 shadow-2xl">
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||
{task.title}
|
||||
</h4>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
|
||||
{/* New Task Modal */}
|
||||
<AnimatePresence>
|
||||
{showNewTaskModal && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/50 z-40"
|
||||
onClick={() => setShowNewTaskModal(false)}
|
||||
/>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-3 sm: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-lg w-full"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="p-4 sm:p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||
<h2 className="text-lg sm:text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Yeni Görev Oluştur
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowNewTaskModal(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>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
const formData = new FormData(e.currentTarget)
|
||||
const title = formData.get('title') as string
|
||||
const description = formData.get('description') as string
|
||||
if (title && description) {
|
||||
handleCreateTask(title, description)
|
||||
}
|
||||
}}
|
||||
className="p-4 sm:p-6 space-y-4"
|
||||
>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Görev Başlığı *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
required
|
||||
placeholder="Görev başlığını yazın"
|
||||
className="w-full px-3 sm:px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 text-sm sm:text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Açıklama *
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
required
|
||||
rows={4}
|
||||
placeholder="Görev detaylarını yazın"
|
||||
className="w-full px-3 sm:px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 text-sm sm:text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3">
|
||||
<p className="text-xs sm:text-sm text-blue-700 dark:text-blue-300">
|
||||
📋 Görev <strong>{columns.find(c => c.id === newTaskColumn)?.title}</strong> kolonuna eklenecek
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 sm:gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowNewTaskModal(false)}
|
||||
className="flex-1 px-3 sm:px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm sm:text-base"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 px-3 sm:px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors text-sm sm:text-base"
|
||||
>
|
||||
Oluştur
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Task Detail Modal */}
|
||||
<AnimatePresence>
|
||||
{selectedTask && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/50 z-40"
|
||||
onClick={() => setSelectedTask(null)}
|
||||
/>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-3 sm: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-3xl w-full max-h-[90vh] overflow-y-auto"
|
||||
>
|
||||
<div className="p-4 sm:p-6 border-b border-gray-200 dark:border-gray-700 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||
<span className={`px-2 sm:px-3 py-1 sm:py-1.5 text-xs sm:text-sm font-medium rounded border ${getPriorityColor(selectedTask.priority)}`}>
|
||||
{getPriorityLabel(selectedTask.priority)}
|
||||
</span>
|
||||
<span className="px-2 sm:px-3 py-1 sm:py-1.5 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 text-xs sm:text-sm font-medium rounded">
|
||||
📁 {selectedTask.project}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedTask(null)}
|
||||
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-4 sm:p-6 space-y-4 sm:space-y-6">
|
||||
<div>
|
||||
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{selectedTask.title}
|
||||
</h2>
|
||||
<p className="text-sm sm:text-base text-gray-700 dark:text-gray-300">
|
||||
{selectedTask.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
|
||||
Durum
|
||||
</p>
|
||||
<select
|
||||
value={selectedTask.status}
|
||||
onChange={(e) => handleStatusChange(selectedTask.id, e.target.value as TaskStatus)}
|
||||
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 focus:ring-2 focus:ring-blue-500 cursor-pointer"
|
||||
>
|
||||
<option value="todo">📋 Yapılacak</option>
|
||||
<option value="in-progress">⚙️ Devam Ediyor</option>
|
||||
<option value="review">👀 İncelemede</option>
|
||||
<option value="done">✅ Tamamlandı</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
|
||||
Son Tarih
|
||||
</p>
|
||||
<div className="flex items-center gap-2 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg">
|
||||
<HiClock className="w-5 h-5 text-gray-400" />
|
||||
<span className="text-gray-900 dark:text-white">
|
||||
{dayjs(selectedTask.dueDate).format('DD MMMM YYYY')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2 sm:mb-3">
|
||||
Atananlar
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 sm:gap-3">
|
||||
{selectedTask.assignedTo.map((user, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-center gap-2 px-2 sm:px-3 py-1.5 sm:py-2 bg-gray-100 dark:bg-gray-700 rounded-lg"
|
||||
>
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={user.fullName}
|
||||
className="w-6 h-6 sm:w-8 sm:h-8 rounded-full"
|
||||
/>
|
||||
<span className="text-xs sm:text-sm text-gray-900 dark:text-white">
|
||||
{user.fullName}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedTask.labels.length > 0 && (
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
|
||||
Etiketler
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedTask.labels.map((label, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="px-2 sm:px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs sm:text-sm rounded"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-6 border-t border-gray-200 dark:border-gray-700 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Oluşturan: {selectedTask.assignedBy.fullName} • {dayjs(selectedTask.createdAt).format('DD MMMM YYYY')}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => handleDeleteTask(selectedTask.id)}
|
||||
className="px-3 sm:px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center gap-2 transition-colors text-sm"
|
||||
>
|
||||
<HiTrash className="w-4 h-4" />
|
||||
Görevi Sil
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</DndContext>
|
||||
)
|
||||
}
|
||||
|
||||
export default TasksModule
|
||||
354
ui/src/components/intranet/Training/index.tsx
Normal file
354
ui/src/components/intranet/Training/index.tsx
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
import React, { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import {
|
||||
HiAcademicCap,
|
||||
HiClock,
|
||||
HiUsers,
|
||||
HiMapPin,
|
||||
HiXMark,
|
||||
HiCheckBadge,
|
||||
HiCalendar
|
||||
} from 'react-icons/hi2'
|
||||
import dayjs from 'dayjs'
|
||||
import { mockTrainings, mockCertificates, Training } from '../../../mocks/mockIntranetData'
|
||||
|
||||
const TrainingModule: React.FC = () => {
|
||||
const [selectedTraining, setSelectedTraining] = useState<Training | null>(null)
|
||||
const [selectedTab, setSelectedTab] = useState<'all' | 'upcoming' | 'ongoing' | 'completed'>('all')
|
||||
const [trainings, setTrainings] = useState<Training[]>(mockTrainings)
|
||||
const [showEnrollSuccess, setShowEnrollSuccess] = useState(false)
|
||||
|
||||
const filteredTrainings = selectedTab === 'all'
|
||||
? trainings
|
||||
: trainings.filter(t => t.status === selectedTab)
|
||||
|
||||
const handleEnroll = (trainingId: string) => {
|
||||
setTrainings(prev => prev.map(t =>
|
||||
t.id === trainingId && t.enrolled < t.maxParticipants
|
||||
? { ...t, enrolled: t.enrolled + 1 }
|
||||
: t
|
||||
))
|
||||
|
||||
// Seçili eğitimi de güncelle
|
||||
if (selectedTraining?.id === trainingId && selectedTraining.enrolled < selectedTraining.maxParticipants) {
|
||||
setSelectedTraining(prev => prev ? { ...prev, enrolled: prev.enrolled + 1 } : null)
|
||||
}
|
||||
|
||||
setShowEnrollSuccess(true)
|
||||
setTimeout(() => setShowEnrollSuccess(false), 3000)
|
||||
}
|
||||
|
||||
const getCategoryColor = (category: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
technical: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
||||
'soft-skills': 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300',
|
||||
management: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||
compliance: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300',
|
||||
other: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||
}
|
||||
return colors[category] || colors.other
|
||||
}
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const badges: Record<string, { bg: string; text: string }> = {
|
||||
upcoming: { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300' },
|
||||
ongoing: { bg: 'bg-green-100 dark:bg-green-900/30', text: 'text-green-700 dark:text-green-300' },
|
||||
completed: { bg: 'bg-gray-100 dark:bg-gray-700', text: 'text-gray-700 dark:text-gray-300' }
|
||||
}
|
||||
return badges[status] || badges.upcoming
|
||||
}
|
||||
|
||||
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">
|
||||
🎓 Eğitimler & Sertifikalar
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Kişisel gelişim ve eğitim kayıtları
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
{[
|
||||
{ label: 'Tümü', value: mockTrainings.length, tab: 'all' as const },
|
||||
{ label: 'Yaklaşan', value: mockTrainings.filter(t => t.status === 'upcoming').length, tab: 'upcoming' as const },
|
||||
{ label: 'Devam Eden', value: mockTrainings.filter(t => t.status === 'ongoing').length, tab: 'ongoing' as const },
|
||||
{ label: 'Tamamlanan', value: mockTrainings.filter(t => t.status === 'completed').length, tab: 'completed' as const }
|
||||
].map((stat, idx) => (
|
||||
<motion.button
|
||||
key={idx}
|
||||
onClick={() => setSelectedTab(stat.tab)}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.1 }}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${
|
||||
selectedTab === stat.tab
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-blue-300'
|
||||
}`}
|
||||
>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">{stat.value}</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">{stat.label}</div>
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Eğitimler */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{filteredTrainings.map((training, idx) => {
|
||||
const badge = getStatusBadge(training.status)
|
||||
return (
|
||||
<motion.div
|
||||
key={training.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.1 }}
|
||||
onClick={() => setSelectedTraining(training)}
|
||||
className="bg-white dark:bg-gray-800 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all cursor-pointer"
|
||||
>
|
||||
{training.thumbnail && (
|
||||
<img
|
||||
src={training.thumbnail}
|
||||
alt={training.title}
|
||||
className="w-full h-48 object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="p-6">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getCategoryColor(training.category)}`}>
|
||||
{training.category}
|
||||
</span>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${badge.bg} ${badge.text}`}>
|
||||
{training.status === 'upcoming' ? 'Yaklaşan' : training.status === 'ongoing' ? 'Devam Ediyor' : 'Tamamlandı'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
{training.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
|
||||
{training.description}
|
||||
</p>
|
||||
|
||||
<div className="space-y-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<div className="flex items-center gap-2">
|
||||
<HiAcademicCap className="w-4 h-4" />
|
||||
<span>{training.instructor}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<HiCalendar className="w-4 h-4" />
|
||||
<span>{dayjs(training.startDate).format('DD MMM YYYY')}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<HiClock className="w-4 h-4" />
|
||||
<span>{training.duration} saat</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<HiUsers className="w-4 h-4" />
|
||||
<span>{training.enrolled} / {training.maxParticipants} katılımcı</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{training.enrolled < training.maxParticipants && training.status === 'upcoming' && (
|
||||
<button
|
||||
onClick={() => handleEnroll(training.id)}
|
||||
className="mt-4 w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||
>
|
||||
Kayıt Ol
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Sertifikalar */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<HiCheckBadge className="w-6 h-6 text-blue-500" />
|
||||
Sertifikalarım
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{mockCertificates.map((cert, idx) => (
|
||||
<motion.div
|
||||
key={cert.id}
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: idx * 0.1 }}
|
||||
className="p-4 rounded-lg border-2 border-blue-200 dark:border-blue-800 bg-blue-50 dark:bg-blue-900/20"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<HiCheckBadge className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
||||
{cert.score && (
|
||||
<span className="px-2 py-1 bg-blue-600 text-white rounded text-xs font-bold">
|
||||
{cert.score}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||
{cert.trainingTitle}
|
||||
</h3>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{dayjs(cert.issueDate).format('DD MMMM YYYY')}
|
||||
</p>
|
||||
|
||||
{cert.expiryDate && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
||||
Geçerlilik: {dayjs(cert.expiryDate).format('DD/MM/YYYY')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button className="mt-3 w-full px-3 py-1 text-sm text-blue-600 dark:text-blue-400 border border-blue-600 dark:border-blue-400 rounded hover:bg-blue-600 hover:text-white dark:hover:bg-blue-600 transition-colors">
|
||||
Görüntüle
|
||||
</button>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Training Detail Modal */}
|
||||
<AnimatePresence>
|
||||
{selectedTraining && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/50 z-40"
|
||||
onClick={() => setSelectedTraining(null)}
|
||||
/>
|
||||
<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="sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-6 flex items-center justify-between z-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Eğitim Detayları
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setSelectedTraining(null)}
|
||||
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-6">
|
||||
{selectedTraining.thumbnail && (
|
||||
<img
|
||||
src={selectedTraining.thumbnail}
|
||||
alt={selectedTraining.title}
|
||||
className="w-full h-64 object-cover rounded-lg"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{selectedTraining.title}
|
||||
</h3>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
{selectedTraining.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Eğitmen</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{selectedTraining.instructor}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Kategori</p>
|
||||
<span className={`inline-block px-3 py-1 rounded-full text-sm ${getCategoryColor(selectedTraining.category)}`}>
|
||||
{selectedTraining.category}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Tarih</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{dayjs(selectedTraining.startDate).format('DD MMM')} - {dayjs(selectedTraining.endDate).format('DD MMM YYYY')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Süre</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{selectedTraining.duration} saat</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Tip</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white capitalize">{selectedTraining.type}</p>
|
||||
</div>
|
||||
{selectedTraining.location && (
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Konum</p>
|
||||
<p className="font-medium text-gray-900 dark:text-white">{selectedTraining.location}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">Doluluk Oranı</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-3">
|
||||
<div
|
||||
className="bg-blue-600 h-3 rounded-full transition-all"
|
||||
style={{ width: `${(selectedTraining.enrolled / selectedTraining.maxParticipants) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{selectedTraining.enrolled} / {selectedTraining.maxParticipants}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedTraining.enrolled < selectedTraining.maxParticipants && selectedTraining.status === 'upcoming' && (
|
||||
<button
|
||||
onClick={() => handleEnroll(selectedTraining.id)}
|
||||
className="w-full px-4 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors font-medium"
|
||||
>
|
||||
Eğitime Kayıt Ol
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Success Notification */}
|
||||
<AnimatePresence>
|
||||
{showEnrollSuccess && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 50 }}
|
||||
className="fixed bottom-6 right-6 bg-green-600 text-white px-6 py-4 rounded-lg shadow-xl flex items-center gap-3 z-50"
|
||||
>
|
||||
<HiCheckBadge className="w-6 h-6" />
|
||||
<div>
|
||||
<p className="font-semibold">Kayıt Başarılı!</p>
|
||||
<p className="text-sm text-green-100">Eğitime kayıt oldunuz.</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TrainingModule
|
||||
380
ui/src/components/intranet/Visitors/index.tsx
Normal file
380
ui/src/components/intranet/Visitors/index.tsx
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
import React, { useState } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { HiUserPlus, HiXMark, HiCheckCircle, HiClock, HiPhone, HiEnvelope } from 'react-icons/hi2'
|
||||
import dayjs from 'dayjs'
|
||||
import { mockVisitors, Visitor } from '../../../mocks/mockIntranetData'
|
||||
|
||||
const VisitorsModule: React.FC = () => {
|
||||
const [selectedStatus, setSelectedStatus] = useState<'all' | 'scheduled' | 'checked-in' | 'checked-out' | 'cancelled'>('all')
|
||||
const [showNewVisitor, setShowNewVisitor] = useState(false)
|
||||
const [visitors, setVisitors] = useState<Visitor[]>(mockVisitors)
|
||||
|
||||
const filteredVisitors = selectedStatus === 'all'
|
||||
? visitors
|
||||
: visitors.filter(v => v.status === selectedStatus)
|
||||
|
||||
const handleCheckIn = (visitorId: string) => {
|
||||
setVisitors(prev => prev.map(v =>
|
||||
v.id === visitorId
|
||||
? { ...v, status: 'checked-in', checkIn: new Date() }
|
||||
: v
|
||||
))
|
||||
}
|
||||
|
||||
const handleCheckOut = (visitorId: string) => {
|
||||
setVisitors(prev => prev.map(v =>
|
||||
v.id === visitorId
|
||||
? { ...v, status: 'checked-out', checkOut: new Date() }
|
||||
: v
|
||||
))
|
||||
}
|
||||
|
||||
const handleCancel = (visitorId: string) => {
|
||||
setVisitors(prev => prev.map(v =>
|
||||
v.id === visitorId
|
||||
? { ...v, status: 'cancelled' }
|
||||
: v
|
||||
))
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors: Record<string, string> = {
|
||||
scheduled: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
||||
'checked-in': 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||
'checked-out': 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300',
|
||||
cancelled: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||
}
|
||||
return colors[status] || colors.scheduled
|
||||
}
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
scheduled: '📅 Planlandı',
|
||||
'checked-in': '✅ Giriş Yaptı',
|
||||
'checked-out': '🚪 Çıkış Yaptı',
|
||||
cancelled: '❌ İptal Edildi'
|
||||
}
|
||||
return labels[status] || status
|
||||
}
|
||||
|
||||
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">
|
||||
👥 Ziyaretçi Yönetimi
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Ziyaretçi kayıtları ve giriş-çıkış takibi
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowNewVisitor(true)}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<HiUserPlus className="w-5 h-5" />
|
||||
Yeni Ziyaretçi
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: 'Tümü', value: mockVisitors.length, status: 'all' as const },
|
||||
{ label: 'Planlandı', value: mockVisitors.filter(v => v.status === 'scheduled').length, status: 'scheduled' as const },
|
||||
{ label: 'İçeride', value: mockVisitors.filter(v => v.status === 'checked-in').length, status: 'checked-in' as const },
|
||||
{ label: 'Çıkış Yaptı', value: mockVisitors.filter(v => v.status === 'checked-out').length, status: 'checked-out' as const }
|
||||
].map((stat, idx) => (
|
||||
<motion.button
|
||||
key={idx}
|
||||
onClick={() => setSelectedStatus(stat.status)}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.1 }}
|
||||
className={`p-4 rounded-lg border-2 transition-all ${
|
||||
selectedStatus === stat.status
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-blue-300'
|
||||
}`}
|
||||
>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">{stat.value}</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">{stat.label}</div>
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Visitors List */}
|
||||
<div className="space-y-4">
|
||||
{filteredVisitors.map((visitor: Visitor, idx: number) => (
|
||||
<motion.div
|
||||
key={visitor.id}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: idx * 0.05 }}
|
||||
className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
{visitor.photo && (
|
||||
<img
|
||||
src={visitor.photo}
|
||||
alt={visitor.fullName}
|
||||
className="w-16 h-16 rounded-full border-4 border-gray-100 dark:border-gray-700"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{visitor.fullName}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">{visitor.company}</p>
|
||||
</div>
|
||||
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(visitor.status)}`}>
|
||||
{getStatusLabel(visitor.status)}
|
||||
</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 mb-1">Ziyaret Tarihi</p>
|
||||
<div className="flex items-center gap-1 text-sm text-gray-900 dark:text-white">
|
||||
<HiClock className="w-4 h-4" />
|
||||
{dayjs(visitor.visitDate).format('DD MMM, HH:mm')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{visitor.checkIn && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Giriş</p>
|
||||
<p className="text-sm font-medium text-green-600 dark:text-green-400">
|
||||
{dayjs(visitor.checkIn).format('HH:mm')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{visitor.checkOut && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Çıkış</p>
|
||||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
{dayjs(visitor.checkOut).format('HH:mm')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{visitor.badgeNumber && (
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Rozet No</p>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{visitor.badgeNumber}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-3">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">İletişim</p>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<HiEnvelope className="w-4 h-4" />
|
||||
{visitor.email}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<HiPhone className="w-4 h-4" />
|
||||
{visitor.phone}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Ev Sahibi</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src={visitor.host.avatar}
|
||||
alt={visitor.host.fullName}
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{visitor.host.fullName}
|
||||
</p>
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{visitor.host.department}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3">
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mb-1">Ziyaret Amacı</p>
|
||||
<p className="text-sm text-gray-900 dark:text-white">{visitor.purpose}</p>
|
||||
</div>
|
||||
|
||||
{visitor.status === 'scheduled' && (
|
||||
<div className="flex gap-2 mt-4">
|
||||
<button
|
||||
onClick={() => handleCheckIn(visitor.id)}
|
||||
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors text-sm"
|
||||
>
|
||||
<HiCheckCircle className="w-4 h-4 inline mr-1" />
|
||||
Giriş Yaptır
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleCancel(visitor.id)}
|
||||
className="px-4 py-2 border border-red-600 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors text-sm"
|
||||
>
|
||||
İptal Et
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{visitor.status === 'checked-in' && (
|
||||
<button
|
||||
onClick={() => handleCheckOut(visitor.id)}
|
||||
className="mt-4 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors text-sm"
|
||||
>
|
||||
Çıkış Yaptır
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* New Visitor Modal */}
|
||||
<AnimatePresence>
|
||||
{showNewVisitor && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/50 z-40"
|
||||
onClick={() => setShowNewVisitor(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">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Yeni Ziyaretçi Kaydı
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowNewVisitor(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 className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Ad Soyad *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ziyaretçi adı"
|
||||
className="w-full px-4 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">
|
||||
Şirket *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Şirket adı"
|
||||
className="w-full px-4 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 className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
E-posta *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="email@example.com"
|
||||
className="w-full px-4 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">
|
||||
Telefon *
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="+90 5XX XXX XX XX"
|
||||
className="w-full px-4 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">
|
||||
Ziyaret Tarihi ve Saati *
|
||||
</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
className="w-full px-4 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">
|
||||
Ev Sahibi (Personel) *
|
||||
</label>
|
||||
<select className="w-full px-4 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>Ahmet Yılmaz - Yazılım Geliştirme</option>
|
||||
<option>Zeynep Kaya - İnsan Kaynakları</option>
|
||||
<option>Mehmet Demir - Yazılım Geliştirme</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Ziyaret Amacı *
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
placeholder="Ziyaret amacını açıklayın"
|
||||
className="w-full px-4 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="flex gap-3 pt-4">
|
||||
<button
|
||||
onClick={() => setShowNewVisitor(false)}
|
||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
||||
Ziyaretçi Kaydet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default VisitorsModule
|
||||
|
|
@ -19,6 +19,7 @@ export const mockEmployees: HrEmployee[] = [
|
|||
email: "ali.ozturk@company.com",
|
||||
phone: "+90 212 555 0100",
|
||||
personalPhone: "+90 532 555 0101",
|
||||
avatar: "https://i.pravatar.cc/150?img=12",
|
||||
nationalId: "12345678901",
|
||||
birthDate: new Date("1988-02-14"),
|
||||
gender: GenderEnum.Male,
|
||||
|
|
@ -76,6 +77,7 @@ export const mockEmployees: HrEmployee[] = [
|
|||
email: "ayse.kaya@company.com",
|
||||
phone: "+90 212 555 0102",
|
||||
personalPhone: "+90 532 555 0103",
|
||||
avatar: "https://i.pravatar.cc/150?img=5",
|
||||
nationalId: "12345678902",
|
||||
birthDate: new Date("1990-08-22"),
|
||||
gender: GenderEnum.Female,
|
||||
|
|
@ -134,6 +136,7 @@ export const mockEmployees: HrEmployee[] = [
|
|||
email: "mehmet.yilmaz@company.com",
|
||||
phone: "+90 212 555 0105",
|
||||
personalPhone: "+90 532 555 0106",
|
||||
avatar: "https://i.pravatar.cc/150?img=8",
|
||||
nationalId: "12345678903",
|
||||
birthDate: new Date("1987-03-12"),
|
||||
gender: GenderEnum.Male,
|
||||
|
|
@ -192,6 +195,7 @@ export const mockEmployees: HrEmployee[] = [
|
|||
email: "selin.demir@company.com",
|
||||
phone: "+90 312 555 0108",
|
||||
personalPhone: "+90 542 555 0109",
|
||||
avatar: "https://i.pravatar.cc/150?img=9",
|
||||
nationalId: "12345678904",
|
||||
birthDate: new Date("1993-05-25"),
|
||||
gender: GenderEnum.Female,
|
||||
|
|
@ -250,6 +254,7 @@ export const mockEmployees: HrEmployee[] = [
|
|||
email: "ahmet.celik@company.com",
|
||||
phone: "+90 212 555 0111",
|
||||
personalPhone: "+90 532 555 0112",
|
||||
avatar: "https://i.pravatar.cc/150?img=33",
|
||||
nationalId: "12345678905",
|
||||
birthDate: new Date("1985-09-10"),
|
||||
gender: GenderEnum.Male,
|
||||
|
|
@ -308,6 +313,7 @@ export const mockEmployees: HrEmployee[] = [
|
|||
email: "zeynep.arslan@company.com",
|
||||
phone: "+90 216 555 0114",
|
||||
personalPhone: "+90 532 555 0115",
|
||||
avatar: "https://i.pravatar.cc/150?img=10",
|
||||
nationalId: "12345678906",
|
||||
birthDate: new Date("1995-01-30"),
|
||||
gender: GenderEnum.Female,
|
||||
|
|
@ -366,6 +372,7 @@ export const mockEmployees: HrEmployee[] = [
|
|||
email: "burak.koc@company.com",
|
||||
phone: "+90 224 555 0117",
|
||||
personalPhone: "+90 532 555 0118",
|
||||
avatar: "https://i.pravatar.cc/150?img=14",
|
||||
nationalId: "12345678907",
|
||||
birthDate: new Date("1991-06-18"),
|
||||
gender: GenderEnum.Male,
|
||||
|
|
@ -424,6 +431,7 @@ export const mockEmployees: HrEmployee[] = [
|
|||
email: "elif.sahin@company.com",
|
||||
phone: "+90 232 555 0120",
|
||||
personalPhone: "+90 532 555 0121",
|
||||
avatar: "https://i.pravatar.cc/150?img=20",
|
||||
nationalId: "12345678908",
|
||||
birthDate: new Date("1989-11-05"),
|
||||
gender: GenderEnum.Female,
|
||||
|
|
@ -482,6 +490,7 @@ export const mockEmployees: HrEmployee[] = [
|
|||
email: "canan.ozturk@company.com",
|
||||
phone: "+90 312 555 0123",
|
||||
personalPhone: "+90 532 555 0124",
|
||||
avatar: "https://i.pravatar.cc/150?img=25",
|
||||
nationalId: "12345678909",
|
||||
birthDate: new Date("1992-04-14"),
|
||||
gender: GenderEnum.Female,
|
||||
|
|
@ -540,6 +549,7 @@ export const mockEmployees: HrEmployee[] = [
|
|||
email: "murat.aydin@company.com",
|
||||
phone: "+90 212 555 0126",
|
||||
personalPhone: "+90 532 555 0127",
|
||||
avatar: "https://i.pravatar.cc/150?img=30",
|
||||
nationalId: "12345678910",
|
||||
birthDate: new Date("1984-12-22"),
|
||||
gender: GenderEnum.Male,
|
||||
|
|
|
|||
1030
ui/src/mocks/mockIntranetData.ts
Normal file
1030
ui/src/mocks/mockIntranetData.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -10,6 +10,7 @@ export interface HrEmployee {
|
|||
email: string
|
||||
phone?: string
|
||||
personalPhone?: string
|
||||
avatar?: string // Avatar URL
|
||||
nationalId: string
|
||||
birthDate: Date
|
||||
gender: GenderEnum
|
||||
|
|
|
|||
|
|
@ -1,9 +1,64 @@
|
|||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { useState } from 'react'
|
||||
import IntranetDashboard from '@/components/intranet/IntranetDashboard'
|
||||
import IntranetSidebar from '@/components/intranet/IntranetSidebar'
|
||||
import LeaveManagement from '@/components/intranet/HR/LeaveManagement'
|
||||
import OvertimeManagement from '@/components/intranet/HR/OvertimeManagement'
|
||||
import ExpenseManagement from '@/components/intranet/HR/ExpenseManagement'
|
||||
import AnnouncementsModule from '@/components/intranet/Announcements'
|
||||
import EventsModule from '@/components/intranet/Events'
|
||||
import DocumentsModule from '@/components/intranet/Documents'
|
||||
import TasksModule from '@/components/intranet/Tasks'
|
||||
import SocialWall from '@/components/intranet/SocialWall'
|
||||
import BirthdaysModule from '@/components/intranet/Birthdays'
|
||||
import TrainingModule from '@/components/intranet/Training'
|
||||
import ReservationsModule from '@/components/intranet/Reservations'
|
||||
import CafeteriaModule from '@/components/intranet/Cafeteria'
|
||||
import SurveysModule from '@/components/intranet/Surveys'
|
||||
import VisitorsModule from '@/components/intranet/Visitors'
|
||||
|
||||
const Dashboard = () => {
|
||||
const { translate } = useLocalization()
|
||||
const [currentPath, setCurrentPath] = useState('/intranet/dashboard')
|
||||
|
||||
const renderContent = () => {
|
||||
switch (currentPath) {
|
||||
case '/intranet/dashboard':
|
||||
return <IntranetDashboard />
|
||||
case '/intranet/social':
|
||||
return <SocialWall />
|
||||
case '/intranet/hr/leave':
|
||||
return <LeaveManagement />
|
||||
case '/intranet/hr/overtime':
|
||||
return <OvertimeManagement />
|
||||
case '/intranet/hr/expense':
|
||||
return <ExpenseManagement />
|
||||
case '/intranet/announcements':
|
||||
return <AnnouncementsModule />
|
||||
case '/intranet/events':
|
||||
return <EventsModule />
|
||||
case '/intranet/documents':
|
||||
return <DocumentsModule />
|
||||
case '/intranet/tasks':
|
||||
return <TasksModule />
|
||||
case '/intranet/birthdays':
|
||||
return <BirthdaysModule />
|
||||
case '/intranet/training':
|
||||
return <TrainingModule />
|
||||
case '/intranet/reservations':
|
||||
return <ReservationsModule />
|
||||
case '/intranet/cafeteria/menu':
|
||||
case '/intranet/cafeteria/shuttle':
|
||||
return <CafeteriaModule />
|
||||
case '/intranet/surveys':
|
||||
return <SurveysModule />
|
||||
case '/intranet/visitors':
|
||||
return <VisitorsModule />
|
||||
default:
|
||||
return <IntranetDashboard />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -12,11 +67,15 @@ const Dashboard = () => {
|
|||
title={translate('::' + 'Dashboard')}
|
||||
defaultTitle="Sözsoft Kurs Platform"
|
||||
/>
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<SocialWall />
|
||||
<div className="flex min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<IntranetSidebar activePath={currentPath} onNavigate={setCurrentPath} />
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Dashboard
|
||||
|
||||
|
|
|
|||
|
|
@ -322,6 +322,11 @@ const EmployeeList: React.FC = () => {
|
|||
<div className="flex-shrink-0 h-10 w-10">
|
||||
<div className="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center">
|
||||
<FaUsers className="h-5 w-5 text-blue-600" />
|
||||
<img
|
||||
src={employee.avatar}
|
||||
alt={employee.fullName}
|
||||
className="w-10 h-10 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
|
|
|
|||
Loading…
Reference in a new issue