sozsoft-platform/ui/src/views/list/Chart.tsx
2026-07-30 17:37:15 +03:00

473 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { CommonProps } from '@/proxy/common'
import { Meta } from '@/proxy/routes/routes'
import { Container } from '@/components/shared'
import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
import DxChart from 'devextreme-react/chart'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { Helmet } from 'react-helmet'
import { useParams, useSearchParams } from 'react-router-dom'
import { GridDto } from '@/proxy/form/models'
import { usePermission } from '@/utils/hooks/usePermission'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { usePWA } from '@/utils/hooks/usePWA'
import { FaSearch } from 'react-icons/fa'
import { buildSeriesDto } from './Utils'
import { ChartSeriesDto } from '@/proxy/admin/charts/models'
import { SelectBoxOption } from '@/types/shared'
import { useStoreState } from '@/store/store'
import ChartDrawer from './ChartDrawer'
import { ListFormJsonRowDto } from '@/proxy/admin/list-form/models'
import { ListFormEditTabs } from '@/proxy/admin/list-form/options'
import {
deleteListFormJsonRow,
postListFormJsonRow,
putListFormJsonRow,
} from '@/services/admin/list-form.service'
import { layoutTypes } from '../admin/listForm/edit/types'
import { useListFormCustomDataSource } from './useListFormCustomDataSource'
import { useListFormColumns } from './useListFormColumns'
import { GridColumnData } from './GridColumnData'
import Toolbar, { Item } from 'devextreme-react/toolbar'
interface ChartProps extends CommonProps, Meta {
id: string
listFormCode: string
filter?: string
isSubForm?: boolean
level?: number
refreshData?: () => Promise<void>
gridDto?: GridDto
refreshGridDto?: () => Promise<void>
}
type LookupDisplayValues = Record<string, Map<string, string>>
const Chart = (props: ChartProps) => {
// State UserId güncellemesi için
const { userName } = useStoreState((s) => s.auth.user)
const { id, listFormCode, isSubForm, gridDto, refreshGridDto } = props
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const isPwaMode = usePWA()
const [initialized, setInitialized] = useState(false)
const [searchParams] = useSearchParams()
const [chartOptions, setChartOptions] = useState<any>()
const { createSelectDataSource } = useListFormCustomDataSource({} as any)
const { loadLookupDisplayValues } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
})
const [lookupDisplayValues, setLookupDisplayValues] = useState<LookupDisplayValues>({})
const params = useParams()
const _listFormCode = props?.listFormCode ?? params?.listFormCode ?? ''
const [openDrawer, setOpenDrawer] = useState(false)
const fieldList = useMemo<SelectBoxOption[]>(() => {
if (!gridDto?.columnFormats) {
return []
}
const fields = new Map<string, SelectBoxOption>()
gridDto.columnFormats.forEach((column) => {
const fieldName = column.fieldName
if (!fieldName || fields.has(fieldName)) {
return
}
fields.set(fieldName, {
value: fieldName,
label: column.captionName ? translate('::' + column.captionName) : fieldName,
})
})
return Array.from(fields.values())
}, [gridDto?.columnFormats, translate])
// Ana state - Chart bu state'e göre render edilir
const [currentSeries, setCurrentSeries] = useState<ChartSeriesDto[]>([])
// Veritabanından gelen orijinal seriler (kaydetme işlemi için)
const [savedSeries, setSavedSeries] = useState<ChartSeriesDto[]>([])
const [searchText, setSearchText] = useState('')
const [prevValue, setPrevValue] = useState('')
const [urlSearchParams, setUrlSearchParams] = useState<URLSearchParams>(
searchParams ? new URLSearchParams(searchParams) : new URLSearchParams(),
)
const lookupColumns = useMemo<GridColumnData[]>(
() =>
gridDto?.columnFormats
.filter((column) => column.fieldName)
.map((column) => ({ dataField: column.fieldName, colData: column })) ?? [],
[gridDto?.columnFormats],
)
useEffect(() => {
let isActive = true
const loadValues = async () => {
try {
const values = await loadLookupDisplayValues(lookupColumns)
if (isActive) {
setLookupDisplayValues(values)
}
} catch (error) {
console.error('Chart lookup load error:', error)
}
}
loadValues()
return () => {
isActive = false
}
}, [lookupColumns, loadLookupDisplayValues])
useEffect(() => {
if (gridDto) {
const initialSeries = gridDto.gridOptions.seriesDto.map((s, index) => ({ ...s, index }))
const userSeriesData = initialSeries.filter((s) => s.userId === userName)
// Kullanıcının serisi varsa onu kullan, yoksa tüm serileri kullan
const seriesToUse = userSeriesData.length > 0 ? userSeriesData : initialSeries
// Sadece ilk yüklemede VEYA refresh sonrası güncelle
if (!initialized) {
setCurrentSeries(seriesToUse)
setSavedSeries(seriesToUse)
setInitialized(true)
} else {
// Refresh sonrası - yeni kaydedilen serileri kullan
setCurrentSeries(seriesToUse)
setSavedSeries(seriesToUse)
}
}
}, [gridDto, userName])
const memoizedChartOptions = useMemo(() => {
if (!gridDto || !initialized) return undefined
const seriesDto = currentSeries
const gridOptions = {
...gridDto.gridOptions,
seriesDto,
}
const dataSource = createSelectDataSource(
gridOptions,
listFormCode,
urlSearchParams,
layoutTypes.chart,
)
const argumentLookupValues = currentSeries
.map((series) => (series.argumentField ? lookupDisplayValues[series.argumentField] : undefined))
.find((values) => values?.size)
return {
dataSource: dataSource,
adjustOnZoom: gridDto.gridOptions.commonDto?.adjustOnZoom ?? true,
containerBackgroundColor:
gridDto.gridOptions.commonDto?.containerBackgroundColor ?? '#FFFFFF',
disabled: gridDto.gridOptions.commonDto?.disabled ?? false,
palette: gridDto.gridOptions.commonDto?.palette ?? 'Material',
paletteExtensionMode: gridDto.gridOptions.commonDto?.paletteExtensionMode ?? 'blend',
title: gridDto.gridOptions.titleDto,
size: gridDto.gridOptions.sizeDto?.useSize
? { width: gridDto.gridOptions.sizeDto.width, height: gridDto.gridOptions.sizeDto.height }
: {
width: openDrawer ? window.innerWidth - 550 : '100%',
height: window.innerHeight - 210,
},
legend: gridDto.gridOptions.legendDto || {
visible: true,
verticalAlignment: 'bottom',
horizontalAlignment: 'center',
},
margin: gridDto.gridOptions.marginDto,
adaptiveLayout: gridDto.gridOptions.adaptivelayoutDto,
defaultPane: gridDto.gridOptions.commonDto?.defaultPane,
scrollBar: gridDto.gridOptions.scrollBarDto,
zoomAndPan: gridDto.gridOptions.zoomAndPanDto,
animation: gridDto.gridOptions.animationDto,
export: gridDto.gridOptions.exportDto,
crosshair: gridDto.gridOptions.crosshairDto,
argumentAxis: {
...gridDto.gridOptions.argumentAxisDto,
label: {
...gridDto.gridOptions.argumentAxisDto?.label,
...(argumentLookupValues
? {
customizeText: (argument: any) => {
const value = argument.value ?? argument.valueText
return argumentLookupValues.get(String(value)) ?? argument.valueText ?? String(value ?? '')
},
}
: {}),
},
},
valueAxis: gridDto.gridOptions.valueAxisDto,
tooltip: gridDto.gridOptions.tooltipDto || {
enabled: true,
shared: false,
format: 'decimal',
},
series: buildSeriesDto(seriesDto),
panes: gridDto.gridOptions.panesDto?.length > 0 ? gridDto.gridOptions.panesDto : undefined,
commonSeriesSettings: gridDto.gridOptions.commonSeriesSettingsDto,
commonPaneSettings: gridDto.gridOptions.commonPaneSettingsDto,
commonAxisSettings: gridDto.gridOptions.commonAxisSettingsDto,
annotations: gridDto.gridOptions.annotationsDto,
commonAnnotationSettings: gridDto.gridOptions.commonAnnotationSettingsDto,
loadingIndicator: {
enabled: true,
},
}
}, [
gridDto,
currentSeries,
initialized,
createSelectDataSource,
listFormCode,
urlSearchParams,
openDrawer,
lookupDisplayValues,
])
useEffect(() => {
if (memoizedChartOptions) {
setChartOptions(memoizedChartOptions)
}
}, [memoizedChartOptions])
const onFilter = useCallback(
(value?: string) => {
const text = value !== undefined ? value.trim() : searchText.trim()
if (!gridDto?.columnFormats) return
const newParams = new URLSearchParams(urlSearchParams.toString())
if (!text) {
newParams.delete('filter')
setUrlSearchParams(newParams)
return
}
const merged = gridDto.columnFormats
.filter(
(col) =>
col.dataType === 'string' &&
col.visible &&
col.width &&
col.allowSearch &&
col.width > 0,
)
.map((col) => [col.fieldName, 'contains', text])
let filter: any = null
if (merged.length === 1) {
filter = merged[0]
} else if (merged.length > 1) {
filter = merged.reduce((acc, f, idx) => {
if (idx === 0) return f
return [acc, 'or', f]
}, null as any)
}
if (filter) {
newParams.set('filter', JSON.stringify(filter))
} else {
newParams.delete('filter')
}
setUrlSearchParams(newParams)
},
[gridDto, urlSearchParams, searchText],
)
const handlePreviewChange = useCallback((series: ChartSeriesDto[]) => {
// Preview değişikliklerini anında chart'a yansıt
setCurrentSeries(series)
}, [])
const handleDrawerClose = useCallback(() => {
setOpenDrawer(false)
// İptal - kaydedilmiş serilere geri dön
setCurrentSeries(savedSeries)
}, [savedSeries])
const onSave = useCallback(
async (newSeries: ChartSeriesDto[]) => {
// 1. Silinecek serileri bul (savedSeries var ama newSeries yok)
const toDelete = savedSeries.filter(
(old: ChartSeriesDto) => !newSeries.some((s) => s.index === old.index),
)
// Index kaymasını önlemek için büyükten küçüğe sırala
toDelete.sort((a: ChartSeriesDto, b: ChartSeriesDto) => b.index - a.index)
for (const old of toDelete) {
await deleteListFormJsonRow(id, ListFormEditTabs.ChartSeries.GeneralJsonRow, old.index)
}
// 2. Yeni veya güncellenen serileri kaydet
for (const series of newSeries) {
const input: ListFormJsonRowDto = {
index: series.index,
fieldName: ListFormEditTabs.ChartSeries.GeneralJsonRow,
itemChartSeries: series,
}
if (series.index === -1) {
await postListFormJsonRow(id, input)
} else {
await putListFormJsonRow(id, input)
}
}
// 3. Yeniden yükle (veritabanından fresh data)
if (props.refreshGridDto) {
setInitialized(false)
await props.refreshGridDto()
// refreshGridDto tamamlandıktan sonra yeni state'ler otomatik setlenecek
} else {
// refreshGridDto yoksa manuel güncelle
setSavedSeries(newSeries)
setCurrentSeries(newSeries)
}
},
[savedSeries, id, props],
)
return (
<Container className={DX_CLASSNAMES}>
{!isSubForm && gridDto && (
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + gridDto?.gridOptions?.title)}
defaultTitle={APP_NAME}
></Helmet>
)}
{_listFormCode && chartOptions && (
<div className="p-1">
<div className="list-view-toolbar-host dx-datagrid-header-panel pivot-toolbar-panel text-black dark:text-white">
<Toolbar width="100%">
<Item
location="before"
locateInMenu="never"
render={() => (
<div className="relative flex items-center">
<FaSearch className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm" />
<input
type="text"
placeholder={translate('::App.Search')}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onFilter(e.currentTarget.value)
setPrevValue(e.currentTarget.value.trim()) // Enter ile tetiklenirse güncelle
}
}}
onBlur={(e) => {
const newValue = e.currentTarget.value.trim()
// 1. Değer değişmemişse => hiçbir şey yapma
if (newValue === prevValue) return
// 2. Yeni değer boş, ama eskiden değer vardı => filtre temizle
// 3. Yeni değer dolu ve eskisinden farklı => filtre uygula
onFilter(newValue)
setPrevValue(newValue)
}}
className="chart-toolbar-search outline-none text-xs text-gray-700 placeholder-gray-400 dark:text-gray-200 dark:placeholder-gray-400"
/>
</div>
)}
/>
<Item
location="after"
widget="dxButton"
locateInMenu="auto"
showText="always"
options={{
icon: 'refresh',
text: translate('::App.Platform.Refresh'),
hint: translate('::App.Platform.Refresh'),
onClick: async () => {
setInitialized(false)
await refreshGridDto?.()
},
}}
/>
<Item
location="after"
widget="dxButton"
locateInMenu="auto"
showText="always"
options={{
icon: 'chart',
text: translate('::ListForms.ListFormEdit.TabChartSeries'),
hint: translate('::ListForms.ListFormEdit.TabChartSeries'),
onClick: () => setOpenDrawer(true),
}}
/>
{checkPermission('App.Listforms.Listform.Update') && (
<Item
location="after"
widget="dxButton"
locateInMenu="auto"
showText="always"
options={{
icon: 'preferences',
text: translate('::ListForms.ListForm.Manage'),
hint: translate('::ListForms.ListForm.Manage'),
onClick: () => {
window.open(
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(
':listFormCode',
listFormCode,
),
isPwaMode ? '_self' : '_blank',
)
},
}}
/>
)}
</Toolbar>
</div>
<div className="border border-gray-200 dark:border-gray-600">
<DxChart key={'DxChart' + _listFormCode} {...chartOptions}></DxChart>
</div>
<ChartDrawer
open={openDrawer}
onClose={handleDrawerClose}
initialSeries={currentSeries}
fieldList={fieldList}
onSave={onSave}
onPreviewChange={handlePreviewChange}
/>
</div>
)}
</Container>
)
}
export default Chart