diff --git a/api/src/Erp.Platform.Domain/Queries/SelectQueryManager.cs b/api/src/Erp.Platform.Domain/Queries/SelectQueryManager.cs index 62ccd011..11b60e8c 100644 --- a/api/src/Erp.Platform.Domain/Queries/SelectQueryManager.cs +++ b/api/src/Erp.Platform.Domain/Queries/SelectQueryManager.cs @@ -684,7 +684,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager var sqlSub = new StringBuilder(); sqlSub.Append("SELECT "); 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($" FROM \"{From}\" AS \"{TableName}\""); sqlSub.Append(GetJoinString()); diff --git a/ui/src/proxy/form/models.ts b/ui/src/proxy/form/models.ts index 9d88c0fc..e3a98b27 100644 --- a/ui/src/proxy/form/models.ts +++ b/ui/src/proxy/form/models.ts @@ -11,7 +11,6 @@ import { GridsEditMode, GridsEditRefreshMode, NewRowPosition, - PagerDisplayMode, SelectionColumnDisplayMode, StartEditAction, StateStoreType, @@ -19,7 +18,6 @@ import { import { FormItemComponent } from 'devextreme/ui/form' import { AuditedEntityDto } from '../abp' import { EditorType2, RowMode } from '../../views/form/types' -import { bool } from 'yup' import { ChartCommonDto, ChartAdaptivelayoutDto, @@ -45,6 +43,7 @@ import { } from '../admin/charts/models' import { ListViewLayoutType } from '@/views/admin/listForm/edit/types' import { SeriesType } from 'devextreme/common/charts' +import { PagerDisplayMode } from 'devextreme/common/grids' //1 export interface SelectListItem { diff --git a/ui/src/views/list/Chart.tsx b/ui/src/views/list/Chart.tsx index 4d1f7191..9b72f311 100644 --- a/ui/src/views/list/Chart.tsx +++ b/ui/src/views/list/Chart.tsx @@ -4,7 +4,7 @@ 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 { useCallback, useEffect, useState } from 'react' import { Helmet } from 'react-helmet' import { useParams, useSearchParams } from 'react-router-dom' import { GridDto } from '@/proxy/form/models' @@ -17,7 +17,7 @@ import { buildSeriesDto } from './Utils' import { ChartSeriesDto } from '@/proxy/admin/charts/models' import { SelectBoxOption } from '@/types/shared' import { useStoreState } from '@/store/store' -import ChartSeriesDialog from './ChartSeriesDialog' +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' @@ -48,7 +48,7 @@ const Chart = (props: ChartProps) => { const { translate } = useLocalization() const { checkPermission } = usePermission() const isPwaMode = usePWA() - const initialized = useRef(false) + const [initialized, setInitialized] = useState(false) const [searchParams] = useSearchParams() const [chartOptions, setChartOptions] = useState() @@ -57,8 +57,14 @@ const Chart = (props: ChartProps) => { const params = useParams() const _listFormCode = props?.listFormCode ?? params?.listFormCode ?? '' - const [openDialog, setOpenDialog] = useState(false) + const [openDrawer, setOpenDrawer] = useState(false) const [fieldList, setFieldList] = useState([]) + + // Ana state - Chart bu state'e göre render edilir + const [currentSeries, setCurrentSeries] = useState([]) + + // Veritabanından gelen orijinal seriler (kaydetme işlemi için) + const [savedSeries, setSavedSeries] = useState([]) const [searchText, setSearchText] = useState('') const [prevValue, setPrevValue] = useState('') @@ -68,27 +74,35 @@ const Chart = (props: ChartProps) => { const [allSeries, setAllSeries] = useState([]) - const [userSeries, setUserSeries] = useState([]) - const [oldSeries, setOldSeries] = useState([]) - useEffect(() => { - if (gridDto && !initialized.current) { + if (gridDto) { const initialSeries = gridDto.gridOptions.seriesDto.map((s, index) => ({ ...s, index })) + const userSeriesData = initialSeries.filter((s) => s.userId === userName) setAllSeries(initialSeries) - setUserSeries(initialSeries.filter((s) => s.userId === userName)) - setOldSeries(initialSeries.filter((s) => s.userId === userName)) - - initialized.current = true + + // 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]) + }, [gridDto, userName]) useEffect(() => { if (!gridDto) return - if (!allSeries) return - if (!initialized.current) return + if (!initialized) 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 = { ...gridDto.gridOptions, @@ -116,8 +130,15 @@ const Chart = (props: ChartProps) => { 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, + : { + 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, @@ -129,7 +150,11 @@ const Chart = (props: ChartProps) => { crosshair: gridDto.gridOptions.crosshairDto, argumentAxis: gridDto.gridOptions.argumentAxisDto, valueAxis: gridDto.gridOptions.valueAxisDto, - tooltip: gridDto.gridOptions.tooltipDto, + tooltip: gridDto.gridOptions.tooltipDto || { + enabled: true, + shared: false, + format: 'decimal', + }, series: buildSeriesDto(seriesDto), @@ -146,7 +171,7 @@ const Chart = (props: ChartProps) => { } setChartOptions(options) - }, [gridDto, allSeries, initialized.current, searchParams, urlSearchParams]) + }, [gridDto, currentSeries, initialized, searchParams, urlSearchParams, openDrawer]) const onFilter = useCallback( (value?: string) => { @@ -220,12 +245,23 @@ const Chart = (props: ChartProps) => { if (props.listFormCode) getFields() }, [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[]) => { - // 1. Silinecek serileri bul (oldSeries var ama newSeries yok) - const toDelete = oldSeries.filter((old) => !newSeries.some((s) => s.index === old.index)) + // 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, b) => b.index - a.index) + toDelete.sort((a: ChartSeriesDto, b: ChartSeriesDto) => b.index - a.index) for (const old of toDelete) { 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) { - initialized.current = false + setInitialized(false) 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) => { > )} {_listFormCode && chartOptions && ( -
+
@@ -296,7 +337,7 @@ const Chart = (props: ChartProps) => { variant={'default'} className="text-sm" onClick={async () => { - initialized.current = false + setInitialized(false) await refreshGridDto?.() }} title="Refresh Data" @@ -307,7 +348,7 @@ const Chart = (props: ChartProps) => { size="xs" variant="default" className="text-sm" - onClick={() => setOpenDialog(true)} + onClick={() => setOpenDrawer(true)} title="Series Özelleştir" > @@ -334,14 +375,20 @@ const Chart = (props: ChartProps) => { )}
- +
+ +
- setOpenDialog(false)} - initialSeries={allSeries.filter((s) => s.userId === userName)} +
)} diff --git a/ui/src/views/list/ChartDrawer.tsx b/ui/src/views/list/ChartDrawer.tsx new file mode 100644 index 00000000..8302b0b1 --- /dev/null +++ b/ui/src/views/list/ChartDrawer.tsx @@ -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(-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 */} +
+ 0 ? initialSeries : [newSeriesValue()], + }} + validationSchema={schema} + onSubmit={async (values, { setSubmitting }) => { + try { + await onSave(values.series) + toast.push(Chart kaydedildi, { + placement: 'top-end', + }) + onClose() + } catch (error: any) { + toast.push( + + Hata + {error} + , + { 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 ( +
+ {/* Header */} +
+

+ 📊 + Chart Series +

+ +
+ + + {/* Add Series Button */} +
+ +
+ + {/* Scrollable Content */} +
+ + {({ remove }) => ( +
+ {values.series.map((series, index) => ( +
+ {/* Header */} +
+ Seri #{index + 1} +
+ + {/* Chart Type Selector */} +
+ + + {({ field, form }: FieldProps) => ( +
+ + {selectedSeriesIndex === index && ( +
+
+ {chartTypeIcons.map((chartType) => ( + + ))} +
+
+ )} +
+ )} +
+
+ + {/* Name */} +
+ + +
+ + {/* Argument Field */} +
+ + + {({ field, form }: FieldProps) => ( + option.value === field.value, + )} + onChange={(option) => { + form.setFieldValue(field.name, option?.value) + }} + placeholder="Select field..." + /> + )} + +
+ + {/* Summary Type */} +
+ + + {({ field, form }: FieldProps) => ( + option.value === field.value, - )} - onChange={(option) => - form.setFieldValue(field.name, option?.value) - } - /> - )} - -
- -
- -
- -
- - {({ field, form }: FieldProps) => ( - option.value === field.value, - )} - onChange={(option) => - form.setFieldValue(field.name, option?.value) - } - menuPlacement="auto" - maxMenuHeight={150} - /> - )} - -
- -
- - {({ field, form }: FieldProps) => ( -