Intranet Dashboard düzenlemesi
This commit is contained in:
parent
5be636531a
commit
533480cc04
1 changed files with 535 additions and 36 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { AnimatePresence } from 'framer-motion'
|
import { AnimatePresence } from 'framer-motion'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import 'dayjs/locale/tr'
|
import 'dayjs/locale/tr'
|
||||||
|
|
@ -39,7 +39,17 @@ dayjs.locale('tr')
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
dayjs.extend(isBetween)
|
dayjs.extend(isBetween)
|
||||||
|
|
||||||
|
interface WidgetConfig {
|
||||||
|
id: string
|
||||||
|
permission: string
|
||||||
|
component: React.ReactNode
|
||||||
|
column: 'left' | 'center' | 'right'
|
||||||
|
}
|
||||||
|
|
||||||
|
const WIDGET_ORDER_KEY = 'dashboard-widget-order'
|
||||||
|
|
||||||
const IntranetDashboard: React.FC = () => {
|
const IntranetDashboard: React.FC = () => {
|
||||||
|
const { checkPermission } = usePermission()
|
||||||
const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null)
|
const [selectedAnnouncement, setSelectedAnnouncement] = useState<Announcement | null>(null)
|
||||||
const [selectedSurvey, setSelectedSurvey] = useState<Survey | null>(null)
|
const [selectedSurvey, setSelectedSurvey] = useState<Survey | null>(null)
|
||||||
const [showSurveyModal, setShowSurveyModal] = useState(false)
|
const [showSurveyModal, setShowSurveyModal] = useState(false)
|
||||||
|
|
@ -47,7 +57,22 @@ const IntranetDashboard: React.FC = () => {
|
||||||
const [showOvertimeModal, setShowOvertimeModal] = useState(false)
|
const [showOvertimeModal, setShowOvertimeModal] = useState(false)
|
||||||
const [showExpenseModal, setShowExpenseModal] = useState(false)
|
const [showExpenseModal, setShowExpenseModal] = useState(false)
|
||||||
const [showReservationModal, setShowReservationModal] = useState(false)
|
const [showReservationModal, setShowReservationModal] = useState(false)
|
||||||
const { checkPermission } = usePermission()
|
const [isDesignMode, setIsDesignMode] = useState(false)
|
||||||
|
const [widgetOrder, setWidgetOrder] = useState<Record<string, string[]>>({
|
||||||
|
left: [],
|
||||||
|
center: [],
|
||||||
|
right: [],
|
||||||
|
})
|
||||||
|
// 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: Survey) => {
|
const handleTakeSurvey = (survey: Survey) => {
|
||||||
setSelectedSurvey(survey)
|
setSelectedSurvey(survey)
|
||||||
|
|
@ -55,8 +80,6 @@ const IntranetDashboard: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmitSurvey = (answers: SurveyAnswer[]) => {
|
const handleSubmitSurvey = (answers: SurveyAnswer[]) => {
|
||||||
console.log('Survey submitted with answers:', answers)
|
|
||||||
// Burada survey cevapları API'ye gönderilecek
|
|
||||||
setShowSurveyModal(false)
|
setShowSurveyModal(false)
|
||||||
setSelectedSurvey(null)
|
setSelectedSurvey(null)
|
||||||
}
|
}
|
||||||
|
|
@ -77,10 +100,371 @@ const IntranetDashboard: React.FC = () => {
|
||||||
setShowReservationModal(false)
|
setShowReservationModal(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Widget metadata (component'lar yerine sadece meta bilgiler)
|
||||||
|
const widgetMetadata = [
|
||||||
|
{ id: 'upcoming-events', permission: 'App.Intranet.Events.Event.Widget', column: 'left' },
|
||||||
|
{ id: 'today-birthdays', permission: 'App.Hr.Employee.Widget', column: 'left' },
|
||||||
|
{ id: 'recent-documents', permission: 'App.Files.Widget', column: 'left' },
|
||||||
|
{ id: 'upcoming-trainings', permission: 'App.Hr.Training.Widget', column: 'left' },
|
||||||
|
{ id: 'active-reservations', permission: 'App.Intranet.Reservation.Widget', column: 'left' },
|
||||||
|
{ id: 'active-surveys', permission: 'App.Intranet.Survey.Widget', column: 'left' },
|
||||||
|
{ id: 'visitors', permission: 'App.Intranet.Visitor.Widget', column: 'left' },
|
||||||
|
{ id: 'expense-management', permission: 'App.Hr.Expense.Widget', column: 'left' },
|
||||||
|
{ id: 'social-wall', permission: 'App.Intranet.SocialPost.Widget', column: 'center' },
|
||||||
|
{
|
||||||
|
id: 'important-announcements',
|
||||||
|
permission: 'App.Intranet.Announcement.Widget',
|
||||||
|
column: 'right',
|
||||||
|
},
|
||||||
|
{ id: 'priority-tasks', permission: 'App.Projects.Tasks.Widget', column: 'right' },
|
||||||
|
{ id: 'meal-weekly-menu', permission: 'App.Intranet.Meal.Widget', column: 'right' },
|
||||||
|
{ id: 'shuttle-schedule', permission: 'App.Intranet.ShuttleRoute.Widget', column: 'right' },
|
||||||
|
{ id: 'leave-management', permission: 'App.Hr.Leave.Widget', column: 'right' },
|
||||||
|
{ id: 'overtime-management', permission: 'App.Hr.Overtime.Widget', column: 'right' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Widget sıralamasını yükle
|
||||||
|
useEffect(() => {
|
||||||
|
const savedOrder = localStorage.getItem(WIDGET_ORDER_KEY)
|
||||||
|
if (savedOrder) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(savedOrder) as Record<string, unknown[]>
|
||||||
|
// Duplicate key'leri temizle
|
||||||
|
const cleanedOrder: Record<string, string[]> = {
|
||||||
|
left: [...new Set((parsed.left || []) as string[])],
|
||||||
|
center: [...new Set((parsed.center || []) as string[])],
|
||||||
|
right: [...new Set((parsed.right || []) as string[])],
|
||||||
|
}
|
||||||
|
setWidgetOrder(cleanedOrder)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Widget order parse error:', error)
|
||||||
|
initializeDefaultOrder()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
initializeDefaultOrder()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
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ı 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 handleDragOver = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.dataTransfer.dropEffect = 'move'
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
case 'upcoming-events':
|
||||||
|
return <UpcomingEvents />
|
||||||
|
case 'today-birthdays':
|
||||||
|
return <TodayBirthdays />
|
||||||
|
case 'recent-documents':
|
||||||
|
return <RecentDocuments />
|
||||||
|
case 'upcoming-trainings':
|
||||||
|
return <UpcomingTrainings />
|
||||||
|
case 'active-reservations':
|
||||||
|
return <ActiveReservations onNewReservation={() => setShowReservationModal(true)} />
|
||||||
|
case 'active-surveys':
|
||||||
|
return <ActiveSurveys onTakeSurvey={handleTakeSurvey} />
|
||||||
|
case 'visitors':
|
||||||
|
return <Visitors />
|
||||||
|
case 'expense-management':
|
||||||
|
return <ExpenseManagement onNewExpense={() => setShowExpenseModal(true)} />
|
||||||
|
case 'social-wall':
|
||||||
|
return <SocialWall />
|
||||||
|
case 'important-announcements':
|
||||||
|
return <ImportantAnnouncements onAnnouncementClick={setSelectedAnnouncement} />
|
||||||
|
case 'priority-tasks':
|
||||||
|
return <PriorityTasks />
|
||||||
|
case 'meal-weekly-menu':
|
||||||
|
return <MealWeeklyMenu />
|
||||||
|
case 'shuttle-schedule':
|
||||||
|
return <ShuttleSchedule />
|
||||||
|
case 'leave-management':
|
||||||
|
return <LeaveManagement onNewLeave={() => setShowLeaveModal(true)} />
|
||||||
|
case 'overtime-management':
|
||||||
|
return <OvertimeManagement onNewOvertime={() => setShowOvertimeModal(true)} />
|
||||||
|
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 (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<div className="mx-auto space-y-4">
|
<div className="mx-auto space-y-4">
|
||||||
<div className="flex items-center justify-end">
|
<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"
|
||||||
|
>
|
||||||
|
🎨 Dizayn Modu
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
🔄 Sıfırla
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||||
<span className="font-medium">Hoş geldiniz,</span>{' '}
|
<span className="font-medium">Hoş geldiniz,</span>{' '}
|
||||||
|
|
@ -89,41 +473,156 @@ const IntranetDashboard: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-11 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-12">
|
||||||
<div className="lg:col-span-3 space-y-6">
|
<div
|
||||||
{checkPermission('App.Intranet.Events.Event.Widget') && <UpcomingEvents />}
|
className={`lg:col-span-3 space-y-6 min-h-[100px] rounded-xl p-1
|
||||||
{checkPermission('App.Hr.Employee.Widget') && <TodayBirthdays />}
|
${
|
||||||
{checkPermission('App.Files.Widget') && <RecentDocuments />}
|
isDesignMode && dragState.targetColumn === 'left' && dragState.targetIndex === null
|
||||||
{checkPermission('App.Hr.Training.Widget') && <UpcomingTrainings />}
|
? 'bg-blue-50/80 dark:bg-blue-900/20 ring-2 ring-blue-300 dark:ring-blue-600 shadow-lg'
|
||||||
{checkPermission('App.Intranet.Reservation.Widget') && (
|
: 'bg-transparent'
|
||||||
<ActiveReservations onNewReservation={() => setShowReservationModal(true)} />
|
}
|
||||||
)}
|
transition-all duration-700 ease-out
|
||||||
{checkPermission('App.Intranet.Survey.Widget') && (
|
`}
|
||||||
<ActiveSurveys onTakeSurvey={handleTakeSurvey} />
|
onDragOver={(e) => {
|
||||||
)}
|
if (!isDesignMode) return
|
||||||
{checkPermission('App.Intranet.Visitor.Widget') && <Visitors />}
|
e.preventDefault()
|
||||||
{checkPermission('App.Hr.Expense.Widget') && (
|
// Throttle: Sadece her 150ms'de bir güncelle
|
||||||
<ExpenseManagement onNewExpense={() => setShowExpenseModal(true)} />
|
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>
|
||||||
|
|
||||||
<div className="lg:col-span-5 space-y-6">
|
<div
|
||||||
{checkPermission('App.Intranet.SocialPost.Widget') && ( <SocialWall />) }
|
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>
|
||||||
|
|
||||||
<div className="lg:col-span-3 space-y-6">
|
<div
|
||||||
{checkPermission('App.Intranet.Announcement.Widget') && (
|
className={`lg:col-span-3 space-y-6 min-h-[100px] rounded-xl p-1
|
||||||
<ImportantAnnouncements onAnnouncementClick={setSelectedAnnouncement} />
|
${
|
||||||
)}
|
isDesignMode && dragState.targetColumn === 'right' && dragState.targetIndex === null
|
||||||
{checkPermission('App.Projects.Tasks.Widget') && <PriorityTasks />}
|
? 'bg-blue-50/80 dark:bg-blue-900/20 ring-2 ring-blue-300 dark:ring-blue-600 shadow-lg'
|
||||||
{checkPermission('App.Intranet.Meal.Widget') && <MealWeeklyMenu />}
|
: 'bg-transparent'
|
||||||
{checkPermission('App.Intranet.ShuttleRoute.Widget') && <ShuttleSchedule />}
|
}
|
||||||
{checkPermission('App.Hr.Leave.Widget') && (
|
transition-all duration-700 ease-out
|
||||||
<LeaveManagement onNewLeave={() => setShowLeaveModal(true)} />
|
`}
|
||||||
)}
|
onDragOver={(e) => {
|
||||||
{checkPermission('App.Hr.Overtime.Widget') && (
|
if (!isDesignMode) return
|
||||||
<OvertimeManagement onNewOvertime={() => setShowOvertimeModal(true)} />
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue