Chart düzenlemesi

This commit is contained in:
Sedat Öztürk 2025-12-01 01:45:43 +03:00
parent ee0b8d8421
commit 55aaad3d31
7 changed files with 557 additions and 318 deletions

View file

@ -684,7 +684,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
var sqlSub = new StringBuilder(); var sqlSub = new StringBuilder();
sqlSub.Append("SELECT "); sqlSub.Append("SELECT ");
sqlSub.Append(string.Join(',', GroupTuples.Select((a) => $"{a.Field} AS \"{a.SelectExpr}\""))); sqlSub.Append(string.Join(',', GroupTuples.Select((a) => $"{a.Field} AS \"{a.SelectExpr}\"")));
sqlSub.Append(string.Join(',', GroupSummaryTuples.Select((a) => $",{a.SummaryType}({a.Field}) AS \"{a.SelectExpr}\""))); sqlSub.Append(string.Join("", GroupSummaryTuples.Select((a) => $",{a.SummaryType}({a.Field}) AS \"{a.SelectExpr}\"")));
sqlSub.Append(",COUNT(1) AS \"Summary_Count\""); sqlSub.Append(",COUNT(1) AS \"Summary_Count\"");
sqlSub.Append($" FROM \"{From}\" AS \"{TableName}\""); sqlSub.Append($" FROM \"{From}\" AS \"{TableName}\"");
sqlSub.Append(GetJoinString()); sqlSub.Append(GetJoinString());

View file

@ -11,7 +11,6 @@ import {
GridsEditMode, GridsEditMode,
GridsEditRefreshMode, GridsEditRefreshMode,
NewRowPosition, NewRowPosition,
PagerDisplayMode,
SelectionColumnDisplayMode, SelectionColumnDisplayMode,
StartEditAction, StartEditAction,
StateStoreType, StateStoreType,
@ -19,7 +18,6 @@ import {
import { FormItemComponent } from 'devextreme/ui/form' import { FormItemComponent } from 'devextreme/ui/form'
import { AuditedEntityDto } from '../abp' import { AuditedEntityDto } from '../abp'
import { EditorType2, RowMode } from '../../views/form/types' import { EditorType2, RowMode } from '../../views/form/types'
import { bool } from 'yup'
import { import {
ChartCommonDto, ChartCommonDto,
ChartAdaptivelayoutDto, ChartAdaptivelayoutDto,
@ -45,6 +43,7 @@ import {
} from '../admin/charts/models' } from '../admin/charts/models'
import { ListViewLayoutType } from '@/views/admin/listForm/edit/types' import { ListViewLayoutType } from '@/views/admin/listForm/edit/types'
import { SeriesType } from 'devextreme/common/charts' import { SeriesType } from 'devextreme/common/charts'
import { PagerDisplayMode } from 'devextreme/common/grids'
//1 //1
export interface SelectListItem { export interface SelectListItem {

View file

@ -4,7 +4,7 @@ import { Container } from '@/components/shared'
import { DX_CLASSNAMES } from '@/constants/app.constant' import { DX_CLASSNAMES } from '@/constants/app.constant'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import DxChart from 'devextreme-react/chart' import DxChart from 'devextreme-react/chart'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { useParams, useSearchParams } from 'react-router-dom' import { useParams, useSearchParams } from 'react-router-dom'
import { GridDto } from '@/proxy/form/models' import { GridDto } from '@/proxy/form/models'
@ -17,7 +17,7 @@ import { buildSeriesDto } from './Utils'
import { ChartSeriesDto } from '@/proxy/admin/charts/models' import { ChartSeriesDto } from '@/proxy/admin/charts/models'
import { SelectBoxOption } from '@/types/shared' import { SelectBoxOption } from '@/types/shared'
import { useStoreState } from '@/store/store' import { useStoreState } from '@/store/store'
import ChartSeriesDialog from './ChartSeriesDialog' import ChartDrawer from './ChartDrawer'
import { getListFormFields } from '@/services/admin/list-form-field.service' import { getListFormFields } from '@/services/admin/list-form-field.service'
import { groupBy } from 'lodash' import { groupBy } from 'lodash'
import { ListFormJsonRowDto } from '@/proxy/admin/list-form/models' import { ListFormJsonRowDto } from '@/proxy/admin/list-form/models'
@ -48,7 +48,7 @@ const Chart = (props: ChartProps) => {
const { translate } = useLocalization() const { translate } = useLocalization()
const { checkPermission } = usePermission() const { checkPermission } = usePermission()
const isPwaMode = usePWA() const isPwaMode = usePWA()
const initialized = useRef(false) const [initialized, setInitialized] = useState(false)
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const [chartOptions, setChartOptions] = useState<any>() const [chartOptions, setChartOptions] = useState<any>()
@ -57,8 +57,14 @@ const Chart = (props: ChartProps) => {
const params = useParams() const params = useParams()
const _listFormCode = props?.listFormCode ?? params?.listFormCode ?? '' const _listFormCode = props?.listFormCode ?? params?.listFormCode ?? ''
const [openDialog, setOpenDialog] = useState(false) const [openDrawer, setOpenDrawer] = useState(false)
const [fieldList, setFieldList] = useState<SelectBoxOption[]>([]) 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 [searchText, setSearchText] = useState('')
const [prevValue, setPrevValue] = useState('') const [prevValue, setPrevValue] = useState('')
@ -68,27 +74,35 @@ const Chart = (props: ChartProps) => {
const [allSeries, setAllSeries] = useState<ChartSeriesDto[]>([]) const [allSeries, setAllSeries] = useState<ChartSeriesDto[]>([])
const [userSeries, setUserSeries] = useState<ChartSeriesDto[]>([])
const [oldSeries, setOldSeries] = useState<ChartSeriesDto[]>([])
useEffect(() => { useEffect(() => {
if (gridDto && !initialized.current) { if (gridDto) {
const initialSeries = gridDto.gridOptions.seriesDto.map((s, index) => ({ ...s, index })) const initialSeries = gridDto.gridOptions.seriesDto.map((s, index) => ({ ...s, index }))
const userSeriesData = initialSeries.filter((s) => s.userId === userName)
setAllSeries(initialSeries) setAllSeries(initialSeries)
setUserSeries(initialSeries.filter((s) => s.userId === userName))
setOldSeries(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
initialized.current = true
// 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]) }, [gridDto, userName])
useEffect(() => { useEffect(() => {
if (!gridDto) return if (!gridDto) return
if (!allSeries) return if (!initialized) return
if (!initialized.current) return
const seriesDto = userSeries.length > 0 ? userSeries : allSeries.length > 0 ? allSeries : [] // Chart her zaman currentSeries'e göre render edilir
const seriesDto = currentSeries
const gridOptions = { const gridOptions = {
...gridDto.gridOptions, ...gridDto.gridOptions,
@ -116,8 +130,15 @@ const Chart = (props: ChartProps) => {
title: gridDto.gridOptions.titleDto, title: gridDto.gridOptions.titleDto,
size: gridDto.gridOptions.sizeDto?.useSize size: gridDto.gridOptions.sizeDto?.useSize
? { width: gridDto.gridOptions.sizeDto.width, height: gridDto.gridOptions.sizeDto.height } ? { width: gridDto.gridOptions.sizeDto.width, height: gridDto.gridOptions.sizeDto.height }
: { width: '100%', height: window.innerHeight - 210 }, : {
legend: gridDto.gridOptions.legendDto, width: openDrawer ? window.innerWidth - 550 : '100%',
height: window.innerHeight - 210
},
legend: gridDto.gridOptions.legendDto || {
visible: true,
verticalAlignment: 'bottom',
horizontalAlignment: 'center',
},
margin: gridDto.gridOptions.marginDto, margin: gridDto.gridOptions.marginDto,
adaptiveLayout: gridDto.gridOptions.adaptivelayoutDto, adaptiveLayout: gridDto.gridOptions.adaptivelayoutDto,
defaultPane: gridDto.gridOptions.commonDto?.defaultPane, defaultPane: gridDto.gridOptions.commonDto?.defaultPane,
@ -129,7 +150,11 @@ const Chart = (props: ChartProps) => {
crosshair: gridDto.gridOptions.crosshairDto, crosshair: gridDto.gridOptions.crosshairDto,
argumentAxis: gridDto.gridOptions.argumentAxisDto, argumentAxis: gridDto.gridOptions.argumentAxisDto,
valueAxis: gridDto.gridOptions.valueAxisDto, valueAxis: gridDto.gridOptions.valueAxisDto,
tooltip: gridDto.gridOptions.tooltipDto, tooltip: gridDto.gridOptions.tooltipDto || {
enabled: true,
shared: false,
format: 'decimal',
},
series: buildSeriesDto(seriesDto), series: buildSeriesDto(seriesDto),
@ -146,7 +171,7 @@ const Chart = (props: ChartProps) => {
} }
setChartOptions(options) setChartOptions(options)
}, [gridDto, allSeries, initialized.current, searchParams, urlSearchParams]) }, [gridDto, currentSeries, initialized, searchParams, urlSearchParams, openDrawer])
const onFilter = useCallback( const onFilter = useCallback(
(value?: string) => { (value?: string) => {
@ -220,12 +245,23 @@ const Chart = (props: ChartProps) => {
if (props.listFormCode) getFields() if (props.listFormCode) getFields()
}, [props.listFormCode]) }, [props.listFormCode])
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[]) => { const onSave = async (newSeries: ChartSeriesDto[]) => {
// 1. Silinecek serileri bul (oldSeries var ama newSeries yok) // 1. Silinecek serileri bul (savedSeries var ama newSeries yok)
const toDelete = oldSeries.filter((old) => !newSeries.some((s) => s.index === old.index)) 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 // Index kaymasını önlemek için büyükten küçüğe sırala
toDelete.sort((a, b) => b.index - a.index) toDelete.sort((a: ChartSeriesDto, b: ChartSeriesDto) => b.index - a.index)
for (const old of toDelete) { for (const old of toDelete) {
await deleteListFormJsonRow(id, ListFormEditTabs.ChartSeries.GeneralJsonRow, old.index) await deleteListFormJsonRow(id, ListFormEditTabs.ChartSeries.GeneralJsonRow, old.index)
@ -246,10 +282,15 @@ const Chart = (props: ChartProps) => {
} }
} }
// 3. Yeniden yükle // 3. Yeniden yükle (veritabanından fresh data)
if (props.refreshGridDto) { if (props.refreshGridDto) {
initialized.current = false setInitialized(false)
await props.refreshGridDto() await props.refreshGridDto()
// refreshGridDto tamamlandıktan sonra yeni state'ler otomatik setlenecek
} else {
// refreshGridDto yoksa manuel güncelle
setSavedSeries(newSeries)
setCurrentSeries(newSeries)
} }
} }
@ -263,7 +304,7 @@ const Chart = (props: ChartProps) => {
></Helmet> ></Helmet>
)} )}
{_listFormCode && chartOptions && ( {_listFormCode && chartOptions && (
<div className="p-1 bg-white dark:bg-neutral-800 dark:border-neutral-700 h-full"> <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="flex justify-end items-center h-full">
<div className="relative pb-1 flex gap-1 border-b-1"> <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" /> <FaSearch className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-sm" />
@ -296,7 +337,7 @@ const Chart = (props: ChartProps) => {
variant={'default'} variant={'default'}
className="text-sm" className="text-sm"
onClick={async () => { onClick={async () => {
initialized.current = false setInitialized(false)
await refreshGridDto?.() await refreshGridDto?.()
}} }}
title="Refresh Data" title="Refresh Data"
@ -307,7 +348,7 @@ const Chart = (props: ChartProps) => {
size="xs" size="xs"
variant="default" variant="default"
className="text-sm" className="text-sm"
onClick={() => setOpenDialog(true)} onClick={() => setOpenDrawer(true)}
title="Series Özelleştir" title="Series Özelleştir"
> >
<FaCrosshairs className="w-3 h-3" /> <FaCrosshairs className="w-3 h-3" />
@ -334,14 +375,20 @@ const Chart = (props: ChartProps) => {
)} )}
</div> </div>
</div> </div>
<DxChart key={'DxChart' + _listFormCode} {...chartOptions}></DxChart> <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>
<ChartSeriesDialog <ChartDrawer
open={openDialog} open={openDrawer}
onClose={() => setOpenDialog(false)} onClose={handleDrawerClose}
initialSeries={allSeries.filter((s) => s.userId === userName)} initialSeries={currentSeries}
fieldList={fieldList} fieldList={fieldList}
onSave={onSave} onSave={onSave}
onPreviewChange={handlePreviewChange}
/> />
</div> </div>
)} )}

View file

@ -0,0 +1,368 @@
import { Button, FormContainer, Input, Notification, Select, toast } from '@/components/ui'
import { Field, FieldArray, Form, Formik, FieldProps } from 'formik'
import { FaMinus, FaPlus, FaTimes, FaSave } from 'react-icons/fa'
import { SelectBoxOption } from '@/types/shared'
import { columnSummaryTypeListOptions } from '../admin/listForm/edit/options'
import { ChartSeriesDto } from '@/proxy/admin/charts/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useStoreState } from '@/store/store'
import { object, array, string } from 'yup'
import { SummaryTypeEnum } from '@/proxy/form/models'
import { useState, useEffect } from 'react'
interface ChartDrawerProps {
open: boolean
onClose: () => void
initialSeries: ChartSeriesDto[]
fieldList: SelectBoxOption[]
onSave: (series: ChartSeriesDto[]) => void
onPreviewChange: (series: ChartSeriesDto[]) => void
}
const schema = object().shape({
series: array().of(
object().shape({
name: string().required('Name Required'),
argumentField: string().required('Argument Field Required'),
valueField: string().required('Value Field Required'),
summaryType: string().required('Summary Type Required'),
}),
),
})
const ChartDrawer = ({
open,
onClose,
initialSeries,
fieldList,
onSave,
onPreviewChange,
}: ChartDrawerProps) => {
const { translate } = useLocalization()
const { userName } = useStoreState((s) => s.auth.user)
const [selectedSeriesIndex, setSelectedSeriesIndex] = useState<number>(-1)
// Chart type icons
const chartTypeIcons = [
{ type: 'line', label: 'Line', icon: '📈' },
{ type: 'bar', label: 'Bar', icon: '📊' },
{ type: 'area', label: 'Area', icon: '🏔️' },
{ type: 'scatter', label: 'Scatter', icon: '🔵' },
{ type: 'spline', label: 'Spline', icon: '〰️' },
{ type: 'stackedbar', label: 'Stacked Bar', icon: '📚' },
{ type: 'stackedarea', label: 'Stacked Area', icon: '⛰️' },
]
const newSeriesValue = () => {
return {
index: -1,
type: 'line',
name: '',
argumentField: '',
valueField: '',
summaryType: SummaryTypeEnum.Sum,
axis: '',
barOverlapGroup: '',
barPadding: 0,
barWidth: 0,
color: '',
cornerRadius: 0,
dashStyle: 'solid',
ignoreEmptyPoints: false,
pane: '',
rangeValue1Field: '',
rangeValue2Field: '',
selectionMode: 'none',
showInLegend: true,
visible: true,
width: 2,
label: {
visible: true,
backgroundColor: '#f05b41',
customizeText: '',
format: 'decimal',
font: {
color: '#FFFFFF',
family: '"Segoe UI", "Helvetica Neue", "Trebuchet MS", Verdana, sans-serif',
size: 12,
weight: 400,
},
},
userId: userName ?? '',
}
}
if (!open) return null
return (
<>
{/* Drawer */}
<div
className={`fixed right-0 top-0 h-full w-[500px] bg-white shadow-2xl z-50 transform transition-transform duration-300 border-l-2 border-gray-300 ${
open ? 'translate-x-0' : 'translate-x-full'
}`}
>
<Formik
enableReinitialize
initialValues={{
series: initialSeries && initialSeries.length > 0 ? initialSeries : [newSeriesValue()],
}}
validationSchema={schema}
onSubmit={async (values, { setSubmitting }) => {
try {
await onSave(values.series)
toast.push(<Notification type="success">Chart kaydedildi</Notification>, {
placement: 'top-end',
})
onClose()
} catch (error: any) {
toast.push(
<Notification type="danger">
Hata
<code>{error}</code>
</Notification>,
{ placement: 'top-end' },
)
} finally {
setSubmitting(false)
}
}}
>
{({ setFieldValue, values, isSubmitting }) => {
// Preview'ı her değişiklikte otomatik güncelle
useEffect(() => {
const validSeries = values.series.filter(
(s) => s.argumentField && s.valueField && s.summaryType,
)
if (validSeries.length > 0) {
onPreviewChange(validSeries)
}
}, [values.series])
return (
<Form className="flex flex-col h-full">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b bg-gray-50">
<h2 className="text-lg font-semibold flex items-center gap-2">
<span>📊</span>
Chart Series
</h2>
<button
type="button"
onClick={onClose}
className="p-2 hover:bg-gray-200 rounded-full transition-colors"
>
<FaTimes />
</button>
</div>
<FormContainer size="sm" className="flex flex-col flex-1 overflow-hidden">
{/* Add Series Button */}
<div className="p-3 border-b">
<Button
variant="solid"
type="button"
size="sm"
onClick={() => {
setFieldValue('series', [...values.series, newSeriesValue()])
}}
className="w-full"
>
<div className="flex items-center justify-center gap-2">
<FaPlus /> Yeni Seri Ekle
</div>
</Button>
</div>
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto p-3">
<FieldArray name="series">
{({ remove }) => (
<div className="space-y-4">
{values.series.map((series, index) => (
<div
key={index}
className="border rounded-lg p-3 bg-gray-50 hover:bg-gray-100 transition-colors"
>
{/* Header */}
<div className="flex items-center justify-between mb-3">
<span className="font-semibold text-sm">Seri #{index + 1}</span>
<Button
type="button"
size="xs"
variant="plain"
icon={<FaMinus />}
className="text-red-500 hover:bg-red-100"
onClick={() => {
remove(index)
}}
/>
</div>
{/* Chart Type Selector */}
<div className="mb-3">
<label className="text-xs font-medium mb-1 block">Chart Type</label>
<Field name={`series[${index}].type`}>
{({ field, form }: FieldProps) => (
<div className="relative">
<button
type="button"
onClick={() =>
setSelectedSeriesIndex(
selectedSeriesIndex === index ? -1 : index,
)
}
className="w-full px-3 py-2 text-left border rounded hover:bg-white flex items-center gap-2 transition-colors"
>
<span className="text-xl">
{chartTypeIcons.find((t) => t.type === field.value)
?.icon || '📊'}
</span>
<span className="text-sm font-medium">
{chartTypeIcons.find((t) => t.type === field.value)
?.label || field.value}
</span>
</button>
{selectedSeriesIndex === index && (
<div className="absolute z-50 mt-1 w-full bg-white border rounded-lg shadow-xl p-2">
<div className="grid grid-cols-2 gap-2">
{chartTypeIcons.map((chartType) => (
<button
key={chartType.type}
type="button"
onClick={() => {
form.setFieldValue(field.name, chartType.type)
setSelectedSeriesIndex(-1)
}}
className={`p-3 rounded-lg hover:bg-blue-50 flex flex-col items-center gap-1 transition-all ${
field.value === chartType.type
? 'bg-blue-100 ring-2 ring-blue-400'
: 'bg-gray-50'
}`}
>
<span className="text-2xl">{chartType.icon}</span>
<span className="text-xs font-medium text-center">
{chartType.label}
</span>
</button>
))}
</div>
</div>
)}
</div>
)}
</Field>
</div>
{/* Name */}
<div className="mb-3">
<label className="text-xs font-medium mb-1 block">Name</label>
<Field
size="sm"
name={`series[${index}].name`}
type="text"
component={Input}
placeholder="Series name"
/>
</div>
{/* Argument Field */}
<div className="mb-3">
<label className="text-xs font-medium mb-1 block">
Argument Field (X-Axis)
</label>
<Field name={`series[${index}].argumentField`}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable={true}
options={fieldList}
value={fieldList?.find(
(option) => option.value === field.value,
)}
onChange={(option) => {
form.setFieldValue(field.name, option?.value)
}}
placeholder="Select field..."
/>
)}
</Field>
</div>
{/* Value Field */}
<div className="mb-3">
<label className="text-xs font-medium mb-1 block">
Value Field (Y-Axis)
</label>
<Field name={`series[${index}].valueField`}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable={true}
options={fieldList}
value={fieldList?.find(
(option) => option.value === field.value,
)}
onChange={(option) => {
form.setFieldValue(field.name, option?.value)
}}
placeholder="Select field..."
/>
)}
</Field>
</div>
{/* Summary Type */}
<div className="mb-3">
<label className="text-xs font-medium mb-1 block">
Summary Type
</label>
<Field name={`series[${index}].summaryType`}>
{({ field, form }: FieldProps) => (
<Select
field={field}
form={form}
isClearable={true}
options={columnSummaryTypeListOptions}
value={columnSummaryTypeListOptions.find(
(option) => option.value === field.value,
)}
onChange={(option) => {
form.setFieldValue(field.name, option?.value)
}}
/>
)}
</Field>
</div>
</div>
))}
</div>
)}
</FieldArray>
</div>
{/* Footer */}
<div className="p-4 border-t bg-gray-50 flex gap-2">
<Button type="button" variant="plain" onClick={onClose} className="flex-1">
İptal
</Button>
<Button variant="solid" loading={isSubmitting} type="submit" className="flex-1">
<div className="flex items-center justify-center gap-2">
<FaSave />
{isSubmitting ? translate('::SavingWithThreeDot') : 'Kaydet'}
</div>
</Button>
</div>
</FormContainer>
</Form>
)
}}
</Formik>
</div>
</>
)
}
export default ChartDrawer

View file

@ -1,279 +0,0 @@
import { Button, FormContainer, Input, Notification, Select, Dialog, toast } from '@/components/ui'
import { Field, FieldArray, Form, Formik, FieldProps } from 'formik'
import { FaMinus, FaPlus } from 'react-icons/fa'
import { SelectBoxOption } from '@/types/shared'
import {
chartSeriesTypeOptions,
columnSummaryTypeListOptions,
} from '../admin/listForm/edit/options'
import { ChartSeriesDto } from '@/proxy/admin/charts/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { useStoreState } from '@/store/store'
import { object, array, string } from 'yup'
import { SummaryTypeEnum } from '@/proxy/form/models'
interface ChartSeriesDialogProps {
open: boolean
onClose: () => void
initialSeries: ChartSeriesDto[]
fieldList: SelectBoxOption[]
onSave: (series: ChartSeriesDto[]) => void
}
const schema = object().shape({
series: array().of(
object().shape({
name: string().required('Name Required'),
argumentField: string().required('Argument Field Required'),
valueField: string().required('Value Field Required'),
summaryType: string().required('Summary Type Required'),
}),
),
})
const ChartSeriesDialog = ({
open,
onClose,
initialSeries,
fieldList,
onSave,
}: ChartSeriesDialogProps) => {
const { translate } = useLocalization()
// State UserId güncellemesi için
const { userName } = useStoreState((s) => s.auth.user)
const newSeriesValue = () => {
return {
index: -1,
type: 'line',
name: '',
argumentField: '',
valueField: '',
summaryType: SummaryTypeEnum.Sum,
axis: '',
barOverlapGroup: '',
barPadding: 0,
barWidth: 0,
color: '',
cornerRadius: 0,
dashStyle: 'solid',
ignoreEmptyPoints: false,
pane: '',
rangeValue1Field: '',
rangeValue2Field: '',
selectionMode: 'none',
showInLegend: true,
visible: true,
width: 2,
label: {
visible: true,
backgroundColor: '#f05b41',
customizeText: '',
format: 'decimal',
font: {
color: '#FFFFFF',
family: '"Segoe UI", "Helvetica Neue", "Trebuchet MS", Verdana, sans-serif',
size: 12,
weight: 400,
},
},
userId: userName ?? '',
}
}
return (
<Dialog isOpen={open} onClose={onClose} width={1200}>
<div className="flex flex-col bg-white p-4 h-[600px]">
<Formik
enableReinitialize
initialValues={{
series: initialSeries && initialSeries.length > 0 ? initialSeries : [newSeriesValue()],
}}
validationSchema={schema}
onSubmit={(values, { setSubmitting }) => {
try {
onSave(values.series)
toast.push(<Notification type="success">{'Chart güncellendi'}</Notification>, {
placement: 'top-end',
})
onClose()
} catch (error: any) {
toast.push(
<Notification type="danger">
Hata
<code>{error}</code>
</Notification>,
{ placement: 'top-end' },
)
} finally {
setSubmitting(false)
}
}}
>
{({ setFieldValue, values, isSubmitting }) => (
<Form className="flex flex-col h-full">
<FormContainer size="sm" className="flex flex-col h-full">
{/* Header */}
<div className="mb-2 pb-2 border-b flex items-center justify-between">
<Button
variant="default"
shape="circle"
type="button"
size="xs"
onClick={() => setFieldValue('series', [...values.series, newSeriesValue()])}
>
<div className="flex items-center gap-1">
<FaPlus /> Seri Ekle
</div>
</Button>
</div>
{/* Kaydırılabilir içerik */}
<div className="flex-1 overflow-y-auto p-1">
<FieldArray name="series">
{({ remove }) => (
<div>
<div className="grid grid-cols-12 gap-2 font-semibold text-xs py-2">
<div className="text-center col-span-1">#</div>
<div className="text-center col-span-2">Type</div>
<div className="text-center col-span-2">Name</div>
<div className="text-center col-span-2">Argument Field</div>
<div className="text-center col-span-2">Value Field</div>
<div className="text-center col-span-2">Summary Type</div>
<div className="text-center col-span-1">#</div>
</div>
{values.series.map((_, index) => (
<div key={index} className="border-b py-1">
<div className="grid grid-cols-12 gap-1 items-center">
<div className="text-center text-xs col-span-1">
{values.series[index].index}
</div>
<div className="text-xs col-span-2">
<Field name={`series[${index}].type`}>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
options={chartSeriesTypeOptions}
isClearable
value={chartSeriesTypeOptions.find(
(option) => option.value === field.value,
)}
onChange={(option) =>
form.setFieldValue(field.name, option?.value)
}
/>
)}
</Field>
</div>
<div className="text-xs col-span-2">
<Field
size="sm"
name={`series[${index}].name`}
type="text"
component={Input}
className="text-xs px-1 py-0 grid-cols-2"
/>
</div>
<div className="text-xs col-span-2">
<Field type="text" name={`series[${index}].argumentField`}>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={fieldList}
value={fieldList?.find(
(option) => option.value === field.value,
)}
onChange={(option) =>
form.setFieldValue(field.name, option?.value)
}
menuPlacement="auto"
maxMenuHeight={150}
/>
)}
</Field>
</div>
<div className="text-xs col-span-2">
<Field type="text" name={`series[${index}].valueField`}>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={fieldList}
value={fieldList?.find(
(option) => option.value === field.value,
)}
onChange={(option) =>
form.setFieldValue(field.name, option?.value)
}
menuPlacement="auto"
maxMenuHeight={150}
/>
)}
</Field>
</div>
<div className="text-xs col-span-2">
<Field type="text" name={`series[${index}].summaryType`}>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
isClearable={true}
options={columnSummaryTypeListOptions}
value={columnSummaryTypeListOptions.find(
(option) => option.value === field.value,
)}
onChange={(option) =>
form.setFieldValue(field.name, option?.value)
}
/>
)}
</Field>
</div>
<div className="flex items-center justify-center gap-1 col-span-1">
<Button
shape="circle"
type="button"
size="xs"
icon={<FaMinus />}
className="bg-slate-100 hover:bg-red-100"
onClick={() => remove(index)}
/>
</div>
</div>
</div>
))}
</div>
)}
</FieldArray>
</div>
{/* Footer */}
<div className="flex gap-2 mt-auto pt-2 border-t text-right justify-end">
<Button
variant="solid"
loading={isSubmitting}
type="submit"
className="ml-auto px-4 py-1 text-sm rounded"
>
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
</div>
</FormContainer>
</Form>
)}
</Formik>
</div>
</Dialog>
)
}
export default ChartSeriesDialog

View file

@ -260,9 +260,26 @@ export function buildSeriesDto(seriesList: ChartSeriesDto[]) {
// her seri için dto oluştur // her seri için dto oluştur
return seriesList.map((s) => ({ return seriesList.map((s) => ({
...s, type: s.type,
argumentField: 'ArgumentField', argumentField: s.argumentField,
valueField: s.name, valueField: s.valueField,
name: s.name || s.valueField,
axis: s.axis,
color: s.color,
dashStyle: s.dashStyle,
width: s.width,
visible: s.visible,
showInLegend: s.showInLegend,
label: {
visible: s.label?.visible || false,
backgroundColor: s.label?.backgroundColor || 'transparent',
customizeText: s.label?.customizeText || undefined,
format: s.label?.format || undefined,
font: s.label?.font || {
color: '#000000',
size: 12,
},
},
})) }))
} }

View file

@ -51,6 +51,45 @@ const useListFormCustomDataSource = ({
createDeleteQuery: searchParams?.get('createDeleteQuery'), createDeleteQuery: searchParams?.get('createDeleteQuery'),
chart: layout === layoutTypes.chart, chart: layout === layoutTypes.chart,
}) })
// Chart için group ve groupSummary parametreleri ekle
if (layout === layoutTypes.chart && gridOptions.seriesDto && gridOptions.seriesDto.length > 0) {
// Tüm series'lerin unique argumentField'larını topla
const allArgumentFields = [...new Set(gridOptions.seriesDto.map(s => s.argumentField).filter(Boolean))] as string[]
// İlk argumentField üzerinden group yap (chart tek bir X ekseni kullanır)
if (allArgumentFields.length > 0) {
loadOptions.group = allArgumentFields.map(field => ({
selector: field as string,
isExpanded: false
}))
}
// Tüm series'lerin valueField'ları için summary hesapla
const groupSummaries: any[] = []
gridOptions.seriesDto.forEach(series => {
if (series.valueField && series.summaryType) {
groupSummaries.push({
selector: series.valueField,
summaryType: series.summaryType as any
})
}
})
if (groupSummaries.length > 0) {
loadOptions.groupSummary = groupSummaries
}
// Parametreleri tekrar oluştur
const chartParameters = getLoadOptions(loadOptions, {
listFormCode,
filter: '',
createDeleteQuery: searchParams?.get('createDeleteQuery'),
chart: layout === layoutTypes.chart,
})
Object.assign(parameters, chartParameters)
}
// 1. Default filter'ı al // 1. Default filter'ı al
const defaultFilter = searchParams?.get('filter') const defaultFilter = searchParams?.get('filter')
? JSON.parse(searchParams.get('filter')!) ? JSON.parse(searchParams.get('filter')!)
@ -113,6 +152,54 @@ const useListFormCustomDataSource = ({
//parameters.filter = JSON.stringify(parameters.filter) //parameters.filter = JSON.stringify(parameters.filter)
const response = await dynamicFetch('list-form-select/select', 'GET', parameters) const response = await dynamicFetch('list-form-select/select', 'GET', parameters)
// Chart için grouped data'yı chart formatına çevir
if (layout === layoutTypes.chart && Array.isArray(response.data.data)) {
const flattenGroupedData = (items: any[], parentKeys: any = {}): any[] => {
const result: any[] = []
items.forEach((item: any) => {
if (item.items && item.items.length > 0) {
// Alt grup var, recursive olarak işle
const currentKeys = { ...parentKeys }
// Bu level'daki key'i ilgili argumentField'a map'le
gridOptions.seriesDto?.forEach(series => {
if (series.argumentField && !currentKeys[series.argumentField]) {
currentKeys[series.argumentField] = item.key
}
})
result.push(...flattenGroupedData(item.items, currentKeys))
} else {
// Leaf node - gerçek data
const transformed: any = { ...parentKeys }
// Son level'daki key'i ekle
gridOptions.seriesDto?.forEach(series => {
if (series.argumentField && item.key !== undefined && !transformed[series.argumentField]) {
transformed[series.argumentField] = item.key
}
})
// Summary değerlerini valueField'lara map'le
if (Array.isArray(item.summary)) {
gridOptions.seriesDto?.forEach((series, index) => {
if (series.valueField && item.summary[index] !== undefined) {
transformed[series.valueField] = item.summary[index]
}
})
}
result.push(transformed)
}
})
return result
}
response.data.data = flattenGroupedData(response.data.data)
}
// Column format multiValue ise, gelen stringi array yapmaliyiz // Column format multiValue ise, gelen stringi array yapmaliyiz
if (columns) { if (columns) {