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

406 lines
14 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 { DX_CLASSNAMES } from '@/constants/app.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
import DxChart from 'devextreme-react/chart'
import { useCallback, useEffect, 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])
useEffect(() => {
if (!gridDto) return
if (!initialized) return
// Chart her zaman currentSeries'e göre render edilir
const seriesDto = currentSeries
const gridOptions = {
...gridDto.gridOptions,
seriesDto,
}
const dataSource = createSelectDataSource(
gridOptions,
listFormCode,
urlSearchParams,
layoutTypes.chart,
)
const options = {
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',
//theme: s(chartDto.commonDto?.theme, 'generic.light'),
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,
},
}
setChartOptions(options)
}, [gridDto, currentSeries, initialized, searchParams, urlSearchParams, openDrawer])
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 = 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' },
)
}
}
useEffect(() => {
if (props.listFormCode) getFields()
}, [props.listFormCode, config])
const handlePreviewChange = (series: ChartSeriesDto[]) => {
// Preview değişikliklerini anında chart'a yansıt
setCurrentSeries(series)
}
const handleDrawerClose = () => {
setOpenDrawer(false)
// İptal - kaydedilmiş serilere geri dön
setCurrentSeries(savedSeries)
}
const onSave = 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)
}
}
return (
<Container className={DX_CLASSNAMES}>
{!isSubForm && gridDto && (
<Helmet
titleTemplate="%s | Erp Platform"
title={translate('::' + gridDto?.gridOptions?.title)}
defaultTitle="Erp Platform"
></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