Intranet Dashboard Layout ve Hide Widget
This commit is contained in:
parent
f3a516eb92
commit
deb7fdfee6
12 changed files with 1110 additions and 940 deletions
|
|
@ -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}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
1165
ui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
25
ui/src/views/intranet/dashboardWidget.ts
Normal file
25
ui/src/views/intranet/dashboardWidget.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue