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 { useCallback, 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 { getFormRecordUrl } 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 { useListFormStateStoring } from '../list/useListFormStateStoring' import { buildFormGroupItems, flattenFormGroups, FormFieldOrder, 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 [fieldOrder, setFieldOrder] = useState() const refForm = useRef(null) const previousFormDataRef = useRef(undefined) // Alan sırası kaydedilince form o anki veriyle yeniden kurulur. const currentFormDataRef = useRef(formData) currentFormDataRef.current = formData 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) // Alan sırası grid/pivot görünümleriyle aynı ListForm state'inde, kullanıcı bazında tutulur. const stateStoring = gridDto?.gridOptions?.stateStoringDto const { customLoadState, customSaveState, storageKey } = useListFormStateStoring({ listFormCode, storageKey: stateStoring?.enabled ? stateStoring.storageKey : undefined, filterPrefix: 'form', showSaveToast: false, saveDebounceMs: 0, }) 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) => { // Form submit DOM olayı, DevExtreme toolbar butonu ise { event } taşıyan nesne verir. const domEvent = e?.event ?? e domEvent?.preventDefault?.() if (!dataSource) return if (!refForm.current?.instance().validate()?.isValid) { return } setLoading(true) try { const formValues = { ...formData } // Store insert/update'i satır verisi olarak döndürür (Grid'in refresh sözleşmesi); // HTTP hatası promise'i reddeder ve catch'e düşer. const keyFieldName = gridDto?.gridOptions.keyFieldName if (mode === 'new') { const result = await dataSource.insert(formValues) const newId = keyFieldName ? result?.[keyFieldName] : undefined if (newId === undefined || newId === null) { throw new Error(translate('::App.ListForm.FormBilgileriKaydedilemedi')) } finishSubmit(getFormRecordUrl(listFormCode, newId)) } else if (mode === 'edit') { await dataSource.update(id, buildUpdateData(formValues)) finishSubmit(getFormRecordUrl(listFormCode, 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, ), ) // Kayıtlı alan sırası form kurulmadan okunur; aksi hâlde form önce varsayılan sırayla çizilip yer değiştirirdi. let isActive = true customLoadState() .catch(() => null) .then((state: any) => { if (!isActive) return const savedOrder: FormFieldOrder | undefined = state?.fieldOrder ?? undefined setFieldOrder(savedOrder) setFormItems( buildFormGroupItems({ gridDto, mode, columns, getLookupDataSource, formData, fieldOrder: savedOrder, }), ) setGridReady(true) }) return () => { isActive = false } // getBandedColumns/getLookupDataSource her render'da yeniden oluşabilir. // eslint-disable-next-line react-hooks/exhaustive-deps }, [gridDto]) /** Formu verilen alan sırasıyla yeniden kurar; kaydetmez (sürükleme sırasında canlı önizleme). */ const previewFieldOrder = useCallback( (order: FormFieldOrder | undefined) => { if (!gridDto) return setFormItems( buildFormGroupItems({ gridDto, mode, columns: getBandedColumns(), getLookupDataSource, formData: currentFormDataRef.current, fieldOrder: order, }), ) }, // getBandedColumns/getLookupDataSource her render'da yeniden oluşabilir. // eslint-disable-next-line react-hooks/exhaustive-deps [gridDto, mode], ) /** Alan sırasını kullanıcının state'ine yazar; `undefined` fabrika sırasına (`EditOrderNo`) döner. */ const saveFieldOrder = useCallback( async (order: FormFieldOrder | undefined) => { await customSaveState(order ? { fieldOrder: order } : null) setFieldOrder(order) previewFieldOrder(order) }, [customSaveState, previewFieldOrder], ) /** * 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(() => { // DevExtreme formData nesnesine yerinde yazar; referans tutulursa önceki değer de yeni // değere dönüşür ve değişen parent alan hiç bulunamaz. const snapshotFormData = (data: any) => (data ? { ...data } : data) if (!gridDto || !formItems.length) { previousFormDataRef.current = snapshotFormData(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 = snapshotFormData(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, 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 = snapshotFormData(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, fieldOrder, previewFieldOrder, saveFieldOrder, canStoreFieldOrder: !!storageKey, } } export { useGridData }