2026-05-05 17:59:30 +00:00
|
|
|
|
import React, { useState, useEffect } from 'react'
|
|
|
|
|
|
import { AnimatePresence } from 'framer-motion'
|
|
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
|
|
|
|
import isBetween from 'dayjs/plugin/isBetween'
|
|
|
|
|
|
import localizedFormat from 'dayjs/plugin/localizedFormat'
|
|
|
|
|
|
|
|
|
|
|
|
// Widgets
|
|
|
|
|
|
import TodayBirthdays from './widgets/TodayBirthdays'
|
|
|
|
|
|
import RecentDocuments from './widgets/RecentDocuments'
|
|
|
|
|
|
import Surveys from './widgets/Surveys'
|
|
|
|
|
|
|
|
|
|
|
|
// Modals
|
|
|
|
|
|
import SurveyModal from './widgets/SurveyModal'
|
|
|
|
|
|
import AnnouncementModal from './widgets/AnnouncementModal'
|
2026-05-07 14:25:06 +00:00
|
|
|
|
import EventModal from './widgets/EventModal'
|
2026-05-05 17:59:30 +00:00
|
|
|
|
|
|
|
|
|
|
// Social Wall
|
|
|
|
|
|
import SocialWall from './SocialWall'
|
|
|
|
|
|
import { Container } from '@/components/shared'
|
|
|
|
|
|
import { usePermission } from '@/utils/hooks/usePermission'
|
|
|
|
|
|
import {
|
|
|
|
|
|
AnnouncementDto,
|
2026-05-07 14:25:06 +00:00
|
|
|
|
EventDto,
|
2026-05-05 17:59:30 +00:00
|
|
|
|
IntranetDashboardDto,
|
|
|
|
|
|
SurveyAnswerDto,
|
|
|
|
|
|
SurveyDto,
|
|
|
|
|
|
} from '@/proxy/intranet/models'
|
|
|
|
|
|
import { intranetService } from '@/services/intranet.service'
|
|
|
|
|
|
import Announcements from './widgets/Announcements'
|
|
|
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
|
import useLocale from '@/utils/hooks/useLocale'
|
|
|
|
|
|
import { currentLocalDate } from '@/utils/dateUtils'
|
|
|
|
|
|
import { useStoreState } from '@/store/store'
|
2026-05-06 14:48:44 +00:00
|
|
|
|
import UpcomingEvents from './widgets/UpcomingEvents'
|
2026-05-05 17:59:30 +00:00
|
|
|
|
|
|
|
|
|
|
dayjs.extend(relativeTime)
|
|
|
|
|
|
dayjs.extend(isBetween)
|
|
|
|
|
|
dayjs.extend(localizedFormat)
|
|
|
|
|
|
|
|
|
|
|
|
const WIDGET_ORDER_KEY = 'dashboard-widget-order'
|
|
|
|
|
|
|
|
|
|
|
|
const IntranetDashboard: React.FC = () => {
|
|
|
|
|
|
const { checkPermission } = usePermission()
|
|
|
|
|
|
const [selectedAnnouncement, setSelectedAnnouncement] = useState<AnnouncementDto | null>(null)
|
2026-05-07 14:25:06 +00:00
|
|
|
|
const [selectedEvent, setSelectedEvent] = useState<EventDto | null>(null)
|
2026-05-05 17:59:30 +00:00
|
|
|
|
const [selectedSurvey, setSelectedSurvey] = useState<SurveyDto | null>(null)
|
|
|
|
|
|
const [showSurveyModal, setShowSurveyModal] = useState(false)
|
|
|
|
|
|
const [isDesignMode, setIsDesignMode] = useState(false)
|
|
|
|
|
|
const [widgetOrder, setWidgetOrder] = useState<Record<string, string[]>>({
|
|
|
|
|
|
left: [],
|
|
|
|
|
|
center: [],
|
|
|
|
|
|
right: [],
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const [intranetDashboard, setIntranetDashboard] = useState<IntranetDashboardDto>()
|
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
|
const currentLocale = useLocale()
|
|
|
|
|
|
|
|
|
|
|
|
const fetchIntranetDashboard = async () => {
|
|
|
|
|
|
const dashboard = await intranetService.getDashboard()
|
|
|
|
|
|
if (dashboard.data) {
|
|
|
|
|
|
setIntranetDashboard(dashboard.data)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchIntranetDashboard()
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
// Drag state'leri birleştirildi
|
|
|
|
|
|
const [dragState, setDragState] = useState<{
|
|
|
|
|
|
draggedId: string | null
|
|
|
|
|
|
targetColumn: string | null
|
|
|
|
|
|
targetIndex: number | null
|
|
|
|
|
|
}>({
|
|
|
|
|
|
draggedId: null,
|
|
|
|
|
|
targetColumn: null,
|
|
|
|
|
|
targetIndex: null,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const handleTakeSurvey = (survey: SurveyDto) => {
|
|
|
|
|
|
setSelectedSurvey(survey)
|
|
|
|
|
|
setShowSurveyModal(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-06 07:54:04 +00:00
|
|
|
|
const handleSubmitSurvey = async (answers: SurveyAnswerDto[]) => {
|
|
|
|
|
|
if (!selectedSurvey) return
|
|
|
|
|
|
try {
|
2026-05-06 13:47:18 +00:00
|
|
|
|
await intranetService.createSurveyResponse(
|
2026-05-06 07:54:04 +00:00
|
|
|
|
selectedSurvey.id,
|
|
|
|
|
|
answers.map((a) => ({
|
|
|
|
|
|
questionId: a.questionId,
|
|
|
|
|
|
questionType: a.questionType,
|
|
|
|
|
|
value: a.value !== undefined && a.value !== null ? String(a.value) : '',
|
|
|
|
|
|
})),
|
|
|
|
|
|
)
|
|
|
|
|
|
await fetchIntranetDashboard()
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Survey submit error', e)
|
|
|
|
|
|
}
|
2026-05-05 17:59:30 +00:00
|
|
|
|
setShowSurveyModal(false)
|
|
|
|
|
|
setSelectedSurvey(null)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Widget metadata (component'lar yerine sadece meta bilgiler)
|
|
|
|
|
|
const widgetMetadata = [
|
|
|
|
|
|
{ id: 'today-birthdays', permission: 'AbpIdentity.Users.Widget', column: 'left' },
|
|
|
|
|
|
{ id: 'documents', permission: 'App.Files.Widget', column: 'left' },
|
|
|
|
|
|
{ id: 'active-surveys', permission: 'App.Intranet.Survey.Widget', column: 'left' },
|
|
|
|
|
|
{ id: 'social-wall', permission: 'App.Intranet.SocialPost.Widget', column: 'center' },
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'announcements',
|
|
|
|
|
|
permission: 'App.Intranet.Announcement.Widget',
|
|
|
|
|
|
column: 'right',
|
|
|
|
|
|
},
|
2026-05-06 14:48:44 +00:00
|
|
|
|
{ id: 'upcoming-events', permission: 'App.Intranet.Events.Event.Widget', column: 'right' },
|
2026-05-05 17:59:30 +00:00
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
// If permissions arrive after mount, initialize default order when needed
|
|
|
|
|
|
const grantedPolicies = useStoreState((state) => state.abpConfig?.config?.auth.grantedPolicies)
|
|
|
|
|
|
|
|
|
|
|
|
const initializeDefaultOrder = () => {
|
|
|
|
|
|
const defaultOrder = {
|
|
|
|
|
|
left: widgetMetadata
|
|
|
|
|
|
.filter((w) => w.column === 'left' && checkPermission(w.permission))
|
|
|
|
|
|
.map((w) => w.id),
|
|
|
|
|
|
center: widgetMetadata
|
|
|
|
|
|
.filter((w) => w.column === 'center' && checkPermission(w.permission))
|
|
|
|
|
|
.map((w) => w.id),
|
|
|
|
|
|
right: widgetMetadata
|
|
|
|
|
|
.filter((w) => w.column === 'right' && checkPermission(w.permission))
|
|
|
|
|
|
.map((w) => w.id),
|
|
|
|
|
|
}
|
|
|
|
|
|
setWidgetOrder(defaultOrder)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Widget sıralamasını yükle — grantedPolicies gelince çalış ki checkPermission doğru çalışsın
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!grantedPolicies) return
|
|
|
|
|
|
|
|
|
|
|
|
const savedOrder = localStorage.getItem(WIDGET_ORDER_KEY)
|
|
|
|
|
|
if (savedOrder) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = JSON.parse(savedOrder) as Record<string, unknown[]>
|
|
|
|
|
|
const order: Record<string, string[]> = {
|
|
|
|
|
|
left: [...new Set((parsed.left || []) as string[])],
|
|
|
|
|
|
center: [...new Set((parsed.center || []) as string[])],
|
|
|
|
|
|
right: [...new Set((parsed.right || []) as string[])],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Kaydedilmiş düzende olmayan ama izni olan widgetları varsayılan kolonuna ekle
|
|
|
|
|
|
const allAssigned = new Set([...order.left, ...order.center, ...order.right])
|
|
|
|
|
|
widgetMetadata.forEach((w) => {
|
|
|
|
|
|
if (!allAssigned.has(w.id) && checkPermission(w.permission)) {
|
|
|
|
|
|
order[w.column as keyof typeof order].push(w.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
setWidgetOrder(order)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('Widget order parse error:', error)
|
|
|
|
|
|
initializeDefaultOrder()
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
initializeDefaultOrder()
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [grantedPolicies])
|
|
|
|
|
|
|
|
|
|
|
|
// Widget sıralamasını kaydet
|
|
|
|
|
|
const saveWidgetOrder = (newOrder: Record<string, string[]>) => {
|
|
|
|
|
|
setWidgetOrder(newOrder)
|
|
|
|
|
|
localStorage.setItem(WIDGET_ORDER_KEY, JSON.stringify(newOrder))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDragStart = (e: React.DragEvent, widgetId: string, column: string) => {
|
|
|
|
|
|
setDragState({ draggedId: widgetId, targetColumn: null, targetIndex: null })
|
|
|
|
|
|
e.dataTransfer.effectAllowed = 'move'
|
|
|
|
|
|
e.dataTransfer.setData('widgetId', widgetId)
|
|
|
|
|
|
e.dataTransfer.setData('sourceColumn', column)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDragEnterWidget = (e: React.DragEvent, column: string, index: number) => {
|
|
|
|
|
|
// Sadece widget'ın üst kısmına yakınsa indicator göster
|
|
|
|
|
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
|
|
|
|
|
const mouseY = e.clientY
|
|
|
|
|
|
const widgetTop = rect.top
|
|
|
|
|
|
const widgetHeight = rect.height
|
|
|
|
|
|
const threshold = widgetHeight * 0.3 // Üst %30'luk alan
|
|
|
|
|
|
|
|
|
|
|
|
if (mouseY - widgetTop < threshold) {
|
|
|
|
|
|
// Üst kısma yakın - indicator göster
|
|
|
|
|
|
setDragState((prev) => ({ ...prev, targetColumn: column, targetIndex: index }))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Widget'ın ortasında veya altında - indicator gösterme
|
|
|
|
|
|
setDragState((prev) => ({ ...prev, targetColumn: column, targetIndex: null }))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDragEnterColumn = (column: string) => {
|
|
|
|
|
|
setDragState((prev) => ({ ...prev, targetColumn: column, targetIndex: null }))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDragLeaveColumn = () => {
|
|
|
|
|
|
setDragState((prev) => ({ ...prev, targetColumn: null, targetIndex: null }))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDrop = (e: React.DragEvent, targetColumn: string, targetIndex?: number) => {
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
|
|
|
|
|
|
const widgetId = e.dataTransfer.getData('widgetId')
|
|
|
|
|
|
const sourceColumn = e.dataTransfer.getData('sourceColumn')
|
|
|
|
|
|
|
|
|
|
|
|
if (!widgetId || !sourceColumn) return
|
|
|
|
|
|
|
|
|
|
|
|
const newOrder = { ...widgetOrder }
|
|
|
|
|
|
|
|
|
|
|
|
// ÖNCE tüm kolonlardan bu widget'ı kaldır (duplicate önleme)
|
|
|
|
|
|
Object.keys(newOrder).forEach((col) => {
|
|
|
|
|
|
newOrder[col] = newOrder[col].filter((id) => id !== widgetId)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// SONRA hedef kolona ekle
|
|
|
|
|
|
if (targetIndex !== undefined) {
|
|
|
|
|
|
newOrder[targetColumn].splice(targetIndex, 0, widgetId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newOrder[targetColumn].push(widgetId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Duplicate'leri temizle
|
|
|
|
|
|
Object.keys(newOrder).forEach((col) => {
|
|
|
|
|
|
newOrder[col] = [...new Set(newOrder[col])]
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
saveWidgetOrder(newOrder)
|
|
|
|
|
|
setDragState({ draggedId: null, targetColumn: null, targetIndex: null })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDragEnd = () => {
|
|
|
|
|
|
setDragState({ draggedId: null, targetColumn: null, targetIndex: null })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Widget component'ını render et
|
|
|
|
|
|
const renderWidgetComponent = (widgetId: string) => {
|
|
|
|
|
|
switch (widgetId) {
|
2026-05-06 14:48:44 +00:00
|
|
|
|
case 'upcoming-events':
|
2026-05-07 14:25:06 +00:00
|
|
|
|
return <UpcomingEvents events={intranetDashboard?.events || []} onEventClick={setSelectedEvent} />
|
2026-05-05 17:59:30 +00:00
|
|
|
|
case 'today-birthdays':
|
|
|
|
|
|
return <TodayBirthdays employees={intranetDashboard?.birthdays || []} />
|
|
|
|
|
|
case 'documents':
|
|
|
|
|
|
return <RecentDocuments documents={intranetDashboard?.documents || []} />
|
|
|
|
|
|
case 'announcements':
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Announcements
|
|
|
|
|
|
announcements={intranetDashboard?.announcements || []}
|
|
|
|
|
|
onAnnouncementClick={setSelectedAnnouncement}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)
|
|
|
|
|
|
case 'active-surveys':
|
|
|
|
|
|
return (
|
2026-05-06 14:48:44 +00:00
|
|
|
|
<Surveys surveys={intranetDashboard?.surveys || []} onTakeSurvey={handleTakeSurvey} />
|
2026-05-05 17:59:30 +00:00
|
|
|
|
)
|
|
|
|
|
|
case 'social-wall':
|
2026-05-08 21:48:23 +00:00
|
|
|
|
return <SocialWall />
|
2026-05-05 17:59:30 +00:00
|
|
|
|
default:
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Widget'ları render et
|
|
|
|
|
|
const renderWidgets = (column: 'left' | 'center' | 'right') => {
|
|
|
|
|
|
const columnWidgets = widgetOrder[column] || []
|
|
|
|
|
|
|
|
|
|
|
|
// Duplicate'leri filtrele
|
|
|
|
|
|
const uniqueWidgets = [...new Set(columnWidgets)]
|
|
|
|
|
|
|
|
|
|
|
|
return uniqueWidgets
|
|
|
|
|
|
.map((widgetId, index) => {
|
|
|
|
|
|
const metadata = widgetMetadata.find((w) => w.id === widgetId)
|
|
|
|
|
|
if (!metadata || !checkPermission(metadata.permission)) return null
|
|
|
|
|
|
|
|
|
|
|
|
const isDragging = dragState.draggedId === widgetId
|
|
|
|
|
|
const isDropTarget = dragState.targetColumn === column && dragState.targetIndex === index
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={`${column}-${widgetId}-${index}`} className="relative group">
|
|
|
|
|
|
{/* Drop indicator - SADECE widget'ların arasına (üst %30'luk alana) gelince göster */}
|
|
|
|
|
|
{isDesignMode && isDropTarget && !isDragging && (
|
|
|
|
|
|
<div className="absolute -top-5 left-0 right-0 z-20 animate-in fade-in slide-in-from-top-2 duration-300">
|
|
|
|
|
|
{/* Çizgi */}
|
|
|
|
|
|
<div className="h-2 bg-gradient-to-r from-transparent via-blue-500 to-transparent rounded-full shadow-lg" />
|
|
|
|
|
|
{/* Badge */}
|
|
|
|
|
|
<div className="absolute -top-4 left-1/2 -translate-x-1/2 bg-gradient-to-r from-blue-500 to-blue-600 text-white text-xs px-4 py-2 rounded-full whitespace-nowrap shadow-xl font-semibold flex items-center gap-2 border-2 border-white dark:border-gray-800">
|
|
|
|
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
|
d="M19 14l-7 7m0 0l-7-7m7 7V3"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span>Buraya Bırak</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
draggable={isDesignMode}
|
|
|
|
|
|
onDragStart={(e) => {
|
|
|
|
|
|
if (isDesignMode) {
|
|
|
|
|
|
handleDragStart(e, widgetId, column)
|
|
|
|
|
|
// Drag ghost image'i gizle
|
|
|
|
|
|
const ghost = document.createElement('div')
|
|
|
|
|
|
ghost.style.opacity = '0'
|
|
|
|
|
|
e.dataTransfer.setDragImage(ghost, 0, 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDragOver={(e) => {
|
|
|
|
|
|
if (!isDesignMode) return
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
// Throttle: Sadece düzenli aralıklarla güncelle
|
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
|
if (
|
|
|
|
|
|
!e.currentTarget.dataset.lastUpdate ||
|
|
|
|
|
|
now - parseInt(e.currentTarget.dataset.lastUpdate) > 150
|
|
|
|
|
|
) {
|
|
|
|
|
|
e.currentTarget.dataset.lastUpdate = now.toString()
|
|
|
|
|
|
handleDragEnterWidget(e, column, index)
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDragLeave={(e) => {
|
|
|
|
|
|
// Widget'tan çıkınca indicator'ı kaldır
|
|
|
|
|
|
if (isDesignMode) {
|
|
|
|
|
|
setDragState((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
targetColumn: prev.targetColumn,
|
|
|
|
|
|
targetIndex: null,
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDrop={(e) => {
|
|
|
|
|
|
if (!isDesignMode) return
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
|
|
|
|
|
|
// Drop pozisyonunu hesapla
|
|
|
|
|
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
|
|
|
|
|
const mouseY = e.clientY
|
|
|
|
|
|
const widgetTop = rect.top
|
|
|
|
|
|
const widgetHeight = rect.height
|
|
|
|
|
|
const threshold = widgetHeight * 0.3
|
|
|
|
|
|
|
|
|
|
|
|
if (mouseY - widgetTop < threshold) {
|
|
|
|
|
|
// Üst kısma bırak - mevcut index'e ekle
|
|
|
|
|
|
handleDrop(e, column, index)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Alt kısma bırak - sonraki index'e ekle
|
|
|
|
|
|
handleDrop(e, column, index + 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDragEnd={handleDragEnd}
|
|
|
|
|
|
className={`
|
|
|
|
|
|
relative
|
|
|
|
|
|
${
|
|
|
|
|
|
isDesignMode
|
|
|
|
|
|
? `border-2 border-dashed rounded-lg cursor-move
|
|
|
|
|
|
${
|
|
|
|
|
|
isDragging
|
|
|
|
|
|
? 'border-blue-400 opacity-70 bg-blue-50/30 dark:bg-blue-900/10'
|
|
|
|
|
|
: 'border-gray-300 dark:border-gray-600 hover:border-blue-400 dark:hover:border-blue-500 hover:shadow-md'
|
|
|
|
|
|
}
|
|
|
|
|
|
transition-all duration-300 ease-out`
|
|
|
|
|
|
: 'border-0 transition-none'
|
|
|
|
|
|
}
|
|
|
|
|
|
`}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
touchAction: 'none',
|
|
|
|
|
|
transition:
|
|
|
|
|
|
'border-color 0.3s ease-out, opacity 0.3s ease-out, box-shadow 0.3s ease-out',
|
|
|
|
|
|
willChange: isDragging ? 'opacity' : 'auto',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* Dragging overlay - daha minimal */}
|
|
|
|
|
|
{isDesignMode && isDragging && (
|
|
|
|
|
|
<div className="absolute inset-0 bg-white/60 dark:bg-gray-900/40 rounded-lg z-10 flex items-center justify-center backdrop-blur-[1px] transition-opacity duration-300">
|
|
|
|
|
|
<div className="bg-gradient-to-r from-blue-500 to-blue-600 text-white px-4 py-2 rounded-lg font-medium shadow-lg flex items-center gap-2">
|
|
|
|
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
|
d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span>Taşınıyor</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`${isDesignMode ? 'p-1.5' : ''} transition-all duration-500 ease-out`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{renderWidgetComponent(widgetId)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Container>
|
|
|
|
|
|
<div className="mx-auto space-y-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
|
{/* Design Mode Toggle */}
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<label
|
|
|
|
|
|
htmlFor="design-mode-toggle"
|
|
|
|
|
|
className="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
|
|
|
|
|
|
>
|
|
|
|
|
|
🎨 {translate('::App.Platform.Intranet.Dashboard.DesignMode')}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<button
|
|
|
|
|
|
id="design-mode-toggle"
|
|
|
|
|
|
onClick={() => setIsDesignMode(!isDesignMode)}
|
|
|
|
|
|
className={`
|
|
|
|
|
|
relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
|
|
|
|
|
${isDesignMode ? 'bg-blue-600' : 'bg-gray-300 dark:bg-gray-600'}
|
|
|
|
|
|
`}
|
|
|
|
|
|
role="switch"
|
|
|
|
|
|
aria-checked={isDesignMode}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={`
|
|
|
|
|
|
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
|
|
|
|
|
|
${isDesignMode ? 'translate-x-6' : 'translate-x-1'}
|
|
|
|
|
|
`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Reset Button - Sadece design mode aktifken görünsün */}
|
|
|
|
|
|
{isDesignMode && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
localStorage.removeItem(WIDGET_ORDER_KEY)
|
|
|
|
|
|
initializeDefaultOrder()
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors flex items-center gap-1"
|
|
|
|
|
|
title="Widget düzenini varsayılana döndür"
|
|
|
|
|
|
>
|
|
|
|
|
|
🔄 {translate('::App.Platform.Intranet.Dashboard.Reset')}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
|
|
|
|
<span className="font-medium">{translate('::AI.Welcome')},</span>{' '}
|
|
|
|
|
|
{currentLocalDate(new Date(), currentLocale || 'tr')}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-12">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`lg:col-span-3 space-y-6 min-h-[100px] rounded-xl p-1
|
|
|
|
|
|
${
|
|
|
|
|
|
isDesignMode && dragState.targetColumn === 'left' && dragState.targetIndex === null
|
|
|
|
|
|
? 'bg-blue-50/80 dark:bg-blue-900/20 ring-2 ring-blue-300 dark:ring-blue-600 shadow-lg'
|
|
|
|
|
|
: 'bg-transparent'
|
|
|
|
|
|
}
|
|
|
|
|
|
transition-all duration-700 ease-out
|
|
|
|
|
|
`}
|
|
|
|
|
|
onDragOver={(e) => {
|
|
|
|
|
|
if (!isDesignMode) return
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
// Throttle: Sadece her 150ms'de bir güncelle
|
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
|
const target = e.currentTarget as HTMLElement
|
|
|
|
|
|
if (
|
|
|
|
|
|
!target.dataset.lastColumnUpdate ||
|
|
|
|
|
|
now - parseInt(target.dataset.lastColumnUpdate) > 150
|
|
|
|
|
|
) {
|
|
|
|
|
|
target.dataset.lastColumnUpdate = now.toString()
|
|
|
|
|
|
handleDragEnterColumn('left')
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDragLeave={() => {
|
|
|
|
|
|
if (isDesignMode) handleDragLeaveColumn()
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDrop={(e) => {
|
|
|
|
|
|
if (!isDesignMode) return
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
const columnWidgets = widgetOrder['left'] || []
|
|
|
|
|
|
handleDrop(e, 'left', columnWidgets.length)
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{renderWidgets('left')}
|
|
|
|
|
|
{isDesignMode &&
|
|
|
|
|
|
dragState.targetColumn === 'left' &&
|
|
|
|
|
|
widgetOrder['left']?.length === 0 && (
|
|
|
|
|
|
<div className="flex items-center justify-center h-40 border-2 border-dashed border-blue-300 dark:border-blue-600 rounded-xl bg-blue-50/50 dark:bg-blue-900/10 transition-all duration-500 ease-out">
|
|
|
|
|
|
<p className="text-blue-600 dark:text-blue-400 font-medium flex items-center gap-2">
|
|
|
|
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
|
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span>Widget'ı buraya bırakın</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`lg:col-span-6 space-y-6 min-h-[100px] rounded-xl p-1
|
|
|
|
|
|
${
|
|
|
|
|
|
isDesignMode &&
|
|
|
|
|
|
dragState.targetColumn === 'center' &&
|
|
|
|
|
|
dragState.targetIndex === null
|
|
|
|
|
|
? 'bg-blue-50/80 dark:bg-blue-900/20 ring-2 ring-blue-300 dark:ring-blue-600 shadow-lg'
|
|
|
|
|
|
: 'bg-transparent'
|
|
|
|
|
|
}
|
|
|
|
|
|
transition-all duration-700 ease-out
|
|
|
|
|
|
`}
|
|
|
|
|
|
onDragOver={(e) => {
|
|
|
|
|
|
if (!isDesignMode) return
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
|
const target = e.currentTarget as HTMLElement
|
|
|
|
|
|
if (
|
|
|
|
|
|
!target.dataset.lastColumnUpdate ||
|
|
|
|
|
|
now - parseInt(target.dataset.lastColumnUpdate) > 150
|
|
|
|
|
|
) {
|
|
|
|
|
|
target.dataset.lastColumnUpdate = now.toString()
|
|
|
|
|
|
handleDragEnterColumn('center')
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDragLeave={() => {
|
|
|
|
|
|
if (isDesignMode) handleDragLeaveColumn()
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDrop={(e) => {
|
|
|
|
|
|
if (!isDesignMode) return
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
const columnWidgets = widgetOrder['center'] || []
|
|
|
|
|
|
handleDrop(e, 'center', columnWidgets.length)
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{renderWidgets('center')}
|
|
|
|
|
|
{isDesignMode &&
|
|
|
|
|
|
dragState.targetColumn === 'center' &&
|
|
|
|
|
|
widgetOrder['center']?.length === 0 && (
|
|
|
|
|
|
<div className="flex items-center justify-center rounded-xl bg-blue-50/50 dark:bg-blue-900/10 transition-all duration-500 ease-out">
|
|
|
|
|
|
<p className="text-blue-600 dark:text-blue-400 font-medium flex items-center gap-2">
|
|
|
|
|
|
<span>Widget'ı buraya bırakın</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`lg:col-span-3 space-y-6 min-h-[100px] rounded-xl p-1
|
|
|
|
|
|
${
|
|
|
|
|
|
isDesignMode && dragState.targetColumn === 'right' && dragState.targetIndex === null
|
|
|
|
|
|
? 'bg-blue-50/80 dark:bg-blue-900/20 ring-2 ring-blue-300 dark:ring-blue-600 shadow-lg'
|
|
|
|
|
|
: 'bg-transparent'
|
|
|
|
|
|
}
|
|
|
|
|
|
transition-all duration-700 ease-out
|
|
|
|
|
|
`}
|
|
|
|
|
|
onDragOver={(e) => {
|
|
|
|
|
|
if (!isDesignMode) return
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
|
const target = e.currentTarget as HTMLElement
|
|
|
|
|
|
if (
|
|
|
|
|
|
!target.dataset.lastColumnUpdate ||
|
|
|
|
|
|
now - parseInt(target.dataset.lastColumnUpdate) > 150
|
|
|
|
|
|
) {
|
|
|
|
|
|
target.dataset.lastColumnUpdate = now.toString()
|
|
|
|
|
|
handleDragEnterColumn('right')
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDragLeave={() => {
|
|
|
|
|
|
if (isDesignMode) handleDragLeaveColumn()
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDrop={(e) => {
|
|
|
|
|
|
if (!isDesignMode) return
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
const columnWidgets = widgetOrder['right'] || []
|
|
|
|
|
|
handleDrop(e, 'right', columnWidgets.length)
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{renderWidgets('right')}
|
|
|
|
|
|
{isDesignMode &&
|
|
|
|
|
|
dragState.targetColumn === 'right' &&
|
|
|
|
|
|
widgetOrder['right']?.length === 0 && (
|
|
|
|
|
|
<div className="flex items-center justify-center h-40 border-2 border-dashed border-blue-300 dark:border-blue-600 rounded-xl bg-blue-50/50 dark:bg-blue-900/10 transition-all duration-500 ease-out">
|
|
|
|
|
|
<p className="text-blue-600 dark:text-blue-400 font-medium flex items-center gap-2">
|
|
|
|
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
|
<path
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
|
strokeWidth={2}
|
|
|
|
|
|
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
<span>Widget'ı buraya bırakın</span>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<AnimatePresence>
|
|
|
|
|
|
{showSurveyModal && selectedSurvey && (
|
|
|
|
|
|
<SurveyModal
|
|
|
|
|
|
survey={selectedSurvey}
|
|
|
|
|
|
onClose={() => setShowSurveyModal(false)}
|
|
|
|
|
|
onSubmit={handleSubmitSurvey}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
|
|
2026-05-07 14:25:06 +00:00
|
|
|
|
<AnimatePresence>
|
|
|
|
|
|
{selectedEvent && (
|
|
|
|
|
|
<EventModal
|
|
|
|
|
|
event={selectedEvent}
|
|
|
|
|
|
onClose={() => setSelectedEvent(null)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
|
|
2026-05-05 17:59:30 +00:00
|
|
|
|
<AnimatePresence>
|
|
|
|
|
|
{selectedAnnouncement && (
|
|
|
|
|
|
<AnnouncementModal
|
|
|
|
|
|
announcement={selectedAnnouncement}
|
|
|
|
|
|
onClose={() => setSelectedAnnouncement(null)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
|
</Container>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default IntranetDashboard
|