import { Notification, toast } from '@/components/ui' import { getList } from '@/services/form.service' import { useLocalization } from '@/utils/hooks/useLocalization' import { usePermission } from '@/utils/hooks/usePermission' import type { FormRef } from 'devextreme-react/form' import CustomStore from 'devextreme/data/custom_store' import type { GroupItem } from 'devextreme/ui/form' import { useEffect, useRef, useState } from 'react' import { useLocation, useNavigate, useSearchParams } from 'react-router-dom' import { GridColumnData } from '../list/GridColumnData' import { PermissionResults, RowMode, SimpleItemWithColData } from './types' import { GridDto } from '@/proxy/form/models' import { getAccessDeniedPath } from '@/utils/routing' import { ROUTES_ENUM } from '@/routes/route.constant' import { useLookupDataSource } from './useLookupDataSource' import { layoutTypes } from '../admin/listForm/edit/types' import { useListFormCustomDataSource } from '../list/useListFormCustomDataSource' import { useListFormColumns } from '../list/useListFormColumns' import { useListFormCustomSources } from '../list/shared/hooks' import { buildFormGroupItems, flattenFormGroups, getCascadeParentFields, } from './shared/formItems' const useGridData = (props: { mode: RowMode listFormCode: string id?: string level?: number isSubForm?: boolean onSubmitAction?: () => void }) => { const { mode, listFormCode, id, isSubForm } = props const [gridReady, setGridReady] = useState(false) const [loading, setLoading] = useState(false) const [filter, setFilter] = useState([]) const [gridDto, setGridDto] = useState() const [dataSource, setDataSource] = useState>() const [commandColumnData, setCommandColumnData] = useState() const [formDataOld, setFormDataOld] = useState() const [formData, setFormData] = useState() const [formItems, setFormItems] = useState([]) const [permissionResults, setPermissionResults] = useState() const refForm = useRef(null) const previousFormDataRef = useRef(undefined) const [searchParams] = useSearchParams() const location = useLocation() const navigate = useNavigate() const { translate } = useLocalization() const { checkPermission } = usePermission() const { getBandedColumns } = useListFormColumns({ gridDto, listFormCode, isSubForm, gridRef: undefined, }) const { createSelectDataSource } = useListFormCustomDataSource({} as any) const { getLookupDataSource } = useLookupDataSource({ listFormCode, isSubForm }) useListFormCustomSources(gridDto) const fetchData = async () => { setLoading(true) try { const response: any = await dataSource?.load({ filter, skip: 0, take: 1 }) const record = response?.data?.[0] setFormData(record) setFormDataOld(record ? { ...record } : undefined) } catch (error: any) { toast.push(, { placement: 'bottom-end', }) } finally { setLoading(false) } } const showSaveSuccess = () => toast.push( {translate('::App.Platform.FormBilgileriKaydedildi')} , { placement: 'bottom-end' }, ) /** Kaydetme sonrası yönlendirme; alt formda üst bileşene haber verilir. */ const finishSubmit = (viewPath: string) => { if (isSubForm) { props.onSubmitAction?.() } else { navigate(viewPath) } showSaveSuccess() } /** Güncelleme isteğinin gövdesini hazırlar. */ const buildUpdateData = (formValues: Record) => { const sendOnlyChanged = gridDto?.gridOptions.editingOptionDto?.sendOnlyChangedFormValuesUpdate const data = sendOnlyChanged && formDataOld ? Object.fromEntries( Object.keys(formValues) .filter((key) => !Object.is(formValues[key], formDataOld[key])) .map((key) => [key, formValues[key]]), ) : { ...formValues } if (gridDto?.gridOptions.keyFieldName) { delete data[gridDto.gridOptions.keyFieldName] } return data } const handleSubmit = async (e: any) => { e.preventDefault() if (!dataSource) return if (!refForm.current?.instance().validate()?.isValid) { return } setLoading(true) try { const formValues = { ...formData } if (mode === 'new') { const result = await dataSource.insert(formValues) if (!result?.data) { throw new Error(translate('::App.ListForm.FormBilgileriKaydedilemedi')) } finishSubmit(result.data) } else if (mode === 'edit') { const result = await dataSource.update(id, buildUpdateData(formValues)) if (!(result?.data > 0)) { throw new Error(translate('::App.ListForm.FormBilgileriKaydedilemedi')) } finishSubmit( ROUTES_ENUM.protected.admin.formView .replace(':listFormCode', encodeURIComponent(listFormCode)) .replace(':id', encodeURIComponent(id!)), ) } } catch (error: any) { toast.push(, { placement: 'bottom-end', }) } finally { setLoading(false) } } useEffect(() => { setGridReady(false) let isActive = true getList({ listFormCode }) .then((response: any) => { if (isActive) setGridDto(response.data) }) .catch((error: unknown) => console.error('GridDto load error:', error)) return () => { isActive = false } }, [listFormCode]) useEffect(() => { setGridReady(false) if (!gridDto) return const { editingOptionDto, permissionDto } = gridDto.gridOptions setPermissionResults({ c: editingOptionDto.allowAdding === true && checkPermission(permissionDto.c), r: checkPermission(permissionDto.r), u: editingOptionDto.allowUpdating === true && checkPermission(permissionDto.u), d: editingOptionDto.allowDeleting === true && checkPermission(permissionDto.d), e: checkPermission(permissionDto.e), i: checkPermission(permissionDto.i), }) const columns = getBandedColumns() setCommandColumnData(columns?.find((column) => column.type === 'buttons')) setDataSource( createSelectDataSource( gridDto.gridOptions, listFormCode, searchParams, layoutTypes.grid, columns, ), ) setFormItems( buildFormGroupItems({ gridDto, mode, columns, getLookupDataSource, formData, }), ) setGridReady(true) // getBandedColumns/getLookupDataSource her render'da yeniden oluşabilir. // eslint-disable-next-line react-hooks/exhaustive-deps }, [gridDto]) /** * formData değiştiğinde yalnızca etkilenen cascading lookup dataSource'larını * yeniden üretir; tüm form item ağacını gereksiz yere yenilemez. */ useEffect(() => { if (!gridDto || !formItems.length) { previousFormDataRef.current = formData return } const previousFormData = previousFormDataRef.current const changedFields = previousFormData ? Object.keys({ ...(previousFormData || {}), ...(formData || {}) }).filter( (field) => !Object.is(previousFormData?.[field], formData?.[field]), ) : [] const shouldRefreshLookup = (item: SimpleItemWithColData) => !previousFormData || getCascadeParentFields(item).some((field) => changedFields.includes(field)) const hasAffectedLookup = flattenFormGroups(formItems).some( (item) => item.colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item), ) if (!hasAffectedLookup) { previousFormDataRef.current = formData return } const updateItems = (items: any[] = []): any[] => items.map((item) => { const colData = item.colData if (colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item)) { const currentDataSource = item.editorOptions?.dataSource // Kullanıcı tarafından elle verilmiş (statik dizi vb.) dataSource korunur. const keepCustomDataSource = currentDataSource !== undefined && typeof currentDataSource?.load !== 'function' return { ...item, editorOptions: { ...item.editorOptions, dataSource: keepCustomDataSource ? currentDataSource : getLookupDataSource(colData?.editorOptions, colData, formData || null), valueExpr: item.editorOptions?.valueExpr ?? colData?.lookupDto?.valueExpr?.toLowerCase(), displayExpr: item.editorOptions?.displayExpr ?? colData?.lookupDto?.displayExpr?.toLowerCase(), }, } } if (item?.items?.length) { return { ...item, items: updateItems(item.items) } } if (item?.tabs?.length) { return { ...item, tabs: item.tabs.map((tab: any) => ({ ...tab, items: updateItems(tab.items) })), } } return item }) previousFormDataRef.current = formData setFormItems((currentItems) => currentItems.map((groupItem) => ({ ...groupItem, items: updateItems(groupItem.items as any[]), tabs: (groupItem as any).tabs?.map((tab: any) => ({ ...tab, items: updateItems(tab.items), })), })), ) // eslint-disable-next-line react-hooks/exhaustive-deps }, [formData, gridDto]) useEffect(() => { if (!gridReady || mode === 'new') return if (id) { setFilter([gridDto?.gridOptions.keyFieldName ?? 'Id', '=', id]) } else { // Form tipi liste formlar ID olmadan açılabilir; kayıt seçimi sunucudaki // DefaultFilter'a bırakılır. fetchData() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [id, gridReady]) useEffect(() => { if (filter?.length) { fetchData() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [filter]) // Yetki kontrolü useEffect(() => { if (!permissionResults) return const denied = (mode === 'new' && !permissionResults.c) || (mode === 'edit' && !permissionResults.u) || (mode === 'view' && !permissionResults.r) if (denied) { navigate(getAccessDeniedPath(location.pathname), { replace: true, state: { from: location } }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [permissionResults]) return { loading, gridDto, dataSource, commandColumnData, filter, formItems, formData, refForm, permissionResults, fetchData, setFormData, handleSubmit, } } export { useGridData }