intranet içn komponentler
This commit is contained in:
parent
84be7170ec
commit
ee7114a2dc
35 changed files with 1897 additions and 7198 deletions
|
|
@ -1,291 +0,0 @@
|
||||||
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="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
|
|
||||||
|
|
@ -1,279 +0,0 @@
|
||||||
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="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
|
|
||||||
|
|
@ -1,207 +0,0 @@
|
||||||
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="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
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,458 +0,0 @@
|
||||||
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="mx-auto space-y-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
||||||
Doküman Yönetimi
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
Şirket dokümanlarına erişin ve yönetin
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Arama ve Filtreler */}
|
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
|
||||||
<div className="flex-1 relative">
|
|
||||||
<HiMagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Doküman ara..."
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HiFunnel className="w-5 h-5 text-gray-400" />
|
|
||||||
{categories.map(cat => (
|
|
||||||
<button
|
|
||||||
key={cat.id}
|
|
||||||
onClick={() => setSelectedCategory(cat.id)}
|
|
||||||
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
selectedCategory === cat.id
|
|
||||||
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
|
||||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{cat.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* İstatistikler */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
Toplam Doküman
|
|
||||||
</h3>
|
|
||||||
<span className="text-2xl">📁</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
|
||||||
{documents.length}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
Kategoriler
|
|
||||||
</h3>
|
|
||||||
<span className="text-2xl">🗂️</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
|
||||||
{Object.keys(groupedDocuments).length}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
Toplam İndirme
|
|
||||||
</h3>
|
|
||||||
<span className="text-2xl">⬇️</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
|
||||||
{documents.reduce((sum, doc) => sum + doc.downloadCount, 0)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
Bu Ay Yeni
|
|
||||||
</h3>
|
|
||||||
<span className="text-2xl">✨</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
|
||||||
{documents.filter(d => dayjs(d.uploadDate).isAfter(dayjs().subtract(30, 'day'))).length}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Doküman Listesi */}
|
|
||||||
{Object.entries(groupedDocuments).map(([category, categoryDocs]) => (
|
|
||||||
<div key={category} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center gap-3">
|
|
||||||
<HiFolder className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
{getCategoryLabel(category)}
|
|
||||||
</h2>
|
|
||||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
({categoryDocs.length} doküman)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
{categoryDocs.map(doc => (
|
|
||||||
<motion.div
|
|
||||||
key={doc.id}
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center text-2xl">
|
|
||||||
{getFileIcon(doc.type)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-start justify-between gap-3 mb-2">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
|
|
||||||
{doc.name}
|
|
||||||
</h3>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setPreviewDocument(doc)}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
title="Önizle"
|
|
||||||
>
|
|
||||||
<HiEye className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDownload(doc)}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
title="İndir"
|
|
||||||
>
|
|
||||||
<HiArrowDownTray className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
|
||||||
{doc.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<img
|
|
||||||
src={doc.uploadedBy.avatar}
|
|
||||||
alt={doc.uploadedBy.fullName}
|
|
||||||
className="w-5 h-5 rounded-full"
|
|
||||||
/>
|
|
||||||
<span>{doc.uploadedBy.fullName}</span>
|
|
||||||
</div>
|
|
||||||
<span>•</span>
|
|
||||||
<span>{dayjs(doc.uploadDate).format('DD MMMM YYYY')}</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>{doc.size}</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>v{doc.version}</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>{doc.downloadCount} indirme</span>
|
|
||||||
</div>
|
|
||||||
{doc.tags.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-2 mt-3">
|
|
||||||
{doc.tags.map((tag, idx) => (
|
|
||||||
<span
|
|
||||||
key={idx}
|
|
||||||
className="px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded"
|
|
||||||
>
|
|
||||||
#{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{filteredDocuments.length === 0 && (
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-12 text-center border border-gray-200 dark:border-gray-700">
|
|
||||||
<HiDocument className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
|
||||||
<p className="text-gray-500 dark:text-gray-400">
|
|
||||||
{searchQuery ? 'Arama kriterlerine uygun doküman bulunamadı' : 'Henüz doküman bulunmuyor'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Doküman Önizleme Modal */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{previewDocument && (
|
|
||||||
<>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
className="fixed inset-0 bg-black/50 z-40"
|
|
||||||
onClick={() => setPreviewDocument(null)}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col"
|
|
||||||
>
|
|
||||||
{/* Header */}
|
|
||||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center text-2xl">
|
|
||||||
{getFileIcon(previewDocument.type)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
{previewDocument.name}
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
{getCategoryLabel(previewDocument.category)} • {previewDocument.size} • v{previewDocument.version}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => handleDownload(previewDocument)}
|
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors"
|
|
||||||
>
|
|
||||||
<HiArrowDownTray className="w-5 h-5" />
|
|
||||||
İndir
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setPreviewDocument(null)}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<HiXMark className="w-5 h-5 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1 overflow-y-auto p-6">
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Açıklama */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
||||||
Açıklama
|
|
||||||
</h3>
|
|
||||||
<p className="text-gray-700 dark:text-gray-300">
|
|
||||||
{previewDocument.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Dosya Önizleme Alanı */}
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
||||||
Önizleme
|
|
||||||
</h3>
|
|
||||||
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-12 text-center bg-gray-50 dark:bg-gray-900/50">
|
|
||||||
<div className="text-6xl mb-4">{getFileIcon(previewDocument.type)}</div>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
|
||||||
{previewDocument.type.toUpperCase()} dosyası önizlemesi desteklenmemektedir
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDownload(previewDocument)}
|
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg inline-flex items-center gap-2 transition-colors"
|
|
||||||
>
|
|
||||||
<HiArrowDownTray className="w-5 h-5" />
|
|
||||||
Dosyayı İndir
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Metadata */}
|
|
||||||
<div className="grid grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
|
||||||
Dosya Bilgileri
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2 text-sm">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-600 dark:text-gray-400">Dosya Tipi:</span>
|
|
||||||
<span className="text-gray-900 dark:text-white font-medium">
|
|
||||||
{previewDocument.type.toUpperCase()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-600 dark:text-gray-400">Boyut:</span>
|
|
||||||
<span className="text-gray-900 dark:text-white font-medium">
|
|
||||||
{previewDocument.size}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-600 dark:text-gray-400">Versiyon:</span>
|
|
||||||
<span className="text-gray-900 dark:text-white font-medium">
|
|
||||||
v{previewDocument.version}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-gray-600 dark:text-gray-400">İndirme:</span>
|
|
||||||
<span className="text-gray-900 dark:text-white font-medium">
|
|
||||||
{previewDocument.downloadCount} kez
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
|
||||||
Yükleyen
|
|
||||||
</h3>
|
|
||||||
<div className="flex items-center gap-3 mb-4">
|
|
||||||
<img
|
|
||||||
src={previewDocument.uploadedBy.avatar}
|
|
||||||
alt={previewDocument.uploadedBy.fullName}
|
|
||||||
className="w-10 h-10 rounded-full"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{previewDocument.uploadedBy.fullName}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-600 dark:text-gray-400">
|
|
||||||
{dayjs(previewDocument.uploadDate).format('DD MMMM YYYY, HH:mm')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Etiketler */}
|
|
||||||
{previewDocument.tags.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
||||||
Etiketler
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{previewDocument.tags.map((tag, idx) => (
|
|
||||||
<span
|
|
||||||
key={idx}
|
|
||||||
className="px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm rounded"
|
|
||||||
>
|
|
||||||
#{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Departmanlar */}
|
|
||||||
{previewDocument.departments.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
||||||
Yetkili Departmanlar
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{previewDocument.departments.map((dept, idx) => (
|
|
||||||
<span
|
|
||||||
key={idx}
|
|
||||||
className="px-3 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm rounded"
|
|
||||||
>
|
|
||||||
{dept}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DocumentsModule
|
|
||||||
|
|
@ -1,464 +0,0 @@
|
||||||
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'
|
|
||||||
import { mockEmployees } from '@/mocks/mockEmployees'
|
|
||||||
|
|
||||||
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: mockEmployees[0],
|
|
||||||
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="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>
|
|
||||||
)}
|
|
||||||
</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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EventsModule
|
|
||||||
|
|
@ -1,480 +0,0 @@
|
||||||
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="mx-auto space-y-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
||||||
Harcama Yönetimi
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
Harcama taleplerinizi oluşturun ve takip edin
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateModal(true)}
|
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors"
|
|
||||||
>
|
|
||||||
<HiPlus className="w-5 h-5" />
|
|
||||||
Yeni Harcama Talebi
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* İstatistikler */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
Bu Ay Toplam
|
|
||||||
</h3>
|
|
||||||
<span className="text-2xl">💰</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
|
||||||
₺{expenseStats.thisMonth.total.toLocaleString()}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
|
||||||
toplam harcama
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
Onaylanan
|
|
||||||
</h3>
|
|
||||||
<span className="text-2xl">✅</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-3xl font-bold text-green-600 dark:text-green-400">
|
|
||||||
₺{expenseStats.thisMonth.approved.toLocaleString()}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
|
||||||
bu ay onaylandı
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
Bekleyen
|
|
||||||
</h3>
|
|
||||||
<span className="text-2xl">⏳</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-3xl font-bold text-yellow-600 dark:text-yellow-400">
|
|
||||||
₺{expenseStats.thisMonth.pending.toLocaleString()}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
|
||||||
onay bekliyor
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
Yıllık Toplam
|
|
||||||
</h3>
|
|
||||||
<span className="text-2xl">📊</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-3xl font-bold text-blue-600 dark:text-blue-400">
|
|
||||||
₺{expenseStats.thisYear.total.toLocaleString()}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
|
||||||
bu yıl toplam
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Kategori Dağılımı */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
||||||
Kategori Bazlı Dağılım (Yıllık)
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{Object.entries(expenseStats.byCategory).map(([category, amount]) => {
|
|
||||||
const percentage = (amount / expenseStats.thisYear.total) * 100
|
|
||||||
return (
|
|
||||||
<div key={category}>
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
|
|
||||||
{getCategoryIcon(category)} {getCategoryLabel(category)}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-semibold text-gray-900 dark:text-white">
|
|
||||||
₺{amount.toLocaleString()} ({percentage.toFixed(1)}%)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
|
||||||
<div
|
|
||||||
className="bg-blue-600 h-2 rounded-full transition-all"
|
|
||||||
style={{ width: `${percentage}%` }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filtreler */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HiFunnel className="w-5 h-5 text-gray-400" />
|
|
||||||
<button
|
|
||||||
onClick={() => setFilterStatus('all')}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
filterStatus === 'all'
|
|
||||||
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Tümü
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setFilterStatus('pending')}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
filterStatus === 'pending'
|
|
||||||
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Bekleyen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setFilterStatus('approved')}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
filterStatus === 'approved'
|
|
||||||
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Onaylanan
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setFilterStatus('rejected')}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
filterStatus === 'rejected'
|
|
||||||
? 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Reddedilen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Harcama Talepleri Listesi */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
Harcama Talepleri
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
{filteredRequests.length > 0 ? (
|
|
||||||
filteredRequests.map((request) => (
|
|
||||||
<div
|
|
||||||
key={request.id}
|
|
||||||
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<span className="text-3xl">{getCategoryIcon(request.category)}</span>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
|
|
||||||
{getCategoryLabel(request.category)}
|
|
||||||
</h3>
|
|
||||||
<span className={`px-2.5 py-1 text-xs rounded-full ${getStatusColor(request.status)}`}>
|
|
||||||
{request.status === 'pending' && '⏳ Beklemede'}
|
|
||||||
{request.status === 'approved' && '✅ Onaylandı'}
|
|
||||||
{request.status === 'rejected' && '❌ Reddedildi'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Tutar</p>
|
|
||||||
<p className="text-lg font-bold text-green-600 dark:text-green-400">
|
|
||||||
{request.amount.toLocaleString()} {request.currency}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Tarih</p>
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{dayjs(request.date).format('DD MMMM YYYY')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Makbuzlar</p>
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{request.receipts.length} dosya
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Talep Tarihi</p>
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{dayjs(request.createdAt).format('DD MMM')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{request.project && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<span className="inline-flex items-center px-2.5 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded text-xs font-medium">
|
|
||||||
📁 {request.project}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
|
||||||
<span className="font-medium">Açıklama:</span> {request.description}
|
|
||||||
</p>
|
|
||||||
{request.receipts.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-2 mb-3">
|
|
||||||
{request.receipts.map((receipt, idx) => (
|
|
||||||
<a
|
|
||||||
key={idx}
|
|
||||||
href={receipt.url}
|
|
||||||
className="inline-flex items-center gap-2 px-3 py-1.5 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
|
||||||
>
|
|
||||||
<HiPaperClip className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
|
||||||
<span className="text-xs text-gray-700 dark:text-gray-300">
|
|
||||||
{receipt.name}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
({receipt.size})
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{request.approver && (
|
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
<HiCheckCircle className="w-4 h-4" />
|
|
||||||
<span>
|
|
||||||
{request.approver.fullName} tarafından{' '}
|
|
||||||
{dayjs(request.approvalDate).format('DD MMMM YYYY')} tarihinde{' '}
|
|
||||||
{request.status === 'approved' ? 'onaylandı' : 'reddedildi'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{request.notes && (
|
|
||||||
<div className="mt-2 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
|
||||||
<p className="text-xs text-gray-700 dark:text-gray-300">
|
|
||||||
<span className="font-medium">Not:</span> {request.notes}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="p-12 text-center">
|
|
||||||
<HiCurrencyDollar className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
|
||||||
<p className="text-gray-500 dark:text-gray-400">
|
|
||||||
{filterStatus === 'all'
|
|
||||||
? 'Henüz harcama talebi bulunmuyor'
|
|
||||||
: `${filterStatus === 'pending' ? 'Bekleyen' : filterStatus === 'approved' ? 'Onaylanan' : 'Reddedilen'} harcama talebi bulunmuyor`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Yeni Harcama Talebi Modal */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{showCreateModal && (
|
|
||||||
<>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
className="fixed inset-0 bg-black/50 z-40"
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
|
||||||
>
|
|
||||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between sticky top-0 bg-white dark:bg-gray-800">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
Yeni Harcama Talebi
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<HiXMark className="w-5 h-5 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Harcama Kategorisi
|
|
||||||
</label>
|
|
||||||
<select className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
|
||||||
<option>Seyahat</option>
|
|
||||||
<option>Yemek</option>
|
|
||||||
<option>Konaklama</option>
|
|
||||||
<option>Ulaşım</option>
|
|
||||||
<option>Diğer</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Tutar
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
placeholder="0.00"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Para Birimi
|
|
||||||
</label>
|
|
||||||
<select className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
|
||||||
<option>TRY</option>
|
|
||||||
<option>USD</option>
|
|
||||||
<option>EUR</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Harcama Tarihi
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Proje (Opsiyonel)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Proje adı"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Açıklama
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
rows={3}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white resize-none"
|
|
||||||
placeholder="Harcama detaylarını açıklayın..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Makbuz / Fatura
|
|
||||||
</label>
|
|
||||||
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-6 text-center hover:border-blue-500 dark:hover:border-blue-500 transition-colors cursor-pointer">
|
|
||||||
<HiArrowUpTray className="w-8 h-8 text-gray-400 mx-auto mb-2" />
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
Dosyaları sürükle-bırak veya <span className="text-blue-600 dark:text-blue-400">tıkla</span>
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
||||||
PDF, JPG, PNG (max 5MB)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 pt-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
|
||||||
className="flex-1 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
|
||||||
>
|
|
||||||
İptal
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
|
||||||
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
Talep Oluştur
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ExpenseManagement
|
|
||||||
|
|
@ -1,426 +0,0 @@
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
|
||||||
import {
|
|
||||||
HiCalendar,
|
|
||||||
HiClock,
|
|
||||||
HiCheckCircle,
|
|
||||||
HiXCircle,
|
|
||||||
HiPlus,
|
|
||||||
HiFunnel,
|
|
||||||
HiXMark
|
|
||||||
} from 'react-icons/hi2'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import 'dayjs/locale/tr'
|
|
||||||
import { mockLeaveRequests } from '../../../mocks/mockIntranetData'
|
|
||||||
import { HrLeave, LeaveStatusEnum } from '@/types/hr'
|
|
||||||
|
|
||||||
dayjs.locale('tr')
|
|
||||||
|
|
||||||
const LeaveManagement: React.FC = () => {
|
|
||||||
const [requests, setRequests] = useState<HrLeave[]>(mockLeaveRequests)
|
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
|
||||||
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'approved' | 'rejected'>('all')
|
|
||||||
|
|
||||||
// İzin bakiyeleri (mock)
|
|
||||||
const leaveBalance = {
|
|
||||||
annual: { total: 20, used: 8, remaining: 12 },
|
|
||||||
sick: { total: 10, used: 2, remaining: 8 },
|
|
||||||
unpaid: { used: 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredRequests = requests.filter(req => {
|
|
||||||
if (filterStatus === 'all') return true
|
|
||||||
return req.status === filterStatus
|
|
||||||
})
|
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
const colors = {
|
|
||||||
pending: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300',
|
|
||||||
approved: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
|
||||||
rejected: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
|
||||||
}
|
|
||||||
return colors[status as keyof typeof colors]
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTypeLabel = (type: string) => {
|
|
||||||
const labels = {
|
|
||||||
annual: 'Yıllık İzin',
|
|
||||||
sick: 'Hastalık İzni',
|
|
||||||
unpaid: 'Ücretsiz İzin',
|
|
||||||
maternity: 'Doğum İzni',
|
|
||||||
other: 'Diğer'
|
|
||||||
}
|
|
||||||
return labels[type as keyof typeof labels]
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTypeIcon = (type: string) => {
|
|
||||||
const icons = {
|
|
||||||
annual: '🏖️',
|
|
||||||
sick: '🏥',
|
|
||||||
unpaid: '💼',
|
|
||||||
maternity: '👶',
|
|
||||||
other: '📋'
|
|
||||||
}
|
|
||||||
return icons[type as keyof typeof icons]
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
|
||||||
<div className="mx-auto space-y-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
||||||
İzin Yönetimi
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
İzin taleplerinizi oluşturun ve takip edin
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateModal(true)}
|
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors"
|
|
||||||
>
|
|
||||||
<HiPlus className="w-5 h-5" />
|
|
||||||
Yeni İzin Talebi
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* İzin Bakiyeleri */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
Yıllık İzin
|
|
||||||
</h3>
|
|
||||||
<span className="text-2xl">🏖️</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-600 dark:text-gray-400">Toplam</span>
|
|
||||||
<span className="font-semibold text-gray-900 dark:text-white">
|
|
||||||
{leaveBalance.annual.total} gün
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-600 dark:text-gray-400">Kullanılan</span>
|
|
||||||
<span className="font-semibold text-orange-600 dark:text-orange-400">
|
|
||||||
{leaveBalance.annual.used} gün
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-600 dark:text-gray-400">Kalan</span>
|
|
||||||
<span className="font-semibold text-green-600 dark:text-green-400">
|
|
||||||
{leaveBalance.annual.remaining} gün
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mt-3">
|
|
||||||
<div
|
|
||||||
className="bg-blue-600 h-2 rounded-full transition-all"
|
|
||||||
style={{
|
|
||||||
width: `${(leaveBalance.annual.used / leaveBalance.annual.total) * 100}%`
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
Hastalık İzni
|
|
||||||
</h3>
|
|
||||||
<span className="text-2xl">🏥</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-600 dark:text-gray-400">Toplam</span>
|
|
||||||
<span className="font-semibold text-gray-900 dark:text-white">
|
|
||||||
{leaveBalance.sick.total} gün
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-600 dark:text-gray-400">Kullanılan</span>
|
|
||||||
<span className="font-semibold text-orange-600 dark:text-orange-400">
|
|
||||||
{leaveBalance.sick.used} gün
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-600 dark:text-gray-400">Kalan</span>
|
|
||||||
<span className="font-semibold text-green-600 dark:text-green-400">
|
|
||||||
{leaveBalance.sick.remaining} gün
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mt-3">
|
|
||||||
<div
|
|
||||||
className="bg-red-600 h-2 rounded-full transition-all"
|
|
||||||
style={{
|
|
||||||
width: `${(leaveBalance.sick.used / leaveBalance.sick.total) * 100}%`
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
||||||
Ücretsiz İzin
|
|
||||||
</h3>
|
|
||||||
<span className="text-2xl">💼</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-600 dark:text-gray-400">Toplam</span>
|
|
||||||
<span className="font-semibold text-gray-900 dark:text-white">
|
|
||||||
Sınırsız
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-gray-600 dark:text-gray-400">Bu yıl kullanılan</span>
|
|
||||||
<span className="font-semibold text-orange-600 dark:text-orange-400">
|
|
||||||
{leaveBalance.unpaid.used} gün
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 text-center">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
Onay sürecinden geçer
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filtreler */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<HiFunnel className="w-5 h-5 text-gray-400" />
|
|
||||||
<button
|
|
||||||
onClick={() => setFilterStatus('all')}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
filterStatus === 'all'
|
|
||||||
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Tümü
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setFilterStatus('pending')}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
filterStatus === 'pending'
|
|
||||||
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Bekleyen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setFilterStatus('approved')}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
filterStatus === 'approved'
|
|
||||||
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Onaylanan
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setFilterStatus('rejected')}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
filterStatus === 'rejected'
|
|
||||||
? 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Reddedilen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* İzin Talepleri Listesi */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
İzin Talepleri
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
{filteredRequests.length > 0 ? (
|
|
||||||
filteredRequests.map((request) => (
|
|
||||||
<div
|
|
||||||
key={request.id}
|
|
||||||
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<span className="text-3xl">{getTypeIcon(request.leaveType)}</span>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
|
|
||||||
{getTypeLabel(request.leaveType)}
|
|
||||||
</h3>
|
|
||||||
<span className={`px-2.5 py-1 text-xs rounded-full ${getStatusColor(request.status)}`}>
|
|
||||||
{request.status === LeaveStatusEnum.Pending && '⏳ Beklemede'}
|
|
||||||
{request.status === LeaveStatusEnum.Approved && '✅ Onaylandı'}
|
|
||||||
{request.status === LeaveStatusEnum.Rejected && '❌ Reddedildi'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Başlangıç</p>
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{dayjs(request.startDate).format('DD MMMM YYYY')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Bitiş</p>
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{dayjs(request.endDate).format('DD MMMM YYYY')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Süre</p>
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{request.days} gün
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Talep Tarihi</p>
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{dayjs(request.creationTime).format('DD MMM YYYY')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
|
||||||
<span className="font-medium">Açıklama:</span> {request.reason}
|
|
||||||
</p>
|
|
||||||
{request.approver && (
|
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
<HiCheckCircle className="w-4 h-4" />
|
|
||||||
<span>
|
|
||||||
{request.approver.fullName} tarafından{' '}
|
|
||||||
{dayjs(request.approvalDate).format('DD MMMM YYYY')} tarihinde{' '}
|
|
||||||
{request.status === 'approved' ? 'onaylandı' : 'reddedildi'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{request.notes && (
|
|
||||||
<div className="mt-2 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
|
||||||
<p className="text-xs text-gray-700 dark:text-gray-300">
|
|
||||||
<span className="font-medium">Not:</span> {request.notes}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="p-12 text-center">
|
|
||||||
<HiCalendar className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
|
||||||
<p className="text-gray-500 dark:text-gray-400">
|
|
||||||
{filterStatus === 'all'
|
|
||||||
? 'Henüz izin talebi bulunmuyor'
|
|
||||||
: `${filterStatus === 'pending' ? 'Bekleyen' : filterStatus === 'approved' ? 'Onaylanan' : 'Reddedilen'} izin talebi bulunmuyor`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Yeni İzin Talebi Modal */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{showCreateModal && (
|
|
||||||
<>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
className="fixed inset-0 bg-black/50 z-40"
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
|
||||||
>
|
|
||||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between sticky top-0 bg-white dark:bg-gray-800">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
Yeni İzin Talebi
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<HiXMark className="w-5 h-5 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
İzin Türü
|
|
||||||
</label>
|
|
||||||
<select className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
|
||||||
<option>Yıllık İzin</option>
|
|
||||||
<option>Hastalık İzni</option>
|
|
||||||
<option>Ücretsiz İzin</option>
|
|
||||||
<option>Doğum İzni</option>
|
|
||||||
<option>Diğer</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Başlangıç Tarihi
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Bitiş Tarihi
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Açıklama
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
rows={4}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white resize-none"
|
|
||||||
placeholder="İzin sebebinizi açıklayın..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 pt-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
|
||||||
className="flex-1 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
|
||||||
>
|
|
||||||
İptal
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
|
||||||
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
Talep Oluştur
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LeaveManagement
|
|
||||||
|
|
@ -1,356 +0,0 @@
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
|
||||||
import { HiClock, HiCheckCircle, HiPlus, HiXMark, HiFunnel } from 'react-icons/hi2'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import 'dayjs/locale/tr'
|
|
||||||
import { mockOvertimeRequests } from '../../../mocks/mockIntranetData'
|
|
||||||
import { HrOvertime, LeaveStatusEnum } from '@/types/hr'
|
|
||||||
|
|
||||||
dayjs.locale('tr')
|
|
||||||
|
|
||||||
const OvertimeManagement: React.FC = () => {
|
|
||||||
const [requests, setRequests] = useState<HrOvertime[]>(mockOvertimeRequests)
|
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
|
||||||
const [filterStatus, setFilterStatus] = useState<'all' | LeaveStatusEnum>('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="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(LeaveStatusEnum.Pending)}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
filterStatus === LeaveStatusEnum.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(LeaveStatusEnum.Approved)}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
filterStatus === LeaveStatusEnum.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(LeaveStatusEnum.Rejected)}
|
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
filterStatus === LeaveStatusEnum.Rejected
|
|
||||||
? 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
|
||||||
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Reddedilen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mesai Talepleri Listesi */}
|
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Mesai Talepleri</h2>
|
|
||||||
</div>
|
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
||||||
{filteredRequests.length > 0 ? (
|
|
||||||
filteredRequests.map((request) => (
|
|
||||||
<div
|
|
||||||
key={request.id}
|
|
||||||
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center">
|
|
||||||
<HiClock className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
|
|
||||||
{dayjs(request.date).format('DD MMMM YYYY dddd')}
|
|
||||||
</h3>
|
|
||||||
<span
|
|
||||||
className={`px-2.5 py-1 text-xs rounded-full ${getStatusColor(request.status)}`}
|
|
||||||
>
|
|
||||||
{request.status === LeaveStatusEnum.Pending && '⏳ Beklemede'}
|
|
||||||
{request.status === LeaveStatusEnum.Approved && '✅ Onaylandı'}
|
|
||||||
{request.status === LeaveStatusEnum.Rejected && '❌ Reddedildi'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Başlangıç</p>
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{request.startTime}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Bitiş</p>
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{request.endTime}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Süre</p>
|
|
||||||
<p className="text-sm font-medium text-blue-600 dark:text-blue-400">
|
|
||||||
{request.duration} saat
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">Talep Tarihi</p>
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
{dayjs(request.creationTime).format('DD MMM')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{request.project && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<span className="inline-flex items-center px-2.5 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded text-xs font-medium">
|
|
||||||
📁 {request.project}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
|
||||||
<span className="font-medium">Açıklama:</span> {request.reason}
|
|
||||||
</p>
|
|
||||||
{request.approver && (
|
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
<HiCheckCircle className="w-4 h-4" />
|
|
||||||
<span>
|
|
||||||
{request.approver.fullName} tarafından{' '}
|
|
||||||
{dayjs(request.approvalDate).format('DD MMMM YYYY')} tarihinde{' '}
|
|
||||||
{request.status === LeaveStatusEnum.Approved
|
|
||||||
? 'onaylandı'
|
|
||||||
: 'reddedildi'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{request.notes && (
|
|
||||||
<div className="mt-2 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
|
||||||
<p className="text-xs text-gray-700 dark:text-gray-300">
|
|
||||||
<span className="font-medium">Not:</span> {request.notes}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="p-12 text-center">
|
|
||||||
<HiClock className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
|
||||||
<p className="text-gray-500 dark:text-gray-400">
|
|
||||||
{filterStatus === 'all'
|
|
||||||
? 'Henüz mesai talebi bulunmuyor'
|
|
||||||
: `${filterStatus === LeaveStatusEnum.Pending ? 'Bekleyen' : filterStatus === LeaveStatusEnum.Approved ? 'Onaylanan' : 'Reddedilen'} mesai talebi bulunmuyor`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Yeni Mesai Talebi Modal */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{showCreateModal && (
|
|
||||||
<>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
className="fixed inset-0 bg-black/50 z-40"
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
|
||||||
>
|
|
||||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between sticky top-0 bg-white dark:bg-gray-800">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
Yeni Mesai Talebi
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<HiXMark className="w-5 h-5 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Tarih
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Başlangıç Saati
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Bitiş Saati
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Proje (Opsiyonel)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Proje adı"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Açıklama
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
rows={4}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white resize-none"
|
|
||||||
placeholder="Mesai yapma sebebini açıklayın..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 pt-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
|
||||||
className="flex-1 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
|
||||||
>
|
|
||||||
İptal
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowCreateModal(false)}
|
|
||||||
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
Talep Oluştur
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OvertimeManagement
|
|
||||||
|
|
@ -1,313 +0,0 @@
|
||||||
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="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>
|
|
||||||
</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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ReservationsModule
|
|
||||||
|
|
@ -1,464 +0,0 @@
|
||||||
import React, { useState, useMemo, useEffect } from 'react'
|
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
|
||||||
import {
|
|
||||||
HiHome,
|
|
||||||
HiCalendar,
|
|
||||||
HiFolder,
|
|
||||||
HiClipboardDocumentList,
|
|
||||||
HiChevronRight,
|
|
||||||
HiChatBubbleLeftRight,
|
|
||||||
HiCake,
|
|
||||||
HiAcademicCap,
|
|
||||||
HiKey,
|
|
||||||
HiBuildingOffice2,
|
|
||||||
HiClipboardDocumentCheck,
|
|
||||||
HiUserPlus,
|
|
||||||
HiBars3,
|
|
||||||
HiChevronLeft,
|
|
||||||
HiCog6Tooth,
|
|
||||||
HiArrowsUpDown,
|
|
||||||
HiSquares2X2,
|
|
||||||
} from 'react-icons/hi2'
|
|
||||||
import {
|
|
||||||
DndContext,
|
|
||||||
closestCenter,
|
|
||||||
KeyboardSensor,
|
|
||||||
PointerSensor,
|
|
||||||
useSensor,
|
|
||||||
useSensors,
|
|
||||||
DragEndEvent,
|
|
||||||
} from '@dnd-kit/core'
|
|
||||||
import {
|
|
||||||
arrayMove,
|
|
||||||
SortableContext,
|
|
||||||
sortableKeyboardCoordinates,
|
|
||||||
useSortable,
|
|
||||||
verticalListSortingStrategy,
|
|
||||||
} from '@dnd-kit/sortable'
|
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
|
||||||
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'])
|
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
|
||||||
const [isDesignMode, setIsDesignMode] = useState(false)
|
|
||||||
const [customOrder, setCustomOrder] = useState<string[]>([])
|
|
||||||
|
|
||||||
// Mobil ekranlarda otomatik daralt
|
|
||||||
useEffect(() => {
|
|
||||||
const handleResize = () => {
|
|
||||||
if (window.innerWidth < 1024) {
|
|
||||||
// lg breakpoint (1024px)
|
|
||||||
setIsCollapsed(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// İlk yükleme
|
|
||||||
handleResize()
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize)
|
|
||||||
return () => window.removeEventListener('resize', handleResize)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// localStorage'dan özel sıralamayı yükle
|
|
||||||
useEffect(() => {
|
|
||||||
const savedOrder = localStorage.getItem('intranet-menu-order')
|
|
||||||
if (savedOrder) {
|
|
||||||
try {
|
|
||||||
setCustomOrder(JSON.parse(savedOrder))
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load menu order:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 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])
|
|
||||||
|
|
||||||
// Menü sıralamasını customOrder'a göre düzenle
|
|
||||||
const orderedMenuItems = useMemo(() => {
|
|
||||||
if (customOrder.length === 0) {
|
|
||||||
return menuItems
|
|
||||||
}
|
|
||||||
|
|
||||||
const ordered = [...menuItems].sort((a, b) => {
|
|
||||||
const indexA = customOrder.indexOf(a.id)
|
|
||||||
const indexB = customOrder.indexOf(b.id)
|
|
||||||
|
|
||||||
// Eğer her ikisi de customOrder'da varsa, sıralarına göre sırala
|
|
||||||
if (indexA !== -1 && indexB !== -1) {
|
|
||||||
return indexA - indexB
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sadece a customOrder'da varsa, a önce gelsin
|
|
||||||
if (indexA !== -1) return -1
|
|
||||||
|
|
||||||
// Sadece b customOrder'da varsa, b önce gelsin
|
|
||||||
if (indexB !== -1) return 1
|
|
||||||
|
|
||||||
// İkisi de yoksa, orijinal sıraları koru
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
|
|
||||||
return ordered
|
|
||||||
}, [menuItems, customOrder])
|
|
||||||
|
|
||||||
// Drag & Drop sensörleri
|
|
||||||
const sensors = useSensors(
|
|
||||||
useSensor(PointerSensor),
|
|
||||||
useSensor(KeyboardSensor, {
|
|
||||||
coordinateGetter: sortableKeyboardCoordinates,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleDragEnd = (event: DragEndEvent) => {
|
|
||||||
const { active, over } = event
|
|
||||||
|
|
||||||
if (over && active.id !== over.id) {
|
|
||||||
const oldIndex = orderedMenuItems.findIndex((item) => item.id === active.id)
|
|
||||||
const newIndex = orderedMenuItems.findIndex((item) => item.id === over.id)
|
|
||||||
|
|
||||||
const newOrder = arrayMove(orderedMenuItems, oldIndex, newIndex)
|
|
||||||
const newOrderIds = newOrder.map((item) => item.id)
|
|
||||||
|
|
||||||
setCustomOrder(newOrderIds)
|
|
||||||
localStorage.setItem('intranet-menu-order', JSON.stringify(newOrderIds))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleDesignMode = () => {
|
|
||||||
setIsDesignMode(!isDesignMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetMenuOrder = () => {
|
|
||||||
setCustomOrder([])
|
|
||||||
localStorage.removeItem('intranet-menu-order')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sortable MenuItem komponenti
|
|
||||||
const SortableMenuItem = ({ item, level = 0 }: { item: MenuItem; level?: number }) => {
|
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
||||||
id: item.id,
|
|
||||||
disabled: !isDesignMode,
|
|
||||||
})
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition,
|
|
||||||
opacity: isDragging ? 0.5 : 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasChildren = item.children && item.children.length > 0
|
|
||||||
const isExpanded = expandedMenus.includes(item.id)
|
|
||||||
const active = isActive(item.path)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={setNodeRef} style={style} key={item.id}>
|
|
||||||
<button
|
|
||||||
{...(isDesignMode ? { ...attributes, ...listeners } : {})}
|
|
||||||
onClick={() => {
|
|
||||||
if (isDesignMode) return // Tasarım modunda navigasyon engellenir
|
|
||||||
|
|
||||||
if (hasChildren) {
|
|
||||||
toggleMenu(item.id)
|
|
||||||
} else if (item.path) {
|
|
||||||
onNavigate(item.path)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={`w-full flex items-center ${
|
|
||||||
isCollapsed ? 'justify-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' : ''} ${
|
|
||||||
isDesignMode
|
|
||||||
? 'cursor-move border-2 border-dashed border-blue-400 dark:border-blue-500'
|
|
||||||
: 'cursor-pointer'
|
|
||||||
}`}
|
|
||||||
title={isCollapsed ? item.label : isDesignMode ? 'Sürükle' : undefined}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
|
||||||
{isDesignMode && !isCollapsed && (
|
|
||||||
<HiArrowsUpDown className="w-4 h-4 flex-shrink-0 text-blue-500" />
|
|
||||||
)}
|
|
||||||
<item.icon className="w-5 h-5 flex-shrink-0" />
|
|
||||||
{!isCollapsed && <span className="font-medium text-sm truncate">{item.label}</span>}
|
|
||||||
</div>
|
|
||||||
{!isCollapsed && (
|
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
|
||||||
{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 && !isDesignMode && (
|
|
||||||
<motion.div
|
|
||||||
animate={{ rotate: isExpanded ? 90 : 0 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
>
|
|
||||||
<HiChevronRight className="w-4 h-4" />
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{hasChildren && (
|
|
||||||
<AnimatePresence>
|
|
||||||
{isExpanded && !isCollapsed && (
|
|
||||||
<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) => (
|
|
||||||
<SortableMenuItem key={child.id} item={child} level={level + 1} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderMenuItem = (item: MenuItem, level: number = 0) => {
|
|
||||||
return <SortableMenuItem key={item.id} item={item} level={level} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Sidebar */}
|
|
||||||
<motion.div
|
|
||||||
initial={false}
|
|
||||||
animate={{
|
|
||||||
width: isCollapsed ? '60px' : '256px',
|
|
||||||
}}
|
|
||||||
transition={{ duration: 0.3 }}
|
|
||||||
className="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-2 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
{!isCollapsed ? (
|
|
||||||
<div className="flex items-center justify-between px-3 py-2.5">
|
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
|
||||||
<HiSquares2X2 className="w-5 h-5 flex-shrink-0 text-blue-600 dark:text-blue-400" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1 flex-shrink-0">
|
|
||||||
<button
|
|
||||||
onClick={toggleDesignMode}
|
|
||||||
className={`p-1.5 rounded transition-colors ${
|
|
||||||
isDesignMode
|
|
||||||
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400'
|
|
||||||
: 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-600 dark:text-gray-400'
|
|
||||||
}`}
|
|
||||||
title={isDesignMode ? 'Düzenlemeyi Bitir' : 'Menü Düzenle'}
|
|
||||||
>
|
|
||||||
<HiCog6Tooth className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsCollapsed(true)}
|
|
||||||
className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
|
|
||||||
title="Daralt"
|
|
||||||
>
|
|
||||||
<HiChevronLeft className="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={() => setIsCollapsed(false)}
|
|
||||||
className="w-full flex justify-center p-2.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
title="Genişlet"
|
|
||||||
>
|
|
||||||
<HiBars3 className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tasarım Modu Bilgi Paneli */}
|
|
||||||
{!isCollapsed && isDesignMode && (
|
|
||||||
<div className="p-4 border-b border-gray-200 dark:border-gray-700 bg-blue-50 dark:bg-blue-900/20">
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<p className="text-xs text-blue-700 dark:text-blue-300 flex items-center gap-2">
|
|
||||||
<HiArrowsUpDown className="w-4 h-4" />
|
|
||||||
Menü öğelerini sürükleyerek sıralayın
|
|
||||||
</p>
|
|
||||||
{customOrder.length > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={resetMenuOrder}
|
|
||||||
className="px-2 py-1 text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 rounded transition-colors"
|
|
||||||
title="Varsayılana Sıfırla"
|
|
||||||
>
|
|
||||||
Sıfırla
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
||||||
<SortableContext
|
|
||||||
items={orderedMenuItems.map((item) => item.id)}
|
|
||||||
strategy={verticalListSortingStrategy}
|
|
||||||
>
|
|
||||||
<nav className="p-2 space-y-1">
|
|
||||||
{orderedMenuItems.map((item) => renderMenuItem(item))}
|
|
||||||
</nav>
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
</motion.div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IntranetSidebar
|
|
||||||
|
|
@ -1,298 +0,0 @@
|
||||||
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="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>
|
|
||||||
)}
|
|
||||||
</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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SurveysModule
|
|
||||||
|
|
@ -1,674 +0,0 @@
|
||||||
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 [dragOverColumn, setDragOverColumn] = useState<TaskStatus | 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 handleDragOver = (event: any) => {
|
|
||||||
const { over } = event
|
|
||||||
|
|
||||||
if (!over) {
|
|
||||||
setDragOverColumn(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if over a column or a task
|
|
||||||
const overColumn = columns.find((col) => col.id === over.id)
|
|
||||||
if (overColumn) {
|
|
||||||
setDragOverColumn(overColumn.id)
|
|
||||||
} else {
|
|
||||||
// over.id is a task, find its column
|
|
||||||
const overTask = tasks.find((t) => t.id === over.id)
|
|
||||||
if (overTask) {
|
|
||||||
setDragOverColumn(overTask.status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDragEnd = (event: DragEndEvent) => {
|
|
||||||
const { active, over } = event
|
|
||||||
setActiveId(null)
|
|
||||||
setDragOverColumn(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}
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
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={`text-sm sm:text-base transition-colors duration-200 ${
|
|
||||||
dragOverColumn === column.id
|
|
||||||
? 'text-red-600 font-bold dark:text-blue-400'
|
|
||||||
: 'text-gray-900 font-semibold dark:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* New Task Modal */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{showNewTaskModal && (
|
|
||||||
<>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
className="fixed inset-0 bg-black/50 z-40"
|
|
||||||
onClick={() => setShowNewTaskModal(false)}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div className="p-4 sm:p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
||||||
<h2 className="text-lg sm:text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
Yeni Görev Oluştur
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowNewTaskModal(false)}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<HiXMark className="w-5 h-5 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
const formData = new FormData(e.currentTarget)
|
|
||||||
const title = formData.get('title') as string
|
|
||||||
const description = formData.get('description') as string
|
|
||||||
if (title && description) {
|
|
||||||
handleCreateTask(title, description)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="p-4 sm:p-6 space-y-4"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Görev Başlığı *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="title"
|
|
||||||
required
|
|
||||||
placeholder="Görev başlığını yazın"
|
|
||||||
className="w-full px-3 sm:px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 text-sm sm:text-base"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Açıklama *
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
name="description"
|
|
||||||
required
|
|
||||||
rows={4}
|
|
||||||
placeholder="Görev detaylarını yazın"
|
|
||||||
className="w-full px-3 sm:px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 text-sm sm:text-base"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3">
|
|
||||||
<p className="text-xs sm:text-sm text-blue-700 dark:text-blue-300">
|
|
||||||
📋 Görev{' '}
|
|
||||||
<strong>{columns.find((c) => c.id === newTaskColumn)?.title}</strong>{' '}
|
|
||||||
kolonuna eklenecek
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2 sm:gap-3 pt-4">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowNewTaskModal(false)}
|
|
||||||
className="flex-1 px-3 sm:px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm sm:text-base"
|
|
||||||
>
|
|
||||||
İptal
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="flex-1 px-3 sm:px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors text-sm sm:text-base"
|
|
||||||
>
|
|
||||||
Oluştur
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
{/* Task Detail Modal */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{selectedTask && (
|
|
||||||
<>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
className="fixed inset-0 bg-black/50 z-40"
|
|
||||||
onClick={() => setSelectedTask(null)}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-3xl w-full max-h-[90vh] overflow-y-auto"
|
|
||||||
>
|
|
||||||
<div className="p-4 sm:p-6 border-b border-gray-200 dark:border-gray-700 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
||||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
|
||||||
<span
|
|
||||||
className={`px-2 sm:px-3 py-1 sm:py-1.5 text-xs sm:text-sm font-medium rounded border ${getPriorityColor(selectedTask.priority)}`}
|
|
||||||
>
|
|
||||||
{getPriorityLabel(selectedTask.priority)}
|
|
||||||
</span>
|
|
||||||
<span className="px-2 sm:px-3 py-1 sm:py-1.5 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 text-xs sm:text-sm font-medium rounded">
|
|
||||||
📁 {selectedTask.project}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setSelectedTask(null)}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<HiXMark className="w-5 h-5 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 sm:p-6 space-y-4 sm:space-y-6">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
|
||||||
{selectedTask.title}
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm sm:text-base text-gray-700 dark:text-gray-300">
|
|
||||||
{selectedTask.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
|
|
||||||
Durum
|
|
||||||
</p>
|
|
||||||
<select
|
|
||||||
value={selectedTask.status}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleStatusChange(selectedTask.id, e.target.value as TaskStatus)
|
|
||||||
}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 cursor-pointer"
|
|
||||||
>
|
|
||||||
<option value="todo">📋 Yapılacak</option>
|
|
||||||
<option value="in-progress">⚙️ Devam Ediyor</option>
|
|
||||||
<option value="review">👀 İncelemede</option>
|
|
||||||
<option value="done">✅ Tamamlandı</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
|
|
||||||
Son Tarih
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center gap-2 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg">
|
|
||||||
<HiClock className="w-5 h-5 text-gray-400" />
|
|
||||||
<span className="text-gray-900 dark:text-white">
|
|
||||||
{dayjs(selectedTask.dueDate).format('DD MMMM YYYY')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2 sm:mb-3">
|
|
||||||
Atananlar
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2 sm:gap-3">
|
|
||||||
{selectedTask.assignedTo.map((user, idx) => (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="flex items-center gap-2 px-2 sm:px-3 py-1.5 sm:py-2 bg-gray-100 dark:bg-gray-700 rounded-lg"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={user.avatar}
|
|
||||||
alt={user.fullName}
|
|
||||||
className="w-6 h-6 sm:w-8 sm:h-8 rounded-full"
|
|
||||||
/>
|
|
||||||
<span className="text-xs sm:text-sm text-gray-900 dark:text-white">
|
|
||||||
{user.fullName}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{selectedTask.labels.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
|
|
||||||
Etiketler
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{selectedTask.labels.map((label, idx) => (
|
|
||||||
<span
|
|
||||||
key={idx}
|
|
||||||
className="px-2 sm:px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs sm:text-sm rounded"
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="pt-6 border-t border-gray-200 dark:border-gray-700 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
Oluşturan: {selectedTask.assignedBy.fullName} •{' '}
|
|
||||||
{dayjs(selectedTask.createdAt).format('DD MMMM YYYY')}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDeleteTask(selectedTask.id)}
|
|
||||||
className="px-3 sm:px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center gap-2 transition-colors text-sm"
|
|
||||||
>
|
|
||||||
<HiTrash className="w-4 h-4" />
|
|
||||||
Görevi Sil
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
</DndContext>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TasksModule
|
|
||||||
|
|
@ -1,399 +0,0 @@
|
||||||
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="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>
|
|
||||||
</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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TrainingModule
|
|
||||||
|
|
@ -1,393 +0,0 @@
|
||||||
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="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?.name}
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* New Visitor Modal */}
|
|
||||||
<AnimatePresence>
|
|
||||||
{showNewVisitor && (
|
|
||||||
<>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
className="fixed inset-0 bg-black/50 z-40"
|
|
||||||
onClick={() => setShowNewVisitor(false)}
|
|
||||||
/>
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
|
||||||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
|
||||||
>
|
|
||||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
||||||
Yeni Ziyaretçi Kaydı
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowNewVisitor(false)}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
<HiXMark className="w-5 h-5 text-gray-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Ad Soyad *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Ziyaretçi adı"
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Şirket *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Şirket adı"
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
E-posta *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
placeholder="email@example.com"
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Telefon *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="tel"
|
|
||||||
placeholder="+90 5XX XXX XX XX"
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Ziyaret Tarihi ve Saati *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="datetime-local"
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Ev Sahibi (Personel) *
|
|
||||||
</label>
|
|
||||||
<select className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
|
||||||
<option>Ahmet Yılmaz - Yazılım Geliştirme</option>
|
|
||||||
<option>Zeynep Kaya - İnsan Kaynakları</option>
|
|
||||||
<option>Mehmet Demir - Yazılım Geliştirme</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
Ziyaret Amacı *
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
rows={3}
|
|
||||||
placeholder="Ziyaret amacını açıklayın"
|
|
||||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3 pt-4">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowNewVisitor(false)}
|
|
||||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
||||||
>
|
|
||||||
İptal
|
|
||||||
</button>
|
|
||||||
<button className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
|
||||||
Ziyaretçi Kaydet
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default VisitorsModule
|
|
||||||
195
ui/src/views/intranet/modals/AnnouncementDetailModal.tsx
Normal file
195
ui/src/views/intranet/modals/AnnouncementDetailModal.tsx
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { FaTimes, FaEye, FaClipboard } from 'react-icons/fa'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { Announcement } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
interface AnnouncementDetailModalProps {
|
||||||
|
announcement: Announcement
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnnouncementDetailModal: React.FC<AnnouncementDetailModalProps> = ({ announcement, onClose }) => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
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(announcement.category)}`}
|
||||||
|
>
|
||||||
|
{announcement.category === 'general' && '📢 Genel'}
|
||||||
|
{announcement.category === 'hr' && '👥 İnsan Kaynakları'}
|
||||||
|
{announcement.category === 'it' && '💻 Bilgi Teknolojileri'}
|
||||||
|
{announcement.category === 'event' && '🎉 Etkinlik'}
|
||||||
|
{announcement.category === 'urgent' && '🚨 Acil'}
|
||||||
|
</span>
|
||||||
|
{announcement.isPinned && (
|
||||||
|
<span className="text-yellow-500 text-sm">📌 Sabitlenmiş</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{announcement.title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes className="w-6 h-6 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Author Info */}
|
||||||
|
<div className="flex items-center gap-3 mt-4">
|
||||||
|
<img
|
||||||
|
src={announcement.author.avatar}
|
||||||
|
alt={announcement.author.fullName}
|
||||||
|
className="w-12 h-12 rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{announcement.author.fullName}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<span>
|
||||||
|
{dayjs(announcement.publishDate).format('DD MMMM YYYY, HH:mm')}
|
||||||
|
</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<FaEye className="w-4 h-4" />
|
||||||
|
{announcement.viewCount} görüntülenme
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
||||||
|
{/* Image if exists */}
|
||||||
|
{announcement.imageUrl && (
|
||||||
|
<img
|
||||||
|
src={announcement.imageUrl}
|
||||||
|
alt={announcement.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">
|
||||||
|
{announcement.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Attachments */}
|
||||||
|
{announcement.attachments &&
|
||||||
|
announcement.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">
|
||||||
|
<FaClipboard className="w-5 h-5" />
|
||||||
|
Ekler ({announcement.attachments.length})
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{announcement.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"
|
||||||
|
>
|
||||||
|
<FaClipboard 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 */}
|
||||||
|
{announcement.departments &&
|
||||||
|
announcement.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">
|
||||||
|
{announcement.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 */}
|
||||||
|
{announcement.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(announcement.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={onClose}
|
||||||
|
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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnnouncementDetailModal
|
||||||
135
ui/src/views/intranet/modals/ExpenseRequestModal.tsx
Normal file
135
ui/src/views/intranet/modals/ExpenseRequestModal.tsx
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { FaTimes } from 'react-icons/fa'
|
||||||
|
|
||||||
|
interface ExpenseRequestModalProps {
|
||||||
|
onClose: () => void
|
||||||
|
onSubmit: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExpenseRequestModal: React.FC<ExpenseRequestModalProps> = ({ onClose, onSubmit }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<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-lg 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">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
Yeni Harcama Talebi
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit()
|
||||||
|
}}
|
||||||
|
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
|
||||||
|
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="travel">✈️ Seyahat</option>
|
||||||
|
<option value="meal">🍽️ Yemek</option>
|
||||||
|
<option value="accommodation">🏨 Konaklama</option>
|
||||||
|
<option value="transport">🚗 Ulaşım</option>
|
||||||
|
<option value="other">📋 Diğer</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Açıklama *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Harcama açıklaması..."
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
required
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="0.00"
|
||||||
|
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>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Tarih *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Not
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
placeholder="Ek bilgi (opsiyonel)..."
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
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-emerald-600 hover:bg-emerald-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Gönder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExpenseRequestModal
|
||||||
120
ui/src/views/intranet/modals/LeaveRequestModal.tsx
Normal file
120
ui/src/views/intranet/modals/LeaveRequestModal.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { FaTimes } from 'react-icons/fa'
|
||||||
|
|
||||||
|
interface LeaveRequestModalProps {
|
||||||
|
onClose: () => void
|
||||||
|
onSubmit: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const LeaveRequestModal: React.FC<LeaveRequestModalProps> = ({ onClose, onSubmit }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<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-lg 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">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
Yeni İzin Talebi
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit()
|
||||||
|
}}
|
||||||
|
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
|
||||||
|
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="annual">🏖️ Yıllık İzin</option>
|
||||||
|
<option value="sick">🏥 Hastalık İzni</option>
|
||||||
|
<option value="unpaid">💼 Ücretsiz İzin</option>
|
||||||
|
<option value="other">📋 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"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Bitiş Tarihi *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Açıklama *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
required
|
||||||
|
rows={3}
|
||||||
|
placeholder="İzin sebebinizi yazınız..."
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
Gönder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LeaveRequestModal
|
||||||
115
ui/src/views/intranet/modals/OvertimeRequestModal.tsx
Normal file
115
ui/src/views/intranet/modals/OvertimeRequestModal.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { FaTimes } from 'react-icons/fa'
|
||||||
|
|
||||||
|
interface OvertimeRequestModalProps {
|
||||||
|
onClose: () => void
|
||||||
|
onSubmit: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const OvertimeRequestModal: React.FC<OvertimeRequestModalProps> = ({ onClose, onSubmit }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<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-lg 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">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
Yeni Mesai Talebi
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit()
|
||||||
|
}}
|
||||||
|
className="p-6 space-y-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Mesai Tarihi *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Bitiş Saati *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Açıklama *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
required
|
||||||
|
rows={3}
|
||||||
|
placeholder="Mesai sebebinizi yazınız..."
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
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-orange-600 hover:bg-orange-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Gönder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OvertimeRequestModal
|
||||||
160
ui/src/views/intranet/modals/ReservationRequestModal.tsx
Normal file
160
ui/src/views/intranet/modals/ReservationRequestModal.tsx
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { FaTimes } from 'react-icons/fa'
|
||||||
|
|
||||||
|
interface ReservationRequestModalProps {
|
||||||
|
onClose: () => void
|
||||||
|
onSubmit: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReservationRequestModal: React.FC<ReservationRequestModalProps> = ({ onClose, onSubmit }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<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-lg 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">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
Yeni Rezervasyon Talebi
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit()
|
||||||
|
}}
|
||||||
|
className="p-6 space-y-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Rezervasyon Türü *
|
||||||
|
</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="room">🏢 Toplantı Odası</option>
|
||||||
|
<option value="vehicle">🚗 Şirket Aracı</option>
|
||||||
|
<option value="equipment">⚙️ Ekipman</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Kaynak Adı *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Örn: Toplantı Salonu A, Şirket Aracı - Plaka, Kamera Seti..."
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Amaç *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Rezervasyon amacını belirtiniz..."
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Katılımcı Sayısı
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
placeholder="Toplantı odası için katılımcı sayısı (opsiyonel)"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Notlar
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
placeholder="Ek bilgi veya özel istekler (opsiyonel)..."
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
ℹ️ Rezervasyon talebiniz yönetici onayına sunulacaktır.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
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-green-600 hover:bg-green-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Gönder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReservationRequestModal
|
||||||
141
ui/src/views/intranet/modals/SurveyModal.tsx
Normal file
141
ui/src/views/intranet/modals/SurveyModal.tsx
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { FaTimes } from 'react-icons/fa'
|
||||||
|
import { Survey } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
interface SurveyModalProps {
|
||||||
|
survey: Survey
|
||||||
|
onClose: () => void
|
||||||
|
onSubmit: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SurveyModal: React.FC<SurveyModalProps> = ({ survey, onClose, onSubmit }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<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">
|
||||||
|
{survey.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{survey.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<FaTimes className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onSubmit()
|
||||||
|
}}
|
||||||
|
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>
|
||||||
|
|
||||||
|
{!survey.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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{survey.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={onClose}
|
||||||
|
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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SurveyModal
|
||||||
59
ui/src/views/intranet/widgets/ActiveReservations.tsx
Normal file
59
ui/src/views/intranet/widgets/ActiveReservations.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { FaKey, FaPlus } from 'react-icons/fa'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockReservations } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
interface ActiveReservationsProps {
|
||||||
|
onNewReservation: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActiveReservations: React.FC<ActiveReservationsProps> = ({ onNewReservation }) => {
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<FaKey className="w-5 h-5" />
|
||||||
|
Aktif Rezervasyonlar
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
{mockReservations
|
||||||
|
.filter((r) => r.status === 'approved')
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((reservation) => (
|
||||||
|
<div
|
||||||
|
key={reservation.id}
|
||||||
|
className="p-3 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-1">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{reservation.resourceName}
|
||||||
|
</h4>
|
||||||
|
<span className="text-xs px-2 py-1 bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300 rounded-full">
|
||||||
|
{reservation.type === 'room' ? '🏢' : reservation.type === 'vehicle' ? '🚗' : '⚙️'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{dayjs(reservation.startDate).format('DD MMM HH:mm')} - {dayjs(reservation.endDate).format('HH:mm')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{mockReservations.filter((r) => r.status === 'approved').length === 0 && (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
|
||||||
|
Aktif rezervasyon yok
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onNewReservation}
|
||||||
|
className="w-full mt-3 px-4 py-2 bg-green-600 hover:bg-green-700 text-white text-sm rounded-lg transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<FaPlus className="w-4 h-4" />
|
||||||
|
Yeni Rezervasyon
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ActiveReservations
|
||||||
58
ui/src/views/intranet/widgets/ActiveSurveys.tsx
Normal file
58
ui/src/views/intranet/widgets/ActiveSurveys.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { FaClipboardCheck } from 'react-icons/fa'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockSurveys, Survey } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
interface ActiveSurveysProps {
|
||||||
|
onTakeSurvey: (survey: Survey) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ActiveSurveys: React.FC<ActiveSurveysProps> = ({ onTakeSurvey }) => {
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<FaClipboardCheck className="w-5 h-5" />
|
||||||
|
Aktif Anketler
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
{mockSurveys
|
||||||
|
.filter((s) => s.status === 'active')
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((survey) => (
|
||||||
|
<div
|
||||||
|
key={survey.id}
|
||||||
|
onClick={() => onTakeSurvey(survey)}
|
||||||
|
className="p-3 rounded-lg bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 hover:bg-purple-100 dark:hover:bg-purple-900/30 cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-1">
|
||||||
|
{survey.title}
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">
|
||||||
|
{survey.totalQuestions} soru
|
||||||
|
</span>
|
||||||
|
<span className="text-purple-600 dark:text-purple-400 font-medium">
|
||||||
|
{survey.responses} yanıt
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
Son tarih: {dayjs(survey.deadline).format('DD MMM')}
|
||||||
|
</p>
|
||||||
|
<button className="mt-2 w-full px-3 py-1.5 bg-purple-600 hover:bg-purple-700 text-white text-xs rounded-md transition-colors">
|
||||||
|
Anketi Doldur
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{mockSurveys.filter((s) => s.status === 'active').length === 0 && (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
|
||||||
|
Aktif anket yok
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ActiveSurveys
|
||||||
74
ui/src/views/intranet/widgets/ExpenseManagement.tsx
Normal file
74
ui/src/views/intranet/widgets/ExpenseManagement.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { FaDollarSign, FaPlus } from 'react-icons/fa'
|
||||||
|
import { mockExpenseRequests } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
interface ExpenseManagementProps {
|
||||||
|
onNewExpense: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExpenseManagement: React.FC<ExpenseManagementProps> = ({ onNewExpense }) => {
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<FaDollarSign className="w-5 h-5" />
|
||||||
|
Harcama Yönetimi
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
{/* Harcama özeti */}
|
||||||
|
<div className="p-3 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg border border-emerald-200 dark:border-emerald-800 mb-4">
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">Bu Ay Toplam</p>
|
||||||
|
<p className="text-2xl font-bold text-emerald-600 dark:text-emerald-400">₺2,370</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">₺2,050 onaylandı</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Son harcama talepleri */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{mockExpenseRequests.slice(0, 3).map((expense) => (
|
||||||
|
<div
|
||||||
|
key={expense.id}
|
||||||
|
className="p-3 rounded-lg bg-gray-50 dark:bg-gray-900/20 border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-1">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{expense.category === 'travel' ? '✈️' :
|
||||||
|
expense.category === 'meal' ? '🍽️' :
|
||||||
|
expense.category === 'accommodation' ? '🏨' :
|
||||||
|
expense.category === 'transport' ? '🚗' : '📋'} {expense.description}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs font-semibold text-emerald-600 dark:text-emerald-400 mt-1">
|
||||||
|
₺{expense.amount.toLocaleString('tr-TR')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`text-xs px-2 py-1 rounded-full ${
|
||||||
|
expense.status === 'approved'
|
||||||
|
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||||
|
: expense.status === 'pending'
|
||||||
|
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300'
|
||||||
|
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{expense.status === 'approved' ? 'Onaylandı' :
|
||||||
|
expense.status === 'pending' ? 'Bekliyor' : 'Reddedildi'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onNewExpense}
|
||||||
|
className="w-full mt-3 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white text-sm rounded-lg transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<FaPlus className="w-4 h-4" />
|
||||||
|
Yeni Harcama Talebi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExpenseManagement
|
||||||
84
ui/src/views/intranet/widgets/ImportantAnnouncements.tsx
Normal file
84
ui/src/views/intranet/widgets/ImportantAnnouncements.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { FaBell, FaEye } from 'react-icons/fa'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockAnnouncements, Announcement } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
interface ImportantAnnouncementsProps {
|
||||||
|
onAnnouncementClick: (announcement: Announcement) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImportantAnnouncements: React.FC<ImportantAnnouncementsProps> = ({ onAnnouncementClick }) => {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<FaBell className="w-5 h-5" />
|
||||||
|
Önemli Duyurular
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{pinnedAnnouncements.map((announcement) => (
|
||||||
|
<div
|
||||||
|
key={announcement.id}
|
||||||
|
onClick={() => onAnnouncementClick(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 className="flex items-center gap-1">
|
||||||
|
<FaEye className="w-3 h-3" />
|
||||||
|
{announcement.viewCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImportantAnnouncements
|
||||||
78
ui/src/views/intranet/widgets/LeaveManagement.tsx
Normal file
78
ui/src/views/intranet/widgets/LeaveManagement.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { FaCalendarAlt, FaPlus } from 'react-icons/fa'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockLeaveRequests } from '../../../mocks/mockIntranetData'
|
||||||
|
import { LeaveStatusEnum, LeaveTypeEnum } from '../../../types/hr'
|
||||||
|
|
||||||
|
interface LeaveManagementProps {
|
||||||
|
onNewLeave: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const LeaveManagement: React.FC<LeaveManagementProps> = ({ onNewLeave }) => {
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<FaCalendarAlt className="w-5 h-5" />
|
||||||
|
İzin Yönetimi
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
{/* İzin bakiye özeti */}
|
||||||
|
<div className="grid grid-cols-2 gap-2 mb-4">
|
||||||
|
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">Yıllık İzin</p>
|
||||||
|
<p className="text-lg font-bold text-blue-600 dark:text-blue-400">12 gün</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800">
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">Hastalık İzni</p>
|
||||||
|
<p className="text-lg font-bold text-green-600 dark:text-green-400">8 gün</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Son izin talepleri */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{mockLeaveRequests.slice(0, 3).map((leave) => (
|
||||||
|
<div
|
||||||
|
key={leave.id}
|
||||||
|
className="p-3 rounded-lg bg-gray-50 dark:bg-gray-900/20 border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-1">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{leave.leaveType === LeaveTypeEnum.Annual ? '🏖️ Yıllık' :
|
||||||
|
leave.leaveType === LeaveTypeEnum.Sick ? '🏥 Hastalık' :
|
||||||
|
leave.leaveType === LeaveTypeEnum.Unpaid ? '💼 Ücretsiz' : '📋 Diğer'} İzin
|
||||||
|
</h4>
|
||||||
|
<span
|
||||||
|
className={`text-xs px-2 py-1 rounded-full ${
|
||||||
|
leave.status === LeaveStatusEnum.Approved
|
||||||
|
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||||
|
: leave.status === LeaveStatusEnum.Pending
|
||||||
|
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300'
|
||||||
|
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{leave.status === LeaveStatusEnum.Approved ? 'Onaylandı' :
|
||||||
|
leave.status === LeaveStatusEnum.Pending ? 'Bekliyor' : 'Reddedildi'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{dayjs(leave.startDate).format('DD MMM')} - {dayjs(leave.endDate).format('DD MMM')} ({leave.totalDays} gün)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onNewLeave}
|
||||||
|
className="w-full mt-3 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<FaPlus className="w-4 h-4" />
|
||||||
|
Yeni İzin Talebi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LeaveManagement
|
||||||
71
ui/src/views/intranet/widgets/OvertimeManagement.tsx
Normal file
71
ui/src/views/intranet/widgets/OvertimeManagement.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { FaClock, FaPlus } from 'react-icons/fa'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockOvertimeRequests } from '../../../mocks/mockIntranetData'
|
||||||
|
import { LeaveStatusEnum } from '../../../types/hr'
|
||||||
|
|
||||||
|
interface OvertimeManagementProps {
|
||||||
|
onNewOvertime: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const OvertimeManagement: React.FC<OvertimeManagementProps> = ({ onNewOvertime }) => {
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<FaClock className="w-5 h-5" />
|
||||||
|
Mesai Yönetimi
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
{/* Mesai özeti */}
|
||||||
|
<div className="p-3 bg-orange-50 dark:bg-orange-900/20 rounded-lg border border-orange-200 dark:border-orange-800 mb-4">
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">Bu Ay Toplam</p>
|
||||||
|
<p className="text-2xl font-bold text-orange-600 dark:text-orange-400">24 saat</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">20 onaylandı, 4 bekliyor</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Son mesai talepleri */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{mockOvertimeRequests.slice(0, 3).map((overtime) => (
|
||||||
|
<div
|
||||||
|
key={overtime.id}
|
||||||
|
className="p-3 rounded-lg bg-gray-50 dark:bg-gray-900/20 border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-1">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
⏰ {overtime.totalHours} saat
|
||||||
|
</h4>
|
||||||
|
<span
|
||||||
|
className={`text-xs px-2 py-1 rounded-full ${
|
||||||
|
overtime.status === LeaveStatusEnum.Approved
|
||||||
|
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||||
|
: overtime.status === LeaveStatusEnum.Pending
|
||||||
|
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300'
|
||||||
|
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{overtime.status === LeaveStatusEnum.Approved ? 'Onaylandı' :
|
||||||
|
overtime.status === LeaveStatusEnum.Pending ? 'Bekliyor' : 'Reddedildi'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{dayjs(overtime.date).format('DD MMM YYYY')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onNewOvertime}
|
||||||
|
className="w-full mt-3 px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white text-sm rounded-lg transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<FaPlus className="w-4 h-4" />
|
||||||
|
Yeni Mesai Talebi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OvertimeManagement
|
||||||
75
ui/src/views/intranet/widgets/PriorityTasks.tsx
Normal file
75
ui/src/views/intranet/widgets/PriorityTasks.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { FaChartBar, FaClock, FaUsers } from 'react-icons/fa'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockTasks } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
const PriorityTasks: React.FC = () => {
|
||||||
|
const priorityTasks = mockTasks
|
||||||
|
.filter((t) => t.priority === 'high' || t.priority === 'urgent')
|
||||||
|
.slice(0, 3)
|
||||||
|
|
||||||
|
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="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">
|
||||||
|
<FaChartBar 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'}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<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">
|
||||||
|
<FaClock className="w-3 h-3" />
|
||||||
|
{dayjs(task.dueDate).format('DD MMM')}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<FaUsers className="w-3 h-3" />
|
||||||
|
{task.assignedTo.length} kişi
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PriorityTasks
|
||||||
65
ui/src/views/intranet/widgets/RecentDocuments.tsx
Normal file
65
ui/src/views/intranet/widgets/RecentDocuments.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { FaFileAlt, FaDownload } from 'react-icons/fa'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockDocuments } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
const RecentDocuments: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<FaFileAlt className="w-5 h-5" />
|
||||||
|
Son Dokümanlar
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{mockDocuments.slice(0, 3).map((doc) => (
|
||||||
|
<div
|
||||||
|
key={doc.id}
|
||||||
|
className="p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||||
|
<FaFileAlt className="w-4 h-4 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||||
|
{doc.name}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{doc.category === 'policy' && '📋 Politika'}
|
||||||
|
{doc.category === 'procedure' && '📝 Prosedür'}
|
||||||
|
{doc.category === 'form' && '📄 Form'}
|
||||||
|
{doc.category === 'template' && '📋 Şablon'}
|
||||||
|
{doc.category === 'report' && '📊 Rapor'}
|
||||||
|
{doc.category === 'other' && '📄 Diğer'}
|
||||||
|
<span className="mx-1">•</span>
|
||||||
|
{doc.size}
|
||||||
|
</p>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center gap-2">
|
||||||
|
<span>{dayjs(doc.uploadDate).fromNow()}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{doc.downloadCount} indirme</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
window.open(doc.url, '_blank')
|
||||||
|
}}
|
||||||
|
className="p-2 hover:bg-blue-100 dark:hover:bg-blue-900/30 rounded-lg transition-colors group"
|
||||||
|
title="İndir"
|
||||||
|
>
|
||||||
|
<FaDownload className="w-5 h-5 text-gray-600 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RecentDocuments
|
||||||
116
ui/src/views/intranet/widgets/ShuttleSchedule.tsx
Normal file
116
ui/src/views/intranet/widgets/ShuttleSchedule.tsx
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { FaTruck } from 'react-icons/fa'
|
||||||
|
import { mockShuttleRoutes } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
const ShuttleSchedule: React.FC = () => {
|
||||||
|
const morningShuttles = mockShuttleRoutes.filter((s) => s.type === 'morning')
|
||||||
|
const eveningShuttles = mockShuttleRoutes.filter((s) => s.type === 'evening')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<FaTruck className="w-5 h-5" />
|
||||||
|
Servis Saatleri
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{/* Sabah Servisleri */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
|
||||||
|
🌅 Sabah Servisleri
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{morningShuttles.slice(0, 2).map((shuttle) => (
|
||||||
|
<div
|
||||||
|
key={shuttle.id}
|
||||||
|
className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{shuttle.name}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{shuttle.route[0]} → {shuttle.route[shuttle.route.length - 1]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm font-semibold text-blue-600 dark:text-blue-400">
|
||||||
|
{shuttle.departureTime}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">
|
||||||
|
{shuttle.arrivalTime}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`px-2 py-1 rounded-full ${
|
||||||
|
shuttle.available > 5
|
||||||
|
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||||
|
: shuttle.available > 0
|
||||||
|
? 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300'
|
||||||
|
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{shuttle.available} yer
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Akşam Servisleri */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
|
||||||
|
🌆 Akşam Servisleri
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{eveningShuttles.slice(0, 2).map((shuttle) => (
|
||||||
|
<div
|
||||||
|
key={shuttle.id}
|
||||||
|
className="p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{shuttle.name}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{shuttle.route[0]} → {shuttle.route[shuttle.route.length - 1]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm font-semibold text-purple-600 dark:text-purple-400">
|
||||||
|
{shuttle.departureTime}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">
|
||||||
|
{shuttle.arrivalTime}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`px-2 py-1 rounded-full ${
|
||||||
|
shuttle.available > 5
|
||||||
|
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||||
|
: shuttle.available > 0
|
||||||
|
? 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300'
|
||||||
|
: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{shuttle.available} yer
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ShuttleSchedule
|
||||||
54
ui/src/views/intranet/widgets/TodayBirthdays.tsx
Normal file
54
ui/src/views/intranet/widgets/TodayBirthdays.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import React from 'react'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockBirthdays } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
const TodayBirthdays: React.FC = () => {
|
||||||
|
const todayBirthdays = mockBirthdays.filter((b) => {
|
||||||
|
const birthDate = dayjs(b.date)
|
||||||
|
const today = dayjs()
|
||||||
|
return birthDate.month() === today.month() && birthDate.date() === today.date()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gradient-to-br from-pink-50 to-purple-50 dark:from-pink-900/20 dark:to-purple-900/20 rounded-lg shadow-sm border border-pink-200 dark:border-pink-800">
|
||||||
|
<div className="p-6 border-b border-pink-200 dark:border-pink-700">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
|
🎂 Bugün Doğanlar
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
{todayBirthdays.length > 0 ? (
|
||||||
|
todayBirthdays.map((birthday, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center gap-3 p-3 bg-white/50 dark:bg-gray-800/50 rounded-lg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={birthday.employee.avatar}
|
||||||
|
alt={birthday.employee.fullName}
|
||||||
|
className="w-12 h-12 rounded-full border-2 border-pink-300 dark:border-pink-700"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||||
|
{birthday.employee.fullName}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{birthday.age} yaşında 🎉
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
||||||
|
{birthday.employee.department?.name || 'Genel'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
|
||||||
|
Bugün doğan yok
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TodayBirthdays
|
||||||
47
ui/src/views/intranet/widgets/TodayEvents.tsx
Normal file
47
ui/src/views/intranet/widgets/TodayEvents.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { FaCalendarAlt } from 'react-icons/fa'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockEvents } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
const TodayEvents: React.FC = () => {
|
||||||
|
const todayEvents = mockEvents.filter(
|
||||||
|
(event) => event.isPublished && dayjs(event.date).isSame(dayjs(), 'day'),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<FaCalendarAlt 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TodayEvents
|
||||||
47
ui/src/views/intranet/widgets/UpcomingEvents.tsx
Normal file
47
ui/src/views/intranet/widgets/UpcomingEvents.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { FaCalendarAlt } from 'react-icons/fa'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockEvents } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
const UpcomingEvents: React.FC = () => {
|
||||||
|
const upcomingEvents = mockEvents.filter(
|
||||||
|
(event) =>
|
||||||
|
event.isPublished &&
|
||||||
|
dayjs(event.date).isAfter(dayjs()) &&
|
||||||
|
dayjs(event.date).isBefore(dayjs().add(7, 'day')),
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<FaCalendarAlt className="w-5 h-5" />
|
||||||
|
Yaklaşan Etkinlikler
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
{upcomingEvents.length > 0 ? (
|
||||||
|
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>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
|
||||||
|
Yaklaşan etkinlik yok
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpcomingEvents
|
||||||
50
ui/src/views/intranet/widgets/UpcomingTrainings.tsx
Normal file
50
ui/src/views/intranet/widgets/UpcomingTrainings.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { FaGraduationCap } from 'react-icons/fa'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockTrainings } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
const UpcomingTrainings: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<FaGraduationCap className="w-5 h-5" />
|
||||||
|
Yaklaşan Eğitimler
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
{mockTrainings
|
||||||
|
.filter((t) => t.status === 'upcoming')
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((training) => (
|
||||||
|
<div
|
||||||
|
key={training.id}
|
||||||
|
className="p-3 rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800"
|
||||||
|
>
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-1">
|
||||||
|
{training.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mb-2">
|
||||||
|
{training.instructor} • {training.duration}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-gray-500 dark:text-gray-400">
|
||||||
|
{dayjs(training.startDate).format('DD MMM')} - {dayjs(training.endDate).format('DD MMM')}
|
||||||
|
</span>
|
||||||
|
<span className="text-blue-600 dark:text-blue-400 font-medium">
|
||||||
|
{training.enrolled}/{training.maxParticipants}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{mockTrainings.filter((t) => t.status === 'upcoming').length === 0 && (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
|
||||||
|
Yaklaşan eğitim yok
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UpcomingTrainings
|
||||||
73
ui/src/views/intranet/widgets/WeeklyMenu.tsx
Normal file
73
ui/src/views/intranet/widgets/WeeklyMenu.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { FaUtensils } from 'react-icons/fa'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import isBetween from 'dayjs/plugin/isBetween'
|
||||||
|
import { mockMealMenus } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
dayjs.extend(isBetween)
|
||||||
|
|
||||||
|
const WeeklyMenu: React.FC = () => {
|
||||||
|
const weekMenus = mockMealMenus.filter((menu) => {
|
||||||
|
const menuDate = dayjs(menu.date)
|
||||||
|
const today = dayjs()
|
||||||
|
const weekStart = today.startOf('week')
|
||||||
|
const weekEnd = today.endOf('week')
|
||||||
|
return menuDate.isBetween(weekStart, weekEnd, null, '[]')
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<FaUtensils className="w-5 h-5" />
|
||||||
|
Haftalık Menü
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{weekMenus.map((menu) => {
|
||||||
|
const isToday = dayjs(menu.date).isSame(dayjs(), 'day')
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={menu.id}
|
||||||
|
className={`p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${
|
||||||
|
isToday ? 'bg-orange-50 dark:bg-orange-900/20' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
|
{menu.dayOfWeek}
|
||||||
|
{isToday && (
|
||||||
|
<span className="text-xs bg-orange-500 text-white px-2 py-0.5 rounded-full">
|
||||||
|
Bugün
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{dayjs(menu.date).format('DD MMMM')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{menu.meals[0]?.items.slice(0, 3).map((item, idx) => (
|
||||||
|
<p key={idx} className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
• {item}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
{menu.meals[0]?.items.length > 3 && (
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-500">
|
||||||
|
+{menu.meals[0].items.length - 3} daha...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WeeklyMenu
|
||||||
Loading…
Reference in a new issue