473 lines
16 KiB
TypeScript
473 lines
16 KiB
TypeScript
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="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
|