2025-09-27 11:51:05 +00:00
|
|
|
|
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'
|
2025-09-29 21:11:15 +00:00
|
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
2025-09-27 11:51:05 +00:00
|
|
|
|
import { Helmet } from 'react-helmet'
|
|
|
|
|
|
import { useParams, useSearchParams } from 'react-router-dom'
|
|
|
|
|
|
import { useListFormCustomDataSource } from '@/shared/useListFormCustomDataSource'
|
|
|
|
|
|
import { GridDto } from '@/proxy/form/models'
|
2025-09-28 18:09:07 +00:00
|
|
|
|
import { usePermission } from '@/utils/hooks/usePermission'
|
2025-09-30 13:11:23 +00:00
|
|
|
|
import { Button, toast, Notification } from '@/components/ui'
|
2025-09-28 18:09:07 +00:00
|
|
|
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
|
|
|
|
|
import { usePWA } from '@/utils/hooks/usePWA'
|
2025-09-29 21:11:15 +00:00
|
|
|
|
import { FaCog, FaCrosshairs, FaMinus, FaPlus, FaSearch, FaSyncAlt } from 'react-icons/fa'
|
2025-09-28 20:22:18 +00:00
|
|
|
|
import { buildSeriesDto } from './Utils'
|
2025-09-29 21:11:15 +00:00
|
|
|
|
import { ChartSeriesDto } from '@/proxy/admin/charts/models'
|
2025-09-30 13:11:23 +00:00
|
|
|
|
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'
|
2025-09-27 11:51:05 +00:00
|
|
|
|
|
|
|
|
|
|
interface ChartProps extends CommonProps, Meta {
|
2025-09-30 13:11:23 +00:00
|
|
|
|
id: string
|
2025-09-27 11:51:05 +00:00
|
|
|
|
listFormCode: string
|
|
|
|
|
|
filter?: string
|
|
|
|
|
|
isSubForm?: boolean
|
|
|
|
|
|
level?: number
|
|
|
|
|
|
refreshData?: () => Promise<void>
|
|
|
|
|
|
gridDto?: GridDto
|
2025-10-12 19:50:01 +00:00
|
|
|
|
refreshGridDto?: () => Promise<void>
|
2025-09-27 11:51:05 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const Chart = (props: ChartProps) => {
|
2025-09-30 13:11:23 +00:00
|
|
|
|
// State UserId güncellemesi için
|
|
|
|
|
|
const { userName } = useStoreState((s) => s.auth.user)
|
|
|
|
|
|
const { id, listFormCode, filter, isSubForm, level, gridDto, refreshGridDto } = props
|
2025-09-27 11:51:05 +00:00
|
|
|
|
const { translate } = useLocalization()
|
2025-09-28 18:09:07 +00:00
|
|
|
|
const { checkPermission } = usePermission()
|
|
|
|
|
|
const isPwaMode = usePWA()
|
2025-09-29 21:11:15 +00:00
|
|
|
|
const initialized = useRef(false)
|
2025-09-27 11:51:05 +00:00
|
|
|
|
|
|
|
|
|
|
const [searchParams] = useSearchParams()
|
|
|
|
|
|
const [chartOptions, setChartOptions] = useState<any>()
|
|
|
|
|
|
const { createSelectDataSource } = useListFormCustomDataSource({})
|
|
|
|
|
|
|
|
|
|
|
|
const params = useParams()
|
|
|
|
|
|
const _listFormCode = props?.listFormCode ?? params?.listFormCode ?? ''
|
|
|
|
|
|
|
2025-09-30 13:11:23 +00:00
|
|
|
|
const [openDialog, setOpenDialog] = useState(false)
|
|
|
|
|
|
const [fieldList, setFieldList] = useState<SelectBoxOption[]>([])
|
|
|
|
|
|
|
2025-09-29 08:33:51 +00:00
|
|
|
|
const [searchText, setSearchText] = useState('')
|
|
|
|
|
|
const [prevValue, setPrevValue] = useState('')
|
|
|
|
|
|
const [urlSearchParams, setUrlSearchParams] = useState<URLSearchParams>(
|
|
|
|
|
|
searchParams ? new URLSearchParams(searchParams) : new URLSearchParams(),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-09-30 13:11:23 +00:00
|
|
|
|
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])
|
2025-09-29 21:11:15 +00:00
|
|
|
|
|
2025-09-27 11:51:05 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!gridDto) return
|
2025-09-30 13:11:23 +00:00
|
|
|
|
if (!allSeries) return
|
|
|
|
|
|
if (!initialized.current) return
|
|
|
|
|
|
|
|
|
|
|
|
const seriesDto = userSeries.length > 0 ? userSeries : allSeries.length > 0 ? allSeries : []
|
2025-09-27 11:51:05 +00:00
|
|
|
|
|
2025-09-29 21:11:15 +00:00
|
|
|
|
const gridOptions = {
|
|
|
|
|
|
...gridDto.gridOptions,
|
2025-09-30 13:11:23 +00:00
|
|
|
|
seriesDto,
|
2025-09-29 21:11:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 13:11:23 +00:00
|
|
|
|
const dataSource = createSelectDataSource(gridOptions, listFormCode, urlSearchParams, [], true)
|
2025-09-27 11:51:05 +00:00
|
|
|
|
|
|
|
|
|
|
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,
|
2025-09-28 18:09:07 +00:00
|
|
|
|
size: gridDto.gridOptions.sizeDto?.useSize
|
|
|
|
|
|
? { width: gridDto.gridOptions.sizeDto.width, height: gridDto.gridOptions.sizeDto.height }
|
|
|
|
|
|
: { width: '100%', height: window.innerHeight - 210 },
|
2025-09-27 11:51:05 +00:00
|
|
|
|
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,
|
|
|
|
|
|
|
2025-09-30 13:11:23 +00:00
|
|
|
|
series: buildSeriesDto(seriesDto),
|
2025-09-28 20:22:18 +00:00
|
|
|
|
|
2025-09-27 11:51:05 +00:00
|
|
|
|
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)
|
2025-09-30 13:11:23 +00:00
|
|
|
|
}, [gridDto, allSeries, initialized.current, searchParams, urlSearchParams])
|
2025-09-29 08:33:51 +00:00
|
|
|
|
|
|
|
|
|
|
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],
|
|
|
|
|
|
)
|
2025-09-27 11:51:05 +00:00
|
|
|
|
|
2025-09-30 13:11:23 +00:00
|
|
|
|
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' },
|
|
|
|
|
|
)
|
2025-09-29 21:11:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-09-30 13:11:23 +00:00
|
|
|
|
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)
|
2025-09-29 21:11:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-30 13:11:23 +00:00
|
|
|
|
// 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()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-29 21:11:15 +00:00
|
|
|
|
|
2025-09-27 11:51:05 +00:00
|
|
|
|
return (
|
|
|
|
|
|
<Container className={DX_CLASSNAMES}>
|
|
|
|
|
|
{!isSubForm && gridDto && (
|
|
|
|
|
|
<Helmet
|
2025-11-03 11:25:05 +00:00
|
|
|
|
titleTemplate="%s | Erp Platform"
|
2025-09-27 11:51:05 +00:00
|
|
|
|
title={translate('::' + gridDto?.gridOptions?.title)}
|
2025-11-03 11:25:05 +00:00
|
|
|
|
defaultTitle="Erp Platform"
|
2025-09-27 11:51:05 +00:00
|
|
|
|
></Helmet>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{_listFormCode && chartOptions && (
|
2025-09-28 20:22:18 +00:00
|
|
|
|
<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">
|
2025-09-28 18:09:07 +00:00
|
|
|
|
<div className="relative pb-1 flex gap-1 border-b-1">
|
2025-09-29 08:33:51 +00:00
|
|
|
|
<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"
|
|
|
|
|
|
/>
|
2025-09-28 20:22:18 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
size="xs"
|
|
|
|
|
|
variant={'default'}
|
|
|
|
|
|
className="text-sm"
|
|
|
|
|
|
onClick={async () => {
|
2025-09-30 13:11:23 +00:00
|
|
|
|
initialized.current = false
|
2025-10-12 19:50:01 +00:00
|
|
|
|
await refreshGridDto?.()
|
2025-09-28 20:22:18 +00:00
|
|
|
|
}}
|
2025-09-29 08:33:51 +00:00
|
|
|
|
title="Refresh Data"
|
2025-09-28 20:22:18 +00:00
|
|
|
|
>
|
|
|
|
|
|
<FaSyncAlt className="w-3 h-3" />
|
|
|
|
|
|
</Button>
|
2025-09-29 21:11:15 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
size="xs"
|
|
|
|
|
|
variant="default"
|
|
|
|
|
|
className="text-sm"
|
|
|
|
|
|
onClick={() => setOpenDialog(true)}
|
2025-09-30 13:11:23 +00:00
|
|
|
|
title="Series Özelleştir"
|
2025-09-29 21:11:15 +00:00
|
|
|
|
>
|
|
|
|
|
|
<FaCrosshairs className="w-3 h-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
2025-09-28 18:09:07 +00:00
|
|
|
|
{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"
|
|
|
|
|
|
>
|
2025-09-29 08:33:51 +00:00
|
|
|
|
<FaCog className="w-3 h-3" />
|
2025-09-28 18:09:07 +00:00
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<DxChart key={'DxChart' + _listFormCode} {...chartOptions}></DxChart>
|
2025-09-29 21:11:15 +00:00
|
|
|
|
|
2025-09-30 13:11:23 +00:00
|
|
|
|
<ChartSeriesDialog
|
|
|
|
|
|
open={openDialog}
|
|
|
|
|
|
onClose={() => setOpenDialog(false)}
|
|
|
|
|
|
initialSeries={allSeries.filter((s) => s.userId === userName)}
|
|
|
|
|
|
fieldList={fieldList}
|
|
|
|
|
|
onSave={onSave}
|
|
|
|
|
|
/>
|
2025-09-28 18:09:07 +00:00
|
|
|
|
</div>
|
2025-09-27 11:51:05 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</Container>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default Chart
|