diff --git a/ui/src/views/intranet/Dashboard.tsx b/ui/src/views/intranet/Dashboard.tsx index 1ef84d81..0a40b553 100644 --- a/ui/src/views/intranet/Dashboard.tsx +++ b/ui/src/views/intranet/Dashboard.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import { AnimatePresence } from 'framer-motion' import dayjs from 'dayjs' import 'dayjs/locale/tr' @@ -39,7 +39,17 @@ dayjs.locale('tr') dayjs.extend(relativeTime) 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 { checkPermission } = usePermission() const [selectedAnnouncement, setSelectedAnnouncement] = useState(null) const [selectedSurvey, setSelectedSurvey] = useState(null) const [showSurveyModal, setShowSurveyModal] = useState(false) @@ -47,7 +57,22 @@ const IntranetDashboard: React.FC = () => { const [showOvertimeModal, setShowOvertimeModal] = useState(false) const [showExpenseModal, setShowExpenseModal] = useState(false) const [showReservationModal, setShowReservationModal] = useState(false) - const { checkPermission } = usePermission() + const [isDesignMode, setIsDesignMode] = useState(false) + const [widgetOrder, setWidgetOrder] = useState>({ + 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) => { setSelectedSurvey(survey) @@ -55,8 +80,6 @@ const IntranetDashboard: React.FC = () => { } const handleSubmitSurvey = (answers: SurveyAnswer[]) => { - console.log('Survey submitted with answers:', answers) - // Burada survey cevapları API'ye gönderilecek setShowSurveyModal(false) setSelectedSurvey(null) } @@ -77,10 +100,371 @@ const IntranetDashboard: React.FC = () => { 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 + // Duplicate key'leri temizle + const cleanedOrder: Record = { + 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) => { + 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 + case 'today-birthdays': + return + case 'recent-documents': + return + case 'upcoming-trainings': + return + case 'active-reservations': + return setShowReservationModal(true)} /> + case 'active-surveys': + return + case 'visitors': + return + case 'expense-management': + return setShowExpenseModal(true)} /> + case 'social-wall': + return + case 'important-announcements': + return + case 'priority-tasks': + return + case 'meal-weekly-menu': + return + case 'shuttle-schedule': + return + case 'leave-management': + return setShowLeaveModal(true)} /> + case 'overtime-management': + return 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 ( +
+ {/* Drop indicator - SADECE widget'ların arasına (üst %30'luk alana) gelince göster */} + {isDesignMode && isDropTarget && !isDragging && ( +
+ {/* Çizgi */} +
+ {/* Badge */} +
+ + + + Buraya Bırak +
+
+ )} + +
{ + 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 && ( +
+
+ + + + Taşınıyor +
+
+ )} +
+ {renderWidgetComponent(widgetId)} +
+
+
+ ) + }) + .filter(Boolean) + } + return (
-
+
+
+ {/* Design Mode Toggle */} +
+ + +
+ + {/* Reset Button - Sadece design mode aktifken görünsün */} + {isDesignMode && ( + + )} +
+

Hoş geldiniz,{' '} @@ -89,41 +473,156 @@ const IntranetDashboard: React.FC = () => {

-
-
- {checkPermission('App.Intranet.Events.Event.Widget') && } - {checkPermission('App.Hr.Employee.Widget') && } - {checkPermission('App.Files.Widget') && } - {checkPermission('App.Hr.Training.Widget') && } - {checkPermission('App.Intranet.Reservation.Widget') && ( - setShowReservationModal(true)} /> - )} - {checkPermission('App.Intranet.Survey.Widget') && ( - - )} - {checkPermission('App.Intranet.Visitor.Widget') && } - {checkPermission('App.Hr.Expense.Widget') && ( - setShowExpenseModal(true)} /> - )} +
+
{ + 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 && ( +
+

+ + + + Widget'ı buraya bırakın +

+
+ )}
-
- {checkPermission('App.Intranet.SocialPost.Widget') && ( ) } +
{ + 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 && ( +
+

+ Widget'ı buraya bırakın +

+
+ )}
-
- {checkPermission('App.Intranet.Announcement.Widget') && ( - - )} - {checkPermission('App.Projects.Tasks.Widget') && } - {checkPermission('App.Intranet.Meal.Widget') && } - {checkPermission('App.Intranet.ShuttleRoute.Widget') && } - {checkPermission('App.Hr.Leave.Widget') && ( - setShowLeaveModal(true)} /> - )} - {checkPermission('App.Hr.Overtime.Widget') && ( - setShowOvertimeModal(true)} /> - )} +
{ + 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 && ( +
+

+ + + + Widget'ı buraya bırakın +

+
+ )}