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": [ "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", "version": "1.0.29",
"buildDate": "2025-09-28", "buildDate": "2025-09-28",
@ -126,7 +155,7 @@
{ {
"version": "1.0.14", "version": "1.0.14",
"buildDate": "2025-09-22", "buildDate": "2025-09-22",
"commit": "1c4ab4f8232b4cd2a39fa66f8101664840113ce5", "commit": "51208b86937484d68b699120d74872067b1c7ef6",
"changeLog": [ "changeLog": [
"Yeni versiyon çıktı uyarı gelecek şekilde düzenlendi.", "Yeni versiyon çıktı uyarı gelecek şekilde düzenlendi.",
"Sağ alt kısımda mesaj çıkacak ve yenile butonu ile uygulama yeni versiyona geçecektir." "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", email: "ali.ozturk@company.com",
phone: "+90 212 555 0100", phone: "+90 212 555 0100",
personalPhone: "+90 532 555 0101", personalPhone: "+90 532 555 0101",
avatar: "https://i.pravatar.cc/150?img=12",
nationalId: "12345678901", nationalId: "12345678901",
birthDate: new Date("1988-02-14"), birthDate: new Date("1988-02-14"),
gender: GenderEnum.Male, gender: GenderEnum.Male,
@ -76,6 +77,7 @@ export const mockEmployees: HrEmployee[] = [
email: "ayse.kaya@company.com", email: "ayse.kaya@company.com",
phone: "+90 212 555 0102", phone: "+90 212 555 0102",
personalPhone: "+90 532 555 0103", personalPhone: "+90 532 555 0103",
avatar: "https://i.pravatar.cc/150?img=5",
nationalId: "12345678902", nationalId: "12345678902",
birthDate: new Date("1990-08-22"), birthDate: new Date("1990-08-22"),
gender: GenderEnum.Female, gender: GenderEnum.Female,
@ -134,6 +136,7 @@ export const mockEmployees: HrEmployee[] = [
email: "mehmet.yilmaz@company.com", email: "mehmet.yilmaz@company.com",
phone: "+90 212 555 0105", phone: "+90 212 555 0105",
personalPhone: "+90 532 555 0106", personalPhone: "+90 532 555 0106",
avatar: "https://i.pravatar.cc/150?img=8",
nationalId: "12345678903", nationalId: "12345678903",
birthDate: new Date("1987-03-12"), birthDate: new Date("1987-03-12"),
gender: GenderEnum.Male, gender: GenderEnum.Male,
@ -192,6 +195,7 @@ export const mockEmployees: HrEmployee[] = [
email: "selin.demir@company.com", email: "selin.demir@company.com",
phone: "+90 312 555 0108", phone: "+90 312 555 0108",
personalPhone: "+90 542 555 0109", personalPhone: "+90 542 555 0109",
avatar: "https://i.pravatar.cc/150?img=9",
nationalId: "12345678904", nationalId: "12345678904",
birthDate: new Date("1993-05-25"), birthDate: new Date("1993-05-25"),
gender: GenderEnum.Female, gender: GenderEnum.Female,
@ -250,6 +254,7 @@ export const mockEmployees: HrEmployee[] = [
email: "ahmet.celik@company.com", email: "ahmet.celik@company.com",
phone: "+90 212 555 0111", phone: "+90 212 555 0111",
personalPhone: "+90 532 555 0112", personalPhone: "+90 532 555 0112",
avatar: "https://i.pravatar.cc/150?img=33",
nationalId: "12345678905", nationalId: "12345678905",
birthDate: new Date("1985-09-10"), birthDate: new Date("1985-09-10"),
gender: GenderEnum.Male, gender: GenderEnum.Male,
@ -308,6 +313,7 @@ export const mockEmployees: HrEmployee[] = [
email: "zeynep.arslan@company.com", email: "zeynep.arslan@company.com",
phone: "+90 216 555 0114", phone: "+90 216 555 0114",
personalPhone: "+90 532 555 0115", personalPhone: "+90 532 555 0115",
avatar: "https://i.pravatar.cc/150?img=10",
nationalId: "12345678906", nationalId: "12345678906",
birthDate: new Date("1995-01-30"), birthDate: new Date("1995-01-30"),
gender: GenderEnum.Female, gender: GenderEnum.Female,
@ -366,6 +372,7 @@ export const mockEmployees: HrEmployee[] = [
email: "burak.koc@company.com", email: "burak.koc@company.com",
phone: "+90 224 555 0117", phone: "+90 224 555 0117",
personalPhone: "+90 532 555 0118", personalPhone: "+90 532 555 0118",
avatar: "https://i.pravatar.cc/150?img=14",
nationalId: "12345678907", nationalId: "12345678907",
birthDate: new Date("1991-06-18"), birthDate: new Date("1991-06-18"),
gender: GenderEnum.Male, gender: GenderEnum.Male,
@ -424,6 +431,7 @@ export const mockEmployees: HrEmployee[] = [
email: "elif.sahin@company.com", email: "elif.sahin@company.com",
phone: "+90 232 555 0120", phone: "+90 232 555 0120",
personalPhone: "+90 532 555 0121", personalPhone: "+90 532 555 0121",
avatar: "https://i.pravatar.cc/150?img=20",
nationalId: "12345678908", nationalId: "12345678908",
birthDate: new Date("1989-11-05"), birthDate: new Date("1989-11-05"),
gender: GenderEnum.Female, gender: GenderEnum.Female,
@ -482,6 +490,7 @@ export const mockEmployees: HrEmployee[] = [
email: "canan.ozturk@company.com", email: "canan.ozturk@company.com",
phone: "+90 312 555 0123", phone: "+90 312 555 0123",
personalPhone: "+90 532 555 0124", personalPhone: "+90 532 555 0124",
avatar: "https://i.pravatar.cc/150?img=25",
nationalId: "12345678909", nationalId: "12345678909",
birthDate: new Date("1992-04-14"), birthDate: new Date("1992-04-14"),
gender: GenderEnum.Female, gender: GenderEnum.Female,
@ -540,6 +549,7 @@ export const mockEmployees: HrEmployee[] = [
email: "murat.aydin@company.com", email: "murat.aydin@company.com",
phone: "+90 212 555 0126", phone: "+90 212 555 0126",
personalPhone: "+90 532 555 0127", personalPhone: "+90 532 555 0127",
avatar: "https://i.pravatar.cc/150?img=30",
nationalId: "12345678910", nationalId: "12345678910",
birthDate: new Date("1984-12-22"), birthDate: new Date("1984-12-22"),
gender: GenderEnum.Male, gender: GenderEnum.Male,

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,9 +1,64 @@
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { Helmet } from 'react-helmet' 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 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 Dashboard = () => {
const { translate } = useLocalization() 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 ( return (
<> <>
@ -12,11 +67,15 @@ const Dashboard = () => {
title={translate('::' + 'Dashboard')} title={translate('::' + 'Dashboard')}
defaultTitle="Sözsoft Kurs Platform" defaultTitle="Sözsoft Kurs Platform"
/> />
<div className="min-h-screen bg-gray-50 dark:bg-gray-900"> <div className="flex min-h-screen bg-gray-50 dark:bg-gray-900">
<SocialWall /> <IntranetSidebar activePath={currentPath} onNavigate={setCurrentPath} />
<div className="flex-1 overflow-y-auto">
{renderContent()}
</div>
</div> </div>
</> </>
) )
} }
export default Dashboard export default Dashboard

View file

@ -322,6 +322,11 @@ const EmployeeList: React.FC = () => {
<div className="flex-shrink-0 h-10 w-10"> <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"> <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" /> <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> </div>
<div className="ml-4"> <div className="ml-4">