Intranet Modulü

This commit is contained in:
Sedat Öztürk 2025-10-19 01:37:20 +03:00
parent c78845f411
commit ddaaa56ea0
21 changed files with 6891 additions and 4 deletions

View file

@ -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."

View 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

View 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

View 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

View 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">
ı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

View 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

View 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">ı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">
ı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ıı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

View 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">ıklama:</span> {request.reason}
</p>
{request.approver && (
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
<HiCheckCircle className="w-4 h-4" />
<span>
{request.approver.fullName} tarafından{' '}
{dayjs(request.approvalDate).format('DD MMMM YYYY')} tarihinde{' '}
{request.status === 'approved' ? 'onaylandı' : 'reddedildi'}
</span>
</div>
)}
{request.notes && (
<div className="mt-2 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<p className="text-xs text-gray-700 dark:text-gray-300">
<span className="font-medium">Not:</span> {request.notes}
</p>
</div>
)}
</div>
</div>
</div>
))
) : (
<div className="p-12 text-center">
<HiCalendar className="w-12 h-12 text-gray-400 mx-auto mb-3" />
<p className="text-gray-500 dark:text-gray-400">
{filterStatus === 'all'
? 'Henüz izin talebi bulunmuyor'
: `${filterStatus === 'pending' ? 'Bekleyen' : filterStatus === 'approved' ? 'Onaylanan' : 'Reddedilen'} izin talebi bulunmuyor`}
</p>
</div>
)}
</div>
</div>
</div>
{/* Yeni İzin Talebi Modal */}
<AnimatePresence>
{showCreateModal && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 z-40"
onClick={() => setShowCreateModal(false)}
/>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
>
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between sticky top-0 bg-white dark:bg-gray-800">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
Yeni İzin Talebi
</h2>
<button
onClick={() => setShowCreateModal(false)}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
<HiXMark className="w-5 h-5 text-gray-500" />
</button>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
İzin Türü
</label>
<select className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
<option>Yıllık İzin</option>
<option>Hastalık İzni</option>
<option>Ücretsiz İzin</option>
<option>Doğum İzni</option>
<option>Diğer</option>
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Başlangıç Tarihi
</label>
<input
type="date"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Bitiş Tarihi
</label>
<input
type="date"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
ıklama
</label>
<textarea
rows={4}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white resize-none"
placeholder="İzin sebebinizi açıklayın..."
/>
</div>
<div className="flex gap-3 pt-4">
<button
onClick={() => setShowCreateModal(false)}
className="flex-1 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
İptal
</button>
<button
onClick={() => setShowCreateModal(false)}
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
>
Talep Oluştur
</button>
</div>
</div>
</motion.div>
</div>
</>
)}
</AnimatePresence>
</div>
)
}
export default LeaveManagement

View 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">ı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">
ı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

View 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

View 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

View 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

View 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

View 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">
ı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

View 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

View 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ıı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

View file

@ -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,

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,7 @@ export interface HrEmployee {
email: string
phone?: string
personalPhone?: string
avatar?: string // Avatar URL
nationalId: string
birthDate: Date
gender: GenderEnum

View file

@ -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

View file

@ -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">