From ddaaa56ea042740ed607834337d3eebdab6a267a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Sun, 19 Oct 2025 01:37:20 +0300 Subject: [PATCH] =?UTF-8?q?Intranet=20Modul=C3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/public/version.json | 33 +- .../intranet/Announcements/index.tsx | 291 +++++ .../components/intranet/Birthdays/index.tsx | 279 +++++ .../components/intranet/Cafeteria/index.tsx | 207 ++++ .../components/intranet/Documents/index.tsx | 458 ++++++++ ui/src/components/intranet/Events/index.tsx | 460 ++++++++ .../intranet/HR/ExpenseManagement.tsx | 480 ++++++++ .../intranet/HR/LeaveManagement.tsx | 426 +++++++ .../intranet/HR/OvertimeManagement.tsx | 376 ++++++ .../intranet/IntranetDashboard/index.tsx | 539 +++++++++ .../components/intranet/IntranetSidebar.tsx | 241 ++++ .../intranet/Reservations/index.tsx | 319 +++++ ui/src/components/intranet/Surveys/index.tsx | 288 +++++ ui/src/components/intranet/Tasks/index.tsx | 655 +++++++++++ ui/src/components/intranet/Training/index.tsx | 354 ++++++ ui/src/components/intranet/Visitors/index.tsx | 380 ++++++ ui/src/mocks/mockEmployees.ts | 10 + ui/src/mocks/mockIntranetData.ts | 1030 +++++++++++++++++ ui/src/types/hr.ts | 1 + ui/src/views/Dashboard.tsx | 63 +- ui/src/views/hr/components/EmployeeList.tsx | 5 + 21 files changed, 6891 insertions(+), 4 deletions(-) create mode 100644 ui/src/components/intranet/Announcements/index.tsx create mode 100644 ui/src/components/intranet/Birthdays/index.tsx create mode 100644 ui/src/components/intranet/Cafeteria/index.tsx create mode 100644 ui/src/components/intranet/Documents/index.tsx create mode 100644 ui/src/components/intranet/Events/index.tsx create mode 100644 ui/src/components/intranet/HR/ExpenseManagement.tsx create mode 100644 ui/src/components/intranet/HR/LeaveManagement.tsx create mode 100644 ui/src/components/intranet/HR/OvertimeManagement.tsx create mode 100644 ui/src/components/intranet/IntranetDashboard/index.tsx create mode 100644 ui/src/components/intranet/IntranetSidebar.tsx create mode 100644 ui/src/components/intranet/Reservations/index.tsx create mode 100644 ui/src/components/intranet/Surveys/index.tsx create mode 100644 ui/src/components/intranet/Tasks/index.tsx create mode 100644 ui/src/components/intranet/Training/index.tsx create mode 100644 ui/src/components/intranet/Visitors/index.tsx create mode 100644 ui/src/mocks/mockIntranetData.ts diff --git a/ui/public/version.json b/ui/public/version.json index 0140dd24..1f2dbe35 100644 --- a/ui/public/version.json +++ b/ui/public/version.json @@ -1,6 +1,35 @@ { - "commit": "64488c5", + "commit": "c78845f", "releases": [ + { + "version": "1.0.32", + "buildDate": "2025-10-15", + "commit": "8564bff367eefb62b1cfd7ac5790097fcf8feaa7", + "changeLog": [ + "Form View ve Form Edit ekranlarında Activity özelliği eklendi. 3 farklı Activity eklenebiliyor ayrıca dosya eklenebiliyor. Bu dosyalar diskte Blob olarak kaydediliyor." + ] + }, + { + "version": "1.0.31", + "buildDate": "2025-10-09", + "commit": "035366ab7020dd77bfe2b5b66ea253e743526ea6", + "changeLog": [ + "- Grid üzerinde Mask ve Format güncellemesi", + "- Allow Column Reordering uygulamasının çalıştırılması" + ] + }, + { + "version": "1.0.30", + "buildDate": "2025-10-08", + "commit": "e45885f5693176257e12ecc05d4ed51f87ef0120", + "changeLog": [ + "- Tenant ve Barch arasında ilişki kuruldu ve listelerde filtreli şekilde listeleniyor.", + "- Genel seederlar düzenlendi.", + "- Yeni tanımlamalar listeleri eklendi. Kayıt Tipi, Kayı Şekli, Program vs.", + "- Default Helper eklendi ve tüm Application Servisler o metoda yönlendirildi.", + "- Tanımlamalar menülere dağıtıldı." + ] + }, { "version": "1.0.29", "buildDate": "2025-09-28", @@ -126,7 +155,7 @@ { "version": "1.0.14", "buildDate": "2025-09-22", - "commit": "1c4ab4f8232b4cd2a39fa66f8101664840113ce5", + "commit": "51208b86937484d68b699120d74872067b1c7ef6", "changeLog": [ "Yeni versiyon çıktı uyarı gelecek şekilde düzenlendi.", "Sağ alt kısımda mesaj çıkacak ve yenile butonu ile uygulama yeni versiyona geçecektir." diff --git a/ui/src/components/intranet/Announcements/index.tsx b/ui/src/components/intranet/Announcements/index.tsx new file mode 100644 index 00000000..f47d6a4d --- /dev/null +++ b/ui/src/components/intranet/Announcements/index.tsx @@ -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(mockAnnouncements) + const [searchQuery, setSearchQuery] = useState('') + const [selectedCategory, setSelectedCategory] = useState('all') + const [selectedAnnouncement, setSelectedAnnouncement] = useState(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 = { + 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 = { + general: 'Genel', + hr: 'İK', + it: 'IT', + event: 'Etkinlik', + urgent: 'Acil' + } + return labels[category] + } + + return ( +
+
+ {/* Header */} +
+

+ Duyurular & Haberler +

+

+ Şirket duyurularını ve haberleri takip edin +

+
+ + {/* Arama ve Filtreler */} +
+
+ + 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" + /> +
+
+ + {categories.map(cat => ( + + ))} +
+
+ + {/* Duyuru Listesi */} +
+ {/* Sol Kolon - Liste */} +
+ {filteredAnnouncements.map(announcement => ( + setSelectedAnnouncement(announcement)} + > + {announcement.imageUrl && ( +
+ {announcement.title} + {announcement.isPinned && ( +
+ 📌 SABİTLENDİ +
+ )} +
+ )} +
+
+

+ {announcement.title} +

+ + {getCategoryBadge(announcement.category)} + +
+

+ {announcement.excerpt} +

+
+
+ {announcement.author.fullName} + {announcement.author.fullName} +
+
+ + {dayjs(announcement.publishDate).format('DD MMMM YYYY')} +
+
+ + {announcement.viewCount} +
+
+
+
+ ))} + + {filteredAnnouncements.length === 0 && ( +
+ +

+ {searchQuery ? 'Arama kriterlerine uygun duyuru bulunamadı' : 'Henüz duyuru bulunmuyor'} +

+
+ )} +
+ + {/* Sağ Kolon - Detay */} +
+ {selectedAnnouncement ? ( +
+ {selectedAnnouncement.imageUrl && ( +
+ {selectedAnnouncement.title} +
+ )} +
+
+ + {getCategoryBadge(selectedAnnouncement.category)} + + {selectedAnnouncement.isPinned && ( + + 📌 Sabitlendi + + )} +
+

+ {selectedAnnouncement.title} +

+
+ {selectedAnnouncement.author.fullName} +
+

+ {selectedAnnouncement.author.fullName} +

+

+ {dayjs(selectedAnnouncement.publishDate).format('DD MMMM YYYY, HH:mm')} +

+
+
+
+

+ {selectedAnnouncement.content} +

+
+ {selectedAnnouncement.attachments && selectedAnnouncement.attachments.length > 0 && ( +
+

+ 📎 Ekler +

+
+ {selectedAnnouncement.attachments.map((attachment, idx) => ( + +
+ 📄 +
+
+

+ {attachment.name} +

+

+ {attachment.size} +

+
+
+ ))} +
+
+ )} + {selectedAnnouncement.departments && selectedAnnouncement.departments.length > 0 && ( +
+

+ Hedef Departmanlar: +

+
+ {selectedAnnouncement.departments.map((dept, idx) => ( + + {dept} + + ))} +
+
+ )} +
+ + {selectedAnnouncement.viewCount} kez görüntülendi +
+
+
+ ) : ( +
+ +

+ Detayları görüntülemek için bir duyuru seçin +

+
+ )} +
+
+
+
+ ) +} + +export default AnnouncementsModule diff --git a/ui/src/components/intranet/Birthdays/index.tsx b/ui/src/components/intranet/Birthdays/index.tsx new file mode 100644 index 00000000..afbdb5c2 --- /dev/null +++ b/ui/src/components/intranet/Birthdays/index.tsx @@ -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 ( +
+
+ {/* Header */} +
+

+ 🎂 Doğum Günleri & Yıldönümleri +

+

+ Ekip üyelerimizin özel günlerini kutlayalım +

+
+ + {/* Stats */} +
+ {/* Bugün */} + +
+ + {todayBirthdays.length} +
+

Bugün Doğanlar

+

+ {todayBirthdays.length > 0 ? 'Kutlama zamanı!' : 'Bugün doğum günü yok'} +

+
+ + {/* Bu Hafta */} + +
+ + {thisWeekBirthdays.length} +
+

Bu Hafta

+

+ {dayjs().startOf('week').format('DD MMM')} - {dayjs().endOf('week').format('DD MMM')} +

+
+ + {/* İş Yıldönümleri */} + +
+ + {thisMonthAnniversaries.length} +
+

Bu Ay Yıldönümü

+

+ {dayjs().format('MMMM')} ayında +

+
+
+ + {/* Bugünün Doğum Günleri */} + {todayBirthdays.length > 0 && ( +
+

+ 🎉 Bugün Doğum Günü Olanlar +

+
+ {todayBirthdays.map((birthday, idx) => ( + +
+ {birthday.employee.fullName} +
+

+ {birthday.employee.fullName} +

+

+ {birthday.employee.department?.name || 'Genel'} +

+

+ {getBirthdayMessage(birthday)} +

+
+
+
+ ))} +
+
+ )} + + {/* Ay Seçici */} +
+
+

+ 📅 Aylık Doğum Günleri +

+ +
+ + {/* Doğum Günleri Listesi */} +
+ {monthBirthdays.map((birthday, idx) => { + const isToday = dayjs(birthday.date).month() === dayjs().month() && + dayjs(birthday.date).date() === dayjs().date() + return ( + +
+ {dayjs(birthday.date).date()} + {months[selectedMonth].substring(0, 3)} +
+ + {birthday.employee.fullName} + +
+

+ {birthday.employee.fullName} +

+

+ {birthday.employee.department?.name || 'Genel'} +

+
+ +
+

+ {birthday.age || dayjs().year() - dayjs(birthday.date).year()} yaşında +

+ {isToday && ( + + 🎂 Bugün! + + )} +
+
+ ) + })} + {monthBirthdays.length === 0 && ( +
+ +

{months[selectedMonth]} ayında doğum günü yok

+
+ )} +
+
+ + {/* İş Yıldönümleri */} + {thisMonthAnniversaries.length > 0 && ( +
+

+ 🎊 Bu Ayki İş Yıldönümleri +

+
+ {thisMonthAnniversaries.map((anniversary: WorkAnniversary, idx: number) => ( + +
+ {anniversary.employee.fullName} +
+

+ {anniversary.employee.fullName} +

+

+ {anniversary.employee.department?.name || 'Genel'} +

+

+ {getAnniversaryMessage(anniversary)} +

+
+
+ {anniversary.years} + YIL +
+
+
+ ))} +
+
+ )} +
+
+ ) +} + +export default BirthdaysModule diff --git a/ui/src/components/intranet/Cafeteria/index.tsx b/ui/src/components/intranet/Cafeteria/index.tsx new file mode 100644 index 00000000..ccd01cdd --- /dev/null +++ b/ui/src/components/intranet/Cafeteria/index.tsx @@ -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 ( +
+
+ {/* Header */} +
+

+ 🍽️ Kafeterya & Servis +

+

+ Yemek menüsü ve servis saatleri +

+
+ + {/* Tabs */} +
+ + +
+ + {/* Menu View */} + {selectedView === 'menu' && ( +
+ {mockMealMenus.map((menu, idx) => ( + +
+
{dayjs(menu.date).format('DD MMM')}
+

{menu.dayOfWeek}

+
+ + {menu.meals.map((meal, mealIdx) => ( +
+
+ + {meal.type === 'lunch' ? '🍽️ Öğle Yemeği' : meal.type} + + {meal.calories && {meal.calories} kcal} +
+
    + {meal.items.map((item, itemIdx) => ( +
  • + + {item} +
  • + ))} +
+
+ ))} +
+ ))} +
+ )} + + {/* Shuttle View */} + {selectedView === 'shuttle' && ( +
+
+

🌅 Sabah Servisleri

+
+ {mockShuttleRoutes.filter(s => s.type === 'morning').map((shuttle, idx) => ( + +
+

{shuttle.name}

+ 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 + +
+ +
+
+ + + {shuttle.departureTime} - {shuttle.arrivalTime} + +
+
+ +
+ {shuttle.route.map((stop, idx) => ( + + {stop} + {idx < shuttle.route.length - 1 && ' → '} + + ))} +
+
+
+ +
+
+
+

+ {shuttle.capacity - shuttle.available} / {shuttle.capacity} dolu +

+ + ))} +
+
+ +
+

🌆 Akşam Servisleri

+
+ {mockShuttleRoutes.filter(s => s.type === 'evening').map((shuttle, idx) => ( + +
+

{shuttle.name}

+ 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 + +
+ +
+
+ + + {shuttle.departureTime} - {shuttle.arrivalTime} + +
+
+ +
+ {shuttle.route.map((stop, idx) => ( + + {stop} + {idx < shuttle.route.length - 1 && ' → '} + + ))} +
+
+
+ +
+
+
+

+ {shuttle.capacity - shuttle.available} / {shuttle.capacity} dolu +

+ + ))} +
+
+
+ )} +
+
+ ) +} + +export default CafeteriaModule diff --git a/ui/src/components/intranet/Documents/index.tsx b/ui/src/components/intranet/Documents/index.tsx new file mode 100644 index 00000000..6280016a --- /dev/null +++ b/ui/src/components/intranet/Documents/index.tsx @@ -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(mockDocuments) + const [searchQuery, setSearchQuery] = useState('') + const [selectedCategory, setSelectedCategory] = useState('all') + const [previewDocument, setPreviewDocument] = useState(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) + + return ( +
+
+ {/* Header */} +
+

+ Doküman Yönetimi +

+

+ Şirket dokümanlarına erişin ve yönetin +

+
+ + {/* Arama ve Filtreler */} +
+
+ + 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" + /> +
+
+ + {categories.map(cat => ( + + ))} +
+
+ + {/* İstatistikler */} +
+
+
+

+ Toplam Doküman +

+ 📁 +
+

+ {documents.length} +

+
+
+
+

+ Kategoriler +

+ 🗂️ +
+

+ {Object.keys(groupedDocuments).length} +

+
+
+
+

+ Toplam İndirme +

+ ⬇️ +
+

+ {documents.reduce((sum, doc) => sum + doc.downloadCount, 0)} +

+
+
+
+

+ Bu Ay Yeni +

+ +
+

+ {documents.filter(d => dayjs(d.uploadDate).isAfter(dayjs().subtract(30, 'day'))).length} +

+
+
+ + {/* Doküman Listesi */} + {Object.entries(groupedDocuments).map(([category, categoryDocs]) => ( +
+
+ +

+ {getCategoryLabel(category)} +

+ + ({categoryDocs.length} doküman) + +
+
+ {categoryDocs.map(doc => ( + +
+
+ {getFileIcon(doc.type)} +
+
+
+

+ {doc.name} +

+
+ + +
+
+

+ {doc.description} +

+
+
+ {doc.uploadedBy.fullName} + {doc.uploadedBy.fullName} +
+ + {dayjs(doc.uploadDate).format('DD MMMM YYYY')} + + {doc.size} + + v{doc.version} + + {doc.downloadCount} indirme +
+ {doc.tags.length > 0 && ( +
+ {doc.tags.map((tag, idx) => ( + + #{tag} + + ))} +
+ )} +
+
+
+ ))} +
+
+ ))} + + {filteredDocuments.length === 0 && ( +
+ +

+ {searchQuery ? 'Arama kriterlerine uygun doküman bulunamadı' : 'Henüz doküman bulunmuyor'} +

+
+ )} +
+ + {/* Doküman Önizleme Modal */} + + {previewDocument && ( + <> + setPreviewDocument(null)} + /> +
+ + {/* Header */} +
+
+
+ {getFileIcon(previewDocument.type)} +
+
+

+ {previewDocument.name} +

+

+ {getCategoryLabel(previewDocument.category)} • {previewDocument.size} • v{previewDocument.version} +

+
+
+
+ + +
+
+ + {/* Content */} +
+
+ {/* Açıklama */} +
+

+ Açıklama +

+

+ {previewDocument.description} +

+
+ + {/* Dosya Önizleme Alanı */} +
+

+ Önizleme +

+
+
{getFileIcon(previewDocument.type)}
+

+ {previewDocument.type.toUpperCase()} dosyası önizlemesi desteklenmemektedir +

+ +
+
+ + {/* Metadata */} +
+
+

+ Dosya Bilgileri +

+
+
+ Dosya Tipi: + + {previewDocument.type.toUpperCase()} + +
+
+ Boyut: + + {previewDocument.size} + +
+
+ Versiyon: + + v{previewDocument.version} + +
+
+ İndirme: + + {previewDocument.downloadCount} kez + +
+
+
+ +
+

+ Yükleyen +

+
+ {previewDocument.uploadedBy.fullName} +
+

+ {previewDocument.uploadedBy.fullName} +

+

+ {dayjs(previewDocument.uploadDate).format('DD MMMM YYYY, HH:mm')} +

+
+
+
+
+ + {/* Etiketler */} + {previewDocument.tags.length > 0 && ( +
+

+ Etiketler +

+
+ {previewDocument.tags.map((tag, idx) => ( + + #{tag} + + ))} +
+
+ )} + + {/* Departmanlar */} + {previewDocument.departments.length > 0 && ( +
+

+ Yetkili Departmanlar +

+
+ {previewDocument.departments.map((dept, idx) => ( + + {dept} + + ))} +
+
+ )} +
+
+
+
+ + )} +
+
+ ) +} + +export default DocumentsModule diff --git a/ui/src/components/intranet/Events/index.tsx b/ui/src/components/intranet/Events/index.tsx new file mode 100644 index 00000000..ea796c07 --- /dev/null +++ b/ui/src/components/intranet/Events/index.tsx @@ -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(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(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 = { + 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 = { + social: '🎉 Sosyal', + training: '📚 Eğitim', + company: '🏢 Kurumsal', + sport: '⚽ Spor', + culture: '🎨 Kültür' + } + return labels[type] || type + } + + return ( +
+
+ {/* Header */} +
+

+ 🎊 Etkinlikler +

+

+ Şirket etkinlikleri, fotoğraflar ve anılar +

+
+ + {/* Filter Tabs */} +
+ {[ + { 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) => ( + + ))} +
+ + {/* Events Grid */} +
+ {filteredEvents.map((event, idx) => ( + setSelectedEvent(event)} + > + {/* Cover Photo */} +
+ {event.title} +
+ + {getTypeLabel(event.type)} + +
+ {event.photos.length > 1 && ( +
+ +{event.photos.length - 1} fotoğraf +
+ )} +
+ + {/* Content */} +
+

+ {event.title} +

+ +

+ {event.description} +

+ +
+
+ + {dayjs(event.date).format('DD MMMM YYYY')} +
+
+ + {event.location} +
+
+ +
+
+ +
+ + {event.comments.length} +
+
+
+ + {event.participants} kişi +
+
+
+
+ ))} +
+ + {filteredEvents.length === 0 && ( +
+

Bu kategoride henüz etkinlik yok

+
+ )} + + {/* Event Detail Modal */} + + {selectedEvent && ( + <> + setSelectedEvent(null)} + /> +
+ e.stopPropagation()} + > + {/* Header */} +
+
+
+
+ + {getTypeLabel(selectedEvent.type)} + + + {dayjs(selectedEvent.date).format('DD MMMM YYYY')} + +
+

+ {selectedEvent.title} +

+

+ {selectedEvent.description} +

+
+ +
+ +
+
+ + {selectedEvent.location} +
+
+ + {selectedEvent.participants} katılımcı +
+
+ {selectedEvent.organizer.fullName} + Düzenleyen: {selectedEvent.organizer.fullName} +
+
+
+ + {/* Photo Gallery */} +
+

+ 📸 Fotoğraflar ({selectedEvent.photos.length}) +

+
+ {selectedEvent.photos.map((photo, idx) => ( + { + setSelectedPhotoIndex(idx) + setShowPhotoModal(true) + }} + > + {`${selectedEvent.title} + + ))} +
+
+ + {/* Comments Section */} +
+
+

+ 💬 Yorumlar ({selectedEvent.comments.length}) +

+
+ +
+
+ + {/* Comments List */} +
+ {selectedEvent.comments.map((comment) => ( +
+ {comment.author.fullName} +
+
+ + {comment.author.fullName} + + + {dayjs(comment.createdAt).fromNow()} + +
+

+ {comment.content} +

+
+ +
+
+
+ ))} +
+ + {/* Add Comment */} +
+ You +
+ 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" + /> + +
+
+
+
+
+ + )} +
+ + {/* Photo Viewer Modal */} + + {showPhotoModal && selectedEvent && ( + <> + setShowPhotoModal(false)} + /> +
+ e.stopPropagation()} + > + {/* Close Button */} + + + {/* Navigation */} + {selectedEvent.photos.length > 1 && ( + <> + + + + )} + + {/* Image */} + {`${selectedEvent.title} + + {/* Counter */} +
+ {selectedPhotoIndex + 1} / {selectedEvent.photos.length} +
+
+
+ + )} +
+
+
+ ) +} + +export default EventsModule diff --git a/ui/src/components/intranet/HR/ExpenseManagement.tsx b/ui/src/components/intranet/HR/ExpenseManagement.tsx new file mode 100644 index 00000000..e19be9ab --- /dev/null +++ b/ui/src/components/intranet/HR/ExpenseManagement.tsx @@ -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(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 ( +
+
+ {/* Header */} +
+
+

+ Harcama Yönetimi +

+

+ Harcama taleplerinizi oluşturun ve takip edin +

+
+ +
+ + {/* İstatistikler */} +
+
+
+

+ Bu Ay Toplam +

+ 💰 +
+

+ ₺{expenseStats.thisMonth.total.toLocaleString()} +

+

+ toplam harcama +

+
+ +
+
+

+ Onaylanan +

+ +
+

+ ₺{expenseStats.thisMonth.approved.toLocaleString()} +

+

+ bu ay onaylandı +

+
+ +
+
+

+ Bekleyen +

+ +
+

+ ₺{expenseStats.thisMonth.pending.toLocaleString()} +

+

+ onay bekliyor +

+
+ +
+
+

+ Yıllık Toplam +

+ 📊 +
+

+ ₺{expenseStats.thisYear.total.toLocaleString()} +

+

+ bu yıl toplam +

+
+
+ + {/* Kategori Dağılımı */} +
+

+ Kategori Bazlı Dağılım (Yıllık) +

+
+ {Object.entries(expenseStats.byCategory).map(([category, amount]) => { + const percentage = (amount / expenseStats.thisYear.total) * 100 + return ( +
+
+ + {getCategoryIcon(category)} {getCategoryLabel(category)} + + + ₺{amount.toLocaleString()} ({percentage.toFixed(1)}%) + +
+
+
+
+
+ ) + })} +
+
+ + {/* Filtreler */} +
+ + + + + +
+ + {/* Harcama Talepleri Listesi */} +
+
+

+ Harcama Talepleri +

+
+
+ {filteredRequests.length > 0 ? ( + filteredRequests.map((request) => ( +
+
+ {getCategoryIcon(request.category)} +
+
+

+ {getCategoryLabel(request.category)} +

+ + {request.status === 'pending' && '⏳ Beklemede'} + {request.status === 'approved' && '✅ Onaylandı'} + {request.status === 'rejected' && '❌ Reddedildi'} + +
+
+
+

Tutar

+

+ {request.amount.toLocaleString()} {request.currency} +

+
+
+

Tarih

+

+ {dayjs(request.date).format('DD MMMM YYYY')} +

+
+
+

Makbuzlar

+

+ {request.receipts.length} dosya +

+
+
+

Talep Tarihi

+

+ {dayjs(request.createdAt).format('DD MMM')} +

+
+
+ {request.project && ( +
+ + 📁 {request.project} + +
+ )} +

+ Açıklama: {request.description} +

+ {request.receipts.length > 0 && ( +
+ {request.receipts.map((receipt, idx) => ( + + + + {receipt.name} + + + ({receipt.size}) + + + ))} +
+ )} + {request.approver && ( +
+ + + {request.approver.fullName} tarafından{' '} + {dayjs(request.approvalDate).format('DD MMMM YYYY')} tarihinde{' '} + {request.status === 'approved' ? 'onaylandı' : 'reddedildi'} + +
+ )} + {request.notes && ( +
+

+ Not: {request.notes} +

+
+ )} +
+
+
+ )) + ) : ( +
+ +

+ {filterStatus === 'all' + ? 'Henüz harcama talebi bulunmuyor' + : `${filterStatus === 'pending' ? 'Bekleyen' : filterStatus === 'approved' ? 'Onaylanan' : 'Reddedilen'} harcama talebi bulunmuyor`} +

+
+ )} +
+
+
+ + {/* Yeni Harcama Talebi Modal */} + + {showCreateModal && ( + <> + setShowCreateModal(false)} + /> +
+ +
+

+ Yeni Harcama Talebi +

+ +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ +