Intranet Modulü
This commit is contained in:
parent
c78845f411
commit
ddaaa56ea0
21 changed files with 6891 additions and 4 deletions
|
|
@ -1,6 +1,35 @@
|
||||||
{
|
{
|
||||||
"commit": "64488c5",
|
"commit": "c78845f",
|
||||||
"releases": [
|
"releases": [
|
||||||
|
{
|
||||||
|
"version": "1.0.32",
|
||||||
|
"buildDate": "2025-10-15",
|
||||||
|
"commit": "8564bff367eefb62b1cfd7ac5790097fcf8feaa7",
|
||||||
|
"changeLog": [
|
||||||
|
"Form View ve Form Edit ekranlarında Activity özelliği eklendi. 3 farklı Activity eklenebiliyor ayrıca dosya eklenebiliyor. Bu dosyalar diskte Blob olarak kaydediliyor."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.0.31",
|
||||||
|
"buildDate": "2025-10-09",
|
||||||
|
"commit": "035366ab7020dd77bfe2b5b66ea253e743526ea6",
|
||||||
|
"changeLog": [
|
||||||
|
"- Grid üzerinde Mask ve Format güncellemesi",
|
||||||
|
"- Allow Column Reordering uygulamasının çalıştırılması"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "1.0.30",
|
||||||
|
"buildDate": "2025-10-08",
|
||||||
|
"commit": "e45885f5693176257e12ecc05d4ed51f87ef0120",
|
||||||
|
"changeLog": [
|
||||||
|
"- Tenant ve Barch arasında ilişki kuruldu ve listelerde filtreli şekilde listeleniyor.",
|
||||||
|
"- Genel seederlar düzenlendi.",
|
||||||
|
"- Yeni tanımlamalar listeleri eklendi. Kayıt Tipi, Kayı Şekli, Program vs.",
|
||||||
|
"- Default Helper eklendi ve tüm Application Servisler o metoda yönlendirildi.",
|
||||||
|
"- Tanımlamalar menülere dağıtıldı."
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "1.0.29",
|
"version": "1.0.29",
|
||||||
"buildDate": "2025-09-28",
|
"buildDate": "2025-09-28",
|
||||||
|
|
@ -126,7 +155,7 @@
|
||||||
{
|
{
|
||||||
"version": "1.0.14",
|
"version": "1.0.14",
|
||||||
"buildDate": "2025-09-22",
|
"buildDate": "2025-09-22",
|
||||||
"commit": "1c4ab4f8232b4cd2a39fa66f8101664840113ce5",
|
"commit": "51208b86937484d68b699120d74872067b1c7ef6",
|
||||||
"changeLog": [
|
"changeLog": [
|
||||||
"Yeni versiyon çıktı uyarı gelecek şekilde düzenlendi.",
|
"Yeni versiyon çıktı uyarı gelecek şekilde düzenlendi.",
|
||||||
"Sağ alt kısımda mesaj çıkacak ve yenile butonu ile uygulama yeni versiyona geçecektir."
|
"Sağ alt kısımda mesaj çıkacak ve yenile butonu ile uygulama yeni versiyona geçecektir."
|
||||||
|
|
|
||||||
291
ui/src/components/intranet/Announcements/index.tsx
Normal file
291
ui/src/components/intranet/Announcements/index.tsx
Normal file
|
|
@ -0,0 +1,291 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
HiBell,
|
||||||
|
HiMagnifyingGlass,
|
||||||
|
HiFunnel,
|
||||||
|
HiEye,
|
||||||
|
HiCalendar,
|
||||||
|
HiUser
|
||||||
|
} from 'react-icons/hi2'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import 'dayjs/locale/tr'
|
||||||
|
import { mockAnnouncements, Announcement } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
dayjs.locale('tr')
|
||||||
|
|
||||||
|
const AnnouncementsModule: React.FC = () => {
|
||||||
|
const [announcements] = useState<Announcement[]>(mockAnnouncements)
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
||||||
|
const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null)
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ id: 'all', label: 'Tümü', color: 'blue' },
|
||||||
|
{ id: 'general', label: 'Genel', color: 'blue' },
|
||||||
|
{ id: 'hr', label: 'İK', color: 'purple' },
|
||||||
|
{ id: 'it', label: 'IT', color: 'orange' },
|
||||||
|
{ id: 'event', label: 'Etkinlik', color: 'green' },
|
||||||
|
{ id: 'urgent', label: 'Acil', color: 'red' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const filteredAnnouncements = announcements.filter(announcement => {
|
||||||
|
const matchesSearch = announcement.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
announcement.content.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
const matchesCategory = selectedCategory === 'all' || announcement.category === selectedCategory
|
||||||
|
return matchesSearch && matchesCategory
|
||||||
|
})
|
||||||
|
|
||||||
|
const getCategoryColor = (category: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
general: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800',
|
||||||
|
hr: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 border-purple-200 dark:border-purple-800',
|
||||||
|
it: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 border-orange-200 dark:border-orange-800',
|
||||||
|
event: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 border-green-200 dark:border-green-800',
|
||||||
|
urgent: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 border-red-200 dark:border-red-800'
|
||||||
|
}
|
||||||
|
return colors[category] || colors.general
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoryBadge = (category: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
general: 'Genel',
|
||||||
|
hr: 'İK',
|
||||||
|
it: 'IT',
|
||||||
|
event: 'Etkinlik',
|
||||||
|
urgent: 'Acil'
|
||||||
|
}
|
||||||
|
return labels[category]
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Duyurular & Haberler
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Şirket duyurularını ve haberleri takip edin
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Arama ve Filtreler */}
|
||||||
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<HiMagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Duyuru ara..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HiFunnel className="w-5 h-5 text-gray-400" />
|
||||||
|
{categories.map(cat => (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
onClick={() => setSelectedCategory(cat.id)}
|
||||||
|
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
selectedCategory === cat.id
|
||||||
|
? `bg-${cat.color}-100 dark:bg-${cat.color}-900/30 text-${cat.color}-700 dark:text-${cat.color}-300`
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{cat.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Duyuru Listesi */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Sol Kolon - Liste */}
|
||||||
|
<div className="lg:col-span-1 space-y-4">
|
||||||
|
{filteredAnnouncements.map(announcement => (
|
||||||
|
<motion.div
|
||||||
|
key={announcement.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className={`bg-white dark:bg-gray-800 rounded-lg border-2 shadow-sm cursor-pointer hover:shadow-md transition-all ${
|
||||||
|
selectedAnnouncement?.id === announcement.id
|
||||||
|
? getCategoryColor(announcement.category)
|
||||||
|
: 'border-gray-200 dark:border-gray-700'
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedAnnouncement(announcement)}
|
||||||
|
>
|
||||||
|
{announcement.imageUrl && (
|
||||||
|
<div className="relative h-48 overflow-hidden rounded-t-lg">
|
||||||
|
<img
|
||||||
|
src={announcement.imageUrl}
|
||||||
|
alt={announcement.title}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
{announcement.isPinned && (
|
||||||
|
<div className="absolute top-3 right-3 px-2.5 py-1 bg-yellow-500 text-white text-xs font-bold rounded-full shadow-lg">
|
||||||
|
📌 SABİTLENDİ
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-start justify-between gap-3 mb-3">
|
||||||
|
<h3 className="text-lg font-bold text-gray-900 dark:text-white flex-1">
|
||||||
|
{announcement.title}
|
||||||
|
</h3>
|
||||||
|
<span className={`px-2.5 py-1 text-xs font-medium rounded-full whitespace-nowrap ${getCategoryColor(announcement.category)}`}>
|
||||||
|
{getCategoryBadge(announcement.category)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
|
||||||
|
{announcement.excerpt}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<img
|
||||||
|
src={announcement.author.avatar}
|
||||||
|
alt={announcement.author.fullName}
|
||||||
|
className="w-6 h-6 rounded-full"
|
||||||
|
/>
|
||||||
|
<span>{announcement.author.fullName}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<HiCalendar className="w-4 h-4" />
|
||||||
|
{dayjs(announcement.publishDate).format('DD MMMM YYYY')}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<HiEye className="w-4 h-4" />
|
||||||
|
{announcement.viewCount}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filteredAnnouncements.length === 0 && (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-12 text-center border border-gray-200 dark:border-gray-700">
|
||||||
|
<HiBell className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
{searchQuery ? 'Arama kriterlerine uygun duyuru bulunamadı' : 'Henüz duyuru bulunmuyor'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sağ Kolon - Detay */}
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
{selectedAnnouncement ? (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm sticky top-6">
|
||||||
|
{selectedAnnouncement.imageUrl && (
|
||||||
|
<div className="relative h-64 overflow-hidden rounded-t-lg">
|
||||||
|
<img
|
||||||
|
src={selectedAnnouncement.imageUrl}
|
||||||
|
alt={selectedAnnouncement.title}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-4">
|
||||||
|
<span className={`px-3 py-1.5 text-sm font-medium rounded-full ${getCategoryColor(selectedAnnouncement.category)}`}>
|
||||||
|
{getCategoryBadge(selectedAnnouncement.category)}
|
||||||
|
</span>
|
||||||
|
{selectedAnnouncement.isPinned && (
|
||||||
|
<span className="ml-2 px-3 py-1.5 bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300 text-sm font-medium rounded-full">
|
||||||
|
📌 Sabitlendi
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
{selectedAnnouncement.title}
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center gap-3 mb-4 pb-4 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<img
|
||||||
|
src={selectedAnnouncement.author.avatar}
|
||||||
|
alt={selectedAnnouncement.author.fullName}
|
||||||
|
className="w-10 h-10 rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{selectedAnnouncement.author.fullName}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{dayjs(selectedAnnouncement.publishDate).format('DD MMMM YYYY, HH:mm')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="prose prose-sm dark:prose-invert max-w-none mb-4">
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 whitespace-pre-line">
|
||||||
|
{selectedAnnouncement.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{selectedAnnouncement.attachments && selectedAnnouncement.attachments.length > 0 && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h4 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
📎 Ekler
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{selectedAnnouncement.attachments.map((attachment, idx) => (
|
||||||
|
<a
|
||||||
|
key={idx}
|
||||||
|
href={attachment.url}
|
||||||
|
className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-700 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="w-8 h-8 bg-blue-100 dark:bg-blue-900/30 rounded flex items-center justify-center">
|
||||||
|
<span className="text-xs">📄</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||||
|
{attachment.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{attachment.size}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedAnnouncement.departments && selectedAnnouncement.departments.length > 0 && (
|
||||||
|
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
||||||
|
Hedef Departmanlar:
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedAnnouncement.departments.map((dept, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded"
|
||||||
|
>
|
||||||
|
{dept}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2 mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<HiEye className="w-4 h-4" />
|
||||||
|
<span>{selectedAnnouncement.viewCount} kez görüntülendi</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-12 text-center sticky top-6">
|
||||||
|
<HiBell className="w-16 h-16 text-gray-300 dark:text-gray-600 mx-auto mb-4" />
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
Detayları görüntülemek için bir duyuru seçin
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnnouncementsModule
|
||||||
279
ui/src/components/intranet/Birthdays/index.tsx
Normal file
279
ui/src/components/intranet/Birthdays/index.tsx
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { HiCake, HiGift, HiCalendar } from 'react-icons/hi2'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import isBetween from 'dayjs/plugin/isBetween'
|
||||||
|
import { mockBirthdays, mockAnniversaries, Birthday, WorkAnniversary } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
dayjs.extend(isBetween)
|
||||||
|
|
||||||
|
const BirthdaysModule: React.FC = () => {
|
||||||
|
const [selectedMonth, setSelectedMonth] = useState(dayjs().month())
|
||||||
|
|
||||||
|
const months = [
|
||||||
|
'Ocak', 'Şubat', 'Mart', 'Nisan', 'Mayıs', 'Haziran',
|
||||||
|
'Temmuz', 'Ağustos', 'Eylül', 'Ekim', 'Kasım', 'Aralık'
|
||||||
|
]
|
||||||
|
|
||||||
|
// Bugünün doğum günleri
|
||||||
|
const todayBirthdays = mockBirthdays.filter(b =>
|
||||||
|
dayjs(b.date).month() === dayjs().month() &&
|
||||||
|
dayjs(b.date).date() === dayjs().date()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bu haftanın doğum günleri
|
||||||
|
const thisWeekBirthdays = mockBirthdays.filter(b => {
|
||||||
|
const birthDate = dayjs().year(dayjs().year()).month(dayjs(b.date).month()).date(dayjs(b.date).date())
|
||||||
|
return birthDate.isBetween(dayjs().startOf('week'), dayjs().endOf('week'), null, '[]')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Seçilen aydaki doğum günleri
|
||||||
|
const monthBirthdays = mockBirthdays.filter(b => dayjs(b.date).month() === selectedMonth)
|
||||||
|
.sort((a, b) => dayjs(a.date).date() - dayjs(b.date).date())
|
||||||
|
|
||||||
|
// Bu ayki iş yıldönümleri
|
||||||
|
const thisMonthAnniversaries = mockAnniversaries.filter((a: WorkAnniversary) => dayjs(a.hireDate).month() === dayjs().month())
|
||||||
|
|
||||||
|
const getBirthdayMessage = (birthday: Birthday) => {
|
||||||
|
const age = birthday.age || dayjs().year() - dayjs(birthday.date).year()
|
||||||
|
return `${age}. yaş günü kutlu olsun! 🎉`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAnniversaryMessage = (anniversary: WorkAnniversary) => {
|
||||||
|
return `${anniversary.years} yıllık iş birliğimiz için teşekkürler! 🎊`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
🎂 Doğum Günleri & Yıldönümleri
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Ekip üyelerimizin özel günlerini kutlayalım
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{/* Bugün */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-gradient-to-br from-pink-500 to-purple-600 rounded-lg p-6 text-white"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<HiCake className="w-8 h-8" />
|
||||||
|
<span className="text-3xl font-bold">{todayBirthdays.length}</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-lg mb-1">Bugün Doğanlar</h3>
|
||||||
|
<p className="text-pink-100 text-sm">
|
||||||
|
{todayBirthdays.length > 0 ? 'Kutlama zamanı!' : 'Bugün doğum günü yok'}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Bu Hafta */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.1 }}
|
||||||
|
className="bg-gradient-to-br from-blue-500 to-cyan-600 rounded-lg p-6 text-white"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<HiGift className="w-8 h-8" />
|
||||||
|
<span className="text-3xl font-bold">{thisWeekBirthdays.length}</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-lg mb-1">Bu Hafta</h3>
|
||||||
|
<p className="text-blue-100 text-sm">
|
||||||
|
{dayjs().startOf('week').format('DD MMM')} - {dayjs().endOf('week').format('DD MMM')}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* İş Yıldönümleri */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
className="bg-gradient-to-br from-orange-500 to-red-600 rounded-lg p-6 text-white"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<HiCalendar className="w-8 h-8" />
|
||||||
|
<span className="text-3xl font-bold">{thisMonthAnniversaries.length}</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-lg mb-1">Bu Ay Yıldönümü</h3>
|
||||||
|
<p className="text-orange-100 text-sm">
|
||||||
|
{dayjs().format('MMMM')} ayında
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bugünün Doğum Günleri */}
|
||||||
|
{todayBirthdays.length > 0 && (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 border-2 border-pink-300 dark:border-pink-700">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
🎉 Bugün Doğum Günü Olanlar
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{todayBirthdays.map((birthday, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
className="bg-gradient-to-r from-pink-50 to-purple-50 dark:from-pink-900/20 dark:to-purple-900/20 rounded-lg p-4 border border-pink-200 dark:border-pink-800"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<img
|
||||||
|
src={birthday.employee.avatar}
|
||||||
|
alt={birthday.employee.fullName}
|
||||||
|
className="w-16 h-16 rounded-full border-4 border-white dark:border-gray-800 shadow-lg"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{birthday.employee.fullName}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{birthday.employee.department?.name || 'Genel'}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-medium text-pink-600 dark:text-pink-400 mt-1">
|
||||||
|
{getBirthdayMessage(birthday)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Ay Seçici */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
📅 Aylık Doğum Günleri
|
||||||
|
</h2>
|
||||||
|
<select
|
||||||
|
value={selectedMonth}
|
||||||
|
onChange={(e) => setSelectedMonth(Number(e.target.value))}
|
||||||
|
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
{months.map((month, idx) => (
|
||||||
|
<option key={idx} value={idx}>
|
||||||
|
{month}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Doğum Günleri Listesi */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{monthBirthdays.map((birthday, idx) => {
|
||||||
|
const isToday = dayjs(birthday.date).month() === dayjs().month() &&
|
||||||
|
dayjs(birthday.date).date() === dayjs().date()
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: idx * 0.05 }}
|
||||||
|
className={`flex items-center gap-4 p-4 rounded-lg border ${
|
||||||
|
isToday
|
||||||
|
? 'bg-pink-50 dark:bg-pink-900/20 border-pink-300 dark:border-pink-700'
|
||||||
|
: 'bg-gray-50 dark:bg-gray-700/50 border-gray-200 dark:border-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`w-16 h-16 rounded-lg flex flex-col items-center justify-center ${
|
||||||
|
isToday
|
||||||
|
? 'bg-pink-500 text-white'
|
||||||
|
: 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300'
|
||||||
|
}`}>
|
||||||
|
<span className="text-2xl font-bold">{dayjs(birthday.date).date()}</span>
|
||||||
|
<span className="text-xs">{months[selectedMonth].substring(0, 3)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src={birthday.employee.avatar}
|
||||||
|
alt={birthday.employee.fullName}
|
||||||
|
className="w-12 h-12 rounded-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{birthday.employee.fullName}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{birthday.employee.department?.name || 'Genel'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{birthday.age || dayjs().year() - dayjs(birthday.date).year()} yaşında
|
||||||
|
</p>
|
||||||
|
{isToday && (
|
||||||
|
<span className="text-xs text-pink-600 dark:text-pink-400 font-medium">
|
||||||
|
🎂 Bugün!
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{monthBirthdays.length === 0 && (
|
||||||
|
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||||
|
<HiCake className="w-16 h-16 mx-auto mb-4 opacity-20" />
|
||||||
|
<p>{months[selectedMonth]} ayında doğum günü yok</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* İş Yıldönümleri */}
|
||||||
|
{thisMonthAnniversaries.length > 0 && (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
🎊 Bu Ayki İş Yıldönümleri
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{thisMonthAnniversaries.map((anniversary: WorkAnniversary, idx: number) => (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
className="bg-gradient-to-r from-orange-50 to-amber-50 dark:from-orange-900/20 dark:to-amber-900/20 rounded-lg p-4 border border-orange-200 dark:border-orange-800"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<img
|
||||||
|
src={anniversary.employee.avatar}
|
||||||
|
alt={anniversary.employee.fullName}
|
||||||
|
className="w-16 h-16 rounded-full border-4 border-white dark:border-gray-800 shadow-lg"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{anniversary.employee.fullName}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{anniversary.employee.department?.name || 'Genel'}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm font-medium text-orange-600 dark:text-orange-400 mt-1">
|
||||||
|
{getAnniversaryMessage(anniversary)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center bg-orange-500 text-white rounded-full w-16 h-16 flex flex-col items-center justify-center">
|
||||||
|
<span className="text-2xl font-bold">{anniversary.years}</span>
|
||||||
|
<span className="text-xs">YIL</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BirthdaysModule
|
||||||
207
ui/src/components/intranet/Cafeteria/index.tsx
Normal file
207
ui/src/components/intranet/Cafeteria/index.tsx
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { HiClock, HiMapPin } from 'react-icons/hi2'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockMealMenus, mockShuttleRoutes } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
const CafeteriaModule: React.FC = () => {
|
||||||
|
const [selectedView, setSelectedView] = useState<'menu' | 'shuttle'>('menu')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
🍽️ Kafeterya & Servis
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Yemek menüsü ve servis saatleri
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="flex gap-2 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedView('menu')}
|
||||||
|
className={`px-6 py-3 font-medium transition-colors ${
|
||||||
|
selectedView === 'menu'
|
||||||
|
? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
📅 Haftalık Menü
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedView('shuttle')}
|
||||||
|
className={`px-6 py-3 font-medium transition-colors ${
|
||||||
|
selectedView === 'shuttle'
|
||||||
|
? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
🚌 Servis Saatleri
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Menu View */}
|
||||||
|
{selectedView === 'menu' && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||||
|
{mockMealMenus.map((menu, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={menu.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400">{dayjs(menu.date).format('DD MMM')}</div>
|
||||||
|
<h3 className="text-lg font-bold text-gray-900 dark:text-white">{menu.dayOfWeek}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{menu.meals.map((meal, mealIdx) => (
|
||||||
|
<div key={mealIdx} className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<span className="font-medium">
|
||||||
|
{meal.type === 'lunch' ? '🍽️ Öğle Yemeği' : meal.type}
|
||||||
|
</span>
|
||||||
|
{meal.calories && <span>{meal.calories} kcal</span>}
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{meal.items.map((item, itemIdx) => (
|
||||||
|
<li key={itemIdx} className="text-sm text-gray-700 dark:text-gray-300 flex items-start">
|
||||||
|
<span className="text-green-500 mr-2">•</span>
|
||||||
|
{item}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Shuttle View */}
|
||||||
|
{selectedView === 'shuttle' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">🌅 Sabah Servisleri</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{mockShuttleRoutes.filter(s => s.type === 'morning').map((shuttle, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={shuttle.id}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{shuttle.name}</h3>
|
||||||
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${
|
||||||
|
shuttle.available > 5
|
||||||
|
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||||
|
: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300'
|
||||||
|
}`}>
|
||||||
|
{shuttle.available} Koltuk
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 mb-4">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<HiClock className="w-4 h-4 text-gray-400" />
|
||||||
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
{shuttle.departureTime} - {shuttle.arrivalTime}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2 text-sm">
|
||||||
|
<HiMapPin className="w-4 h-4 text-gray-400 mt-0.5" />
|
||||||
|
<div className="text-gray-700 dark:text-gray-300">
|
||||||
|
{shuttle.route.map((stop, idx) => (
|
||||||
|
<span key={idx}>
|
||||||
|
{stop}
|
||||||
|
{idx < shuttle.route.length - 1 && ' → '}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2 rounded-full"
|
||||||
|
style={{ width: `${((shuttle.capacity - shuttle.available) / shuttle.capacity) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||||
|
{shuttle.capacity - shuttle.available} / {shuttle.capacity} dolu
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">🌆 Akşam Servisleri</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{mockShuttleRoutes.filter(s => s.type === 'evening').map((shuttle, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={shuttle.id}
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{shuttle.name}</h3>
|
||||||
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${
|
||||||
|
shuttle.available > 5
|
||||||
|
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||||
|
: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300'
|
||||||
|
}`}>
|
||||||
|
{shuttle.available} Koltuk
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 mb-4">
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<HiClock className="w-4 h-4 text-gray-400" />
|
||||||
|
<span className="text-gray-700 dark:text-gray-300">
|
||||||
|
{shuttle.departureTime} - {shuttle.arrivalTime}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2 text-sm">
|
||||||
|
<HiMapPin className="w-4 h-4 text-gray-400 mt-0.5" />
|
||||||
|
<div className="text-gray-700 dark:text-gray-300">
|
||||||
|
{shuttle.route.map((stop, idx) => (
|
||||||
|
<span key={idx}>
|
||||||
|
{stop}
|
||||||
|
{idx < shuttle.route.length - 1 && ' → '}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2 rounded-full"
|
||||||
|
style={{ width: `${((shuttle.capacity - shuttle.available) / shuttle.capacity) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 text-center">
|
||||||
|
{shuttle.capacity - shuttle.available} / {shuttle.capacity} dolu
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CafeteriaModule
|
||||||
458
ui/src/components/intranet/Documents/index.tsx
Normal file
458
ui/src/components/intranet/Documents/index.tsx
Normal file
|
|
@ -0,0 +1,458 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
HiMagnifyingGlass,
|
||||||
|
HiFunnel,
|
||||||
|
HiArrowDownTray,
|
||||||
|
HiEye,
|
||||||
|
HiFolder,
|
||||||
|
HiDocument,
|
||||||
|
HiXMark
|
||||||
|
} from 'react-icons/hi2'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import 'dayjs/locale/tr'
|
||||||
|
import { mockDocuments, Document } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
dayjs.locale('tr')
|
||||||
|
|
||||||
|
const DocumentsModule: React.FC = () => {
|
||||||
|
const [documents] = useState<Document[]>(mockDocuments)
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
||||||
|
const [previewDocument, setPreviewDocument] = useState<Document | null>(null)
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ id: 'all', label: 'Tümü' },
|
||||||
|
{ id: 'policy', label: 'Politika' },
|
||||||
|
{ id: 'procedure', label: 'Prosedür' },
|
||||||
|
{ id: 'form', label: 'Form' },
|
||||||
|
{ id: 'template', label: 'Şablon' },
|
||||||
|
{ id: 'report', label: 'Rapor' },
|
||||||
|
{ id: 'other', label: 'Diğer' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleDownload = (doc: Document) => {
|
||||||
|
// Gerçek indirme işlemi
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = doc.url
|
||||||
|
link.download = doc.name
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredDocuments = documents.filter(doc => {
|
||||||
|
const matchesSearch = doc.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
doc.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
doc.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
|
const matchesCategory = selectedCategory === 'all' || doc.category === selectedCategory
|
||||||
|
return matchesSearch && matchesCategory
|
||||||
|
})
|
||||||
|
|
||||||
|
const getFileIcon = (type: string) => {
|
||||||
|
const icons = {
|
||||||
|
pdf: '📄',
|
||||||
|
doc: '📝',
|
||||||
|
xls: '📊',
|
||||||
|
ppt: '📊',
|
||||||
|
other: '📎'
|
||||||
|
}
|
||||||
|
return icons[type as keyof typeof icons] || icons.other
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoryLabel = (category: string) => {
|
||||||
|
const labels = {
|
||||||
|
policy: 'Politika',
|
||||||
|
procedure: 'Prosedür',
|
||||||
|
form: 'Form',
|
||||||
|
template: 'Şablon',
|
||||||
|
report: 'Rapor',
|
||||||
|
other: 'Diğer'
|
||||||
|
}
|
||||||
|
return labels[category as keyof typeof labels]
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedDocuments = filteredDocuments.reduce((acc, doc) => {
|
||||||
|
if (!acc[doc.category]) {
|
||||||
|
acc[doc.category] = []
|
||||||
|
}
|
||||||
|
acc[doc.category].push(doc)
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, Document[]>)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Doküman Yönetimi
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Şirket dokümanlarına erişin ve yönetin
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Arama ve Filtreler */}
|
||||||
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<HiMagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Doküman ara..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HiFunnel className="w-5 h-5 text-gray-400" />
|
||||||
|
{categories.map(cat => (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
onClick={() => setSelectedCategory(cat.id)}
|
||||||
|
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
selectedCategory === cat.id
|
||||||
|
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{cat.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* İstatistikler */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Toplam Doküman
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">📁</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{documents.length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Kategoriler
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">🗂️</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{Object.keys(groupedDocuments).length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Toplam İndirme
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">⬇️</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{documents.reduce((sum, doc) => sum + doc.downloadCount, 0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Bu Ay Yeni
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">✨</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{documents.filter(d => dayjs(d.uploadDate).isAfter(dayjs().subtract(30, 'day'))).length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Doküman Listesi */}
|
||||||
|
{Object.entries(groupedDocuments).map(([category, categoryDocs]) => (
|
||||||
|
<div key={category} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center gap-3">
|
||||||
|
<HiFolder className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{getCategoryLabel(category)}
|
||||||
|
</h2>
|
||||||
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
({categoryDocs.length} doküman)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{categoryDocs.map(doc => (
|
||||||
|
<motion.div
|
||||||
|
key={doc.id}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center text-2xl">
|
||||||
|
{getFileIcon(doc.type)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-start justify-between gap-3 mb-2">
|
||||||
|
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
|
||||||
|
{doc.name}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setPreviewDocument(doc)}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
title="Önizle"
|
||||||
|
>
|
||||||
|
<HiEye className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDownload(doc)}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
title="İndir"
|
||||||
|
>
|
||||||
|
<HiArrowDownTray className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||||
|
{doc.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<img
|
||||||
|
src={doc.uploadedBy.avatar}
|
||||||
|
alt={doc.uploadedBy.fullName}
|
||||||
|
className="w-5 h-5 rounded-full"
|
||||||
|
/>
|
||||||
|
<span>{doc.uploadedBy.fullName}</span>
|
||||||
|
</div>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{dayjs(doc.uploadDate).format('DD MMMM YYYY')}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{doc.size}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>v{doc.version}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{doc.downloadCount} indirme</span>
|
||||||
|
</div>
|
||||||
|
{doc.tags.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mt-3">
|
||||||
|
{doc.tags.map((tag, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="px-2 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded"
|
||||||
|
>
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filteredDocuments.length === 0 && (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-12 text-center border border-gray-200 dark:border-gray-700">
|
||||||
|
<HiDocument className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
{searchQuery ? 'Arama kriterlerine uygun doküman bulunamadı' : 'Henüz doküman bulunmuyor'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Doküman Önizleme Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{previewDocument && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={() => setPreviewDocument(null)}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center text-2xl">
|
||||||
|
{getFileIcon(previewDocument.type)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
{previewDocument.name}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{getCategoryLabel(previewDocument.category)} • {previewDocument.size} • v{previewDocument.version}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleDownload(previewDocument)}
|
||||||
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<HiArrowDownTray className="w-5 h-5" />
|
||||||
|
İndir
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setPreviewDocument(null)}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<HiXMark className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Açıklama */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
Açıklama
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300">
|
||||||
|
{previewDocument.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dosya Önizleme Alanı */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
Önizleme
|
||||||
|
</h3>
|
||||||
|
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-12 text-center bg-gray-50 dark:bg-gray-900/50">
|
||||||
|
<div className="text-6xl mb-4">{getFileIcon(previewDocument.type)}</div>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{previewDocument.type.toUpperCase()} dosyası önizlemesi desteklenmemektedir
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDownload(previewDocument)}
|
||||||
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg inline-flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<HiArrowDownTray className="w-5 h-5" />
|
||||||
|
Dosyayı İndir
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Metadata */}
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
||||||
|
Dosya Bilgileri
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">Dosya Tipi:</span>
|
||||||
|
<span className="text-gray-900 dark:text-white font-medium">
|
||||||
|
{previewDocument.type.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">Boyut:</span>
|
||||||
|
<span className="text-gray-900 dark:text-white font-medium">
|
||||||
|
{previewDocument.size}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">Versiyon:</span>
|
||||||
|
<span className="text-gray-900 dark:text-white font-medium">
|
||||||
|
v{previewDocument.version}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">İndirme:</span>
|
||||||
|
<span className="text-gray-900 dark:text-white font-medium">
|
||||||
|
{previewDocument.downloadCount} kez
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
||||||
|
Yükleyen
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<img
|
||||||
|
src={previewDocument.uploadedBy.avatar}
|
||||||
|
alt={previewDocument.uploadedBy.fullName}
|
||||||
|
className="w-10 h-10 rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{previewDocument.uploadedBy.fullName}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{dayjs(previewDocument.uploadDate).format('DD MMMM YYYY, HH:mm')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Etiketler */}
|
||||||
|
{previewDocument.tags.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
Etiketler
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{previewDocument.tags.map((tag, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm rounded"
|
||||||
|
>
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Departmanlar */}
|
||||||
|
{previewDocument.departments.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
Yetkili Departmanlar
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{previewDocument.departments.map((dept, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="px-3 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm rounded"
|
||||||
|
>
|
||||||
|
{dept}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DocumentsModule
|
||||||
460
ui/src/components/intranet/Events/index.tsx
Normal file
460
ui/src/components/intranet/Events/index.tsx
Normal file
|
|
@ -0,0 +1,460 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
HiHeart,
|
||||||
|
HiChatBubbleLeft,
|
||||||
|
HiMapPin,
|
||||||
|
HiUsers,
|
||||||
|
HiCalendar,
|
||||||
|
HiXMark,
|
||||||
|
HiChevronLeft,
|
||||||
|
HiChevronRight
|
||||||
|
} from 'react-icons/hi2'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import 'dayjs/locale/tr'
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
import { mockEvents, CalendarEvent, EventComment } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
dayjs.locale('tr')
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
const EventsModule: React.FC = () => {
|
||||||
|
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null)
|
||||||
|
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState(0)
|
||||||
|
const [showPhotoModal, setShowPhotoModal] = useState(false)
|
||||||
|
const [selectedFilter, setSelectedFilter] = useState<'all' | 'social' | 'training' | 'company' | 'sport' | 'culture'>('all')
|
||||||
|
const [newComment, setNewComment] = useState('')
|
||||||
|
const [events, setEvents] = useState<CalendarEvent[]>(mockEvents)
|
||||||
|
|
||||||
|
const filteredEvents = selectedFilter === 'all'
|
||||||
|
? events.filter(e => e.isPublished)
|
||||||
|
: events.filter(e => e.isPublished && e.type === selectedFilter)
|
||||||
|
|
||||||
|
const handleLikeEvent = (eventId: string) => {
|
||||||
|
setEvents(prev => prev.map(e =>
|
||||||
|
e.id === eventId ? { ...e, likes: e.likes + 1 } : e
|
||||||
|
))
|
||||||
|
if (selectedEvent?.id === eventId) {
|
||||||
|
setSelectedEvent(prev => prev ? { ...prev, likes: prev.likes + 1 } : null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddComment = (eventId: string) => {
|
||||||
|
if (!newComment.trim()) return
|
||||||
|
|
||||||
|
const comment: EventComment = {
|
||||||
|
id: `c${Date.now()}`,
|
||||||
|
author: {
|
||||||
|
id: 'current-user',
|
||||||
|
fullName: 'Sedat Öztürk',
|
||||||
|
avatar: 'https://ui-avatars.com/api/?name=Sedat+Ozturk&background=3b82f6&color=fff'
|
||||||
|
},
|
||||||
|
content: newComment,
|
||||||
|
createdAt: new Date(),
|
||||||
|
likes: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
setEvents(prev => prev.map(e =>
|
||||||
|
e.id === eventId ? { ...e, comments: [...e.comments, comment] } : e
|
||||||
|
))
|
||||||
|
|
||||||
|
if (selectedEvent?.id === eventId) {
|
||||||
|
setSelectedEvent(prev => prev ? { ...prev, comments: [...prev.comments, comment] } : null)
|
||||||
|
}
|
||||||
|
|
||||||
|
setNewComment('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeColor = (type: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
social: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300',
|
||||||
|
training: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
||||||
|
company: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||||
|
sport: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300',
|
||||||
|
culture: 'bg-pink-100 dark:bg-pink-900/30 text-pink-700 dark:text-pink-300'
|
||||||
|
}
|
||||||
|
return colors[type] || colors.social
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeLabel = (type: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
social: '🎉 Sosyal',
|
||||||
|
training: '📚 Eğitim',
|
||||||
|
company: '🏢 Kurumsal',
|
||||||
|
sport: '⚽ Spor',
|
||||||
|
culture: '🎨 Kültür'
|
||||||
|
}
|
||||||
|
return labels[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
🎊 Etkinlikler
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Şirket etkinlikleri, fotoğraflar ve anılar
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter Tabs */}
|
||||||
|
<div className="flex gap-3 overflow-x-auto pb-2">
|
||||||
|
{[
|
||||||
|
{ value: 'all' as const, label: '🌟 Tümü' },
|
||||||
|
{ value: 'social' as const, label: '🎉 Sosyal' },
|
||||||
|
{ value: 'training' as const, label: '📚 Eğitim' },
|
||||||
|
{ value: 'company' as const, label: '🏢 Kurumsal' },
|
||||||
|
{ value: 'sport' as const, label: '⚽ Spor' },
|
||||||
|
{ value: 'culture' as const, label: '🎨 Kültür' }
|
||||||
|
].map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.value}
|
||||||
|
onClick={() => setSelectedFilter(tab.value)}
|
||||||
|
className={`px-4 py-2 rounded-lg border-2 transition-all whitespace-nowrap ${
|
||||||
|
selectedFilter === tab.value
|
||||||
|
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
||||||
|
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Events Grid */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{filteredEvents.map((event, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={event.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 hover:shadow-xl transition-all cursor-pointer"
|
||||||
|
onClick={() => setSelectedEvent(event)}
|
||||||
|
>
|
||||||
|
{/* Cover Photo */}
|
||||||
|
<div className="relative h-48 overflow-hidden">
|
||||||
|
<img
|
||||||
|
src={event.photos[0]}
|
||||||
|
alt={event.title}
|
||||||
|
className="w-full h-full object-cover hover:scale-110 transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
<div className="absolute top-3 right-3">
|
||||||
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getTypeColor(event.type)}`}>
|
||||||
|
{getTypeLabel(event.type)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{event.photos.length > 1 && (
|
||||||
|
<div className="absolute bottom-3 right-3 bg-black/70 text-white px-2 py-1 rounded text-xs">
|
||||||
|
+{event.photos.length - 1} fotoğraf
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white line-clamp-2">
|
||||||
|
{event.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||||
|
{event.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<HiCalendar className="w-4 h-4" />
|
||||||
|
{dayjs(event.date).format('DD MMMM YYYY')}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<HiMapPin className="w-4 h-4" />
|
||||||
|
{event.location}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center gap-4 text-sm">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleLikeEvent(event.id)
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1 text-gray-600 dark:text-gray-400 hover:text-red-500 transition-colors"
|
||||||
|
>
|
||||||
|
<HiHeart className="w-5 h-5" />
|
||||||
|
{event.likes}
|
||||||
|
</button>
|
||||||
|
<div className="flex items-center gap-1 text-gray-600 dark:text-gray-400">
|
||||||
|
<HiChatBubbleLeft className="w-5 h-5" />
|
||||||
|
{event.comments.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<HiUsers className="w-4 h-4" />
|
||||||
|
{event.participants} kişi
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredEvents.length === 0 && (
|
||||||
|
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||||
|
<p className="text-lg">Bu kategoride henüz etkinlik yok</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Event Detail Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedEvent && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={() => setSelectedEvent(null)}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 overflow-y-auto">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto my-8"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-6 z-10">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getTypeColor(selectedEvent.type)}`}>
|
||||||
|
{getTypeLabel(selectedEvent.type)}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{dayjs(selectedEvent.date).format('DD MMMM YYYY')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{selectedEvent.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-2">
|
||||||
|
{selectedEvent.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedEvent(null)}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<HiXMark className="w-6 h-6 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-6 mt-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HiMapPin className="w-5 h-5" />
|
||||||
|
{selectedEvent.location}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HiUsers className="w-5 h-5" />
|
||||||
|
{selectedEvent.participants} katılımcı
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<img
|
||||||
|
src={selectedEvent.organizer.avatar}
|
||||||
|
alt={selectedEvent.organizer.fullName}
|
||||||
|
className="w-6 h-6 rounded-full"
|
||||||
|
/>
|
||||||
|
<span>Düzenleyen: {selectedEvent.organizer.fullName}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Photo Gallery */}
|
||||||
|
<div className="p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
|
📸 Fotoğraflar ({selectedEvent.photos.length})
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
{selectedEvent.photos.map((photo, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
className="relative aspect-square rounded-lg overflow-hidden cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedPhotoIndex(idx)
|
||||||
|
setShowPhotoModal(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={photo}
|
||||||
|
alt={`${selectedEvent.title} - ${idx + 1}`}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Comments Section */}
|
||||||
|
<div className="p-6 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
💬 Yorumlar ({selectedEvent.comments.length})
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-4 text-sm">
|
||||||
|
<button
|
||||||
|
onClick={() => handleLikeEvent(selectedEvent.id)}
|
||||||
|
className="flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
<HiHeart className="w-5 h-5 text-red-500" />
|
||||||
|
{selectedEvent.likes} beğeni
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Comments List */}
|
||||||
|
<div className="space-y-4 mb-4">
|
||||||
|
{selectedEvent.comments.map((comment) => (
|
||||||
|
<div
|
||||||
|
key={comment.id}
|
||||||
|
className="flex gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={comment.author.avatar}
|
||||||
|
alt={comment.author.fullName}
|
||||||
|
className="w-10 h-10 rounded-full"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{comment.author.fullName}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{dayjs(comment.createdAt).fromNow()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 text-sm">
|
||||||
|
{comment.content}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<button className="text-xs text-gray-500 hover:text-red-500 transition-colors">
|
||||||
|
❤️ {comment.likes} beğeni
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Comment */}
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<img
|
||||||
|
src="https://ui-avatars.com/api/?name=Sedat+Ozturk&background=3b82f6&color=fff"
|
||||||
|
alt="You"
|
||||||
|
className="w-10 h-10 rounded-full"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newComment}
|
||||||
|
onChange={(e) => setNewComment(e.target.value)}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
handleAddComment(selectedEvent.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Yorumunuzu yazın..."
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => handleAddComment(selectedEvent.id)}
|
||||||
|
disabled={!newComment.trim()}
|
||||||
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Gönder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Photo Viewer Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showPhotoModal && selectedEvent && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/90 z-50"
|
||||||
|
onClick={() => setShowPhotoModal(false)}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9 }}
|
||||||
|
className="relative max-w-5xl w-full"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Close Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPhotoModal(false)}
|
||||||
|
className="absolute top-4 right-4 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white z-10"
|
||||||
|
>
|
||||||
|
<HiXMark className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
{selectedEvent.photos.length > 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedPhotoIndex((prev) =>
|
||||||
|
prev === 0 ? selectedEvent.photos.length - 1 : prev - 1
|
||||||
|
)}
|
||||||
|
className="absolute left-4 top-1/2 -translate-y-1/2 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white"
|
||||||
|
>
|
||||||
|
<HiChevronLeft className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedPhotoIndex((prev) =>
|
||||||
|
prev === selectedEvent.photos.length - 1 ? 0 : prev + 1
|
||||||
|
)}
|
||||||
|
className="absolute right-4 top-1/2 -translate-y-1/2 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white"
|
||||||
|
>
|
||||||
|
<HiChevronRight className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Image */}
|
||||||
|
<img
|
||||||
|
src={selectedEvent.photos[selectedPhotoIndex]}
|
||||||
|
alt={`${selectedEvent.title} - ${selectedPhotoIndex + 1}`}
|
||||||
|
className="w-full h-auto max-h-[80vh] object-contain rounded-lg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Counter */}
|
||||||
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 px-4 py-2 bg-black/70 text-white rounded-full text-sm">
|
||||||
|
{selectedPhotoIndex + 1} / {selectedEvent.photos.length}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventsModule
|
||||||
480
ui/src/components/intranet/HR/ExpenseManagement.tsx
Normal file
480
ui/src/components/intranet/HR/ExpenseManagement.tsx
Normal file
|
|
@ -0,0 +1,480 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
HiCurrencyDollar,
|
||||||
|
HiCheckCircle,
|
||||||
|
HiPlus,
|
||||||
|
HiFunnel,
|
||||||
|
HiXMark,
|
||||||
|
HiPaperClip,
|
||||||
|
HiArrowUpTray
|
||||||
|
} from 'react-icons/hi2'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import 'dayjs/locale/tr'
|
||||||
|
import { mockExpenseRequests, ExpenseRequest } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
dayjs.locale('tr')
|
||||||
|
|
||||||
|
const ExpenseManagement: React.FC = () => {
|
||||||
|
const [requests, setRequests] = useState<ExpenseRequest[]>(mockExpenseRequests)
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||||
|
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'approved' | 'rejected'>('all')
|
||||||
|
|
||||||
|
// Harcama istatistikleri (mock)
|
||||||
|
const expenseStats = {
|
||||||
|
thisMonth: { total: 2370, approved: 2050, pending: 320, rejected: 0 },
|
||||||
|
thisYear: { total: 28450, approved: 26100, pending: 1800, rejected: 550 },
|
||||||
|
byCategory: {
|
||||||
|
travel: 12300,
|
||||||
|
meal: 6800,
|
||||||
|
accommodation: 5400,
|
||||||
|
transport: 2950,
|
||||||
|
other: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredRequests = requests.filter(req => {
|
||||||
|
if (filterStatus === 'all') return true
|
||||||
|
return req.status === filterStatus
|
||||||
|
})
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const colors = {
|
||||||
|
pending: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300',
|
||||||
|
approved: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||||
|
rejected: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
}
|
||||||
|
return colors[status as keyof typeof colors]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoryLabel = (category: string) => {
|
||||||
|
const labels = {
|
||||||
|
travel: 'Seyahat',
|
||||||
|
meal: 'Yemek',
|
||||||
|
accommodation: 'Konaklama',
|
||||||
|
transport: 'Ulaşım',
|
||||||
|
other: 'Diğer'
|
||||||
|
}
|
||||||
|
return labels[category as keyof typeof labels]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoryIcon = (category: string) => {
|
||||||
|
const icons = {
|
||||||
|
travel: '✈️',
|
||||||
|
meal: '🍽️',
|
||||||
|
accommodation: '🏨',
|
||||||
|
transport: '🚗',
|
||||||
|
other: '📋'
|
||||||
|
}
|
||||||
|
return icons[category as keyof typeof icons]
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Harcama Yönetimi
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Harcama taleplerinizi oluşturun ve takip edin
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<HiPlus className="w-5 h-5" />
|
||||||
|
Yeni Harcama Talebi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* İstatistikler */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Bu Ay Toplam
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">💰</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
|
₺{expenseStats.thisMonth.total.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
toplam harcama
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Onaylanan
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">✅</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold text-green-600 dark:text-green-400">
|
||||||
|
₺{expenseStats.thisMonth.approved.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
bu ay onaylandı
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Bekleyen
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">⏳</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold text-yellow-600 dark:text-yellow-400">
|
||||||
|
₺{expenseStats.thisMonth.pending.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
onay bekliyor
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Yıllık Toplam
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">📊</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold text-blue-600 dark:text-blue-400">
|
||||||
|
₺{expenseStats.thisYear.total.toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
bu yıl toplam
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kategori Dağılımı */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
|
Kategori Bazlı Dağılım (Yıllık)
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{Object.entries(expenseStats.byCategory).map(([category, amount]) => {
|
||||||
|
const percentage = (amount / expenseStats.thisYear.total) * 100
|
||||||
|
return (
|
||||||
|
<div key={category}>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 flex items-center gap-2">
|
||||||
|
{getCategoryIcon(category)} {getCategoryLabel(category)}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||||
|
₺{amount.toLocaleString()} ({percentage.toFixed(1)}%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2 rounded-full transition-all"
|
||||||
|
style={{ width: `${percentage}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filtreler */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HiFunnel className="w-5 h-5 text-gray-400" />
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterStatus('all')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
filterStatus === 'all'
|
||||||
|
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Tümü
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterStatus('pending')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
filterStatus === 'pending'
|
||||||
|
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Bekleyen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterStatus('approved')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
filterStatus === 'approved'
|
||||||
|
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Onaylanan
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterStatus('rejected')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
filterStatus === 'rejected'
|
||||||
|
? 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Reddedilen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Harcama Talepleri Listesi */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Harcama Talepleri
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{filteredRequests.length > 0 ? (
|
||||||
|
filteredRequests.map((request) => (
|
||||||
|
<div
|
||||||
|
key={request.id}
|
||||||
|
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<span className="text-3xl">{getCategoryIcon(request.category)}</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
|
||||||
|
{getCategoryLabel(request.category)}
|
||||||
|
</h3>
|
||||||
|
<span className={`px-2.5 py-1 text-xs rounded-full ${getStatusColor(request.status)}`}>
|
||||||
|
{request.status === 'pending' && '⏳ Beklemede'}
|
||||||
|
{request.status === 'approved' && '✅ Onaylandı'}
|
||||||
|
{request.status === 'rejected' && '❌ Reddedildi'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Tutar</p>
|
||||||
|
<p className="text-lg font-bold text-green-600 dark:text-green-400">
|
||||||
|
{request.amount.toLocaleString()} {request.currency}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Tarih</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{dayjs(request.date).format('DD MMMM YYYY')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Makbuzlar</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{request.receipts.length} dosya
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Talep Tarihi</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{dayjs(request.createdAt).format('DD MMM')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{request.project && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="inline-flex items-center px-2.5 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded text-xs font-medium">
|
||||||
|
📁 {request.project}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||||
|
<span className="font-medium">Açıklama:</span> {request.description}
|
||||||
|
</p>
|
||||||
|
{request.receipts.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
{request.receipts.map((receipt, idx) => (
|
||||||
|
<a
|
||||||
|
key={idx}
|
||||||
|
href={receipt.url}
|
||||||
|
className="inline-flex items-center gap-2 px-3 py-1.5 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<HiPaperClip className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
||||||
|
<span className="text-xs text-gray-700 dark:text-gray-300">
|
||||||
|
{receipt.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
({receipt.size})
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{request.approver && (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<HiCheckCircle className="w-4 h-4" />
|
||||||
|
<span>
|
||||||
|
{request.approver.fullName} tarafından{' '}
|
||||||
|
{dayjs(request.approvalDate).format('DD MMMM YYYY')} tarihinde{' '}
|
||||||
|
{request.status === 'approved' ? 'onaylandı' : 'reddedildi'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{request.notes && (
|
||||||
|
<div className="mt-2 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||||
|
<p className="text-xs text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="font-medium">Not:</span> {request.notes}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="p-12 text-center">
|
||||||
|
<HiCurrencyDollar className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
{filterStatus === 'all'
|
||||||
|
? 'Henüz harcama talebi bulunmuyor'
|
||||||
|
: `${filterStatus === 'pending' ? 'Bekleyen' : filterStatus === 'approved' ? 'Onaylanan' : 'Reddedilen'} harcama talebi bulunmuyor`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Yeni Harcama Talebi Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showCreateModal && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between sticky top-0 bg-white dark:bg-gray-800">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
Yeni Harcama Talebi
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<HiXMark className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Harcama Kategorisi
|
||||||
|
</label>
|
||||||
|
<select className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
||||||
|
<option>Seyahat</option>
|
||||||
|
<option>Yemek</option>
|
||||||
|
<option>Konaklama</option>
|
||||||
|
<option>Ulaşım</option>
|
||||||
|
<option>Diğer</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Tutar
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
placeholder="0.00"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Para Birimi
|
||||||
|
</label>
|
||||||
|
<select className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
||||||
|
<option>TRY</option>
|
||||||
|
<option>USD</option>
|
||||||
|
<option>EUR</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Harcama Tarihi
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Proje (Opsiyonel)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Proje adı"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Açıklama
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white resize-none"
|
||||||
|
placeholder="Harcama detaylarını açıklayın..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Makbuz / Fatura
|
||||||
|
</label>
|
||||||
|
<div className="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-6 text-center hover:border-blue-500 dark:hover:border-blue-500 transition-colors cursor-pointer">
|
||||||
|
<HiArrowUpTray className="w-8 h-8 text-gray-400 mx-auto mb-2" />
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Dosyaları sürükle-bırak veya <span className="text-blue-600 dark:text-blue-400">tıkla</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
PDF, JPG, PNG (max 5MB)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
className="flex-1 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
İptal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Talep Oluştur
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExpenseManagement
|
||||||
426
ui/src/components/intranet/HR/LeaveManagement.tsx
Normal file
426
ui/src/components/intranet/HR/LeaveManagement.tsx
Normal file
|
|
@ -0,0 +1,426 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
HiCalendar,
|
||||||
|
HiClock,
|
||||||
|
HiCheckCircle,
|
||||||
|
HiXCircle,
|
||||||
|
HiPlus,
|
||||||
|
HiFunnel,
|
||||||
|
HiXMark
|
||||||
|
} from 'react-icons/hi2'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import 'dayjs/locale/tr'
|
||||||
|
import { mockLeaveRequests } from '../../../mocks/mockIntranetData'
|
||||||
|
import { HrLeave } from '@/types/hr'
|
||||||
|
|
||||||
|
dayjs.locale('tr')
|
||||||
|
|
||||||
|
const LeaveManagement: React.FC = () => {
|
||||||
|
const [requests, setRequests] = useState<HrLeave[]>(mockLeaveRequests)
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||||
|
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'approved' | 'rejected'>('all')
|
||||||
|
|
||||||
|
// İzin bakiyeleri (mock)
|
||||||
|
const leaveBalance = {
|
||||||
|
annual: { total: 20, used: 8, remaining: 12 },
|
||||||
|
sick: { total: 10, used: 2, remaining: 8 },
|
||||||
|
unpaid: { used: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredRequests = requests.filter(req => {
|
||||||
|
if (filterStatus === 'all') return true
|
||||||
|
return req.status === filterStatus
|
||||||
|
})
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const colors = {
|
||||||
|
pending: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300',
|
||||||
|
approved: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||||
|
rejected: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
}
|
||||||
|
return colors[status as keyof typeof colors]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeLabel = (type: string) => {
|
||||||
|
const labels = {
|
||||||
|
annual: 'Yıllık İzin',
|
||||||
|
sick: 'Hastalık İzni',
|
||||||
|
unpaid: 'Ücretsiz İzin',
|
||||||
|
maternity: 'Doğum İzni',
|
||||||
|
other: 'Diğer'
|
||||||
|
}
|
||||||
|
return labels[type as keyof typeof labels]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeIcon = (type: string) => {
|
||||||
|
const icons = {
|
||||||
|
annual: '🏖️',
|
||||||
|
sick: '🏥',
|
||||||
|
unpaid: '💼',
|
||||||
|
maternity: '👶',
|
||||||
|
other: '📋'
|
||||||
|
}
|
||||||
|
return icons[type as keyof typeof icons]
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
İzin Yönetimi
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
İzin taleplerinizi oluşturun ve takip edin
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<HiPlus className="w-5 h-5" />
|
||||||
|
Yeni İzin Talebi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* İzin Bakiyeleri */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Yıllık İzin
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">🏖️</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">Toplam</span>
|
||||||
|
<span className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{leaveBalance.annual.total} gün
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">Kullanılan</span>
|
||||||
|
<span className="font-semibold text-orange-600 dark:text-orange-400">
|
||||||
|
{leaveBalance.annual.used} gün
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">Kalan</span>
|
||||||
|
<span className="font-semibold text-green-600 dark:text-green-400">
|
||||||
|
{leaveBalance.annual.remaining} gün
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mt-3">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2 rounded-full transition-all"
|
||||||
|
style={{
|
||||||
|
width: `${(leaveBalance.annual.used / leaveBalance.annual.total) * 100}%`
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Hastalık İzni
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">🏥</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">Toplam</span>
|
||||||
|
<span className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{leaveBalance.sick.total} gün
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">Kullanılan</span>
|
||||||
|
<span className="font-semibold text-orange-600 dark:text-orange-400">
|
||||||
|
{leaveBalance.sick.used} gün
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">Kalan</span>
|
||||||
|
<span className="font-semibold text-green-600 dark:text-green-400">
|
||||||
|
{leaveBalance.sick.remaining} gün
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2 mt-3">
|
||||||
|
<div
|
||||||
|
className="bg-red-600 h-2 rounded-full transition-all"
|
||||||
|
style={{
|
||||||
|
width: `${(leaveBalance.sick.used / leaveBalance.sick.total) * 100}%`
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Ücretsiz İzin
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">💼</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">Toplam</span>
|
||||||
|
<span className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
Sınırsız
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-600 dark:text-gray-400">Bu yıl kullanılan</span>
|
||||||
|
<span className="font-semibold text-orange-600 dark:text-orange-400">
|
||||||
|
{leaveBalance.unpaid.used} gün
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 text-center">
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Onay sürecinden geçer
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filtreler */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HiFunnel className="w-5 h-5 text-gray-400" />
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterStatus('all')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
filterStatus === 'all'
|
||||||
|
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Tümü
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterStatus('pending')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
filterStatus === 'pending'
|
||||||
|
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Bekleyen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterStatus('approved')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
filterStatus === 'approved'
|
||||||
|
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Onaylanan
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterStatus('rejected')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
filterStatus === 'rejected'
|
||||||
|
? 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Reddedilen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* İzin Talepleri Listesi */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
İzin Talepleri
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{filteredRequests.length > 0 ? (
|
||||||
|
filteredRequests.map((request) => (
|
||||||
|
<div
|
||||||
|
key={request.id}
|
||||||
|
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<span className="text-3xl">{getTypeIcon(request.type)}</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
|
||||||
|
{getTypeLabel(request.type)}
|
||||||
|
</h3>
|
||||||
|
<span className={`px-2.5 py-1 text-xs rounded-full ${getStatusColor(request.status)}`}>
|
||||||
|
{request.status === 'pending' && '⏳ Beklemede'}
|
||||||
|
{request.status === 'approved' && '✅ Onaylandı'}
|
||||||
|
{request.status === 'rejected' && '❌ Reddedildi'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Başlangıç</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{dayjs(request.startDate).format('DD MMMM YYYY')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Bitiş</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{dayjs(request.endDate).format('DD MMMM YYYY')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Süre</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{request.days} gün
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Talep Tarihi</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{dayjs(request.createdAt).format('DD MMM YYYY')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||||
|
<span className="font-medium">Açıklama:</span> {request.reason}
|
||||||
|
</p>
|
||||||
|
{request.approver && (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<HiCheckCircle className="w-4 h-4" />
|
||||||
|
<span>
|
||||||
|
{request.approver.fullName} tarafından{' '}
|
||||||
|
{dayjs(request.approvalDate).format('DD MMMM YYYY')} tarihinde{' '}
|
||||||
|
{request.status === 'approved' ? 'onaylandı' : 'reddedildi'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{request.notes && (
|
||||||
|
<div className="mt-2 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||||
|
<p className="text-xs text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="font-medium">Not:</span> {request.notes}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="p-12 text-center">
|
||||||
|
<HiCalendar className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
{filterStatus === 'all'
|
||||||
|
? 'Henüz izin talebi bulunmuyor'
|
||||||
|
: `${filterStatus === 'pending' ? 'Bekleyen' : filterStatus === 'approved' ? 'Onaylanan' : 'Reddedilen'} izin talebi bulunmuyor`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Yeni İzin Talebi Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showCreateModal && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between sticky top-0 bg-white dark:bg-gray-800">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
Yeni İzin Talebi
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<HiXMark className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
İzin Türü
|
||||||
|
</label>
|
||||||
|
<select className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
||||||
|
<option>Yıllık İzin</option>
|
||||||
|
<option>Hastalık İzni</option>
|
||||||
|
<option>Ücretsiz İzin</option>
|
||||||
|
<option>Doğum İzni</option>
|
||||||
|
<option>Diğer</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Başlangıç Tarihi
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Bitiş Tarihi
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Açıklama
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white resize-none"
|
||||||
|
placeholder="İzin sebebinizi açıklayın..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
className="flex-1 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
İptal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Talep Oluştur
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LeaveManagement
|
||||||
376
ui/src/components/intranet/HR/OvertimeManagement.tsx
Normal file
376
ui/src/components/intranet/HR/OvertimeManagement.tsx
Normal file
|
|
@ -0,0 +1,376 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
HiClock,
|
||||||
|
HiCheckCircle,
|
||||||
|
HiPlus,
|
||||||
|
HiXMark,
|
||||||
|
HiFunnel
|
||||||
|
} from 'react-icons/hi2'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import 'dayjs/locale/tr'
|
||||||
|
import { mockOvertimeRequests } from '../../../mocks/mockIntranetData'
|
||||||
|
import { HrOvertime } from '@/types/hr'
|
||||||
|
|
||||||
|
dayjs.locale('tr')
|
||||||
|
|
||||||
|
const OvertimeManagement: React.FC = () => {
|
||||||
|
const [requests, setRequests] = useState<HrOvertime[]>(mockOvertimeRequests)
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||||
|
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'approved' | 'rejected'>('all')
|
||||||
|
|
||||||
|
// Mesai istatistikleri (mock)
|
||||||
|
const overtimeStats = {
|
||||||
|
thisMonth: { total: 24, approved: 20, pending: 4, rejected: 0 },
|
||||||
|
thisYear: { total: 156, approved: 142, pending: 8, rejected: 6 },
|
||||||
|
averagePerMonth: 13
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredRequests = requests.filter(req => {
|
||||||
|
if (filterStatus === 'all') return true
|
||||||
|
return req.status === filterStatus
|
||||||
|
})
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const colors = {
|
||||||
|
pending: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300',
|
||||||
|
approved: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||||
|
rejected: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
}
|
||||||
|
return colors[status as keyof typeof colors]
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Mesai Yönetimi
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Fazla mesai taleplerinizi oluşturun ve takip edin
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(true)}
|
||||||
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<HiPlus className="w-5 h-5" />
|
||||||
|
Yeni Mesai Talebi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* İstatistikler */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Bu Ay Toplam
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">⏰</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{overtimeStats.thisMonth.total}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
saat fazla mesai
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Onaylanan
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">✅</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold text-green-600 dark:text-green-400">
|
||||||
|
{overtimeStats.thisMonth.approved}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
bu ay onaylandı
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Bekleyen
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">⏳</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold text-yellow-600 dark:text-yellow-400">
|
||||||
|
{overtimeStats.thisMonth.pending}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
onay bekliyor
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Yıllık Toplam
|
||||||
|
</h3>
|
||||||
|
<span className="text-2xl">📊</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-3xl font-bold text-blue-600 dark:text-blue-400">
|
||||||
|
{overtimeStats.thisYear.total}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
saat bu yıl
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filtreler */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HiFunnel className="w-5 h-5 text-gray-400" />
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterStatus('all')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
filterStatus === 'all'
|
||||||
|
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Tümü
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterStatus('pending')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
filterStatus === 'pending'
|
||||||
|
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Bekleyen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterStatus('approved')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
filterStatus === 'approved'
|
||||||
|
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Onaylanan
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setFilterStatus('rejected')}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
filterStatus === 'rejected'
|
||||||
|
? 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Reddedilen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mesai Talepleri Listesi */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Mesai Talepleri
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{filteredRequests.length > 0 ? (
|
||||||
|
filteredRequests.map((request) => (
|
||||||
|
<div
|
||||||
|
key={request.id}
|
||||||
|
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center">
|
||||||
|
<HiClock className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
|
||||||
|
{dayjs(request.date).format('DD MMMM YYYY dddd')}
|
||||||
|
</h3>
|
||||||
|
<span className={`px-2.5 py-1 text-xs rounded-full ${getStatusColor(request.status)}`}>
|
||||||
|
{request.status === 'pending' && '⏳ Beklemede'}
|
||||||
|
{request.status === 'approved' && '✅ Onaylandı'}
|
||||||
|
{request.status === 'rejected' && '❌ Reddedildi'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Başlangıç</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{request.startTime}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Bitiş</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{request.endTime}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Süre</p>
|
||||||
|
<p className="text-sm font-medium text-blue-600 dark:text-blue-400">
|
||||||
|
{request.duration} saat
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Talep Tarihi</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{dayjs(request.createdAt).format('DD MMM')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{request.project && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<span className="inline-flex items-center px-2.5 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded text-xs font-medium">
|
||||||
|
📁 {request.project}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||||
|
<span className="font-medium">Açıklama:</span> {request.reason}
|
||||||
|
</p>
|
||||||
|
{request.approver && (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<HiCheckCircle className="w-4 h-4" />
|
||||||
|
<span>
|
||||||
|
{request.approver.fullName} tarafından{' '}
|
||||||
|
{dayjs(request.approvalDate).format('DD MMMM YYYY')} tarihinde{' '}
|
||||||
|
{request.status === 'approved' ? 'onaylandı' : 'reddedildi'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{request.notes && (
|
||||||
|
<div className="mt-2 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||||
|
<p className="text-xs text-gray-700 dark:text-gray-300">
|
||||||
|
<span className="font-medium">Not:</span> {request.notes}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="p-12 text-center">
|
||||||
|
<HiClock className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
{filterStatus === 'all'
|
||||||
|
? 'Henüz mesai talebi bulunmuyor'
|
||||||
|
: `${filterStatus === 'pending' ? 'Bekleyen' : filterStatus === 'approved' ? 'Onaylanan' : 'Reddedilen'} mesai talebi bulunmuyor`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Yeni Mesai Talebi Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showCreateModal && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between sticky top-0 bg-white dark:bg-gray-800">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
Yeni Mesai Talebi
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<HiXMark className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Tarih
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Başlangıç Saati
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Bitiş Saati
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Proje (Opsiyonel)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Proje adı"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Açıklama
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white resize-none"
|
||||||
|
placeholder="Mesai yapma sebebini açıklayın..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
className="flex-1 px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
İptal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowCreateModal(false)}
|
||||||
|
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Talep Oluştur
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OvertimeManagement
|
||||||
539
ui/src/components/intranet/IntranetDashboard/index.tsx
Normal file
539
ui/src/components/intranet/IntranetDashboard/index.tsx
Normal file
|
|
@ -0,0 +1,539 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
HiBell as BellIcon,
|
||||||
|
HiCalendar as CalendarIcon,
|
||||||
|
HiDocumentText as DocumentTextIcon,
|
||||||
|
HiChartBar as ChartBarIcon,
|
||||||
|
HiUserGroup as UserGroupIcon,
|
||||||
|
HiClock as ClockIcon,
|
||||||
|
HiSparkles as SparklesIcon,
|
||||||
|
HiArrowTrendingUp as ArrowTrendingUpIcon,
|
||||||
|
HiArrowTrendingDown as ArrowTrendingDownIcon,
|
||||||
|
HiXMark,
|
||||||
|
HiEye,
|
||||||
|
HiPaperClip
|
||||||
|
} from 'react-icons/hi2'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import 'dayjs/locale/tr'
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
import isBetween from 'dayjs/plugin/isBetween'
|
||||||
|
import {
|
||||||
|
mockAnnouncements,
|
||||||
|
mockEvents,
|
||||||
|
mockBirthdays,
|
||||||
|
mockAnniversaries,
|
||||||
|
mockQuickLinks,
|
||||||
|
mockTasks,
|
||||||
|
Announcement
|
||||||
|
} from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
dayjs.locale('tr')
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
dayjs.extend(isBetween)
|
||||||
|
|
||||||
|
const IntranetDashboard: React.FC = () => {
|
||||||
|
const [selectedDate] = useState(new Date())
|
||||||
|
const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null)
|
||||||
|
|
||||||
|
// Bugünün etkinlikleri
|
||||||
|
const todayEvents = mockEvents.filter(event =>
|
||||||
|
event.isPublished && dayjs(event.date).isSame(dayjs(), 'day')
|
||||||
|
)
|
||||||
|
|
||||||
|
// Yaklaşan etkinlikler (7 gün içinde)
|
||||||
|
const upcomingEvents = mockEvents.filter(event =>
|
||||||
|
event.isPublished &&
|
||||||
|
dayjs(event.date).isAfter(dayjs()) &&
|
||||||
|
dayjs(event.date).isBefore(dayjs().add(7, 'day'))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bu haftaki doğum günleri
|
||||||
|
const weekBirthdays = mockBirthdays.filter(b =>
|
||||||
|
dayjs(b.date).isBetween(dayjs().startOf('week'), dayjs().endOf('week'))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bu ayki iş yıldönümleri
|
||||||
|
const monthAnniversaries = mockAnniversaries.filter(a =>
|
||||||
|
dayjs(a.hireDate).month() === dayjs().month()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Öncelikli görevler
|
||||||
|
const priorityTasks = mockTasks.filter(t =>
|
||||||
|
t.priority === 'high' || t.priority === 'urgent'
|
||||||
|
).slice(0, 3)
|
||||||
|
|
||||||
|
// Sabitlenmiş duyurular
|
||||||
|
const pinnedAnnouncements = mockAnnouncements
|
||||||
|
.filter(a => a.isPinned)
|
||||||
|
.slice(0, 3)
|
||||||
|
|
||||||
|
const getCategoryColor = (category: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
general: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
||||||
|
hr: 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300',
|
||||||
|
it: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300',
|
||||||
|
event: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||||
|
urgent: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
}
|
||||||
|
return colors[category] || colors.general
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPriorityColor = (priority: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
low: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300',
|
||||||
|
medium: 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-300',
|
||||||
|
high: 'bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-300',
|
||||||
|
urgent: 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-300'
|
||||||
|
}
|
||||||
|
return colors[priority]
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Hoş geldiniz, {dayjs().format('DD MMMM YYYY dddd')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg relative">
|
||||||
|
<BellIcon className="w-6 h-6 text-gray-600 dark:text-gray-400" />
|
||||||
|
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Grid */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* Sol Kolon - Duyurular */}
|
||||||
|
<div className="lg:col-span-2 space-y-6">
|
||||||
|
{/* Duyurular */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
|
<BellIcon className="w-5 h-5" />
|
||||||
|
Önemli Duyurular
|
||||||
|
</h2>
|
||||||
|
<button className="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">
|
||||||
|
Tümünü Gör
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{pinnedAnnouncements.map((announcement) => (
|
||||||
|
<div
|
||||||
|
key={announcement.id}
|
||||||
|
onClick={() => setSelectedAnnouncement(announcement)}
|
||||||
|
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<img
|
||||||
|
src={announcement.author.avatar}
|
||||||
|
alt={announcement.author.fullName}
|
||||||
|
className="w-10 h-10 rounded-full"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
|
||||||
|
{announcement.title}
|
||||||
|
</h3>
|
||||||
|
<span className={`px-2 py-1 text-xs rounded-full ${getCategoryColor(announcement.category)}`}>
|
||||||
|
{announcement.category === 'general' && 'Genel'}
|
||||||
|
{announcement.category === 'hr' && 'İK'}
|
||||||
|
{announcement.category === 'it' && 'IT'}
|
||||||
|
{announcement.category === 'event' && 'Etkinlik'}
|
||||||
|
{announcement.category === 'urgent' && 'Acil'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||||
|
{announcement.excerpt}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-4 mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<span>{announcement.author.fullName}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{dayjs(announcement.publishDate).fromNow()}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{announcement.viewCount} görüntülenme</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hızlı Erişim */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2 mb-4">
|
||||||
|
<SparklesIcon className="w-5 h-5" />
|
||||||
|
Hızlı Erişim
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
{mockQuickLinks.map((link) => (
|
||||||
|
<motion.a
|
||||||
|
key={link.id}
|
||||||
|
href={link.url}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
className="flex flex-col items-center gap-2 p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-blue-500 dark:hover:border-blue-500 transition-colors"
|
||||||
|
style={{ borderColor: link.color + '40' }}
|
||||||
|
>
|
||||||
|
<span className="text-3xl">{link.icon}</span>
|
||||||
|
<span className="text-sm font-medium text-gray-900 dark:text-white text-center">
|
||||||
|
{link.name}
|
||||||
|
</span>
|
||||||
|
</motion.a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Öncelikli Görevler */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
|
<ChartBarIcon className="w-5 h-5" />
|
||||||
|
Öncelikli Görevler
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{priorityTasks.map((task) => (
|
||||||
|
<div key={task.id} className="p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mt-1 rounded border-gray-300 dark:border-gray-600"
|
||||||
|
checked={task.status === 'done'}
|
||||||
|
/>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<h3 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{task.title}
|
||||||
|
</h3>
|
||||||
|
<span className={`px-2 py-0.5 text-xs rounded ${getPriorityColor(task.priority)}`}>
|
||||||
|
{task.priority === 'urgent' && '🔥 Acil'}
|
||||||
|
{task.priority === 'high' && 'Yüksek'}
|
||||||
|
{task.priority === 'medium' && 'Orta'}
|
||||||
|
{task.priority === 'low' && 'Düşük'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mb-2">
|
||||||
|
{task.description}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-3 text-xs text-gray-500">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<ClockIcon className="w-3 h-3" />
|
||||||
|
{dayjs(task.dueDate).format('DD MMM')}
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<UserGroupIcon className="w-3 h-3" />
|
||||||
|
{task.assignedTo.length} kişi
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sağ Kolon - Etkinlikler & Kutlamalar */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Bugünün Etkinlikleri */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
|
<CalendarIcon className="w-5 h-5" />
|
||||||
|
Bugün
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
{todayEvents.length > 0 ? (
|
||||||
|
todayEvents.map((event) => (
|
||||||
|
<div
|
||||||
|
key={event.id}
|
||||||
|
className="p-3 rounded-lg border-l-4 bg-gray-50 dark:bg-gray-700/50 border-l-blue-500"
|
||||||
|
>
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{event.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{dayjs(event.date).format('DD MMMM YYYY')} - {event.location}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||||
|
{event.participants} katılımcı
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
|
||||||
|
Bugün etkinlik yok
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Yaklaşan Etkinlikler */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
|
<CalendarIcon className="w-5 h-5" />
|
||||||
|
Yaklaşan Etkinlikler
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
{upcomingEvents.slice(0, 3).map((event) => (
|
||||||
|
<div
|
||||||
|
key={event.id}
|
||||||
|
className="p-3 rounded-lg border-l-4 bg-gray-50 dark:bg-gray-700/50 border-l-green-500"
|
||||||
|
>
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{event.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{dayjs(event.date).format('DD MMMM YYYY')} - {event.location}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Doğum Günleri */}
|
||||||
|
{weekBirthdays.length > 0 && (
|
||||||
|
<div className="bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-lg shadow-sm border border-purple-200 dark:border-purple-800">
|
||||||
|
<div className="p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2 mb-4">
|
||||||
|
🎂 Bu Hafta Doğanlar
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{weekBirthdays.map((birthday, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-3">
|
||||||
|
<img
|
||||||
|
src={birthday.employee.avatar}
|
||||||
|
alt={birthday.employee.fullName}
|
||||||
|
className="w-10 h-10 rounded-full border-2 border-purple-200 dark:border-purple-700"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{birthday.employee.fullName}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{dayjs(birthday.date).format('DD MMMM')} • {birthday.age} yaşında
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* İş Yıldönümleri */}
|
||||||
|
{monthAnniversaries.length > 0 && (
|
||||||
|
<div className="bg-gradient-to-br from-blue-50 to-cyan-50 dark:from-blue-900/20 dark:to-cyan-900/20 rounded-lg shadow-sm border border-blue-200 dark:border-blue-800">
|
||||||
|
<div className="p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2 mb-4">
|
||||||
|
🎉 İş Yıldönümleri
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{monthAnniversaries.map((anniversary, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-3">
|
||||||
|
<img
|
||||||
|
src={anniversary.employee.avatar}
|
||||||
|
alt={anniversary.employee.fullName}
|
||||||
|
className="w-10 h-10 rounded-full border-2 border-blue-200 dark:border-blue-700"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{anniversary.employee.fullName}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{anniversary.years} yıldır bizimle! 🎊
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Announcement Detail Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedAnnouncement && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={() => setSelectedAnnouncement(null)}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 overflow-y-auto">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-3xl w-full my-8"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<span className={`px-3 py-1 text-xs font-medium rounded-full ${getCategoryColor(selectedAnnouncement.category)}`}>
|
||||||
|
{selectedAnnouncement.category === 'general' && '📢 Genel'}
|
||||||
|
{selectedAnnouncement.category === 'hr' && '👥 İnsan Kaynakları'}
|
||||||
|
{selectedAnnouncement.category === 'it' && '💻 Bilgi Teknolojileri'}
|
||||||
|
{selectedAnnouncement.category === 'event' && '🎉 Etkinlik'}
|
||||||
|
{selectedAnnouncement.category === 'urgent' && '🚨 Acil'}
|
||||||
|
</span>
|
||||||
|
{selectedAnnouncement.isPinned && (
|
||||||
|
<span className="text-yellow-500 text-sm">📌 Sabitlenmiş</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
{selectedAnnouncement.title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedAnnouncement(null)}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<HiXMark className="w-6 h-6 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Author Info */}
|
||||||
|
<div className="flex items-center gap-3 mt-4">
|
||||||
|
<img
|
||||||
|
src={selectedAnnouncement.author.avatar}
|
||||||
|
alt={selectedAnnouncement.author.fullName}
|
||||||
|
className="w-12 h-12 rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{selectedAnnouncement.author.fullName}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-3 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<span>{dayjs(selectedAnnouncement.publishDate).format('DD MMMM YYYY, HH:mm')}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<HiEye className="w-4 h-4" />
|
||||||
|
{selectedAnnouncement.viewCount} görüntülenme
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
||||||
|
{/* Image if exists */}
|
||||||
|
{selectedAnnouncement.imageUrl && (
|
||||||
|
<img
|
||||||
|
src={selectedAnnouncement.imageUrl}
|
||||||
|
alt={selectedAnnouncement.title}
|
||||||
|
className="w-full rounded-lg mb-6"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Full Content */}
|
||||||
|
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 whitespace-pre-line">
|
||||||
|
{selectedAnnouncement.content}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Attachments */}
|
||||||
|
{selectedAnnouncement.attachments && selectedAnnouncement.attachments.length > 0 && (
|
||||||
|
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
|
||||||
|
<HiPaperClip className="w-5 h-5" />
|
||||||
|
Ekler ({selectedAnnouncement.attachments.length})
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{selectedAnnouncement.attachments.map((attachment, idx) => (
|
||||||
|
<a
|
||||||
|
key={idx}
|
||||||
|
href={attachment.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<HiPaperClip className="w-5 h-5 text-gray-400" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||||
|
{attachment.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{attachment.size}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-blue-600 dark:text-blue-400">
|
||||||
|
İndir
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Departments */}
|
||||||
|
{selectedAnnouncement.departments && selectedAnnouncement.departments.length > 0 && (
|
||||||
|
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-900 dark:text-white mb-3">
|
||||||
|
Hedef Departmanlar
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedAnnouncement.departments.map((dept, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="px-3 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm rounded-full"
|
||||||
|
>
|
||||||
|
{dept}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Expiry Date */}
|
||||||
|
{selectedAnnouncement.expiryDate && (
|
||||||
|
<div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<span className="font-medium">Son Geçerlilik Tarihi:</span>{' '}
|
||||||
|
{dayjs(selectedAnnouncement.expiryDate).format('DD MMMM YYYY')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="p-6 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedAnnouncement(null)}
|
||||||
|
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Kapat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IntranetDashboard
|
||||||
241
ui/src/components/intranet/IntranetSidebar.tsx
Normal file
241
ui/src/components/intranet/IntranetSidebar.tsx
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
import React, { useState, useMemo } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
HiHome,
|
||||||
|
HiCalendar,
|
||||||
|
HiFolder,
|
||||||
|
HiClipboardDocumentList,
|
||||||
|
HiChevronRight,
|
||||||
|
HiChatBubbleLeftRight,
|
||||||
|
HiCake,
|
||||||
|
HiAcademicCap,
|
||||||
|
HiKey,
|
||||||
|
HiBuildingOffice2,
|
||||||
|
HiClipboardDocumentCheck,
|
||||||
|
HiUserPlus,
|
||||||
|
} from 'react-icons/hi2'
|
||||||
|
import {
|
||||||
|
mockTasks,
|
||||||
|
mockEvents,
|
||||||
|
mockDocuments,
|
||||||
|
mockBirthdays,
|
||||||
|
mockTrainings,
|
||||||
|
mockReservations,
|
||||||
|
mockSurveys,
|
||||||
|
mockVisitors,
|
||||||
|
} from '../../mocks/mockIntranetData'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
|
interface MenuItem {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
icon: React.ElementType
|
||||||
|
path?: string
|
||||||
|
badge?: number
|
||||||
|
children?: MenuItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMenuItems = (badgeCounts: any): MenuItem[] => [
|
||||||
|
{
|
||||||
|
id: 'dashboard',
|
||||||
|
label: 'Ana Sayfa',
|
||||||
|
icon: HiHome,
|
||||||
|
path: '/intranet/dashboard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'social',
|
||||||
|
label: 'Sosyal Akış',
|
||||||
|
icon: HiChatBubbleLeftRight,
|
||||||
|
path: '/intranet/social',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'events',
|
||||||
|
label: 'Etkinlikler',
|
||||||
|
icon: HiCalendar,
|
||||||
|
path: '/intranet/events',
|
||||||
|
badge: badgeCounts.events || undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'documents',
|
||||||
|
label: 'Dokümanlar',
|
||||||
|
icon: HiFolder,
|
||||||
|
path: '/intranet/documents',
|
||||||
|
badge: badgeCounts.documents || undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tasks',
|
||||||
|
label: 'Görevler',
|
||||||
|
icon: HiClipboardDocumentList,
|
||||||
|
path: '/intranet/tasks',
|
||||||
|
badge: badgeCounts.tasks || undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'birthdays',
|
||||||
|
label: 'Doğum Günleri',
|
||||||
|
icon: HiCake,
|
||||||
|
path: '/intranet/birthdays',
|
||||||
|
badge: badgeCounts.birthdays || undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'training',
|
||||||
|
label: 'Eğitimler',
|
||||||
|
icon: HiAcademicCap,
|
||||||
|
path: '/intranet/training',
|
||||||
|
badge: badgeCounts.training || undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reservations',
|
||||||
|
label: 'Rezervasyonlar',
|
||||||
|
icon: HiKey,
|
||||||
|
path: '/intranet/reservations',
|
||||||
|
badge: badgeCounts.reservations || undefined,
|
||||||
|
},
|
||||||
|
{ id: 'hr-leave', label: 'İzin Yönetimi', icon: HiCalendar, path: '/intranet/hr/leave' },
|
||||||
|
{
|
||||||
|
id: 'hr-overtime',
|
||||||
|
label: 'Mesai Yönetimi',
|
||||||
|
icon: HiClipboardDocumentList,
|
||||||
|
path: '/intranet/hr/overtime',
|
||||||
|
},
|
||||||
|
{ id: 'hr-expense', label: 'Harcama Yönetimi', icon: HiFolder, path: '/intranet/hr/expense' },
|
||||||
|
{
|
||||||
|
id: 'cafeteria',
|
||||||
|
label: 'Kafeterya & Servis',
|
||||||
|
icon: HiBuildingOffice2,
|
||||||
|
path: '/intranet/cafeteria/shuttle',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'surveys',
|
||||||
|
label: 'Anketler',
|
||||||
|
icon: HiClipboardDocumentCheck,
|
||||||
|
path: '/intranet/surveys',
|
||||||
|
badge: badgeCounts.surveys || undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'visitors',
|
||||||
|
label: 'Ziyaretçi Yönetimi',
|
||||||
|
icon: HiUserPlus,
|
||||||
|
path: '/intranet/visitors',
|
||||||
|
badge: badgeCounts.visitors || undefined,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
interface IntranetSidebarProps {
|
||||||
|
activePath: string
|
||||||
|
onNavigate: (path: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const IntranetSidebar: React.FC<IntranetSidebarProps> = ({ activePath, onNavigate }) => {
|
||||||
|
const [expandedMenus, setExpandedMenus] = useState<string[]>(['hr'])
|
||||||
|
|
||||||
|
// Dinamik badge sayılarını hesapla
|
||||||
|
const badgeCounts = useMemo(() => {
|
||||||
|
const today = dayjs()
|
||||||
|
|
||||||
|
return {
|
||||||
|
tasks: mockTasks.filter((t) => t.status !== 'done').length,
|
||||||
|
events: mockEvents.filter(
|
||||||
|
(e) =>
|
||||||
|
e.isPublished && (dayjs(e.date).isAfter(today) || dayjs(e.date).isSame(today, 'day')),
|
||||||
|
).length,
|
||||||
|
documents: mockDocuments.length,
|
||||||
|
birthdays: mockBirthdays.filter((b) => {
|
||||||
|
const birthMonth = dayjs(b.date).month()
|
||||||
|
const currentMonth = today.month()
|
||||||
|
return birthMonth === currentMonth
|
||||||
|
}).length,
|
||||||
|
training: mockTrainings.filter((t) => t.status === 'upcoming').length,
|
||||||
|
reservations: mockReservations.filter(
|
||||||
|
(r) => r.status === 'approved' || r.status === 'pending',
|
||||||
|
).length,
|
||||||
|
surveys: mockSurveys.filter((s) => s.status === 'active').length,
|
||||||
|
visitors: mockVisitors.filter((v) => v.status === 'scheduled' || v.status === 'checked-in')
|
||||||
|
.length,
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const toggleMenu = (id: string) => {
|
||||||
|
setExpandedMenus((prev) =>
|
||||||
|
prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActive = (path?: string) => {
|
||||||
|
if (!path) return false
|
||||||
|
return activePath === path || activePath.startsWith(path + '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuItems = useMemo(() => getMenuItems(badgeCounts), [badgeCounts])
|
||||||
|
|
||||||
|
const renderMenuItem = (item: MenuItem, level: number = 0) => {
|
||||||
|
const hasChildren = item.children && item.children.length > 0
|
||||||
|
const isExpanded = expandedMenus.includes(item.id)
|
||||||
|
const active = isActive(item.path)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={item.id}>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (hasChildren) {
|
||||||
|
toggleMenu(item.id)
|
||||||
|
} else if (item.path) {
|
||||||
|
onNavigate(item.path)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`w-full flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors ${
|
||||||
|
active
|
||||||
|
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
||||||
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||||
|
} ${level > 0 ? 'ml-6' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<item.icon className="w-5 h-5" />
|
||||||
|
<span className="font-medium text-sm">{item.label}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{item.badge && item.badge > 0 && (
|
||||||
|
<span className="px-2 py-0.5 bg-red-500 text-white text-xs rounded-full">
|
||||||
|
{item.badge}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{hasChildren && (
|
||||||
|
<motion.div animate={{ rotate: isExpanded ? 90 : 0 }} transition={{ duration: 0.2 }}>
|
||||||
|
<HiChevronRight className="w-4 h-4" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{hasChildren && (
|
||||||
|
<AnimatePresence>
|
||||||
|
{isExpanded && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="mt-1 space-y-1">
|
||||||
|
{item.children!.map((child) => renderMenuItem(child, level + 1))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 h-screen sticky top-0 overflow-y-auto">
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 className="text-xl font-bold text-gray-900 dark:text-white">İntranet Portal</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="p-4 space-y-1">{menuItems.map((item) => renderMenuItem(item))}</nav>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IntranetSidebar
|
||||||
319
ui/src/components/intranet/Reservations/index.tsx
Normal file
319
ui/src/components/intranet/Reservations/index.tsx
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
HiKey,
|
||||||
|
HiCalendar,
|
||||||
|
HiTruck,
|
||||||
|
HiCog,
|
||||||
|
HiPlus,
|
||||||
|
HiXMark
|
||||||
|
} from 'react-icons/hi2'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockReservations, Reservation } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
const ReservationsModule: React.FC = () => {
|
||||||
|
const [selectedType, setSelectedType] = useState<'all' | 'room' | 'vehicle' | 'equipment'>('all')
|
||||||
|
const [showNewReservation, setShowNewReservation] = useState(false)
|
||||||
|
|
||||||
|
const filteredReservations = selectedType === 'all'
|
||||||
|
? mockReservations
|
||||||
|
: mockReservations.filter(r => r.type === selectedType)
|
||||||
|
|
||||||
|
const getTypeIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'room':
|
||||||
|
return <HiKey className="w-5 h-5" />
|
||||||
|
case 'vehicle':
|
||||||
|
return <HiTruck className="w-5 h-5" />
|
||||||
|
case 'equipment':
|
||||||
|
return <HiCog className="w-5 h-5" />
|
||||||
|
default:
|
||||||
|
return <HiKey className="w-5 h-5" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeLabel = (type: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
room: 'Toplantı Salonu',
|
||||||
|
vehicle: 'Araç',
|
||||||
|
equipment: 'Ekipman'
|
||||||
|
}
|
||||||
|
return labels[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
pending: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300',
|
||||||
|
approved: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||||
|
rejected: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300',
|
||||||
|
completed: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||||
|
}
|
||||||
|
return colors[status] || colors.pending
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusLabel = (status: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
pending: 'Bekliyor',
|
||||||
|
approved: 'Onaylandı',
|
||||||
|
rejected: 'Reddedildi',
|
||||||
|
completed: 'Tamamlandı'
|
||||||
|
}
|
||||||
|
return labels[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
🔑 Rezervasyonlar
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Oda, araç ve ekipman rezervasyonları
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewReservation(true)}
|
||||||
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<HiPlus className="w-5 h-5" />
|
||||||
|
Yeni Rezervasyon
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Type Filter */}
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{[
|
||||||
|
{ value: 'all' as const, label: 'Tümü', icon: HiCalendar },
|
||||||
|
{ value: 'room' as const, label: 'Toplantı Salonu', icon: HiKey },
|
||||||
|
{ value: 'vehicle' as const, label: 'Araç', icon: HiTruck },
|
||||||
|
{ value: 'equipment' as const, label: 'Ekipman', icon: HiCog }
|
||||||
|
].map((type) => (
|
||||||
|
<button
|
||||||
|
key={type.value}
|
||||||
|
onClick={() => setSelectedType(type.value)}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${
|
||||||
|
selectedType === type.value
|
||||||
|
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
||||||
|
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<type.icon className="w-5 h-5" />
|
||||||
|
{type.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reservations List */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{filteredReservations.map((reservation: Reservation, idx: number) => (
|
||||||
|
<motion.div
|
||||||
|
key={reservation.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: idx * 0.05 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900/30 rounded-lg flex items-center justify-center text-blue-600 dark:text-blue-400">
|
||||||
|
{getTypeIcon(reservation.type)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{reservation.resourceName}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{getTypeLabel(reservation.type)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(reservation.status)}`}>
|
||||||
|
{getStatusLabel(reservation.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Başlangıç</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{dayjs(reservation.startDate).format('DD MMM, HH:mm')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Bitiş</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{dayjs(reservation.endDate).format('DD MMM, HH:mm')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Rezerve Eden</p>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<img
|
||||||
|
src={reservation.bookedBy.avatar}
|
||||||
|
alt={reservation.bookedBy.fullName}
|
||||||
|
className="w-5 h-5 rounded-full"
|
||||||
|
/>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{reservation.bookedBy.fullName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{reservation.participants && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Katılımcı</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{reservation.participants} kişi
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Amaç: {reservation.purpose}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{reservation.notes && (
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3">
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
<strong>Not:</strong> {reservation.notes}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filteredReservations.length === 0 && (
|
||||||
|
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||||
|
<HiCalendar className="w-16 h-16 mx-auto mb-4 opacity-20" />
|
||||||
|
<p>Rezervasyon bulunamadı</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* New Reservation Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showNewReservation && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={() => setShowNewReservation(false)}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full"
|
||||||
|
>
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
Yeni Rezervasyon Oluştur
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewReservation(false)}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<HiXMark className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Rezervasyon Tipi
|
||||||
|
</label>
|
||||||
|
<select className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
||||||
|
<option>Toplantı Salonu</option>
|
||||||
|
<option>Araç</option>
|
||||||
|
<option>Ekipman</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Kaynak Seçin
|
||||||
|
</label>
|
||||||
|
<select className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
||||||
|
<option>Toplantı Salonu A</option>
|
||||||
|
<option>Toplantı Salonu B</option>
|
||||||
|
<option>Eğitim Salonu</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Başlangıç Tarihi
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Bitiş Tarihi
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Amaç
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Rezervasyon amacını yazın"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Notlar (Opsiyonel)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
placeholder="Ek notlarınızı yazın"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewReservation(false)}
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
İptal
|
||||||
|
</button>
|
||||||
|
<button className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
||||||
|
Rezervasyon Oluştur
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReservationsModule
|
||||||
288
ui/src/components/intranet/Surveys/index.tsx
Normal file
288
ui/src/components/intranet/Surveys/index.tsx
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { HiClipboardDocumentCheck, HiClock, HiCheckCircle, HiXMark } from 'react-icons/hi2'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockSurveys, Survey } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
const SurveysModule: React.FC = () => {
|
||||||
|
const [selectedStatus, setSelectedStatus] = useState<'all' | 'active' | 'draft' | 'closed'>('all')
|
||||||
|
const [showSurveyModal, setShowSurveyModal] = useState(false)
|
||||||
|
const [selectedSurvey, setSelectedSurvey] = useState<Survey | null>(null)
|
||||||
|
|
||||||
|
const filteredSurveys = selectedStatus === 'all'
|
||||||
|
? mockSurveys
|
||||||
|
: mockSurveys.filter(s => s.status === selectedStatus)
|
||||||
|
|
||||||
|
const handleTakeSurvey = (survey: Survey) => {
|
||||||
|
setSelectedSurvey(survey)
|
||||||
|
setShowSurveyModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmitSurvey = () => {
|
||||||
|
// Anket gönderildi
|
||||||
|
setShowSurveyModal(false)
|
||||||
|
setSelectedSurvey(null)
|
||||||
|
// Başarı mesajı gösterilebilir
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
draft: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300',
|
||||||
|
active: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||||
|
closed: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
}
|
||||||
|
return colors[status] || colors.draft
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
📊 Anketler & Memnuniyet Formları
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Geri bildirim ve değerlendirme anketleri
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter */}
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{[
|
||||||
|
{ value: 'all' as const, label: 'Tümü' },
|
||||||
|
{ value: 'active' as const, label: 'Aktif' },
|
||||||
|
{ value: 'draft' as const, label: 'Taslak' },
|
||||||
|
{ value: 'closed' as const, label: 'Kapalı' }
|
||||||
|
].map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.value}
|
||||||
|
onClick={() => setSelectedStatus(tab.value)}
|
||||||
|
className={`px-4 py-2 rounded-lg border-2 transition-all ${
|
||||||
|
selectedStatus === tab.value
|
||||||
|
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300'
|
||||||
|
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Surveys List */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{filteredSurveys.map((survey: Survey, idx: number) => (
|
||||||
|
<motion.div
|
||||||
|
key={survey.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white pr-4">
|
||||||
|
{survey.title}
|
||||||
|
</h3>
|
||||||
|
<span className={`px-3 py-1 rounded-full text-xs font-medium whitespace-nowrap ${getStatusColor(survey.status)}`}>
|
||||||
|
{survey.status === 'active' ? 'Aktif' : survey.status === 'draft' ? 'Taslak' : 'Kapalı'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{survey.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Soru Sayısı</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">{survey.totalQuestions} soru</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Yanıt</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">{survey.responses} kişi</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Son Tarih</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{dayjs(survey.deadline).format('DD MMM YYYY')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">Gizlilik</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{survey.isAnonymous ? '🔒 Anonim' : '👤 İsimli'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">Hedef Kitle</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{survey.targetAudience.map((audience, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-xs rounded"
|
||||||
|
>
|
||||||
|
{audience}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{survey.status === 'active' && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleTakeSurvey(survey)}
|
||||||
|
className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Anketi Doldur
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{survey.status === 'closed' && (
|
||||||
|
<div className="flex items-center justify-center gap-2 py-2 text-green-600 dark:text-green-400">
|
||||||
|
<HiCheckCircle className="w-5 h-5" />
|
||||||
|
<span className="text-sm font-medium">Tamamlandı</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredSurveys.length === 0 && (
|
||||||
|
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||||
|
<HiClipboardDocumentCheck className="w-16 h-16 mx-auto mb-4 opacity-20" />
|
||||||
|
<p>Anket bulunamadı</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Survey Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showSurveyModal && selectedSurvey && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={() => setShowSurveyModal(false)}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between sticky top-0 bg-white dark:bg-gray-800 z-10">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
{selectedSurvey.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
{selectedSurvey.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowSurveyModal(false)}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<HiXMark className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
handleSubmitSurvey()
|
||||||
|
}}
|
||||||
|
className="p-6 space-y-6"
|
||||||
|
>
|
||||||
|
{/* Örnek Anket Soruları */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
1. Genel memnuniyet düzeyiniz nedir? *
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{[1, 2, 3, 4, 5].map((rating) => (
|
||||||
|
<label key={rating} className="flex items-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<input type="radio" name="rating" value={rating} required />
|
||||||
|
<span className="text-sm text-gray-900 dark:text-white">{rating}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
2. Hangi departmanda çalışıyorsunuz? *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="">Seçiniz</option>
|
||||||
|
<option value="it">Bilgi Teknolojileri</option>
|
||||||
|
<option value="hr">İnsan Kaynakları</option>
|
||||||
|
<option value="finance">Finans</option>
|
||||||
|
<option value="sales">Satış</option>
|
||||||
|
<option value="marketing">Pazarlama</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
3. Görüş ve önerileriniz
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
placeholder="Yorumlarınızı buraya yazabilirsiniz..."
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!selectedSurvey.isAnonymous && (
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3">
|
||||||
|
<p className="text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
ℹ️ Bu anket isim belirtilerek doldurulmaktadır. Yanıtlarınız kaydedilecektir.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedSurvey.isAnonymous && (
|
||||||
|
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg p-3">
|
||||||
|
<p className="text-sm text-green-700 dark:text-green-300">
|
||||||
|
✅ Bu anket anonimdir. Kimlik bilgileriniz kaydedilmeyecektir.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowSurveyModal(false)}
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
İptal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Anketi Gönder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SurveysModule
|
||||||
655
ui/src/components/intranet/Tasks/index.tsx
Normal file
655
ui/src/components/intranet/Tasks/index.tsx
Normal file
|
|
@ -0,0 +1,655 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
DragEndEvent,
|
||||||
|
DragOverlay,
|
||||||
|
DragStartEvent,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
closestCorners,
|
||||||
|
useDroppable
|
||||||
|
} from '@dnd-kit/core'
|
||||||
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
|
||||||
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
|
import {
|
||||||
|
HiPlus,
|
||||||
|
HiXMark,
|
||||||
|
HiClock,
|
||||||
|
HiChatBubbleLeftRight,
|
||||||
|
HiPaperClip,
|
||||||
|
HiTrash
|
||||||
|
} from 'react-icons/hi2'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import 'dayjs/locale/tr'
|
||||||
|
import { mockTasks, Task } from '../../../mocks/mockIntranetData'
|
||||||
|
import { Badge } from '@/components/ui'
|
||||||
|
|
||||||
|
dayjs.locale('tr')
|
||||||
|
|
||||||
|
type TaskStatus = 'todo' | 'in-progress' | 'review' | 'done'
|
||||||
|
|
||||||
|
// Droppable Column Component
|
||||||
|
interface DroppableColumnProps {
|
||||||
|
id: TaskStatus
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const DroppableColumn: React.FC<DroppableColumnProps> = ({ id, children }) => {
|
||||||
|
const { setNodeRef } = useDroppable({ id })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={setNodeRef} >
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sortable Task Card Component
|
||||||
|
interface SortableTaskCardProps {
|
||||||
|
task: Task
|
||||||
|
onTaskClick: (task: Task) => void
|
||||||
|
getPriorityColor: (priority: string) => string
|
||||||
|
getPriorityLabel: (priority: string) => string
|
||||||
|
isOverdue: (date: Date | string) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableTaskCard: React.FC<SortableTaskCardProps> = ({
|
||||||
|
task,
|
||||||
|
onTaskClick,
|
||||||
|
getPriorityColor,
|
||||||
|
getPriorityLabel,
|
||||||
|
isOverdue
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging
|
||||||
|
} = useSortable({ id: task.id })
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.5 : 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const overdue = isOverdue(task.dueDate) && task.status !== 'done'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
className={`bg-white dark:bg-gray-800 rounded-lg p-3 sm:p-4 border-2 cursor-move hover:shadow-lg transition-all ${
|
||||||
|
overdue
|
||||||
|
? 'border-red-300 dark:border-red-700'
|
||||||
|
: 'border-gray-200 dark:border-gray-700'
|
||||||
|
} ${isDragging ? 'shadow-2xl ring-4 ring-blue-500/50' : ''}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (!(e.target as HTMLElement).closest('[data-no-click]')) {
|
||||||
|
onTaskClick(task)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2 sm:mb-3">
|
||||||
|
<span className={`px-2 py-1 text-xs font-medium rounded border ${getPriorityColor(task.priority)}`}>
|
||||||
|
{getPriorityLabel(task.priority)}
|
||||||
|
</span>
|
||||||
|
{overdue && (
|
||||||
|
<span className="text-xs text-red-600 dark:text-red-400 font-medium">
|
||||||
|
⚠️ Gecikmiş
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
{task.title}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2">
|
||||||
|
{task.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<span className="inline-flex items-center px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded text-xs">
|
||||||
|
📁 {task.project}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{task.labels.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1 mb-3">
|
||||||
|
{task.labels.map((label, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<HiClock className="w-4 h-4" />
|
||||||
|
{dayjs(task.dueDate).format('DD MMM')}
|
||||||
|
</div>
|
||||||
|
{task.comments > 0 && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<HiChatBubbleLeftRight className="w-4 h-4" />
|
||||||
|
{task.comments}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{task.attachments && task.attachments.length > 0 && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<HiPaperClip className="w-4 h-4" />
|
||||||
|
{task.attachments.length}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex -space-x-2">
|
||||||
|
{task.assignedTo.slice(0, 3).map((assignee, idx) => (
|
||||||
|
<img
|
||||||
|
key={idx}
|
||||||
|
src={assignee.avatar}
|
||||||
|
alt={assignee.fullName}
|
||||||
|
className="w-6 h-6 rounded-full border-2 border-white dark:border-gray-800"
|
||||||
|
title={assignee.fullName}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{task.assignedTo.length > 3 && (
|
||||||
|
<div className="w-6 h-6 rounded-full border-2 border-white dark:border-gray-800 bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
+{task.assignedTo.length - 3}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TasksModule: React.FC = () => {
|
||||||
|
const [tasks, setTasks] = useState<Task[]>(mockTasks)
|
||||||
|
const [selectedTask, setSelectedTask] = useState<Task | null>(null)
|
||||||
|
const [activeId, setActiveId] = useState<string | null>(null)
|
||||||
|
const [showNewTaskModal, setShowNewTaskModal] = useState(false)
|
||||||
|
const [newTaskColumn, setNewTaskColumn] = useState<TaskStatus>('todo')
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 8,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const columns: { id: TaskStatus; title: string; icon: string; color: string }[] = [
|
||||||
|
{ id: 'todo', title: 'Yapılacak', icon: '📋', color: 'gray' },
|
||||||
|
{ id: 'in-progress', title: 'Devam Ediyor', icon: '⚙️', color: 'blue' },
|
||||||
|
{ id: 'review', title: 'İncelemede', icon: '👀', color: 'yellow' },
|
||||||
|
{ id: 'done', title: 'Tamamlandı', icon: '✅', color: 'green' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleDragStart = (event: DragStartEvent) => {
|
||||||
|
setActiveId(event.active.id as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
|
const { active, over } = event
|
||||||
|
setActiveId(null)
|
||||||
|
|
||||||
|
if (!over) return
|
||||||
|
|
||||||
|
const taskId = active.id as string
|
||||||
|
|
||||||
|
// over.id could be either a column id or a task id
|
||||||
|
// If it's a column id (from DroppableColumn), use it directly
|
||||||
|
// If it's a task id, find that task's column
|
||||||
|
let newStatus: TaskStatus
|
||||||
|
|
||||||
|
const overColumn = columns.find(col => col.id === over.id)
|
||||||
|
if (overColumn) {
|
||||||
|
newStatus = overColumn.id
|
||||||
|
} else {
|
||||||
|
// over.id is a task, find its column
|
||||||
|
const overTask = tasks.find(t => t.id === over.id)
|
||||||
|
if (!overTask) return
|
||||||
|
newStatus = overTask.status
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update task status
|
||||||
|
setTasks(prevTasks =>
|
||||||
|
prevTasks.map(task =>
|
||||||
|
task.id === taskId ? { ...task, status: newStatus } : task
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (selectedTask?.id === taskId) {
|
||||||
|
setSelectedTask(prev => prev ? { ...prev, status: newStatus } : null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStatusChange = (taskId: string, newStatus: TaskStatus) => {
|
||||||
|
setTasks(prevTasks =>
|
||||||
|
prevTasks.map(task =>
|
||||||
|
task.id === taskId ? { ...task, status: newStatus } : task
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (selectedTask?.id === taskId) {
|
||||||
|
setSelectedTask(prev => prev ? { ...prev, status: newStatus } : null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddTask = (status: TaskStatus) => {
|
||||||
|
setNewTaskColumn(status)
|
||||||
|
setShowNewTaskModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreateTask = (title: string, description: string) => {
|
||||||
|
const newTask: Task = {
|
||||||
|
id: `task-${Date.now()}`,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
project: 'Genel',
|
||||||
|
assignedTo: [mockTasks[0].assignedTo[0]], // Default assignee
|
||||||
|
assignedBy: mockTasks[0].assignedBy,
|
||||||
|
priority: 'medium',
|
||||||
|
status: newTaskColumn,
|
||||||
|
dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now
|
||||||
|
createdAt: new Date(),
|
||||||
|
labels: [],
|
||||||
|
comments: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
setTasks(prev => [...prev, newTask])
|
||||||
|
setShowNewTaskModal(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteTask = (taskId: string) => {
|
||||||
|
if (window.confirm('Bu görevi silmek istediğinizden emin misiniz?')) {
|
||||||
|
setTasks(prevTasks => prevTasks.filter(task => task.id !== taskId))
|
||||||
|
setSelectedTask(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTasksByStatus = (status: TaskStatus) => {
|
||||||
|
return tasks.filter((task: Task) => task.status === status)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPriorityColor = (priority: string) => {
|
||||||
|
const colors = {
|
||||||
|
low: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border-gray-200 dark:border-gray-600',
|
||||||
|
medium: 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-300 border-blue-200 dark:border-blue-700',
|
||||||
|
high: 'bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-300 border-orange-200 dark:border-orange-700',
|
||||||
|
urgent: 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-300 border-red-200 dark:border-red-700'
|
||||||
|
}
|
||||||
|
return colors[priority as keyof typeof colors]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPriorityLabel = (priority: string) => {
|
||||||
|
const labels = {
|
||||||
|
low: 'Düşük',
|
||||||
|
medium: 'Orta',
|
||||||
|
high: 'Yüksek',
|
||||||
|
urgent: '🔥 Acil'
|
||||||
|
}
|
||||||
|
return labels[priority as keyof typeof labels]
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOverdue = (dueDate: Date | string) => {
|
||||||
|
return dayjs(dueDate).isBefore(dayjs(), 'day')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCorners}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-3 sm:p-4 md:p-6">
|
||||||
|
<div className="max-w-[1600px] mx-auto space-y-4 md:space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Görev & Proje Yönetimi
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm sm:text-base text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Görevleri Kanban board ile yönetin
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center gap-2 text-xs sm:text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<span className="font-medium">Toplam:</span>
|
||||||
|
<span>{tasks.length} görev</span>
|
||||||
|
</div>
|
||||||
|
<button className="px-3 sm:px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors text-sm sm:text-base">
|
||||||
|
<HiPlus className="w-4 h-4 sm:w-5 sm:h-5" />
|
||||||
|
<span className="hidden sm:inline">Yeni Görev</span>
|
||||||
|
<span className="sm:hidden">Yeni</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Kanban Board */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-4 overflow-x-auto pb-4">
|
||||||
|
|
||||||
|
<div className="kanban-container sm:contents">
|
||||||
|
{columns.map(column => {
|
||||||
|
const columnTasks = getTasksByStatus(column.id)
|
||||||
|
return (
|
||||||
|
<DroppableColumn key={column.id} id={column.id}>
|
||||||
|
<SortableContext
|
||||||
|
id={column.id}
|
||||||
|
items={columnTasks.map(t => t.id)}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="kanban-column flex flex-col gap-2 sm:gap-3 bg-gray-100 dark:bg-gray-800/50 rounded-lg p-3 sm:p-4 min-h-[400px] sm:min-h-[500px] lg:min-h-[600px]"
|
||||||
|
data-status={column.id}
|
||||||
|
>
|
||||||
|
{/* Column Header */}
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-lg sm:text-xl">{column.icon}</span>
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white text-sm sm:text-base">
|
||||||
|
{column.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<Badge content={columnTasks.length}></Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tasks */}
|
||||||
|
<div className="space-y-3 flex-1">
|
||||||
|
{columnTasks.map(task => (
|
||||||
|
<SortableTaskCard
|
||||||
|
key={task.id}
|
||||||
|
task={task}
|
||||||
|
onTaskClick={setSelectedTask}
|
||||||
|
getPriorityColor={getPriorityColor}
|
||||||
|
getPriorityLabel={getPriorityLabel}
|
||||||
|
isOverdue={isOverdue}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Add Task Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => handleAddTask(column.id)}
|
||||||
|
className="w-full p-3 sm:p-4 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-blue-500 dark:hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/10 transition-colors text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400"
|
||||||
|
>
|
||||||
|
<HiPlus className="w-4 h-4 sm:w-5 sm:h-5 mx-auto" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
</DroppableColumn>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* DragOverlay */}
|
||||||
|
<DragOverlay>
|
||||||
|
{activeId ? (
|
||||||
|
<div className="opacity-50">
|
||||||
|
{(() => {
|
||||||
|
const task = tasks.find(t => t.id === activeId)
|
||||||
|
if (!task) return null
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 border-2 border-blue-500 shadow-2xl">
|
||||||
|
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{task.title}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</DragOverlay>
|
||||||
|
|
||||||
|
{/* New Task Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showNewTaskModal && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={() => setShowNewTaskModal(false)}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="p-4 sm:p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||||
|
<h2 className="text-lg sm:text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
Yeni Görev Oluştur
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewTaskModal(false)}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<HiXMark className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const formData = new FormData(e.currentTarget)
|
||||||
|
const title = formData.get('title') as string
|
||||||
|
const description = formData.get('description') as string
|
||||||
|
if (title && description) {
|
||||||
|
handleCreateTask(title, description)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-4 sm:p-6 space-y-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Görev Başlığı *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
required
|
||||||
|
placeholder="Görev başlığını yazın"
|
||||||
|
className="w-full px-3 sm:px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 text-sm sm:text-base"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Açıklama *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
name="description"
|
||||||
|
required
|
||||||
|
rows={4}
|
||||||
|
placeholder="Görev detaylarını yazın"
|
||||||
|
className="w-full px-3 sm:px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 text-sm sm:text-base"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3">
|
||||||
|
<p className="text-xs sm:text-sm text-blue-700 dark:text-blue-300">
|
||||||
|
📋 Görev <strong>{columns.find(c => c.id === newTaskColumn)?.title}</strong> kolonuna eklenecek
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 sm:gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowNewTaskModal(false)}
|
||||||
|
className="flex-1 px-3 sm:px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm sm:text-base"
|
||||||
|
>
|
||||||
|
İptal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="flex-1 px-3 sm:px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors text-sm sm:text-base"
|
||||||
|
>
|
||||||
|
Oluştur
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Task Detail Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedTask && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={() => setSelectedTask(null)}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-3xl w-full max-h-[90vh] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div className="p-4 sm:p-6 border-b border-gray-200 dark:border-gray-700 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
|
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||||
|
<span className={`px-2 sm:px-3 py-1 sm:py-1.5 text-xs sm:text-sm font-medium rounded border ${getPriorityColor(selectedTask.priority)}`}>
|
||||||
|
{getPriorityLabel(selectedTask.priority)}
|
||||||
|
</span>
|
||||||
|
<span className="px-2 sm:px-3 py-1 sm:py-1.5 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 text-xs sm:text-sm font-medium rounded">
|
||||||
|
📁 {selectedTask.project}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedTask(null)}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<HiXMark className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 sm:p-6 space-y-4 sm:space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
{selectedTask.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm sm:text-base text-gray-700 dark:text-gray-300">
|
||||||
|
{selectedTask.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
|
||||||
|
Durum
|
||||||
|
</p>
|
||||||
|
<select
|
||||||
|
value={selectedTask.status}
|
||||||
|
onChange={(e) => handleStatusChange(selectedTask.id, e.target.value as TaskStatus)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="todo">📋 Yapılacak</option>
|
||||||
|
<option value="in-progress">⚙️ Devam Ediyor</option>
|
||||||
|
<option value="review">👀 İncelemede</option>
|
||||||
|
<option value="done">✅ Tamamlandı</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
|
||||||
|
Son Tarih
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg">
|
||||||
|
<HiClock className="w-5 h-5 text-gray-400" />
|
||||||
|
<span className="text-gray-900 dark:text-white">
|
||||||
|
{dayjs(selectedTask.dueDate).format('DD MMMM YYYY')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2 sm:mb-3">
|
||||||
|
Atananlar
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2 sm:gap-3">
|
||||||
|
{selectedTask.assignedTo.map((user, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex items-center gap-2 px-2 sm:px-3 py-1.5 sm:py-2 bg-gray-100 dark:bg-gray-700 rounded-lg"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={user.avatar}
|
||||||
|
alt={user.fullName}
|
||||||
|
className="w-6 h-6 sm:w-8 sm:h-8 rounded-full"
|
||||||
|
/>
|
||||||
|
<span className="text-xs sm:text-sm text-gray-900 dark:text-white">
|
||||||
|
{user.fullName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedTask.labels.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
|
||||||
|
Etiketler
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedTask.labels.map((label, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="px-2 sm:px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs sm:text-sm rounded"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="pt-6 border-t border-gray-200 dark:border-gray-700 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Oluşturan: {selectedTask.assignedBy.fullName} • {dayjs(selectedTask.createdAt).format('DD MMMM YYYY')}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteTask(selectedTask.id)}
|
||||||
|
className="px-3 sm:px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center gap-2 transition-colors text-sm"
|
||||||
|
>
|
||||||
|
<HiTrash className="w-4 h-4" />
|
||||||
|
Görevi Sil
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DndContext>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TasksModule
|
||||||
354
ui/src/components/intranet/Training/index.tsx
Normal file
354
ui/src/components/intranet/Training/index.tsx
Normal file
|
|
@ -0,0 +1,354 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
HiAcademicCap,
|
||||||
|
HiClock,
|
||||||
|
HiUsers,
|
||||||
|
HiMapPin,
|
||||||
|
HiXMark,
|
||||||
|
HiCheckBadge,
|
||||||
|
HiCalendar
|
||||||
|
} from 'react-icons/hi2'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockTrainings, mockCertificates, Training } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
const TrainingModule: React.FC = () => {
|
||||||
|
const [selectedTraining, setSelectedTraining] = useState<Training | null>(null)
|
||||||
|
const [selectedTab, setSelectedTab] = useState<'all' | 'upcoming' | 'ongoing' | 'completed'>('all')
|
||||||
|
const [trainings, setTrainings] = useState<Training[]>(mockTrainings)
|
||||||
|
const [showEnrollSuccess, setShowEnrollSuccess] = useState(false)
|
||||||
|
|
||||||
|
const filteredTrainings = selectedTab === 'all'
|
||||||
|
? trainings
|
||||||
|
: trainings.filter(t => t.status === selectedTab)
|
||||||
|
|
||||||
|
const handleEnroll = (trainingId: string) => {
|
||||||
|
setTrainings(prev => prev.map(t =>
|
||||||
|
t.id === trainingId && t.enrolled < t.maxParticipants
|
||||||
|
? { ...t, enrolled: t.enrolled + 1 }
|
||||||
|
: t
|
||||||
|
))
|
||||||
|
|
||||||
|
// Seçili eğitimi de güncelle
|
||||||
|
if (selectedTraining?.id === trainingId && selectedTraining.enrolled < selectedTraining.maxParticipants) {
|
||||||
|
setSelectedTraining(prev => prev ? { ...prev, enrolled: prev.enrolled + 1 } : null)
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowEnrollSuccess(true)
|
||||||
|
setTimeout(() => setShowEnrollSuccess(false), 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCategoryColor = (category: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
technical: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
||||||
|
'soft-skills': 'bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300',
|
||||||
|
management: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||||
|
compliance: 'bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300',
|
||||||
|
other: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||||
|
}
|
||||||
|
return colors[category] || colors.other
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string) => {
|
||||||
|
const badges: Record<string, { bg: string; text: string }> = {
|
||||||
|
upcoming: { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-700 dark:text-blue-300' },
|
||||||
|
ongoing: { bg: 'bg-green-100 dark:bg-green-900/30', text: 'text-green-700 dark:text-green-300' },
|
||||||
|
completed: { bg: 'bg-gray-100 dark:bg-gray-700', text: 'text-gray-700 dark:text-gray-300' }
|
||||||
|
}
|
||||||
|
return badges[status] || badges.upcoming
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
🎓 Eğitimler & Sertifikalar
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Kişisel gelişim ve eğitim kayıtları
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||||
|
{[
|
||||||
|
{ label: 'Tümü', value: mockTrainings.length, tab: 'all' as const },
|
||||||
|
{ label: 'Yaklaşan', value: mockTrainings.filter(t => t.status === 'upcoming').length, tab: 'upcoming' as const },
|
||||||
|
{ label: 'Devam Eden', value: mockTrainings.filter(t => t.status === 'ongoing').length, tab: 'ongoing' as const },
|
||||||
|
{ label: 'Tamamlanan', value: mockTrainings.filter(t => t.status === 'completed').length, tab: 'completed' as const }
|
||||||
|
].map((stat, idx) => (
|
||||||
|
<motion.button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => setSelectedTab(stat.tab)}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
className={`p-4 rounded-lg border-2 transition-all ${
|
||||||
|
selectedTab === stat.tab
|
||||||
|
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||||
|
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-blue-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-3xl font-bold text-gray-900 dark:text-white">{stat.value}</div>
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">{stat.label}</div>
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Eğitimler */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
{filteredTrainings.map((training, idx) => {
|
||||||
|
const badge = getStatusBadge(training.status)
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={training.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
onClick={() => setSelectedTraining(training)}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg overflow-hidden border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all cursor-pointer"
|
||||||
|
>
|
||||||
|
{training.thumbnail && (
|
||||||
|
<img
|
||||||
|
src={training.thumbnail}
|
||||||
|
alt={training.title}
|
||||||
|
className="w-full h-48 object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getCategoryColor(training.category)}`}>
|
||||||
|
{training.category}
|
||||||
|
</span>
|
||||||
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${badge.bg} ${badge.text}`}>
|
||||||
|
{training.status === 'upcoming' ? 'Yaklaşan' : training.status === 'ongoing' ? 'Devam Ediyor' : 'Tamamlandı'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
{training.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-2">
|
||||||
|
{training.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HiAcademicCap className="w-4 h-4" />
|
||||||
|
<span>{training.instructor}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HiCalendar className="w-4 h-4" />
|
||||||
|
<span>{dayjs(training.startDate).format('DD MMM YYYY')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HiClock className="w-4 h-4" />
|
||||||
|
<span>{training.duration} saat</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<HiUsers className="w-4 h-4" />
|
||||||
|
<span>{training.enrolled} / {training.maxParticipants} katılımcı</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{training.enrolled < training.maxParticipants && training.status === 'upcoming' && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleEnroll(training.id)}
|
||||||
|
className="mt-4 w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Kayıt Ol
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sertifikalar */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||||
|
<HiCheckBadge className="w-6 h-6 text-blue-500" />
|
||||||
|
Sertifikalarım
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{mockCertificates.map((cert, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={cert.id}
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
className="p-4 rounded-lg border-2 border-blue-200 dark:border-blue-800 bg-blue-50 dark:bg-blue-900/20"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<HiCheckBadge className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
||||||
|
{cert.score && (
|
||||||
|
<span className="px-2 py-1 bg-blue-600 text-white rounded text-xs font-bold">
|
||||||
|
{cert.score}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
{cert.trainingTitle}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{dayjs(cert.issueDate).format('DD MMMM YYYY')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{cert.expiryDate && (
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
||||||
|
Geçerlilik: {dayjs(cert.expiryDate).format('DD/MM/YYYY')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button className="mt-3 w-full px-3 py-1 text-sm text-blue-600 dark:text-blue-400 border border-blue-600 dark:border-blue-400 rounded hover:bg-blue-600 hover:text-white dark:hover:bg-blue-600 transition-colors">
|
||||||
|
Görüntüle
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Training Detail Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedTraining && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={() => setSelectedTraining(null)}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div className="sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 p-6 flex items-center justify-between z-10">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
Eğitim Detayları
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedTraining(null)}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<HiXMark className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{selectedTraining.thumbnail && (
|
||||||
|
<img
|
||||||
|
src={selectedTraining.thumbnail}
|
||||||
|
alt={selectedTraining.title}
|
||||||
|
className="w-full h-64 object-cover rounded-lg"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
{selectedTraining.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300">
|
||||||
|
{selectedTraining.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Eğitmen</p>
|
||||||
|
<p className="font-medium text-gray-900 dark:text-white">{selectedTraining.instructor}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Kategori</p>
|
||||||
|
<span className={`inline-block px-3 py-1 rounded-full text-sm ${getCategoryColor(selectedTraining.category)}`}>
|
||||||
|
{selectedTraining.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Tarih</p>
|
||||||
|
<p className="font-medium text-gray-900 dark:text-white">
|
||||||
|
{dayjs(selectedTraining.startDate).format('DD MMM')} - {dayjs(selectedTraining.endDate).format('DD MMM YYYY')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Süre</p>
|
||||||
|
<p className="font-medium text-gray-900 dark:text-white">{selectedTraining.duration} saat</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Tip</p>
|
||||||
|
<p className="font-medium text-gray-900 dark:text-white capitalize">{selectedTraining.type}</p>
|
||||||
|
</div>
|
||||||
|
{selectedTraining.location && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">Konum</p>
|
||||||
|
<p className="font-medium text-gray-900 dark:text-white">{selectedTraining.location}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">Doluluk Oranı</p>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-3">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-3 rounded-full transition-all"
|
||||||
|
style={{ width: `${(selectedTraining.enrolled / selectedTraining.maxParticipants) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{selectedTraining.enrolled} / {selectedTraining.maxParticipants}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedTraining.enrolled < selectedTraining.maxParticipants && selectedTraining.status === 'upcoming' && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleEnroll(selectedTraining.id)}
|
||||||
|
className="w-full px-4 py-3 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors font-medium"
|
||||||
|
>
|
||||||
|
Eğitime Kayıt Ol
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Success Notification */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showEnrollSuccess && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 50 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 50 }}
|
||||||
|
className="fixed bottom-6 right-6 bg-green-600 text-white px-6 py-4 rounded-lg shadow-xl flex items-center gap-3 z-50"
|
||||||
|
>
|
||||||
|
<HiCheckBadge className="w-6 h-6" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Kayıt Başarılı!</p>
|
||||||
|
<p className="text-sm text-green-100">Eğitime kayıt oldunuz.</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TrainingModule
|
||||||
380
ui/src/components/intranet/Visitors/index.tsx
Normal file
380
ui/src/components/intranet/Visitors/index.tsx
Normal file
|
|
@ -0,0 +1,380 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { HiUserPlus, HiXMark, HiCheckCircle, HiClock, HiPhone, HiEnvelope } from 'react-icons/hi2'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { mockVisitors, Visitor } from '../../../mocks/mockIntranetData'
|
||||||
|
|
||||||
|
const VisitorsModule: React.FC = () => {
|
||||||
|
const [selectedStatus, setSelectedStatus] = useState<'all' | 'scheduled' | 'checked-in' | 'checked-out' | 'cancelled'>('all')
|
||||||
|
const [showNewVisitor, setShowNewVisitor] = useState(false)
|
||||||
|
const [visitors, setVisitors] = useState<Visitor[]>(mockVisitors)
|
||||||
|
|
||||||
|
const filteredVisitors = selectedStatus === 'all'
|
||||||
|
? visitors
|
||||||
|
: visitors.filter(v => v.status === selectedStatus)
|
||||||
|
|
||||||
|
const handleCheckIn = (visitorId: string) => {
|
||||||
|
setVisitors(prev => prev.map(v =>
|
||||||
|
v.id === visitorId
|
||||||
|
? { ...v, status: 'checked-in', checkIn: new Date() }
|
||||||
|
: v
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCheckOut = (visitorId: string) => {
|
||||||
|
setVisitors(prev => prev.map(v =>
|
||||||
|
v.id === visitorId
|
||||||
|
? { ...v, status: 'checked-out', checkOut: new Date() }
|
||||||
|
: v
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = (visitorId: string) => {
|
||||||
|
setVisitors(prev => prev.map(v =>
|
||||||
|
v.id === visitorId
|
||||||
|
? { ...v, status: 'cancelled' }
|
||||||
|
: v
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
scheduled: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
||||||
|
'checked-in': 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||||
|
'checked-out': 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300',
|
||||||
|
cancelled: 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300'
|
||||||
|
}
|
||||||
|
return colors[status] || colors.scheduled
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusLabel = (status: string) => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
scheduled: '📅 Planlandı',
|
||||||
|
'checked-in': '✅ Giriş Yaptı',
|
||||||
|
'checked-out': '🚪 Çıkış Yaptı',
|
||||||
|
cancelled: '❌ İptal Edildi'
|
||||||
|
}
|
||||||
|
return labels[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
👥 Ziyaretçi Yönetimi
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Ziyaretçi kayıtları ve giriş-çıkış takibi
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewVisitor(true)}
|
||||||
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors"
|
||||||
|
>
|
||||||
|
<HiUserPlus className="w-5 h-5" />
|
||||||
|
Yeni Ziyaretçi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
{[
|
||||||
|
{ label: 'Tümü', value: mockVisitors.length, status: 'all' as const },
|
||||||
|
{ label: 'Planlandı', value: mockVisitors.filter(v => v.status === 'scheduled').length, status: 'scheduled' as const },
|
||||||
|
{ label: 'İçeride', value: mockVisitors.filter(v => v.status === 'checked-in').length, status: 'checked-in' as const },
|
||||||
|
{ label: 'Çıkış Yaptı', value: mockVisitors.filter(v => v.status === 'checked-out').length, status: 'checked-out' as const }
|
||||||
|
].map((stat, idx) => (
|
||||||
|
<motion.button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => setSelectedStatus(stat.status)}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: idx * 0.1 }}
|
||||||
|
className={`p-4 rounded-lg border-2 transition-all ${
|
||||||
|
selectedStatus === stat.status
|
||||||
|
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||||
|
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-blue-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-3xl font-bold text-gray-900 dark:text-white">{stat.value}</div>
|
||||||
|
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1">{stat.label}</div>
|
||||||
|
</motion.button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Visitors List */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{filteredVisitors.map((visitor: Visitor, idx: number) => (
|
||||||
|
<motion.div
|
||||||
|
key={visitor.id}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: idx * 0.05 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-all"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
{visitor.photo && (
|
||||||
|
<img
|
||||||
|
src={visitor.photo}
|
||||||
|
alt={visitor.fullName}
|
||||||
|
className="w-16 h-16 rounded-full border-4 border-gray-100 dark:border-gray-700"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
{visitor.fullName}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">{visitor.company}</p>
|
||||||
|
</div>
|
||||||
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(visitor.status)}`}>
|
||||||
|
{getStatusLabel(visitor.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Ziyaret Tarihi</p>
|
||||||
|
<div className="flex items-center gap-1 text-sm text-gray-900 dark:text-white">
|
||||||
|
<HiClock className="w-4 h-4" />
|
||||||
|
{dayjs(visitor.visitDate).format('DD MMM, HH:mm')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{visitor.checkIn && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Giriş</p>
|
||||||
|
<p className="text-sm font-medium text-green-600 dark:text-green-400">
|
||||||
|
{dayjs(visitor.checkIn).format('HH:mm')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{visitor.checkOut && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Çıkış</p>
|
||||||
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
{dayjs(visitor.checkOut).format('HH:mm')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{visitor.badgeNumber && (
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Rozet No</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{visitor.badgeNumber}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">İletişim</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<HiEnvelope className="w-4 h-4" />
|
||||||
|
{visitor.email}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<HiPhone className="w-4 h-4" />
|
||||||
|
{visitor.phone}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Ev Sahibi</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<img
|
||||||
|
src={visitor.host.avatar}
|
||||||
|
alt={visitor.host.fullName}
|
||||||
|
className="w-8 h-8 rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
{visitor.host.fullName}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{visitor.host.department}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3">
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mb-1">Ziyaret Amacı</p>
|
||||||
|
<p className="text-sm text-gray-900 dark:text-white">{visitor.purpose}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{visitor.status === 'scheduled' && (
|
||||||
|
<div className="flex gap-2 mt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => handleCheckIn(visitor.id)}
|
||||||
|
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
<HiCheckCircle className="w-4 h-4 inline mr-1" />
|
||||||
|
Giriş Yaptır
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCancel(visitor.id)}
|
||||||
|
className="px-4 py-2 border border-red-600 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
İptal Et
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{visitor.status === 'checked-in' && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleCheckOut(visitor.id)}
|
||||||
|
className="mt-4 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
Çıkış Yaptır
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* New Visitor Modal */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showNewVisitor && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 z-40"
|
||||||
|
onClick={() => setShowNewVisitor(false)}
|
||||||
|
/>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
Yeni Ziyaretçi Kaydı
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewVisitor(false)}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<HiXMark className="w-5 h-5 text-gray-500" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Ad Soyad *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Ziyaretçi adı"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Şirket *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Şirket adı"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
E-posta *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="email@example.com"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Telefon *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
placeholder="+90 5XX XXX XX XX"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Ziyaret Tarihi ve Saati *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Ev Sahibi (Personel) *
|
||||||
|
</label>
|
||||||
|
<select className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
||||||
|
<option>Ahmet Yılmaz - Yazılım Geliştirme</option>
|
||||||
|
<option>Zeynep Kaya - İnsan Kaynakları</option>
|
||||||
|
<option>Mehmet Demir - Yazılım Geliştirme</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Ziyaret Amacı *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
placeholder="Ziyaret amacını açıklayın"
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewVisitor(false)}
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
İptal
|
||||||
|
</button>
|
||||||
|
<button className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
||||||
|
Ziyaretçi Kaydet
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VisitorsModule
|
||||||
|
|
@ -19,6 +19,7 @@ export const mockEmployees: HrEmployee[] = [
|
||||||
email: "ali.ozturk@company.com",
|
email: "ali.ozturk@company.com",
|
||||||
phone: "+90 212 555 0100",
|
phone: "+90 212 555 0100",
|
||||||
personalPhone: "+90 532 555 0101",
|
personalPhone: "+90 532 555 0101",
|
||||||
|
avatar: "https://i.pravatar.cc/150?img=12",
|
||||||
nationalId: "12345678901",
|
nationalId: "12345678901",
|
||||||
birthDate: new Date("1988-02-14"),
|
birthDate: new Date("1988-02-14"),
|
||||||
gender: GenderEnum.Male,
|
gender: GenderEnum.Male,
|
||||||
|
|
@ -76,6 +77,7 @@ export const mockEmployees: HrEmployee[] = [
|
||||||
email: "ayse.kaya@company.com",
|
email: "ayse.kaya@company.com",
|
||||||
phone: "+90 212 555 0102",
|
phone: "+90 212 555 0102",
|
||||||
personalPhone: "+90 532 555 0103",
|
personalPhone: "+90 532 555 0103",
|
||||||
|
avatar: "https://i.pravatar.cc/150?img=5",
|
||||||
nationalId: "12345678902",
|
nationalId: "12345678902",
|
||||||
birthDate: new Date("1990-08-22"),
|
birthDate: new Date("1990-08-22"),
|
||||||
gender: GenderEnum.Female,
|
gender: GenderEnum.Female,
|
||||||
|
|
@ -134,6 +136,7 @@ export const mockEmployees: HrEmployee[] = [
|
||||||
email: "mehmet.yilmaz@company.com",
|
email: "mehmet.yilmaz@company.com",
|
||||||
phone: "+90 212 555 0105",
|
phone: "+90 212 555 0105",
|
||||||
personalPhone: "+90 532 555 0106",
|
personalPhone: "+90 532 555 0106",
|
||||||
|
avatar: "https://i.pravatar.cc/150?img=8",
|
||||||
nationalId: "12345678903",
|
nationalId: "12345678903",
|
||||||
birthDate: new Date("1987-03-12"),
|
birthDate: new Date("1987-03-12"),
|
||||||
gender: GenderEnum.Male,
|
gender: GenderEnum.Male,
|
||||||
|
|
@ -192,6 +195,7 @@ export const mockEmployees: HrEmployee[] = [
|
||||||
email: "selin.demir@company.com",
|
email: "selin.demir@company.com",
|
||||||
phone: "+90 312 555 0108",
|
phone: "+90 312 555 0108",
|
||||||
personalPhone: "+90 542 555 0109",
|
personalPhone: "+90 542 555 0109",
|
||||||
|
avatar: "https://i.pravatar.cc/150?img=9",
|
||||||
nationalId: "12345678904",
|
nationalId: "12345678904",
|
||||||
birthDate: new Date("1993-05-25"),
|
birthDate: new Date("1993-05-25"),
|
||||||
gender: GenderEnum.Female,
|
gender: GenderEnum.Female,
|
||||||
|
|
@ -250,6 +254,7 @@ export const mockEmployees: HrEmployee[] = [
|
||||||
email: "ahmet.celik@company.com",
|
email: "ahmet.celik@company.com",
|
||||||
phone: "+90 212 555 0111",
|
phone: "+90 212 555 0111",
|
||||||
personalPhone: "+90 532 555 0112",
|
personalPhone: "+90 532 555 0112",
|
||||||
|
avatar: "https://i.pravatar.cc/150?img=33",
|
||||||
nationalId: "12345678905",
|
nationalId: "12345678905",
|
||||||
birthDate: new Date("1985-09-10"),
|
birthDate: new Date("1985-09-10"),
|
||||||
gender: GenderEnum.Male,
|
gender: GenderEnum.Male,
|
||||||
|
|
@ -308,6 +313,7 @@ export const mockEmployees: HrEmployee[] = [
|
||||||
email: "zeynep.arslan@company.com",
|
email: "zeynep.arslan@company.com",
|
||||||
phone: "+90 216 555 0114",
|
phone: "+90 216 555 0114",
|
||||||
personalPhone: "+90 532 555 0115",
|
personalPhone: "+90 532 555 0115",
|
||||||
|
avatar: "https://i.pravatar.cc/150?img=10",
|
||||||
nationalId: "12345678906",
|
nationalId: "12345678906",
|
||||||
birthDate: new Date("1995-01-30"),
|
birthDate: new Date("1995-01-30"),
|
||||||
gender: GenderEnum.Female,
|
gender: GenderEnum.Female,
|
||||||
|
|
@ -366,6 +372,7 @@ export const mockEmployees: HrEmployee[] = [
|
||||||
email: "burak.koc@company.com",
|
email: "burak.koc@company.com",
|
||||||
phone: "+90 224 555 0117",
|
phone: "+90 224 555 0117",
|
||||||
personalPhone: "+90 532 555 0118",
|
personalPhone: "+90 532 555 0118",
|
||||||
|
avatar: "https://i.pravatar.cc/150?img=14",
|
||||||
nationalId: "12345678907",
|
nationalId: "12345678907",
|
||||||
birthDate: new Date("1991-06-18"),
|
birthDate: new Date("1991-06-18"),
|
||||||
gender: GenderEnum.Male,
|
gender: GenderEnum.Male,
|
||||||
|
|
@ -424,6 +431,7 @@ export const mockEmployees: HrEmployee[] = [
|
||||||
email: "elif.sahin@company.com",
|
email: "elif.sahin@company.com",
|
||||||
phone: "+90 232 555 0120",
|
phone: "+90 232 555 0120",
|
||||||
personalPhone: "+90 532 555 0121",
|
personalPhone: "+90 532 555 0121",
|
||||||
|
avatar: "https://i.pravatar.cc/150?img=20",
|
||||||
nationalId: "12345678908",
|
nationalId: "12345678908",
|
||||||
birthDate: new Date("1989-11-05"),
|
birthDate: new Date("1989-11-05"),
|
||||||
gender: GenderEnum.Female,
|
gender: GenderEnum.Female,
|
||||||
|
|
@ -482,6 +490,7 @@ export const mockEmployees: HrEmployee[] = [
|
||||||
email: "canan.ozturk@company.com",
|
email: "canan.ozturk@company.com",
|
||||||
phone: "+90 312 555 0123",
|
phone: "+90 312 555 0123",
|
||||||
personalPhone: "+90 532 555 0124",
|
personalPhone: "+90 532 555 0124",
|
||||||
|
avatar: "https://i.pravatar.cc/150?img=25",
|
||||||
nationalId: "12345678909",
|
nationalId: "12345678909",
|
||||||
birthDate: new Date("1992-04-14"),
|
birthDate: new Date("1992-04-14"),
|
||||||
gender: GenderEnum.Female,
|
gender: GenderEnum.Female,
|
||||||
|
|
@ -540,6 +549,7 @@ export const mockEmployees: HrEmployee[] = [
|
||||||
email: "murat.aydin@company.com",
|
email: "murat.aydin@company.com",
|
||||||
phone: "+90 212 555 0126",
|
phone: "+90 212 555 0126",
|
||||||
personalPhone: "+90 532 555 0127",
|
personalPhone: "+90 532 555 0127",
|
||||||
|
avatar: "https://i.pravatar.cc/150?img=30",
|
||||||
nationalId: "12345678910",
|
nationalId: "12345678910",
|
||||||
birthDate: new Date("1984-12-22"),
|
birthDate: new Date("1984-12-22"),
|
||||||
gender: GenderEnum.Male,
|
gender: GenderEnum.Male,
|
||||||
|
|
|
||||||
1030
ui/src/mocks/mockIntranetData.ts
Normal file
1030
ui/src/mocks/mockIntranetData.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -10,6 +10,7 @@ export interface HrEmployee {
|
||||||
email: string
|
email: string
|
||||||
phone?: string
|
phone?: string
|
||||||
personalPhone?: string
|
personalPhone?: string
|
||||||
|
avatar?: string // Avatar URL
|
||||||
nationalId: string
|
nationalId: string
|
||||||
birthDate: Date
|
birthDate: Date
|
||||||
gender: GenderEnum
|
gender: GenderEnum
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,64 @@
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import IntranetDashboard from '@/components/intranet/IntranetDashboard'
|
||||||
|
import IntranetSidebar from '@/components/intranet/IntranetSidebar'
|
||||||
|
import LeaveManagement from '@/components/intranet/HR/LeaveManagement'
|
||||||
|
import OvertimeManagement from '@/components/intranet/HR/OvertimeManagement'
|
||||||
|
import ExpenseManagement from '@/components/intranet/HR/ExpenseManagement'
|
||||||
|
import AnnouncementsModule from '@/components/intranet/Announcements'
|
||||||
|
import EventsModule from '@/components/intranet/Events'
|
||||||
|
import DocumentsModule from '@/components/intranet/Documents'
|
||||||
|
import TasksModule from '@/components/intranet/Tasks'
|
||||||
import SocialWall from '@/components/intranet/SocialWall'
|
import SocialWall from '@/components/intranet/SocialWall'
|
||||||
|
import BirthdaysModule from '@/components/intranet/Birthdays'
|
||||||
|
import TrainingModule from '@/components/intranet/Training'
|
||||||
|
import ReservationsModule from '@/components/intranet/Reservations'
|
||||||
|
import CafeteriaModule from '@/components/intranet/Cafeteria'
|
||||||
|
import SurveysModule from '@/components/intranet/Surveys'
|
||||||
|
import VisitorsModule from '@/components/intranet/Visitors'
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
const [currentPath, setCurrentPath] = useState('/intranet/dashboard')
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
switch (currentPath) {
|
||||||
|
case '/intranet/dashboard':
|
||||||
|
return <IntranetDashboard />
|
||||||
|
case '/intranet/social':
|
||||||
|
return <SocialWall />
|
||||||
|
case '/intranet/hr/leave':
|
||||||
|
return <LeaveManagement />
|
||||||
|
case '/intranet/hr/overtime':
|
||||||
|
return <OvertimeManagement />
|
||||||
|
case '/intranet/hr/expense':
|
||||||
|
return <ExpenseManagement />
|
||||||
|
case '/intranet/announcements':
|
||||||
|
return <AnnouncementsModule />
|
||||||
|
case '/intranet/events':
|
||||||
|
return <EventsModule />
|
||||||
|
case '/intranet/documents':
|
||||||
|
return <DocumentsModule />
|
||||||
|
case '/intranet/tasks':
|
||||||
|
return <TasksModule />
|
||||||
|
case '/intranet/birthdays':
|
||||||
|
return <BirthdaysModule />
|
||||||
|
case '/intranet/training':
|
||||||
|
return <TrainingModule />
|
||||||
|
case '/intranet/reservations':
|
||||||
|
return <ReservationsModule />
|
||||||
|
case '/intranet/cafeteria/menu':
|
||||||
|
case '/intranet/cafeteria/shuttle':
|
||||||
|
return <CafeteriaModule />
|
||||||
|
case '/intranet/surveys':
|
||||||
|
return <SurveysModule />
|
||||||
|
case '/intranet/visitors':
|
||||||
|
return <VisitorsModule />
|
||||||
|
default:
|
||||||
|
return <IntranetDashboard />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -12,11 +67,15 @@ const Dashboard = () => {
|
||||||
title={translate('::' + 'Dashboard')}
|
title={translate('::' + 'Dashboard')}
|
||||||
defaultTitle="Sözsoft Kurs Platform"
|
defaultTitle="Sözsoft Kurs Platform"
|
||||||
/>
|
/>
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
<div className="flex min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
<SocialWall />
|
<IntranetSidebar activePath={currentPath} onNavigate={setCurrentPath} />
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dashboard
|
export default Dashboard
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,11 @@ const EmployeeList: React.FC = () => {
|
||||||
<div className="flex-shrink-0 h-10 w-10">
|
<div className="flex-shrink-0 h-10 w-10">
|
||||||
<div className="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center">
|
<div className="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center">
|
||||||
<FaUsers className="h-5 w-5 text-blue-600" />
|
<FaUsers className="h-5 w-5 text-blue-600" />
|
||||||
|
<img
|
||||||
|
src={employee.avatar}
|
||||||
|
alt={employee.fullName}
|
||||||
|
className="w-10 h-10 rounded-full"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4">
|
<div className="ml-4">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue