Intranet Dashboard Layout ve Hide Widget

This commit is contained in:
Sedat Öztürk 2026-07-01 20:37:44 +03:00
parent f3a516eb92
commit deb7fdfee6
12 changed files with 1110 additions and 940 deletions

View file

@ -16,7 +16,6 @@ public class LanguageSeederDto
{
public List<Language> Languages { get; set; }
public List<LanguageTextsSeedDto> LanguageTexts { get; set; }
public List<LanguageTextsSeedDto> LanguageFieldTitles { get; set; }
}
public class LanguageDataSeeder : IDataSeedContributor, ITransientDependency
@ -101,44 +100,5 @@ public class LanguageDataSeeder : IDataSeedContributor, ITransientDependency
throw new Exception($"Hata veren Kod:' ResourceName='{item.ResourceName}', Key='{item.Key}' Message='{ex.Message}'");
}
}
foreach (var item in items.LanguageFieldTitles)
{
try
{
if (!keys.Any(a => a.Key == item.Key))
{
await _languageKey.InsertAsync(new()
{
Key = item.Key,
ResourceName = item.ResourceName,
});
}
if (!texts.Any(a => a.CultureName == "en" && a.Key == item.Key))
{
await _languagesText.InsertAsync(new()
{
CultureName = "en",
Key = item.Key,
Value = item.En,
ResourceName = item.ResourceName,
});
}
if (!texts.Any(a => a.CultureName == "tr" && a.Key == item.Key))
{
await _languagesText.InsertAsync(new()
{
CultureName = "tr",
Key = item.Key,
Value = item.Tr,
ResourceName = item.ResourceName,
});
}
}
catch (Exception ex)
{
throw new Exception($"Hata veren Kod:' ResourceName='{item.ResourceName}', Key='{item.Key}' Message='{ex.Message}'");
}
}
}
}

View file

@ -13578,14 +13578,126 @@
"tr": "Dizayn Mod",
"en": "Design Mode"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.Layout",
"tr": "Yerleşim",
"en": "Layout"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.Layout.Default",
"tr": "Standart",
"en": "Standard"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.Layout.CenterFocus",
"tr": "Merkez odaklı",
"en": "Center focused"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.Layout.Balanced",
"tr": "Dengeli",
"en": "Balanced"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.Layout.WideLeft",
"tr": "Sol panel geniş",
"en": "Wide left panel"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.Layout.WideRight",
"tr": "Sağ panel geniş",
"en": "Wide right panel"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.HiddenWidgets",
"tr": "Gizli bileşenler",
"en": "Hidden widgets"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.HideWidget",
"tr": "Bileşeni kapat",
"en": "Hide widget"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.ShowWidget",
"tr": "Bileşeni aç",
"en": "Show widget"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.DropHere",
"tr": "Buraya bırak",
"en": "Drop here"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.Moving",
"tr": "Taşınıyor",
"en": "Moving"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.DropWidgetHere",
"tr": "Widget'ı buraya bırakın",
"en": "Drop the widget here"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.ResetTitle",
"tr": "Widget düzenini varsayılana döndür",
"en": "Restore the default widget layout"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.Widget.Birthdays",
"tr": "Bugünün doğum günleri",
"en": "Today's birthdays"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.Widget.Documents",
"tr": "Son dokümanlar",
"en": "Recent documents"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.Widget.Surveys",
"tr": "Aktif anketler",
"en": "Active surveys"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.Widget.SocialWall",
"tr": "Sosyal duvar",
"en": "Social wall"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.Widget.Announcements",
"tr": "Duyurular",
"en": "Announcements"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.Widget.Events",
"tr": "Yaklaşan etkinlikler",
"en": "Upcoming events"
},
{
"resourceName": "Platform",
"key": "App.Platform.Intranet.Dashboard.Reset",
"tr": "Sıfırla",
"en": "Reset"
}
],
"LanguageFieldTitles": [
},
{
"resourceName": "Platform",
"key": "App.Intranet.Announcement.AnnouncementLike",

1165
ui/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,18 +5,10 @@ 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'
import EventModal from './widgets/EventModal'
// Social Wall
import SocialWall from './SocialWall'
import { Container } from '@/components/shared'
import { usePermission } from '@/utils/hooks/usePermission'
import {
@ -27,19 +19,71 @@ import {
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'
import Events from './widgets/Events'
import Button from '@/components/ui/Button'
import { LuX } from 'react-icons/lu'
import type { DashboardWidgetColumn, DashboardWidgetDefinition } from './dashboardWidget'
dayjs.extend(relativeTime)
dayjs.extend(isBetween)
dayjs.extend(localizedFormat)
const WIDGET_ORDER_KEY = 'dashboard-widget-order'
const WIDGET_VISIBILITY_KEY = 'dashboard-hidden-widgets'
const DASHBOARD_LAYOUT_KEY = 'dashboard-layout'
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
type DashboardLayoutId = (typeof layoutPresets)[number]['id']
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',
}
const IntranetDashboard: React.FC = () => {
const { checkPermission } = usePermission()
@ -48,7 +92,23 @@ const IntranetDashboard: React.FC = () => {
const [selectedSurvey, setSelectedSurvey] = useState<SurveyDto | null>(null)
const [showSurveyModal, setShowSurveyModal] = useState(false)
const [isDesignMode, setIsDesignMode] = useState(false)
const [widgetOrder, setWidgetOrder] = useState<Record<string, string[]>>({
const [hiddenWidgetIds, setHiddenWidgetIds] = useState<string[]>(() => {
try {
const savedIds: unknown = JSON.parse(localStorage.getItem(WIDGET_VISIBILITY_KEY) || '[]')
return Array.isArray(savedIds)
? savedIds.filter((id): id is string => typeof id === 'string')
: []
} catch {
return []
}
})
const [selectedLayout, setSelectedLayout] = useState<DashboardLayoutId>(() => {
const savedLayout = localStorage.getItem(DASHBOARD_LAYOUT_KEY)
return layoutPresets.some((layout) => layout.id === savedLayout)
? (savedLayout as DashboardLayoutId)
: 'default'
})
const [widgetOrder, setWidgetOrder] = useState<Record<DashboardWidgetColumn, string[]>>({
left: [],
center: [],
right: [],
@ -69,10 +129,9 @@ const IntranetDashboard: React.FC = () => {
fetchIntranetDashboard()
}, [])
// Drag state'leri birleştirildi
const [dragState, setDragState] = useState<{
draggedId: string | null
targetColumn: string | null
targetColumn: DashboardWidgetColumn | null
targetIndex: number | null
}>({
draggedId: null,
@ -136,39 +195,20 @@ const IntranetDashboard: React.FC = () => {
)
}
// 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',
},
{ id: 'upcoming-events', permission: 'App.Intranet.Events.Event.Widget', column: 'right' },
]
// 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),
const defaultOrder: Record<DashboardWidgetColumn, string[]> = {
left: [],
center: [],
right: [],
}
dashboardWidgets.forEach((widget) => {
if (checkPermission(widget.permission)) defaultOrder[widget.column].push(widget.id)
})
setWidgetOrder(defaultOrder)
}
// Widget sıralamasını yükle — grantedPolicies gelince çalış ki checkPermission doğru çalışsın
useEffect(() => {
if (!grantedPolicies) return
@ -176,15 +216,14 @@ const IntranetDashboard: React.FC = () => {
if (savedOrder) {
try {
const parsed = JSON.parse(savedOrder) as Record<string, unknown[]>
const order: Record<string, string[]> = {
const order: Record<DashboardWidgetColumn, 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) => {
dashboardWidgets.forEach((w) => {
if (!allAssigned.has(w.id) && checkPermission(w.permission)) {
order[w.column as keyof typeof order].push(w.id)
}
@ -200,37 +239,52 @@ const IntranetDashboard: React.FC = () => {
}
}, [grantedPolicies])
// Widget sıralamasını kaydet
const saveWidgetOrder = (newOrder: Record<string, string[]>) => {
const saveWidgetOrder = (newOrder: Record<DashboardWidgetColumn, string[]>) => {
setWidgetOrder(newOrder)
localStorage.setItem(WIDGET_ORDER_KEY, JSON.stringify(newOrder))
}
const handleDragStart = (e: React.DragEvent, widgetId: string, column: string) => {
const setWidgetVisibility = (widgetId: string, visible: boolean) => {
setHiddenWidgetIds((current) => {
const next = visible
? current.filter((id) => id !== widgetId)
: [...new Set([...current, widgetId])]
localStorage.setItem(WIDGET_VISIBILITY_KEY, JSON.stringify(next))
return next
})
}
const selectLayout = (layoutId: DashboardLayoutId) => {
setSelectedLayout(layoutId)
localStorage.setItem(DASHBOARD_LAYOUT_KEY, layoutId)
}
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) => {
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 handleDragEnterWidget = (
e: React.DragEvent,
column: DashboardWidgetColumn,
index: number,
) => {
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
if (e.clientY - rect.top < rect.height * 0.3) {
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) => {
const handleDragEnterColumn = (column: DashboardWidgetColumn) => {
setDragState((prev) => ({ ...prev, targetColumn: column, targetIndex: null }))
}
@ -238,34 +292,29 @@ const IntranetDashboard: React.FC = () => {
setDragState((prev) => ({ ...prev, targetColumn: null, targetIndex: null }))
}
const handleDrop = (e: React.DragEvent, targetColumn: string, targetIndex?: number) => {
const handleDrop = (
e: React.DragEvent,
targetColumn: DashboardWidgetColumn,
targetIndex?: number,
) => {
e.preventDefault()
e.stopPropagation()
const widgetId = e.dataTransfer.getData('widgetId')
const sourceColumn = e.dataTransfer.getData('sourceColumn')
if (!widgetId || !sourceColumn) return
if (!widgetId) return
const newOrder = { ...widgetOrder }
// ÖNCE tüm kolonlardan bu widget'ı kaldır (duplicate önleme)
Object.keys(newOrder).forEach((col) => {
dashboardColumns.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 })
}
@ -274,61 +323,49 @@ const IntranetDashboard: React.FC = () => {
setDragState({ draggedId: null, targetColumn: null, targetIndex: null })
}
// Widget component'ını render et
const renderWidgetComponent = (widgetId: string) => {
switch (widgetId) {
case 'upcoming-events':
return (
<Events
events={intranetDashboard?.events || []}
onEventClick={setSelectedEvent}
/>
)
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 (
<Surveys surveys={intranetDashboard?.surveys || []} onTakeSurvey={handleTakeSurvey} />
)
case 'social-wall':
return <SocialWall />
default:
return null
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)
}
}
// Widget'ları render et
const renderWidgets = (column: 'left' | 'center' | 'right') => {
const handleColumnDrop = (e: React.DragEvent<HTMLDivElement>, column: DashboardWidgetColumn) => {
if (!isDesignMode) return
handleDrop(e, column, widgetOrder[column].length)
}
const renderWidgets = (column: DashboardWidgetColumn) => {
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 metadata = dashboardWidgets.find((w) => w.id === widgetId)
if (
!metadata ||
!checkPermission(metadata.permission) ||
hiddenWidgetIds.includes(widgetId)
)
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 */}
<div key={`${column}-${widgetId}`} className="relative group">
{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
@ -338,7 +375,7 @@ const IntranetDashboard: React.FC = () => {
d="M19 14l-7 7m0 0l-7-7m7 7V3"
/>
</svg>
<span>Buraya Bırak</span>
<span>{translate('::App.Platform.Intranet.Dashboard.DropHere')}</span>
</div>
</div>
)}
@ -347,18 +384,13 @@ const IntranetDashboard: React.FC = () => {
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)
handleDragStart(e, widgetId)
}
}}
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 ||
@ -368,8 +400,7 @@ const IntranetDashboard: React.FC = () => {
handleDragEnterWidget(e, column, index)
}
}}
onDragLeave={(e) => {
// Widget'tan çıkınca indicator'ı kaldır
onDragLeave={() => {
if (isDesignMode) {
setDragState((prev) => ({
...prev,
@ -382,18 +413,10 @@ const IntranetDashboard: React.FC = () => {
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
if (e.clientY - rect.top < rect.height * 0.3) {
handleDrop(e, column, index)
} else {
// Alt kısma bırak - sonraki index'e ekle
handleDrop(e, column, index + 1)
}
}}
@ -419,7 +442,23 @@ const IntranetDashboard: React.FC = () => {
willChange: isDragging ? 'opacity' : 'auto',
}}
>
{/* Dragging overlay - daha minimal */}
{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>
)}
{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">
@ -431,14 +470,19 @@ const IntranetDashboard: React.FC = () => {
d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"
/>
</svg>
<span>Taşınıyor</span>
<span>{translate('::App.Platform.Intranet.Dashboard.Moving')}</span>
</div>
</div>
)}
<div
className={`${isDesignMode ? 'p-1.5' : ''} transition-all duration-500 ease-out`}
>
{renderWidgetComponent(widgetId)}
{metadata.render({
dashboard: intranetDashboard,
openAnnouncement: setSelectedAnnouncement,
openEvent: setSelectedEvent,
takeSurvey: handleTakeSurvey,
})}
</div>
</div>
</div>
@ -450,213 +494,145 @@ const IntranetDashboard: React.FC = () => {
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)}
variant="plain"
shape="none"
className={`
<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={`
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'}
`}
role="switch"
aria-checked={isDesignMode}
>
<span
className={`
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 */}
/>
</Button>
{isDesignMode && (
<Button
onClick={() => {
localStorage.removeItem(WIDGET_ORDER_KEY)
localStorage.removeItem(WIDGET_VISIBILITY_KEY)
localStorage.removeItem(DASHBOARD_LAYOUT_KEY)
setHiddenWidgetIds([])
setSelectedLayout('default')
initializeDefaultOrder()
}}
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"
title="Widget düzenini varsayılana döndür"
title={translate('::App.Platform.Intranet.Dashboard.ResetTitle')}
>
🔄 {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>
{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"
onClick={() => selectLayout(layout.id)}
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>
</div>
{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>
))}
</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>
{dashboardColumns.map((column, index) => {
const isEmptyDropTarget =
isDesignMode && dragState.targetColumn === column && widgetOrder[column].length === 0
<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>
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>
)
})}
</div>
</div>

View file

@ -311,7 +311,10 @@ const PostItem: React.FC<PostItemProps> = ({ post, onLike, onComment, onDelete,
{/* Content */}
<div className="mb-3">
<p className="text-gray-800 dark:text-gray-200 whitespace-pre-wrap">{post.content}</p>
<div
className="whitespace-pre-wrap text-gray-800 dark:text-gray-200"
dangerouslySetInnerHTML={{ __html: post.content || '' }}
/>
{renderMedia()}
</div>

View file

@ -6,6 +6,7 @@ import CreatePost from './CreatePost'
import { SocialMediaDto, SocialPostDto } from '@/proxy/intranet/models'
import { intranetService } from '@/services/intranet.service'
import Button from '@/components/ui/Button'
import type { DashboardWidgetDefinition } from '../dashboardWidget'
const PAGE_SIZE = 10
@ -256,4 +257,13 @@ const SocialWall: React.FC = () => {
)
}
export const dashboardWidget: DashboardWidgetDefinition = {
id: 'social-wall',
permission: 'App.Intranet.SocialPost.Widget',
column: 'center',
labelKey: '::App.Platform.Intranet.Dashboard.Widget.SocialWall',
order: 40,
render: () => <SocialWall />,
}
export default SocialWall

View file

@ -0,0 +1,25 @@
import type { ReactNode } from 'react'
import type {
AnnouncementDto,
EventDto,
IntranetDashboardDto,
SurveyDto,
} from '@/proxy/intranet/models'
export type DashboardWidgetColumn = 'left' | 'center' | 'right'
export interface DashboardWidgetContext {
dashboard?: IntranetDashboardDto
openAnnouncement: (announcement: AnnouncementDto) => void
openEvent: (event: EventDto) => void
takeSurvey: (survey: SurveyDto) => void
}
export interface DashboardWidgetDefinition {
id: string
permission: string
column: DashboardWidgetColumn
labelKey: string
order: number
render: (context: DashboardWidgetContext) => ReactNode
}

View file

@ -6,6 +6,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
import { Avatar } from '@/components/ui'
import { AVATAR_URL } from '@/constants/app.constant'
import { categoryColor } from './widgetUtils'
import type { DashboardWidgetDefinition } from '../dashboardWidget'
interface AnnouncementsProps {
announcements: AnnouncementDto[]
@ -115,4 +116,18 @@ const Announcements: React.FC<AnnouncementsProps> = ({ announcements, onAnnounce
)
}
export const dashboardWidget: DashboardWidgetDefinition = {
id: 'announcements',
permission: 'App.Intranet.Announcement.Widget',
column: 'right',
labelKey: '::App.Platform.Intranet.Dashboard.Widget.Announcements',
order: 50,
render: ({ dashboard, openAnnouncement }) => (
<Announcements
announcements={dashboard?.announcements || []}
onAnnouncementClick={openAnnouncement}
/>
),
}
export default Announcements

View file

@ -8,6 +8,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
import { Avatar } from '@/components/ui'
import { AVATAR_URL } from '@/constants/app.constant'
import { imageSrc } from './widgetUtils'
import type { DashboardWidgetDefinition } from '../dashboardWidget'
interface EventsProps {
events: EventDto[]
@ -29,11 +30,8 @@ const Events: React.FC<EventsProps> = ({ events, onEventClick }) => {
.filter((event) => event?.isPublished && !dayjs(event.date).isBefore(now, 'day'))
.sort((left, right) => dayjs(left.date).valueOf() - dayjs(right.date).valueOf())
const getUserName = (event: EventDto) => (
event.user?.fullName ||
[event.user?.name, event.user?.surname].filter(Boolean).join(' ') ||
'-'
)
const getUserName = (event: EventDto) =>
event.user?.fullName || [event.user?.name, event.user?.surname].filter(Boolean).join(' ') || '-'
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
@ -55,7 +53,9 @@ const Events: React.FC<EventsProps> = ({ events, onEventClick }) => {
>
<div className="flex items-start gap-4">
<div className="flex-1 min-w-0">
<h4 className="text-base font-semibold text-gray-900 dark:text-white">{event.name}</h4>
<h4 className="text-base font-semibold text-gray-900 dark:text-white">
{event.name}
</h4>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{currentLocalDate(event.date, currentLocale || 'tr')} - {event.place}
</p>
@ -76,7 +76,9 @@ const Events: React.FC<EventsProps> = ({ events, onEventClick }) => {
{event.likes > 0 && (
<>
<span></span>
<span className={`flex items-center gap-1 ${event.isLiked ? 'text-red-500' : ''}`}>
<span
className={`flex items-center gap-1 ${event.isLiked ? 'text-red-500' : ''}`}
>
<FaHeart className="w-3 h-3" />
{event.likes}
</span>
@ -110,5 +112,15 @@ const Events: React.FC<EventsProps> = ({ events, onEventClick }) => {
)
}
export default Events
export const dashboardWidget: DashboardWidgetDefinition = {
id: 'upcoming-events',
permission: 'App.Intranet.Events.Event.Widget',
column: 'right',
labelKey: '::App.Platform.Intranet.Dashboard.Widget.Events',
order: 60,
render: ({ dashboard, openEvent }) => (
<Events events={dashboard?.events || []} onEventClick={openEvent} />
),
}
export default Events

View file

@ -6,6 +6,7 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
import { getFileIcon, getFileType } from '@/proxy/intranet/utils'
import { FILE_URL } from '@/constants/app.constant'
import Button from '@/components/ui/Button'
import type { DashboardWidgetDefinition } from '../dashboardWidget'
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B'
@ -94,4 +95,13 @@ const RecentDocuments: React.FC<{ documents: DocumentDto[] }> = ({ documents })
)
}
export const dashboardWidget: DashboardWidgetDefinition = {
id: 'documents',
permission: 'App.Files.Widget',
column: 'left',
labelKey: '::App.Platform.Intranet.Dashboard.Widget.Documents',
order: 20,
render: ({ dashboard }) => <RecentDocuments documents={dashboard?.documents || []} />,
}
export default RecentDocuments

View file

@ -6,6 +6,7 @@ import useLocale from '@/utils/hooks/useLocale'
import { currentLocalDate } from '@/utils/dateUtils'
import { useLocalization } from '@/utils/hooks/useLocalization'
import Button from '@/components/ui/Button'
import type { DashboardWidgetDefinition } from '../dashboardWidget'
interface SurveysProps {
surveys?: SurveyDto[]
@ -184,9 +185,7 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
variant="plain"
icon={
<FaArrowRight
className={`h-3 w-3 transition-transform ${
isHovered ? 'translate-x-1' : ''
}`}
className={`h-3 w-3 transition-transform ${isHovered ? 'translate-x-1' : ''}`}
/>
}
className={`w-full !h-auto !justify-center gap-2 !rounded-lg !px-4 !py-3 text-sm font-medium text-white shadow-sm transition-all duration-300 hover:shadow-md focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-purple-400 ${
@ -224,4 +223,15 @@ const Surveys: React.FC<SurveysProps> = ({ surveys, onTakeSurvey }) => {
)
}
export const dashboardWidget: DashboardWidgetDefinition = {
id: 'active-surveys',
permission: 'App.Intranet.Survey.Widget',
column: 'left',
labelKey: '::App.Platform.Intranet.Dashboard.Widget.Surveys',
order: 30,
render: ({ dashboard, takeSurvey }) => (
<Surveys surveys={dashboard?.surveys || []} onTakeSurvey={takeSurvey} />
),
}
export default Surveys

View file

@ -4,10 +4,11 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
import { UserInfoViewModel } from '@/proxy/admin/models'
import { Avatar } from '@/components/ui'
import { AVATAR_URL } from '@/constants/app.constant'
import type { DashboardWidgetDefinition } from '../dashboardWidget'
const TodayBirthdays: React.FC<{ employees: UserInfoViewModel[] }> = ({ employees }) => {
const today = dayjs()
const { translate } = useLocalization();
const { translate } = useLocalization()
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
@ -20,10 +21,7 @@ const TodayBirthdays: React.FC<{ employees: UserInfoViewModel[] }> = ({ employee
{employees.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{employees.map((birthday, index) => (
<div
key={index}
className="flex items-center gap-2 p-2"
>
<div key={index} className="flex items-center gap-2 p-2">
<Avatar
size={48}
shape="circle"
@ -35,7 +33,10 @@ const TodayBirthdays: React.FC<{ employees: UserInfoViewModel[] }> = ({ employee
{birthday.fullName}
</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
{translate('::App.Platform.Intranet.Widgets.TodayBirthdays.Age', { age: today.diff(dayjs(birthday.birthDate), 'year') })} 🎉
{translate('::App.Platform.Intranet.Widgets.TodayBirthdays.Age', {
age: today.diff(dayjs(birthday.birthDate), 'year'),
})}{' '}
🎉
</p>
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
{birthday.department?.name}
@ -54,4 +55,13 @@ const TodayBirthdays: React.FC<{ employees: UserInfoViewModel[] }> = ({ employee
)
}
export const dashboardWidget: DashboardWidgetDefinition = {
id: 'today-birthdays',
permission: 'AbpIdentity.Users.Widget',
column: 'left',
labelKey: '::App.Platform.Intranet.Dashboard.Widget.Birthdays',
order: 10,
render: ({ dashboard }) => <TodayBirthdays employees={dashboard?.birthdays || []} />,
}
export default TodayBirthdays