2026-06-17 08:46:18 +00:00
|
|
|
import React, { useMemo, useEffect, useState, useCallback, useRef } from 'react'
|
2026-02-24 20:44:16 +00:00
|
|
|
import { Container } from '@/components/shared'
|
|
|
|
|
import { Helmet } from 'react-helmet'
|
|
|
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
|
|
|
import { useParams, useLocation } from 'react-router-dom'
|
2026-06-16 09:04:55 +00:00
|
|
|
import ReportViewer, {
|
2026-06-17 08:46:18 +00:00
|
|
|
Callbacks,
|
2026-06-16 09:04:55 +00:00
|
|
|
RequestOptions,
|
|
|
|
|
} from 'devexpress-reporting-react/dx-report-viewer'
|
|
|
|
|
import { ajaxSetup } from '@devexpress/analytics-core/analytics-utils-native'
|
2026-06-17 08:46:18 +00:00
|
|
|
import { ZoomAutoBy } from 'devexpress-reporting/viewer/constants'
|
2026-02-24 20:44:16 +00:00
|
|
|
import { APP_NAME } from '@/constants/app.constant'
|
2026-06-16 12:54:01 +00:00
|
|
|
import { MODE_DARK } from '@/constants/theme.constant'
|
2026-06-16 09:04:55 +00:00
|
|
|
import { useStoreState } from '@/store'
|
2026-06-16 21:19:14 +00:00
|
|
|
import { createReportUrl } from './reportRouteParams'
|
2026-02-24 20:44:16 +00:00
|
|
|
|
2026-06-16 12:54:01 +00:00
|
|
|
const VIEWER_THEME_LINK_ID = 'devexpress-report-viewer-theme'
|
|
|
|
|
const VIEWER_DARK_OVERRIDES_ID = 'devexpress-report-viewer-dark-overrides'
|
2026-06-17 08:46:18 +00:00
|
|
|
const DEFAULT_REPORT_ZOOM = 1
|
2026-06-16 12:54:01 +00:00
|
|
|
|
|
|
|
|
const getViewerClassName = (isDarkMode: boolean) =>
|
|
|
|
|
[
|
|
|
|
|
'dx-viewport',
|
|
|
|
|
'dx-device-desktop',
|
|
|
|
|
'dx-device-generic',
|
|
|
|
|
'dx-theme-generic',
|
|
|
|
|
'dx-theme-generic-typography',
|
|
|
|
|
isDarkMode ? 'dx-color-scheme-dark' : 'dx-color-scheme-light',
|
|
|
|
|
'report-viewer-shell',
|
|
|
|
|
isDarkMode ? 'report-viewer-shell-dark' : '',
|
|
|
|
|
]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(' ')
|
|
|
|
|
|
|
|
|
|
const loadViewerCss = (isDarkMode: boolean) => {
|
2026-02-24 20:44:16 +00:00
|
|
|
const styles = [
|
|
|
|
|
new URL('@devexpress/analytics-core/dist/css/dx-analytics.common.css', import.meta.url).href,
|
|
|
|
|
new URL('devexpress-reporting/dist/css/dx-webdocumentviewer.css', import.meta.url).href,
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
styles.forEach((href) => {
|
|
|
|
|
if (document.querySelector(`link[href="${href}"]`)) return
|
|
|
|
|
const link = document.createElement('link')
|
|
|
|
|
link.rel = 'stylesheet'
|
|
|
|
|
link.href = href
|
|
|
|
|
document.head.appendChild(link)
|
|
|
|
|
})
|
2026-06-16 12:54:01 +00:00
|
|
|
|
|
|
|
|
const themeHref = isDarkMode
|
|
|
|
|
? new URL('@devexpress/analytics-core/dist/css/dx-analytics.dark.css', import.meta.url).href
|
|
|
|
|
: new URL('@devexpress/analytics-core/dist/css/dx-analytics.light.css', import.meta.url).href
|
|
|
|
|
|
|
|
|
|
let themeLink = document.getElementById(VIEWER_THEME_LINK_ID) as HTMLLinkElement | null
|
|
|
|
|
if (!themeLink) {
|
|
|
|
|
themeLink = document.createElement('link')
|
|
|
|
|
themeLink.id = VIEWER_THEME_LINK_ID
|
|
|
|
|
themeLink.rel = 'stylesheet'
|
|
|
|
|
document.head.appendChild(themeLink)
|
|
|
|
|
}
|
|
|
|
|
if (themeLink.href !== themeHref) {
|
|
|
|
|
themeLink.href = themeHref
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!document.getElementById(VIEWER_DARK_OVERRIDES_ID)) {
|
|
|
|
|
const style = document.createElement('style')
|
|
|
|
|
style.id = VIEWER_DARK_OVERRIDES_ID
|
|
|
|
|
style.textContent = `
|
|
|
|
|
.report-viewer-shell {
|
|
|
|
|
min-height: calc(100vh - 4rem);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.report-viewer-shell-dark {
|
|
|
|
|
background: #111827;
|
|
|
|
|
color: #e5e7eb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.report-viewer-shell-dark .dxrd-preview,
|
|
|
|
|
.report-viewer-shell-dark .dxrd-preview-wrapper,
|
|
|
|
|
.report-viewer-shell-dark .dxrd-toolbar-wrapper,
|
|
|
|
|
.report-viewer-shell-dark .dxrd-right-panel,
|
|
|
|
|
.report-viewer-shell-dark .dxrd-search-wrapper {
|
|
|
|
|
background-color: #111827;
|
|
|
|
|
}
|
|
|
|
|
`
|
|
|
|
|
document.head.appendChild(style)
|
|
|
|
|
}
|
2026-02-24 20:44:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DevexpressReportViewer: React.FC = () => {
|
|
|
|
|
const { translate } = useLocalization()
|
2026-06-16 21:19:14 +00:00
|
|
|
const { report, listFormCode, id } = useParams<{
|
|
|
|
|
report: string
|
2026-06-16 14:46:20 +00:00
|
|
|
listFormCode: string
|
2026-06-16 21:19:14 +00:00
|
|
|
id: string
|
2026-06-16 14:46:20 +00:00
|
|
|
}>()
|
2026-02-24 20:44:16 +00:00
|
|
|
const location = useLocation()
|
2026-06-16 09:04:55 +00:00
|
|
|
const token = useStoreState((state) => state.auth.session.token)
|
2026-06-16 12:54:01 +00:00
|
|
|
const themeMode = useStoreState((state) => state.theme.mode)
|
2026-06-17 18:49:13 +00:00
|
|
|
const cultureName = useStoreState((state) => state.locale.currentLang)
|
|
|
|
|
const configCultureName = useStoreState(
|
|
|
|
|
(state) => state.abpConfig.config?.localization.currentCulture.cultureName,
|
|
|
|
|
)
|
2026-06-16 09:04:55 +00:00
|
|
|
const [isRequestAuthReady, setIsRequestAuthReady] = useState(false)
|
2026-06-17 08:46:18 +00:00
|
|
|
const previewModelRef = useRef<any>(null)
|
2026-06-16 12:54:01 +00:00
|
|
|
const isDarkMode = themeMode === MODE_DARK
|
2026-06-17 18:49:13 +00:00
|
|
|
const reportCultureName = cultureName || configCultureName
|
2026-02-24 20:44:16 +00:00
|
|
|
|
2026-06-17 08:46:18 +00:00
|
|
|
const getReportPreview = useCallback((eventOrModel?: any) => {
|
|
|
|
|
const eventArgs = eventOrModel?.args
|
|
|
|
|
const sender = eventOrModel?.sender
|
|
|
|
|
const candidates = [
|
|
|
|
|
eventArgs?.reportPreview,
|
|
|
|
|
eventArgs?.ReportPreview,
|
|
|
|
|
eventArgs?.previewModel?.reportPreview,
|
|
|
|
|
eventOrModel?.reportPreview,
|
|
|
|
|
eventOrModel?.ReportPreview,
|
|
|
|
|
eventOrModel?.previewModel?.reportPreview,
|
|
|
|
|
sender?.GetReportPreview?.(),
|
|
|
|
|
sender?.previewModel?.reportPreview,
|
|
|
|
|
previewModelRef.current,
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return candidates.find(
|
|
|
|
|
(candidate) =>
|
|
|
|
|
candidate &&
|
|
|
|
|
('zoom' in candidate ||
|
|
|
|
|
'originalZoom' in candidate ||
|
|
|
|
|
'autoFitBy' in candidate),
|
|
|
|
|
)
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const setDefaultZoom = useCallback((eventOrModel?: any) => {
|
|
|
|
|
const preview = getReportPreview(eventOrModel)
|
|
|
|
|
if (!preview) return
|
|
|
|
|
|
|
|
|
|
previewModelRef.current = preview
|
|
|
|
|
preview.autoFitBy = ZoomAutoBy.None
|
|
|
|
|
preview.originalZoom = DEFAULT_REPORT_ZOOM
|
|
|
|
|
preview.zoom = DEFAULT_REPORT_ZOOM
|
|
|
|
|
}, [getReportPreview])
|
|
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
useEffect(() => {
|
2026-06-16 12:54:01 +00:00
|
|
|
loadViewerCss(isDarkMode)
|
|
|
|
|
}, [isDarkMode])
|
2026-02-24 20:44:16 +00:00
|
|
|
|
2026-06-16 09:04:55 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
setIsRequestAuthReady(false)
|
|
|
|
|
const authorization = token ? `Bearer ${token}` : undefined
|
|
|
|
|
|
|
|
|
|
const ajaxHeaders = ajaxSetup.ajaxSettings.headers ?? {}
|
|
|
|
|
|
|
|
|
|
if (authorization) {
|
|
|
|
|
ajaxSetup.ajaxSettings.headers = {
|
|
|
|
|
...ajaxHeaders,
|
|
|
|
|
Authorization: authorization,
|
2026-06-17 18:49:13 +00:00
|
|
|
...(reportCultureName ? { 'Accept-Language': reportCultureName } : {}),
|
2026-06-16 09:04:55 +00:00
|
|
|
}
|
|
|
|
|
setIsRequestAuthReady(true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 18:49:13 +00:00
|
|
|
const {
|
|
|
|
|
Authorization: _ajaxAuthorization,
|
|
|
|
|
'Accept-Language': _ajaxAcceptLanguage,
|
|
|
|
|
...nextAjaxHeaders
|
|
|
|
|
} = ajaxHeaders
|
|
|
|
|
if (reportCultureName) {
|
|
|
|
|
nextAjaxHeaders['Accept-Language'] = reportCultureName
|
|
|
|
|
}
|
2026-06-16 09:04:55 +00:00
|
|
|
ajaxSetup.ajaxSettings.headers = nextAjaxHeaders
|
|
|
|
|
setIsRequestAuthReady(true)
|
2026-06-17 18:49:13 +00:00
|
|
|
}, [token, reportCultureName])
|
2026-06-16 09:04:55 +00:00
|
|
|
|
2026-02-24 20:44:16 +00:00
|
|
|
const reportUrlWithParams = useMemo(() => {
|
2026-06-17 18:49:13 +00:00
|
|
|
const searchParams = new URLSearchParams(location.search)
|
|
|
|
|
if (reportCultureName) {
|
|
|
|
|
searchParams.set('cultureName', reportCultureName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const search = searchParams.toString()
|
|
|
|
|
return createReportUrl({ report, id, listFormCode }, search ? `?${search}` : '')
|
|
|
|
|
}, [report, id, listFormCode, location.search, reportCultureName])
|
2026-02-24 20:44:16 +00:00
|
|
|
|
2026-06-16 14:46:20 +00:00
|
|
|
if (!reportUrlWithParams) return null
|
2026-02-24 20:44:16 +00:00
|
|
|
|
|
|
|
|
return (
|
2026-06-16 12:54:01 +00:00
|
|
|
<Container className={getViewerClassName(isDarkMode)}>
|
2026-02-24 20:44:16 +00:00
|
|
|
<Helmet
|
|
|
|
|
titleTemplate={`%s | ${APP_NAME}`}
|
|
|
|
|
title={translate('::App.Reports')}
|
|
|
|
|
defaultTitle={APP_NAME}
|
|
|
|
|
/>
|
|
|
|
|
|
2026-06-16 09:04:55 +00:00
|
|
|
{isRequestAuthReady && (
|
2026-02-24 20:44:16 +00:00
|
|
|
<ReportViewer reportUrl={reportUrlWithParams}>
|
2026-06-17 08:46:18 +00:00
|
|
|
<Callbacks
|
|
|
|
|
BeforeRender={setDefaultZoom}
|
|
|
|
|
DocumentReady={setDefaultZoom}
|
|
|
|
|
/>
|
2026-02-24 20:44:16 +00:00
|
|
|
<RequestOptions
|
2026-06-16 09:04:55 +00:00
|
|
|
host={import.meta.env.VITE_API_URL}
|
|
|
|
|
invokeAction="/DXXRDV"
|
2026-02-24 20:44:16 +00:00
|
|
|
/>
|
|
|
|
|
</ReportViewer>
|
2026-06-16 09:04:55 +00:00
|
|
|
)}
|
2026-02-24 20:44:16 +00:00
|
|
|
</Container>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default DevexpressReportViewer
|