erp-platform/ui/src/views/list/Chart.tsx
Sedat ÖZTÜRK 4e555e9cc6 Kurs -> Erp
2025-11-03 14:25:05 +03:00

346 lines
12 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 '@/@types/common'
import { Meta } from '@/@types/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, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import { useParams, useSearchParams } from 'react-router-dom'
import { useListFormCustomDataSource } from '@/shared/useListFormCustomDataSource'
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, FaMinus, FaPlus, FaSearch, FaSyncAlt } from 'react-icons/fa'
import { buildSeriesDto } from './Utils'
import { ChartSeriesDto } from '@/proxy/admin/charts/models'
import { SelectBoxOption } from '@/shared/types'
import { useStoreState } from '@/store/store'
import ChartSeriesDialog from './ChartSeriesDialog'
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'
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 = useRef(false)
const [searchParams] = useSearchParams()
const [chartOptions, setChartOptions] = useState<any>()
const { createSelectDataSource } = useListFormCustomDataSource({})
const params = useParams()
const _listFormCode = props?.listFormCode ?? params?.listFormCode ?? ''
const [openDialog, setOpenDialog] = useState(false)
const [fieldList, setFieldList] = useState<SelectBoxOption[]>([])
const [searchText, setSearchText] = useState('')
const [prevValue, setPrevValue] = useState('')
const [urlSearchParams, setUrlSearchParams] = useState<URLSearchParams>(
searchParams ? new URLSearchParams(searchParams) : new URLSearchParams(),
)
const [allSeries, setAllSeries] = useState<ChartSeriesDto[]>([])
const [userSeries, setUserSeries] = useState<ChartSeriesDto[]>([])
const [oldSeries, setOldSeries] = useState<ChartSeriesDto[]>([])
useEffect(() => {
if (gridDto && !initialized.current) {
const initialSeries = gridDto.gridOptions.seriesDto.map((s, index) => ({ ...s, index }))
setAllSeries(initialSeries)
setUserSeries(initialSeries.filter((s) => s.userId === userName))
setOldSeries(initialSeries.filter((s) => s.userId === userName))
initialized.current = true
}
}, [gridDto])
useEffect(() => {
if (!gridDto) return
if (!allSeries) return
if (!initialized.current) return
const seriesDto = userSeries.length > 0 ? userSeries : allSeries.length > 0 ? allSeries : []
const gridOptions = {
...gridDto.gridOptions,
seriesDto,
}
const dataSource = createSelectDataSource(gridOptions, listFormCode, urlSearchParams, [], true)
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: '100%', height: window.innerHeight - 210 },
legend: gridDto.gridOptions.legendDto,
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,
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, allSeries, initialized.current, searchParams, urlSearchParams])
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((a) => ({ value: a, label: a })))
}
} 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])
const onSave = async (newSeries: ChartSeriesDto[]) => {
// 1. Silinecek serileri bul (oldSeries var ama newSeries yok)
const toDelete = oldSeries.filter((old) => !newSeries.some((s) => s.index === old.index))
// Index kaymasını önlemek için büyükten küçüğe sırala
toDelete.sort((a, b) => 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
if (props.refreshGridDto) {
initialized.current = false
await props.refreshGridDto()
}
}
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">
<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="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"
onClick={async () => {
initialized.current = false
await refreshGridDto?.()
}}
title="Refresh Data"
>
<FaSyncAlt className="w-3 h-3" />
</Button>
<Button
size="xs"
variant="default"
className="text-sm"
onClick={() => setOpenDialog(true)}
title="Series Özelleştir"
>
<FaCrosshairs className="w-3 h-3" />
</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="Form Manager"
>
<FaCog className="w-3 h-3" />
</Button>
)}
</div>
</div>
<DxChart key={'DxChart' + _listFormCode} {...chartOptions}></DxChart>
<ChartSeriesDialog
open={openDialog}
onClose={() => setOpenDialog(false)}
initialSeries={allSeries.filter((s) => s.userId === userName)}
fieldList={fieldList}
onSave={onSave}
/>
</div>
)}
</Container>
)
}
export default Chart