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'
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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 { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
|
import useLocale from '@/utils/hooks/useLocale'
|
|
|
|
|
|
import { currentLocalDate } from '@/utils/dateUtils'
|
2026-07-09 17:55:07 +00:00
|
|
|
|
import { useStoreActions, useStoreState } from '@/store/store'
|
2026-06-25 22:55:44 +00:00
|
|
|
|
import Button from '@/components/ui/Button'
|
2026-07-01 17:37:44 +00:00
|
|
|
|
import { LuX } from 'react-icons/lu'
|
|
|
|
|
|
import type { DashboardWidgetColumn, DashboardWidgetDefinition } from './dashboardWidget'
|
2026-05-05 17:59:30 +00:00
|
|
|
|
|
|
|
|
|
|
dayjs.extend(relativeTime)
|
|
|
|
|
|
dayjs.extend(isBetween)
|
|
|
|
|
|
dayjs.extend(localizedFormat)
|
|
|
|
|
|
|
2026-07-01 17:37:44 +00:00
|
|
|
|
const dashboardColumns: DashboardWidgetColumn[] = ['left', 'center', 'right']
|
|
|
|
|
|
|
|
|
|
|
|
type DashboardWidgetModule = { dashboardWidget?: DashboardWidgetDefinition }
|
|
|
|
|
|
|
|
|
|
|
|
const dashboardWidgets = Object.values(
|
|
|
|
|
|
import.meta.glob<DashboardWidgetModule>(['./widgets/*.tsx', './SocialWall/index.tsx'], {
|
|
|
|
|
|
eager: true,
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
|
|
|
|
|
.map((module) => module.dashboardWidget)
|
|
|
|
|
|
.filter((widget): widget is DashboardWidgetDefinition => Boolean(widget))
|
|
|
|
|
|
.sort((left, right) => left.order - right.order)
|
|
|
|
|
|
|
|
|
|
|
|
const layoutPresets = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'default',
|
|
|
|
|
|
columns: [3, 6, 3],
|
|
|
|
|
|
labelKey: '::App.Platform.Intranet.Dashboard.Layout.Default',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'wide-center',
|
|
|
|
|
|
columns: [2, 8, 2],
|
|
|
|
|
|
labelKey: '::App.Platform.Intranet.Dashboard.Layout.CenterFocus',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'balanced',
|
|
|
|
|
|
columns: [4, 4, 4],
|
|
|
|
|
|
labelKey: '::App.Platform.Intranet.Dashboard.Layout.Balanced',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'wide-left',
|
|
|
|
|
|
columns: [4, 6, 2],
|
|
|
|
|
|
labelKey: '::App.Platform.Intranet.Dashboard.Layout.WideLeft',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: 'wide-right',
|
|
|
|
|
|
columns: [2, 6, 4],
|
|
|
|
|
|
labelKey: '::App.Platform.Intranet.Dashboard.Layout.WideRight',
|
|
|
|
|
|
},
|
|
|
|
|
|
] as const
|
|
|
|
|
|
|
|
|
|
|
|
const columnSpanClasses: Record<number, string> = {
|
|
|
|
|
|
2: 'lg:col-span-2',
|
|
|
|
|
|
3: 'lg:col-span-3',
|
|
|
|
|
|
4: 'lg:col-span-4',
|
|
|
|
|
|
6: 'lg:col-span-6',
|
|
|
|
|
|
8: 'lg:col-span-8',
|
|
|
|
|
|
}
|
2026-05-05 17:59:30 +00:00
|
|
|
|
|
|
|
|
|
|
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)
|
2026-07-09 17:55:07 +00:00
|
|
|
|
const { dashboardLayout: selectedLayout, hiddenWidgetIds, widgetOrder } = useStoreState(
|
|
|
|
|
|
(state) => state.admin.dashboard,
|
|
|
|
|
|
)
|
|
|
|
|
|
const {
|
|
|
|
|
|
setDashboardLayout,
|
|
|
|
|
|
setHiddenWidgetIds,
|
|
|
|
|
|
setWidgetOrder,
|
|
|
|
|
|
reset: resetDashboard,
|
|
|
|
|
|
} = useStoreActions((actions) => actions.admin.dashboard)
|
2026-05-05 17:59:30 +00:00
|
|
|
|
|
|
|
|
|
|
const [intranetDashboard, setIntranetDashboard] = useState<IntranetDashboardDto>()
|
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
|
const currentLocale = useLocale()
|
|
|
|
|
|
|
|
|
|
|
|
const fetchIntranetDashboard = async () => {
|
2026-08-11 13:18:17 +00:00
|
|
|
|
try {
|
|
|
|
|
|
const dashboard = await intranetService.getDashboard()
|
|
|
|
|
|
if (dashboard.data) {
|
|
|
|
|
|
setIntranetDashboard(dashboard.data)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// hata apiService tarafından ele alınıyor
|
2026-05-05 17:59:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchIntranetDashboard()
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
const [dragState, setDragState] = useState<{
|
|
|
|
|
|
draggedId: string | null
|
2026-07-01 17:37:44 +00:00
|
|
|
|
targetColumn: DashboardWidgetColumn | null
|
2026-05-05 17:59:30 +00:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 19:02:53 +00:00
|
|
|
|
const handleEventLikeChange = (updatedEvent: EventDto) => {
|
|
|
|
|
|
setSelectedEvent((current) => (current ? { ...current, ...updatedEvent } : updatedEvent))
|
|
|
|
|
|
setIntranetDashboard((current) =>
|
|
|
|
|
|
current
|
|
|
|
|
|
? {
|
|
|
|
|
|
...current,
|
|
|
|
|
|
events: current.events.map((event) =>
|
|
|
|
|
|
event.id === updatedEvent.id ? { ...event, ...updatedEvent } : event,
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
: current,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleAnnouncementLikeChange = (updatedAnnouncement: AnnouncementDto) => {
|
|
|
|
|
|
setSelectedAnnouncement((current) =>
|
|
|
|
|
|
current ? { ...current, ...updatedAnnouncement } : updatedAnnouncement,
|
|
|
|
|
|
)
|
|
|
|
|
|
setIntranetDashboard((current) =>
|
|
|
|
|
|
current
|
|
|
|
|
|
? {
|
|
|
|
|
|
...current,
|
|
|
|
|
|
announcements: current.announcements.map((announcement) =>
|
|
|
|
|
|
announcement.id === updatedAnnouncement.id
|
|
|
|
|
|
? { ...announcement, ...updatedAnnouncement }
|
|
|
|
|
|
: announcement,
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
: current,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:59:30 +00:00
|
|
|
|
const grantedPolicies = useStoreState((state) => state.abpConfig?.config?.auth.grantedPolicies)
|
|
|
|
|
|
|
|
|
|
|
|
const initializeDefaultOrder = () => {
|
2026-07-01 17:37:44 +00:00
|
|
|
|
const defaultOrder: Record<DashboardWidgetColumn, string[]> = {
|
|
|
|
|
|
left: [],
|
|
|
|
|
|
center: [],
|
|
|
|
|
|
right: [],
|
2026-05-05 17:59:30 +00:00
|
|
|
|
}
|
2026-07-01 17:37:44 +00:00
|
|
|
|
dashboardWidgets.forEach((widget) => {
|
|
|
|
|
|
if (checkPermission(widget.permission)) defaultOrder[widget.column].push(widget.id)
|
|
|
|
|
|
})
|
2026-05-05 17:59:30 +00:00
|
|
|
|
setWidgetOrder(defaultOrder)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!grantedPolicies) return
|
|
|
|
|
|
|
2026-07-09 17:55:07 +00:00
|
|
|
|
const hasSavedOrder = dashboardColumns.some((column) => widgetOrder[column].length > 0)
|
2026-08-11 13:18:17 +00:00
|
|
|
|
if (!hasSavedOrder) {
|
2026-05-05 17:59:30 +00:00
|
|
|
|
initializeDefaultOrder()
|
2026-08-11 13:18:17 +00:00
|
|
|
|
return
|
2026-05-05 17:59:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 13:18:17 +00:00
|
|
|
|
// Kayıtlı sıralamayı tekilleştir ve henüz yerleştirilmemiş widget'ları ekle
|
|
|
|
|
|
const order: Record<DashboardWidgetColumn, string[]> = {
|
|
|
|
|
|
left: [...new Set(widgetOrder.left || [])],
|
|
|
|
|
|
center: [...new Set(widgetOrder.center || [])],
|
|
|
|
|
|
right: [...new Set(widgetOrder.right || [])],
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const allAssigned = new Set([...order.left, ...order.center, ...order.right])
|
|
|
|
|
|
dashboardWidgets.forEach((widget) => {
|
|
|
|
|
|
if (!allAssigned.has(widget.id) && checkPermission(widget.permission)) {
|
|
|
|
|
|
order[widget.column].push(widget.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
setWidgetOrder(order)
|
|
|
|
|
|
}, [grantedPolicies])
|
2026-05-05 17:59:30 +00:00
|
|
|
|
|
2026-07-01 17:37:44 +00:00
|
|
|
|
const setWidgetVisibility = (widgetId: string, visible: boolean) => {
|
2026-07-09 17:55:07 +00:00
|
|
|
|
const next = visible
|
|
|
|
|
|
? hiddenWidgetIds.filter((id) => id !== widgetId)
|
|
|
|
|
|
: [...new Set([...hiddenWidgetIds, widgetId])]
|
|
|
|
|
|
setHiddenWidgetIds(next)
|
2026-07-01 17:37:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const activeLayout =
|
|
|
|
|
|
layoutPresets.find((layout) => layout.id === selectedLayout) || layoutPresets[0]
|
|
|
|
|
|
const hiddenWidgets = dashboardWidgets.filter(
|
|
|
|
|
|
(widget) => hiddenWidgetIds.includes(widget.id) && checkPermission(widget.permission),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const handleDragStart = (e: React.DragEvent, widgetId: string) => {
|
2026-05-05 17:59:30 +00:00
|
|
|
|
setDragState({ draggedId: widgetId, targetColumn: null, targetIndex: null })
|
|
|
|
|
|
e.dataTransfer.effectAllowed = 'move'
|
|
|
|
|
|
e.dataTransfer.setData('widgetId', widgetId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 17:37:44 +00:00
|
|
|
|
const handleDragEnterWidget = (
|
|
|
|
|
|
e: React.DragEvent,
|
|
|
|
|
|
column: DashboardWidgetColumn,
|
|
|
|
|
|
index: number,
|
|
|
|
|
|
) => {
|
2026-05-05 17:59:30 +00:00
|
|
|
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
2026-07-01 17:37:44 +00:00
|
|
|
|
if (e.clientY - rect.top < rect.height * 0.3) {
|
2026-05-05 17:59:30 +00:00
|
|
|
|
setDragState((prev) => ({ ...prev, targetColumn: column, targetIndex: index }))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setDragState((prev) => ({ ...prev, targetColumn: column, targetIndex: null }))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 17:37:44 +00:00
|
|
|
|
const handleDragEnterColumn = (column: DashboardWidgetColumn) => {
|
2026-05-05 17:59:30 +00:00
|
|
|
|
setDragState((prev) => ({ ...prev, targetColumn: column, targetIndex: null }))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDragLeaveColumn = () => {
|
|
|
|
|
|
setDragState((prev) => ({ ...prev, targetColumn: null, targetIndex: null }))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 17:37:44 +00:00
|
|
|
|
const handleDrop = (
|
|
|
|
|
|
e: React.DragEvent,
|
|
|
|
|
|
targetColumn: DashboardWidgetColumn,
|
|
|
|
|
|
targetIndex?: number,
|
|
|
|
|
|
) => {
|
2026-05-05 17:59:30 +00:00
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
|
|
|
|
|
|
const widgetId = e.dataTransfer.getData('widgetId')
|
2026-07-01 17:37:44 +00:00
|
|
|
|
if (!widgetId) return
|
2026-05-05 17:59:30 +00:00
|
|
|
|
|
|
|
|
|
|
const newOrder = { ...widgetOrder }
|
|
|
|
|
|
|
2026-07-01 17:37:44 +00:00
|
|
|
|
dashboardColumns.forEach((col) => {
|
2026-05-05 17:59:30 +00:00
|
|
|
|
newOrder[col] = newOrder[col].filter((id) => id !== widgetId)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (targetIndex !== undefined) {
|
|
|
|
|
|
newOrder[targetColumn].splice(targetIndex, 0, widgetId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newOrder[targetColumn].push(widgetId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 13:18:17 +00:00
|
|
|
|
setWidgetOrder(newOrder)
|
2026-05-05 17:59:30 +00:00
|
|
|
|
setDragState({ draggedId: null, targetColumn: null, targetIndex: null })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDragEnd = () => {
|
|
|
|
|
|
setDragState({ draggedId: null, targetColumn: null, targetIndex: null })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 17:37:44 +00:00
|
|
|
|
const handleColumnDragOver = (
|
|
|
|
|
|
e: React.DragEvent<HTMLDivElement>,
|
|
|
|
|
|
column: DashboardWidgetColumn,
|
|
|
|
|
|
) => {
|
|
|
|
|
|
if (!isDesignMode) return
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
|
|
|
|
|
|
const now = Date.now()
|
|
|
|
|
|
const lastUpdate = Number(e.currentTarget.dataset.lastColumnUpdate || 0)
|
|
|
|
|
|
if (now - lastUpdate > 150) {
|
|
|
|
|
|
e.currentTarget.dataset.lastColumnUpdate = now.toString()
|
|
|
|
|
|
handleDragEnterColumn(column)
|
2026-05-05 17:59:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-01 17:37:44 +00:00
|
|
|
|
const handleColumnDrop = (e: React.DragEvent<HTMLDivElement>, column: DashboardWidgetColumn) => {
|
|
|
|
|
|
if (!isDesignMode) return
|
|
|
|
|
|
handleDrop(e, column, widgetOrder[column].length)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const renderWidgets = (column: DashboardWidgetColumn) => {
|
2026-05-05 17:59:30 +00:00
|
|
|
|
const columnWidgets = widgetOrder[column] || []
|
|
|
|
|
|
|
|
|
|
|
|
const uniqueWidgets = [...new Set(columnWidgets)]
|
|
|
|
|
|
|
|
|
|
|
|
return uniqueWidgets
|
|
|
|
|
|
.map((widgetId, index) => {
|
2026-07-01 17:37:44 +00:00
|
|
|
|
const metadata = dashboardWidgets.find((w) => w.id === widgetId)
|
|
|
|
|
|
if (
|
|
|
|
|
|
!metadata ||
|
|
|
|
|
|
!checkPermission(metadata.permission) ||
|
|
|
|
|
|
hiddenWidgetIds.includes(widgetId)
|
|
|
|
|
|
)
|
|
|
|
|
|
return null
|
2026-05-05 17:59:30 +00:00
|
|
|
|
|
|
|
|
|
|
const isDragging = dragState.draggedId === widgetId
|
|
|
|
|
|
const isDropTarget = dragState.targetColumn === column && dragState.targetIndex === index
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-07-01 17:37:44 +00:00
|
|
|
|
<div key={`${column}-${widgetId}`} className="relative group">
|
2026-05-05 17:59:30 +00:00
|
|
|
|
{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">
|
|
|
|
|
|
<div className="h-2 bg-gradient-to-r from-transparent via-blue-500 to-transparent rounded-full shadow-lg" />
|
|
|
|
|
|
<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>
|
2026-07-01 17:37:44 +00:00
|
|
|
|
<span>{translate('::App.Platform.Intranet.Dashboard.DropHere')}</span>
|
2026-05-05 17:59:30 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
draggable={isDesignMode}
|
|
|
|
|
|
onDragStart={(e) => {
|
|
|
|
|
|
if (isDesignMode) {
|
2026-07-01 17:37:44 +00:00
|
|
|
|
handleDragStart(e, widgetId)
|
2026-05-05 17:59:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDragOver={(e) => {
|
|
|
|
|
|
if (!isDesignMode) return
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
2026-07-01 17:37:44 +00:00
|
|
|
|
onDragLeave={() => {
|
2026-05-05 17:59:30 +00:00
|
|
|
|
if (isDesignMode) {
|
|
|
|
|
|
setDragState((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
targetColumn: prev.targetColumn,
|
|
|
|
|
|
targetIndex: null,
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onDrop={(e) => {
|
|
|
|
|
|
if (!isDesignMode) return
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
|
|
|
|
|
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
2026-07-01 17:37:44 +00:00
|
|
|
|
if (e.clientY - rect.top < rect.height * 0.3) {
|
2026-05-05 17:59:30 +00:00
|
|
|
|
handleDrop(e, column, index)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
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={{
|
2026-06-05 05:44:01 +00:00
|
|
|
|
touchAction: isDesignMode ? 'none' : 'pan-y',
|
2026-05-05 17:59:30 +00:00
|
|
|
|
transition:
|
|
|
|
|
|
'border-color 0.3s ease-out, opacity 0.3s ease-out, box-shadow 0.3s ease-out',
|
|
|
|
|
|
willChange: isDragging ? 'opacity' : 'auto',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-07-01 17:37:44 +00:00
|
|
|
|
{isDesignMode && !isDragging && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="absolute right-2 top-2 z-20 flex h-7 w-7 items-center justify-center rounded-full bg-white text-gray-500 shadow-sm dark:bg-gray-800 dark:text-gray-300"
|
|
|
|
|
|
title={translate('::App.Platform.Intranet.Dashboard.HideWidget')}
|
|
|
|
|
|
aria-label={`${translate('::App.Platform.Intranet.Dashboard.HideWidget')}: ${translate(metadata.labelKey)}`}
|
|
|
|
|
|
draggable={false}
|
|
|
|
|
|
onMouseDown={(e) => e.stopPropagation()}
|
|
|
|
|
|
onDragStart={(e) => e.stopPropagation()}
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.stopPropagation()
|
|
|
|
|
|
setWidgetVisibility(widgetId, false)
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<LuX className="h-4 w-4" aria-hidden="true" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
2026-05-05 17:59:30 +00:00
|
|
|
|
{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>
|
2026-07-01 17:37:44 +00:00
|
|
|
|
<span>{translate('::App.Platform.Intranet.Dashboard.Moving')}</span>
|
2026-05-05 17:59:30 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`${isDesignMode ? 'p-1.5' : ''} transition-all duration-500 ease-out`}
|
|
|
|
|
|
>
|
2026-07-01 17:37:44 +00:00
|
|
|
|
{metadata.render({
|
|
|
|
|
|
dashboard: intranetDashboard,
|
|
|
|
|
|
openAnnouncement: setSelectedAnnouncement,
|
|
|
|
|
|
openEvent: setSelectedEvent,
|
|
|
|
|
|
takeSurvey: handleTakeSurvey,
|
|
|
|
|
|
})}
|
2026-05-05 17:59:30 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Container>
|
|
|
|
|
|
<div className="mx-auto space-y-4">
|
2026-07-01 17:37:44 +00:00
|
|
|
|
<div className="relative flex flex-wrap items-center justify-between gap-4 lg:min-h-10">
|
|
|
|
|
|
<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)}
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
|
|
|
|
|
className={`
|
2026-06-25 22:55:44 +00:00
|
|
|
|
relative !inline-flex !h-6 !w-11 !min-w-11 !items-center !justify-start !rounded-full !border-0 !px-0 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
|
|
|
|
|
|
${isDesignMode ? '!bg-blue-600 hover:!bg-blue-600' : '!bg-gray-300 hover:!bg-gray-300 dark:!bg-gray-600 dark:hover:!bg-gray-600'}
|
2026-05-05 17:59:30 +00:00
|
|
|
|
`}
|
2026-07-01 17:37:44 +00:00
|
|
|
|
role="switch"
|
|
|
|
|
|
aria-checked={isDesignMode}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={`
|
2026-05-05 17:59:30 +00:00
|
|
|
|
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
|
|
|
|
|
|
${isDesignMode ? 'translate-x-6' : 'translate-x-1'}
|
|
|
|
|
|
`}
|
2026-07-01 17:37:44 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</Button>
|
2026-05-05 17:59:30 +00:00
|
|
|
|
{isDesignMode && (
|
2026-06-25 22:55:44 +00:00
|
|
|
|
<Button
|
2026-05-05 17:59:30 +00:00
|
|
|
|
onClick={() => {
|
2026-07-09 17:55:07 +00:00
|
|
|
|
resetDashboard()
|
2026-05-05 17:59:30 +00:00
|
|
|
|
initializeDefaultOrder()
|
|
|
|
|
|
}}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
|
|
|
|
|
className="!h-auto !items-center gap-1 !rounded-none !px-0 !py-0 text-sm text-gray-500 transition-colors hover:!bg-transparent hover:text-gray-700 active:!bg-transparent focus:!bg-transparent dark:text-gray-400 dark:hover:text-gray-200"
|
2026-07-01 17:37:44 +00:00
|
|
|
|
title={translate('::App.Platform.Intranet.Dashboard.ResetTitle')}
|
2026-05-05 17:59:30 +00:00
|
|
|
|
>
|
2026-08-06 13:17:59 +00:00
|
|
|
|
🔄 {translate('::Reset')}
|
2026-06-25 22:55:44 +00:00
|
|
|
|
</Button>
|
2026-05-05 17:59:30 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-01 17:37:44 +00:00
|
|
|
|
{isDesignMode && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="order-last flex w-full items-center justify-center gap-1 lg:absolute lg:left-1/2 lg:top-1/2 lg:order-none lg:w-auto lg:-translate-x-1/2 lg:-translate-y-1/2"
|
|
|
|
|
|
role="group"
|
|
|
|
|
|
aria-label={translate('::App.Platform.Intranet.Dashboard.Layout')}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="mr-1 text-sm text-gray-500 dark:text-gray-400">
|
|
|
|
|
|
{translate('::App.Platform.Intranet.Dashboard.Layout')}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{layoutPresets.map((layout) => (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
key={layout.id}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
2026-08-11 13:18:17 +00:00
|
|
|
|
onClick={() => setDashboardLayout(layout.id)}
|
2026-07-01 17:37:44 +00:00
|
|
|
|
className={`!h-8 !rounded-md !px-2 ${selectedLayout === layout.id ? '!bg-blue-600 text-white hover:!bg-blue-600' : '!bg-gray-100 text-gray-600 hover:!bg-gray-200 dark:!bg-gray-700 dark:text-gray-200'}`}
|
|
|
|
|
|
title={`${translate(layout.labelKey)} (${layout.columns.join(' / ')})`}
|
|
|
|
|
|
aria-label={`${translate(layout.labelKey)} (${layout.columns.join(' / ')})`}
|
|
|
|
|
|
aria-pressed={selectedLayout === layout.id}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="flex h-4 w-12 gap-0.5" aria-hidden="true">
|
|
|
|
|
|
{layout.columns.map((width, index) => (
|
|
|
|
|
|
<span
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
className="h-full rounded-sm bg-current opacity-70"
|
|
|
|
|
|
style={{ flex: width }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<p className="mt-1 text-gray-600 dark:text-gray-400">
|
|
|
|
|
|
<span className="font-medium">{translate('::AI.Welcome')},</span>{' '}
|
|
|
|
|
|
{currentLocalDate(new Date(), currentLocale || 'tr')}
|
|
|
|
|
|
</p>
|
2026-05-05 17:59:30 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-01 17:37:44 +00:00
|
|
|
|
{isDesignMode && hiddenWidgets.length > 0 && (
|
|
|
|
|
|
<div className="flex flex-wrap items-center gap-1">
|
|
|
|
|
|
<span className="mr-1 text-sm text-gray-500 dark:text-gray-400">
|
|
|
|
|
|
{translate('::App.Platform.Intranet.Dashboard.HiddenWidgets')}:
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{hiddenWidgets.map((widget) => (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
key={widget.id}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="plain"
|
|
|
|
|
|
shape="none"
|
|
|
|
|
|
className="!h-8 !rounded-md !bg-emerald-50 !px-2 text-xs text-emerald-700 hover:!bg-emerald-100 dark:!bg-emerald-950 dark:text-emerald-300"
|
|
|
|
|
|
onClick={() => setWidgetVisibility(widget.id, true)}
|
|
|
|
|
|
title={translate('::App.Platform.Intranet.Dashboard.ShowWidget')}
|
|
|
|
|
|
>
|
|
|
|
|
|
+ {translate(widget.labelKey)}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
))}
|
2026-05-05 17:59:30 +00:00
|
|
|
|
</div>
|
2026-07-01 17:37:44 +00:00
|
|
|
|
)}
|
2026-05-05 17:59:30 +00:00
|
|
|
|
|
2026-07-01 17:37:44 +00:00
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-12">
|
|
|
|
|
|
{dashboardColumns.map((column, index) => {
|
|
|
|
|
|
const isEmptyDropTarget =
|
|
|
|
|
|
isDesignMode && dragState.targetColumn === column && widgetOrder[column].length === 0
|
2026-05-05 17:59:30 +00:00
|
|
|
|
|
2026-07-01 17:37:44 +00:00
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={column}
|
|
|
|
|
|
className={`${columnSpanClasses[activeLayout.columns[index]]} min-h-[100px] space-y-6 rounded-xl p-1 ${
|
|
|
|
|
|
isDesignMode &&
|
|
|
|
|
|
dragState.targetColumn === column &&
|
|
|
|
|
|
dragState.targetIndex === null
|
|
|
|
|
|
? 'bg-blue-50/80 ring-2 ring-blue-300 shadow-lg dark:bg-blue-900/20 dark:ring-blue-600'
|
|
|
|
|
|
: 'bg-transparent'
|
|
|
|
|
|
} transition-all duration-700 ease-out`}
|
|
|
|
|
|
onDragOver={(e) => handleColumnDragOver(e, column)}
|
|
|
|
|
|
onDragLeave={handleDragLeaveColumn}
|
|
|
|
|
|
onDrop={(e) => handleColumnDrop(e, column)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{renderWidgets(column)}
|
|
|
|
|
|
{isEmptyDropTarget && (
|
|
|
|
|
|
<div className="flex h-40 items-center justify-center rounded-xl border-2 border-dashed border-blue-300 bg-blue-50/50 dark:border-blue-600 dark:bg-blue-900/10">
|
|
|
|
|
|
<span className="font-medium text-blue-600 dark:text-blue-400">
|
|
|
|
|
|
{translate('::App.Platform.Intranet.Dashboard.DropWidgetHere')}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
2026-05-05 17:59:30 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<AnimatePresence>
|
|
|
|
|
|
{showSurveyModal && selectedSurvey && (
|
|
|
|
|
|
<SurveyModal
|
|
|
|
|
|
survey={selectedSurvey}
|
|
|
|
|
|
onClose={() => setShowSurveyModal(false)}
|
|
|
|
|
|
onSubmit={handleSubmitSurvey}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
|
|
2026-05-07 14:25:06 +00:00
|
|
|
|
<AnimatePresence>
|
|
|
|
|
|
{selectedEvent && (
|
2026-06-26 19:02:53 +00:00
|
|
|
|
<EventModal
|
|
|
|
|
|
event={selectedEvent}
|
|
|
|
|
|
onClose={() => setSelectedEvent(null)}
|
|
|
|
|
|
onLikeChange={handleEventLikeChange}
|
|
|
|
|
|
/>
|
2026-05-07 14:25:06 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
|
|
2026-05-05 17:59:30 +00:00
|
|
|
|
<AnimatePresence>
|
|
|
|
|
|
{selectedAnnouncement && (
|
|
|
|
|
|
<AnnouncementModal
|
|
|
|
|
|
announcement={selectedAnnouncement}
|
|
|
|
|
|
onClose={() => setSelectedAnnouncement(null)}
|
2026-06-26 19:02:53 +00:00
|
|
|
|
onLikeChange={handleAnnouncementLikeChange}
|
2026-05-05 17:59:30 +00:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
|
</Container>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default IntranetDashboard
|