sozsoft-platform/ui/src/views/list/Chart.tsx

408 lines
14 KiB
TypeScript
Raw Normal View History

2026-02-24 20:44:16 +00:00
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 { Button, toast, Notification } from '@/components/ui'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { usePWA } from '@/utils/hooks/usePWA'
import { FaCog, FaCrosshairs, FaSearch, FaSyncAlt } 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 { getListFormFields } from '@/services/admin/list-form-field.service'
import { groupBy } from 'lodash'
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'
interface ChartProps extends CommonProps, Meta {
id: string
listFormCode: string
filter?: string
isSubForm?: boolean
level?: number
refreshData?: () => Promise<void>
gridDto?: GridDto
refreshGridDto?: () => Promise<void>
}
const Chart = (props: ChartProps) => {
// State UserId güncellemesi için
const { userName } = useStoreState((s) => s.auth.user)
const { id, listFormCode, filter, isSubForm, level, 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 config = useStoreState((state) => state.abpConfig.config)
const params = useParams()
const _listFormCode = props?.listFormCode ?? params?.listFormCode ?? ''
const [openDrawer, setOpenDrawer] = useState(false)
const [fieldList, setFieldList] = useState<SelectBoxOption[]>([])
// 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 [allSeries, setAllSeries] = useState<ChartSeriesDto[]>([])
useEffect(() => {
if (gridDto) {
const initialSeries = gridDto.gridOptions.seriesDto.map((s, index) => ({ ...s, index }))
const userSeriesData = initialSeries.filter((s) => s.userId === userName)
setAllSeries(initialSeries)
// 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,
)
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,
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])
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 getFields = useCallback(async () => {
if (!props.listFormCode) return
try {
const resp = await getListFormFields({
listFormCode: props.listFormCode,
sorting: 'ListOrderNo',
maxResultCount: 1000,
})
if (resp.data?.items) {
const fieldNames = groupBy(resp?.data?.items, 'fieldName')
setFieldList(Object.keys(fieldNames).map((fieldName) => {
const firstItem = fieldNames[fieldName][0]
const label = firstItem.captionName
? translate('::' + firstItem.captionName)
: fieldName
return { value: fieldName, label }
}))
}
} catch (error: any) {
toast.push(
<Notification type="danger" duration={2000}>
Alanlar getirilemedi {error.toString()}
</Notification>,
{ placement: 'top-end' },
)
}
}, [props.listFormCode, translate])
useEffect(() => {
if (props.listFormCode) getFields()
}, [props.listFormCode, config])
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 bg-white dark:bg-neutral-800 dark:border-neutral-700 h-full relative">
<div className="flex justify-end items-center h-full">
<div className="relative pb-1 flex gap-1 border-b-1">
<FaSearch className="absolute left-2 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="p-1 pl-6 pr-2 border border-1 outline-none text-xs text-gray-700 dark:text-gray-200 placeholder-gray-400 rounded"
/>
<Button
size="xs"
variant={'default'}
className="text-sm flex items-center gap-1"
onClick={async () => {
setInitialized(false)
await refreshGridDto?.()
}}
title={translate('::App.Platform.Refresh')}
>
<FaSyncAlt className="w-3 h-3" /> {translate('::App.Platform.Refresh')}
</Button>
<Button
size="xs"
variant="default"
className="text-sm flex items-center gap-1"
onClick={() => setOpenDrawer(true)}
title={translate('::ListForms.ListFormEdit.TabChartSeries')}
>
<FaCrosshairs className="w-3 h-3" /> {translate('::ListForms.ListFormEdit.TabChartSeries')}
</Button>
{checkPermission(gridDto?.gridOptions.permissionDto.u) && (
<Button
size="xs"
variant={'default'}
className="text-sm"
onClick={() => {
window.open(
ROUTES_ENUM.protected.saas.listFormManagement.edit.replace(
':listFormCode',
listFormCode,
),
isPwaMode ? '_self' : '_blank',
)
}}
title={translate('::ListForms.ListForm.Manage')}
>
<FaCog className="w-3 h-3" />
</Button>
)}
</div>
</div>
<div
className={`transition-all duration-300 ${openDrawer ? 'mr-[500px]' : 'mr-0'}`}
style={{ width: openDrawer ? 'calc(100% - 500px)' : '100%' }}
>
<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