/** * Lookup cascade (bağımlı açılır liste) davranışı. * Grid, Tree ve form sayfası aynı kuralları paylaşır. */ import type { ColumnFormatDto, GridDto } from '@/proxy/form/models' import { getActiveEditingForm, getEditingRow } from './editingForm' type CascadeInfo = { parentFields: string[] } export type CascadeMaps = { /** child fieldName -> cascade bilgisi */ cascadeFieldsMap: Map /** parentField -> etkilediği child alan adları */ parentToChildrenMap: Map> /** fieldName -> değişince boşaltılacak alanlar */ clearFieldsMap: Map } const splitFields = (value?: string | null) => (value ?? '') .split(',') .map((field) => field.trim()) .filter(Boolean) export const getCascadeParentFieldsOf = (colFormat?: Pick) => splitFields(colFormat?.lookupDto?.cascadeParentFields) /** * Bir alan değiştiğinde boşaltılacak alanlar: ebeveyni bu alan olan çocuklar * (`CascadeParentFields`) ile alanın kendi `CascadeEmptyFields` listesi. */ export const getCascadeClearFields = ( columns: (Pick | undefined)[], fieldName: string, ) => { const fields = new Set() columns.forEach((column) => { if (!column?.fieldName) return if (column.fieldName === fieldName) { splitFields(column.lookupDto?.cascadeEmptyFields).forEach((field) => fields.add(field)) } else if (getCascadeParentFieldsOf(column).includes(fieldName)) { fields.add(column.fieldName) } }) fields.delete(fieldName) return [...fields] } /** Ebeveynlerinden biri boşsa çocuk kilitlenir (bool ebeveynde `false` da boştur). */ export const isCascadeParentEmpty = (parentFields: string[], data?: Record) => parentFields.some((parentField) => !data?.[parentField]) /** Ekranın cascade ilişkilerini tek geçişte çıkarır; `clearFieldsMap` `getCascadeClearFields` ile aynıdır. */ export const createCascadeMaps = (gridDto?: GridDto): CascadeMaps => { const cascadeFieldsMap = new Map() const parentToChildrenMap = new Map>() const clearFieldSets = new Map>() const addClearField = (fieldName: string, clearField: string) => { if (fieldName === clearField) return const fields = clearFieldSets.get(fieldName) ?? new Set() fields.add(clearField) clearFieldSets.set(fieldName, fields) } gridDto?.columnFormats.forEach((col) => { const fieldName = col.fieldName if (!fieldName) return splitFields(col.lookupDto?.cascadeEmptyFields).forEach((field) => addClearField(fieldName, field), ) const parentFields = getCascadeParentFieldsOf(col) if (!parentFields.length) return cascadeFieldsMap.set(fieldName, { parentFields }) parentFields.forEach((parentField) => { const children = parentToChildrenMap.get(parentField) ?? new Set() children.add(fieldName) parentToChildrenMap.set(parentField, children) addClearField(parentField, fieldName) }) }) const clearFieldsMap = new Map( [...clearFieldSets].map(([fieldName, fields]) => [fieldName, [...fields]]), ) return { cascadeFieldsMap, parentToChildrenMap, clearFieldsMap } } /** Alan cascade zincirinde (ebeveyn ya da çocuk olarak) yer alıyor mu? */ export const hasCascadeBehavior = (maps: CascadeMaps, fieldName: string) => maps.cascadeFieldsMap.has(fieldName) || maps.parentToChildrenMap.has(fieldName) || maps.clearFieldsMap.has(fieldName) /** Parent değiştikten sonra etkilenen çocuk editörlerin kilidini yeniler. */ const updateChildDisabledStates = ( grid: any, gridDto: GridDto, maps: CascadeMaps, children: Iterable, rowData: Record, ) => { const form = getActiveEditingForm(grid) if (!form?.getEditor) return for (const childFieldName of children) { const childInfo = maps.cascadeFieldsMap.get(childFieldName) if (!childInfo) continue try { const childColumn = gridDto.columnFormats.find( (column) => column.fieldName === childFieldName, ) const childEditorOptions = childColumn?.editorOptions ? JSON.parse(childColumn.editorOptions) : {} form .getEditor(childFieldName) ?.option( 'disabled', childEditorOptions?.disabled === true || isCascadeParentEmpty(childInfo.parentFields, rowData), ) } catch (err) { console.error(`Cascade child editor güncellenemedi: "${childFieldName}"`, err) } } } /** * Bir alanın değeri değiştiğinde cascade kurallarını düzenlenen satıra uygular: * bağımlı alanları boşaltır ve çocuk editörlerin kilidini yeniler. Grid editörleri * (`onEditorPreparing`) ve platform editörleri (TagBox, GridBox…) aynı yoldan geçer. */ export const applyCascadeChange = ( grid: any, gridDto: GridDto, maps: CascadeMaps, fieldName: string, value: any, ) => { if (!grid?.getVisibleRows) return const { rowIndex, rowData } = getEditingRow(grid) const clearFields = maps.clearFieldsMap.get(fieldName) ?? [] if (rowIndex >= 0) { clearFields.forEach((childField) => grid.cellValue(rowIndex, childField, null)) } const affectedChildren = maps.parentToChildrenMap.get(fieldName) if (!affectedChildren?.size || !rowData) return const nextRowData = { ...rowData, [fieldName]: value, ...Object.fromEntries(clearFields.map((field) => [field, null])), } updateChildDisabledStates(grid, gridDto, maps, affectedChildren, nextRowData) } type AttachCascadeParams = { editor: any gridDto: GridDto maps: CascadeMaps } /** * `onEditorPreparing` içinde cascade davranışını editöre bağlar: * - değer değişince bağımlı alanları temizler ve çocukların kilidini yeniler, * - form ilk açıldığında parent'ı boş olan alanı devre dışı bırakır. */ export const attachCascadeBehavior = ({ editor, gridDto, maps }: AttachCascadeParams) => { const fieldName = editor.dataField as string const previousHandler = editor.editorOptions.onValueChanged editor.editorOptions.onValueChanged = (e: any) => { previousHandler?.(e) applyCascadeChange(editor.component, gridDto, maps, fieldName, e.value) } const cascadeInfo = maps.cascadeFieldsMap.get(fieldName) const { rowData } = getEditingRow(editor.component) if (cascadeInfo && rowData && isCascadeParentEmpty(cascadeInfo.parentFields, rowData)) { editor.editorOptions.disabled = true } }