import { useState, useCallback, useMemo, useEffect } from 'react' import { Button, Dialog, Notification, toast, Checkbox } from '@/components/ui' import { FaPlus, FaTrash, FaArrowUp, FaArrowDown, FaTable, FaCloudUploadAlt, FaCheck, FaChevronRight, FaLink, FaEdit, FaTimes, FaArrowRight, } from 'react-icons/fa' import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { MenuService } from '@/services/menu.service' import { developerKitService } from '@/services/developerKit.service' import type { CreateUpdateSqlTableDto, CreateUpdateSqlTableFieldDto } from '@/proxy/developerKit/models' import { useLocalization } from '@/utils/hooks/useLocalization' // ─── Types ──────────────────────────────────────────────────────────────────── type SqlDataType = | 'nvarchar' | 'nvarchar(MAX)' | 'int' | 'bigint' | 'decimal' | 'float' | 'bit' | 'datetime2' | 'date' | 'uniqueidentifier' | 'money' interface ColumnDefinition { id: string columnName: string dataType: SqlDataType maxLength: string isNullable: boolean defaultValue: string description: string } interface MenuOption { value: string label: string } interface TableSettings { menuValue: string menuPrefix: string entityName: string tableName: string displayName: string description: string isActive: boolean isFullAudited: boolean isMultiTenant: boolean } interface TableDesignerDialogProps { isOpen: boolean onClose: () => void dataSource: string | null onDeployed?: () => void initialTableData?: { schemaName: string; tableName: string } | null } type RelationshipType = 'OneToOne' | 'OneToMany' type CascadeBehavior = 'NoAction' | 'Cascade' | 'SetNull' | 'Restrict' interface FkRelationshipDefinition { id: string /** Existing DB constraint name — set when loaded from database (edit mode) */ constraintName?: string relationshipType: RelationshipType fkColumnName: string referencedTable: string referencedColumn: string cascadeDelete: CascadeBehavior cascadeUpdate: CascadeBehavior isRequired: boolean description: string } // ─── Constants ──────────────────────────────────────────────────────────────── const DATA_TYPES: { value: SqlDataType; label: string }[] = [ { value: 'nvarchar', label: 'String (nvarchar)' }, { value: 'nvarchar(MAX)', label: 'Text (nvarchar MAX)' }, { value: 'int', label: 'Int (int)' }, { value: 'bigint', label: 'Long (bigint)' }, { value: 'decimal', label: 'Decimal (decimal 18,4)' }, { value: 'float', label: 'Float (float)' }, { value: 'bit', label: 'Bool (bit)' }, { value: 'datetime2', label: 'DateTime (datetime2)' }, { value: 'date', label: 'Date (date)' }, { value: 'uniqueidentifier', label: 'Guid (uniqueidentifier)' }, { value: 'money', label: 'Money (money)' }, ] const FULL_AUDIT_COLUMNS: ColumnDefinition[] = [ { id: '__CreationTime', columnName: 'CreationTime', dataType: 'datetime2', maxLength: '', isNullable: false, defaultValue: 'GETUTCDATE()', description: 'Record creation time', }, { id: '__CreatorId', columnName: 'CreatorId', dataType: 'uniqueidentifier', maxLength: '', isNullable: true, defaultValue: '', description: 'Creator user ID', }, { id: '__LastModificationTime', columnName: 'LastModificationTime', dataType: 'datetime2', maxLength: '', isNullable: true, defaultValue: '', description: 'Last modification time', }, { id: '__LastModifierId', columnName: 'LastModifierId', dataType: 'uniqueidentifier', maxLength: '', isNullable: true, defaultValue: '', description: 'Last modifier user ID', }, { id: '__IsDeleted', columnName: 'IsDeleted', dataType: 'bit', maxLength: '', isNullable: false, defaultValue: '0', description: 'Soft delete flag', }, { id: '__DeletionTime', columnName: 'DeletionTime', dataType: 'datetime2', maxLength: '', isNullable: true, defaultValue: '', description: 'Deletion time', }, { id: '__DeleterId', columnName: 'DeleterId', dataType: 'uniqueidentifier', maxLength: '', isNullable: true, defaultValue: '', description: 'Deleter user ID', }, ] const REL_TYPES: { value: RelationshipType; label: string; desc: string }[] = [ { value: 'OneToMany', label: '1 → N', desc: 'Bire-çok' }, { value: 'OneToOne', label: '1 → 1', desc: 'Bire-bir' }, ] const CASCADE_OPTIONS: { value: CascadeBehavior; label: string }[] = [ { value: 'NoAction', label: 'No Action' }, { value: 'Cascade', label: 'Cascade' }, { value: 'SetNull', label: 'Set Null' }, { value: 'Restrict', label: 'Restrict' }, ] const EMPTY_FK: Omit = { relationshipType: 'OneToMany', fkColumnName: '', referencedTable: '', referencedColumn: 'Id', cascadeDelete: 'NoAction', cascadeUpdate: 'NoAction', isRequired: false, description: '', } const TENANT_COLUMN: ColumnDefinition = { id: '__TenantId', columnName: 'TenantId', dataType: 'uniqueidentifier', maxLength: '', isNullable: true, defaultValue: '', description: 'Tenant ID for multi-tenancy', } // ─── T-SQL Generator ────────────────────────────────────────────────────────── function colToSqlLine(col: ColumnDefinition, addComma = true): string { let typeSql: string switch (col.dataType) { case 'nvarchar': typeSql = `nvarchar(${col.maxLength || '100'})` break case 'nvarchar(MAX)': typeSql = 'nvarchar(MAX)' break case 'decimal': typeSql = 'decimal(18, 4)' break default: typeSql = col.dataType } const nullPart = col.isNullable ? 'NULL' : 'NOT NULL' const defaultPart = col.defaultValue ? ` DEFAULT ${col.defaultValue}` : '' return ` [${col.columnName}] ${typeSql} ${nullPart}${defaultPart}${addComma ? ',' : ''}` } function generateCreateTableSql( columns: ColumnDefinition[], settings: TableSettings, relationships: FkRelationshipDefinition[], ): string { const tableName = settings.tableName || 'NewTable' const fullTableName = `[dbo].[${tableName}]` const userCols = columns.filter((c) => c.columnName.trim()) const auditCols = settings.isFullAudited ? FULL_AUDIT_COLUMNS : [] const tenantCols = settings.isMultiTenant ? [TENANT_COLUMN] : [] const allBodyCols = [...userCols, ...auditCols, ...tenantCols] // When Full Audited: auto-add Id (PK) + audit cols // When not Full Audited: user is responsible for their own Id column const bodyLines = settings.isFullAudited ? [ ` [Id] uniqueidentifier NOT NULL DEFAULT NEWID(),`, ...allBodyCols.map((c) => colToSqlLine(c, true)), ` CONSTRAINT [PK_${tableName}] PRIMARY KEY NONCLUSTERED ([Id])`, ] : allBodyCols.map((c, i) => colToSqlLine(c, i < allBodyCols.length - 1)) // FK constraint lines const fkLines: string[] = [] for (const rel of relationships) { if (!rel.fkColumnName.trim() || !rel.referencedTable.trim()) continue const constraintName = `FK_${tableName}_${rel.fkColumnName}` const cascadeDelete = rel.cascadeDelete === 'NoAction' ? 'NO ACTION' : rel.cascadeDelete.replace(/([A-Z])/g, ' $1').trim().toUpperCase() const cascadeUpdate = rel.cascadeUpdate === 'NoAction' ? 'NO ACTION' : rel.cascadeUpdate.replace(/([A-Z])/g, ' $1').trim().toUpperCase() fkLines.push('') fkLines.push(`ALTER TABLE ${fullTableName}`) fkLines.push(` ADD CONSTRAINT [${constraintName}]`) fkLines.push(` FOREIGN KEY ([${rel.fkColumnName}])`) fkLines.push(` REFERENCES [dbo].[${rel.referencedTable}] ([${rel.referencedColumn || 'Id'}])`) fkLines.push(` ON DELETE ${cascadeDelete}`) fkLines.push(` ON UPDATE ${cascadeUpdate};`) } const lines: string[] = [ `/* ── Table: ${fullTableName} ── */`, ...(settings.entityName ? [`/* Entity Name: ${settings.entityName} */`] : []), ...(settings.isFullAudited ? ['/* Full Audited Entity (Id + audit columns auto-added) */'] : []), ...(settings.isMultiTenant ? ['/* Multi-Tenant */'] : []), ...(fkLines.length > 0 ? ['/* Foreign Key Constraints */'] : []), '', `CREATE TABLE ${fullTableName}`, `(`, ...bodyLines, `);`, ...fkLines, '', `/* Verify: SELECT TOP 10 * FROM ${fullTableName}; */`, ] return lines.join('\n') } // ─── Helpers ────────────────────────────────────────────────────────────────── /** Convert a DatabaseColumnDto (from API) to a ColumnDefinition for the grid */ function dbColToColumnDef(col: { columnName: string dataType: string isNullable: boolean maxLength?: number }): ColumnDefinition { const dt = col.dataType.toLowerCase().trim() let dataType: SqlDataType = 'nvarchar' let maxLength = '' if (dt === 'nvarchar' || dt === 'varchar' || dt === 'char' || dt === 'nchar') { dataType = col.maxLength === -1 ? 'nvarchar(MAX)' : 'nvarchar' maxLength = col.maxLength && col.maxLength > 0 ? String(col.maxLength) : '' } else if (dt === 'int') { dataType = 'int' } else if (dt === 'bigint') { dataType = 'bigint' } else if (dt === 'decimal' || dt === 'numeric') { dataType = 'decimal' } else if (dt === 'float' || dt === 'real') { dataType = 'float' } else if (dt === 'bit') { dataType = 'bit' } else if (dt.startsWith('datetime') || dt === 'smalldatetime') { dataType = 'datetime2' } else if (dt === 'date') { dataType = 'date' } else if (dt === 'uniqueidentifier') { dataType = 'uniqueidentifier' } else if (dt === 'money' || dt === 'smallmoney') { dataType = 'money' } return { id: crypto.randomUUID(), columnName: col.columnName, dataType, maxLength, isNullable: col.isNullable, defaultValue: '', description: '', } } /** Generate ALTER TABLE diff SQL (edit mode) */ function generateAlterTableSql( originalCols: ColumnDefinition[], currentCols: ColumnDefinition[], tableName: string, relationships: FkRelationshipDefinition[], originalRelationships: FkRelationshipDefinition[], ): string { const fullTableName = `[dbo].[${tableName}]` const lines: string[] = [`/* ── ALTER TABLE: ${fullTableName} ── */`, ''] let hasChanges = false // Build id-based maps for O(1) lookup const origById = new Map() originalCols.forEach((c) => origById.set(c.id, c)) const curById = new Map() currentCols.filter((c) => c.columnName.trim()).forEach((c) => curById.set(c.id, c)) // ➕ Added columns (id exists in current but NOT in original) currentCols .filter((c) => c.columnName.trim()) .forEach((col) => { if (!origById.has(col.id)) { hasChanges = true lines.push(`-- ➕ Yeni Sütun: [${col.columnName}]`) lines.push(`ALTER TABLE ${fullTableName}`) lines.push(` ADD ${colToSqlLine(col, false).trimStart()};`) lines.push('') } }) // ❌ Silinen sütunlar (id exists in original but NOT in current) originalCols.forEach((col) => { if (!curById.has(col.id)) { hasChanges = true lines.push(`-- ❌ Silinen Sütun: [${col.columnName}]`) lines.push(`ALTER TABLE ${fullTableName}`) lines.push(` DROP COLUMN [${col.columnName}];`) lines.push('') } }) // ✏️ Renamed / Modified columns (same id in both) currentCols .filter((c) => c.columnName.trim()) .forEach((col) => { const orig = origById.get(col.id) if (!orig) return // new column, already handled above const nameChanged = orig.columnName.trim().toLowerCase() !== col.columnName.trim().toLowerCase() const typeChanged = orig.dataType !== col.dataType || orig.maxLength !== col.maxLength const nullChanged = orig.isNullable !== col.isNullable // Rename: use sp_rename to preserve data if (nameChanged) { hasChanges = true lines.push(`-- 🔄 Sütun Yeniden Adlandırma: [${orig.columnName}] → [${col.columnName}]`) lines.push( `EXEC sp_rename 'dbo.${tableName}.${orig.columnName}', '${col.columnName}', 'COLUMN';`, ) lines.push('') } // Type or nullability change: ALTER COLUMN (uses new name if also renamed) if (typeChanged || nullChanged) { hasChanges = true const label = nameChanged ? `-- ✏️ Değiştirilen Sütun (yeniden adlandırıldıktan sonra): [${col.columnName}]` : `-- ✏️ Değiştirilen Sütun: [${col.columnName}]` lines.push(label) lines.push(`ALTER TABLE ${fullTableName}`) lines.push( ` ALTER COLUMN ${colToSqlLine({ ...col, defaultValue: '' }, false).trimStart()};`, ) lines.push('') } }) // 🔗 FK Diff: drop removed / drop+re-add modified / add new const fkCascadeSql = (b: CascadeBehavior) => b === 'NoAction' ? 'NO ACTION' : b.replace(/([A-Z])/g, ' $1').trim().toUpperCase() const addFkSql = (rel: FkRelationshipDefinition) => { const cname = rel.constraintName ?? `FK_${tableName}_${rel.fkColumnName}` lines.push(`-- 🔗 FK Ekle: [${rel.fkColumnName}] → [${rel.referencedTable}]`) lines.push(`ALTER TABLE ${fullTableName}`) lines.push(` ADD CONSTRAINT [${cname}]`) lines.push(` FOREIGN KEY ([${rel.fkColumnName}])`) lines.push(` REFERENCES [dbo].[${rel.referencedTable}] ([${rel.referencedColumn || 'Id'}])`) lines.push(` ON DELETE ${fkCascadeSql(rel.cascadeDelete)}`) lines.push(` ON UPDATE ${fkCascadeSql(rel.cascadeUpdate)};`) lines.push('') } const origRelById = new Map() originalRelationships.forEach((r) => origRelById.set(r.id, r)) const curRelById = new Map() relationships.forEach((r) => curRelById.set(r.id, r)) // Removed FKs → DROP CONSTRAINT originalRelationships.forEach((orig) => { if (!curRelById.has(orig.id)) { hasChanges = true const cname = orig.constraintName ?? `FK_${tableName}_${orig.fkColumnName}` lines.push(`-- ❌ FK Kaldır: [${orig.constraintName ?? cname}]`) lines.push(`ALTER TABLE ${fullTableName}`) lines.push(` DROP CONSTRAINT [${cname}];`) lines.push('') } }) // Changed FKs → DROP old + ADD new relationships.forEach((rel) => { if (!rel.fkColumnName.trim() || !rel.referencedTable.trim()) return const orig = origRelById.get(rel.id) if (orig) { // Existing FK — check if anything changed const changed = orig.fkColumnName !== rel.fkColumnName || orig.referencedTable !== rel.referencedTable || orig.referencedColumn !== rel.referencedColumn || orig.cascadeDelete !== rel.cascadeDelete || orig.cascadeUpdate !== rel.cascadeUpdate if (changed) { hasChanges = true const cname = orig.constraintName ?? `FK_${tableName}_${orig.fkColumnName}` lines.push(`-- ✏️ FK Güncelle (drop + re-add): [${cname}]`) lines.push(`ALTER TABLE ${fullTableName}`) lines.push(` DROP CONSTRAINT [${cname}];`) lines.push('') addFkSql(rel) } } else { // New FK hasChanges = true addFkSql(rel) } }) if (!hasChanges) { lines.push( '/* ℹ️ Henüz değişiklik yapılmadı. Sütunları ekleyin/silin/düzeyin ya da ilişki ekleyin. */', ) } return lines.join('\n') } // Map TableDesigner data-type → SqlTableField type function colTypeToEntityFieldType( dt: SqlDataType, ): CreateUpdateSqlTableFieldDto['type'] { if (dt === 'int' || dt === 'bigint') return 'number' if (dt === 'bit') return 'boolean' if (dt === 'datetime2' || dt === 'date') return 'date' if (dt === 'uniqueidentifier') return 'guid' if (dt === 'decimal' || dt === 'float' || dt === 'money') return 'decimal' return 'string' // nvarchar, nvarchar(MAX), etc. } const STEPS = ['Sütun Tasarımı', 'Entity Ayarları', 'İlişkiler', 'T-SQL Önizleme'] as const type Step = 0 | 1 | 2 | 3 const createEmptyColumn = (): ColumnDefinition => ({ id: crypto.randomUUID(), columnName: '', dataType: 'nvarchar', maxLength: '100', isNullable: true, defaultValue: '', description: '', }) const DEFAULT_SETTINGS: TableSettings = { menuValue: '', menuPrefix: '', entityName: '', tableName: '', displayName: '', description: '', isActive: true, isFullAudited: true, isMultiTenant: false, } // ─── Component ──────────────────────────────────────────────────────────────── const SqlTableDesignerDialog = ({ isOpen, onClose, dataSource, onDeployed, initialTableData, }: TableDesignerDialogProps) => { const { translate } = useLocalization() const isEditMode = !!initialTableData const [step, setStep] = useState(0) const [isDeploying, setIsDeploying] = useState(false) const [columns, setColumns] = useState([createEmptyColumn()]) const [originalColumns, setOriginalColumns] = useState([]) const [colsLoading, setColsLoading] = useState(false) const [settings, setSettings] = useState(DEFAULT_SETTINGS) const [menuOptions, setMenuOptions] = useState([]) const [menuLoading, setMenuLoading] = useState(false) const [relationships, setRelationships] = useState([]) const [originalRelationships, setOriginalRelationships] = useState([]) const [fksLoading, setFksLoading] = useState(false) const [fkModalOpen, setFkModalOpen] = useState(false) const [editingFkId, setEditingFkId] = useState(null) const [fkForm, setFkForm] = useState>(EMPTY_FK) const [dbTables, setDbTables] = useState<{ schemaName: string; tableName: string }[]>([]) const [targetTableColumns, setTargetTableColumns] = useState([]) const [targetColsLoading, setTargetColsLoading] = useState(false) useEffect(() => { if (!isOpen) return setMenuLoading(true) const svc = new MenuService() svc .getListMainMenu() .then((res) => { const opts: MenuOption[] = (res.data || []).map((m: any) => ({ value: m.shortName, label: m.displayName, })) setMenuOptions(opts) }) .catch(() => {}) .finally(() => setMenuLoading(false)) if (dataSource) { sqlObjectManagerService .getAllObjects(dataSource) .then((res) => setDbTables(res.data?.tables ?? [])) .catch(() => {}) } // Edit mode: load table's existing columns + FK constraints if (initialTableData && dataSource) { setColsLoading(true) sqlObjectManagerService .getTableColumns(dataSource, initialTableData.schemaName, initialTableData.tableName) .then((res) => { const defs = (res.data ?? []).map(dbColToColumnDef) setColumns(defs.length > 0 ? defs : [createEmptyColumn()]) setOriginalColumns(defs) setSettings((s) => ({ ...s, tableName: initialTableData.tableName })) }) .catch(() => {}) .finally(() => setColsLoading(false)) // Load SqlTable metadata (menu, entityName, displayName, flags…) developerKitService .getSqlTables() .then((res) => { const match = (res.items ?? []).find( (e) => e.tableName.toLowerCase() === initialTableData.tableName.toLowerCase(), ) if (match) { setSettings((s) => ({ ...s, menuValue: match.menu, menuPrefix: match.menu, entityName: match.name, displayName: match.displayName, description: match.description ?? '', isActive: match.isActive, isFullAudited: match.isFullAuditedEntity, isMultiTenant: match.isMultiTenant, })) } else { // Table not registered in developerKit — derive defaults from the table name. // e.g. "Sas_T_SqlStoredProcedure" → menu: "Sas", entityName/displayName: "SqlStoredProcedure" const parts = initialTableData.tableName.split('_') const derivedMenu = parts[0] ?? '' console.log('Derived menu from table name:', derivedMenu) const derivedEntity = parts[parts.length - 1] ?? initialTableData.tableName setSettings((s) => ({ ...s, menuValue: derivedMenu, menuPrefix: derivedMenu, entityName: derivedEntity, displayName: derivedEntity, })) } }) .catch(() => {}) // Load existing FK constraints const fkQuery = [ 'SELECT', ' fk.name AS constraintName,', ' col.name AS fkColumnName,', ' ref_t.name AS referencedTable,', ' ref_c.name AS referencedColumn,', ' fk.delete_referential_action_desc AS cascadeDelete,', ' fk.update_referential_action_desc AS cascadeUpdate', 'FROM sys.foreign_keys fk', 'INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id', 'INNER JOIN sys.columns col ON fkc.parent_object_id = col.object_id AND fkc.parent_column_id = col.column_id', 'INNER JOIN sys.tables ref_t ON fkc.referenced_object_id = ref_t.object_id', 'INNER JOIN sys.columns ref_c ON fkc.referenced_object_id = ref_c.object_id AND fkc.referenced_column_id = ref_c.column_id', `WHERE fk.parent_object_id = OBJECT_ID('${initialTableData.schemaName}.${initialTableData.tableName}')`, ].join('\n') const mapCascade = (v: string): CascadeBehavior => { if (v === 'CASCADE') return 'Cascade' if (v === 'SET_NULL') return 'SetNull' if (v === 'SET_DEFAULT') return 'Restrict' return 'NoAction' } setFksLoading(true) sqlObjectManagerService .executeQuery({ queryText: fkQuery, dataSourceCode: dataSource }) .then((res) => { const rows: any[] = res.data?.data ?? [] const fkDefs: FkRelationshipDefinition[] = rows.map((r) => ({ id: crypto.randomUUID(), constraintName: r.constraintName, relationshipType: 'OneToMany' as RelationshipType, fkColumnName: r.fkColumnName, referencedTable: r.referencedTable, referencedColumn: r.referencedColumn, cascadeDelete: mapCascade(r.cascadeDelete), cascadeUpdate: mapCascade(r.cascadeUpdate), isRequired: false, description: '', })) setRelationships(fkDefs) setOriginalRelationships(fkDefs) }) .catch(() => {}) .finally(() => setFksLoading(false)) } }, [isOpen, dataSource, initialTableData]) const generatedSql = useMemo( () => isEditMode ? generateAlterTableSql( originalColumns, columns, settings.tableName || initialTableData?.tableName || '', relationships, originalRelationships, ) : generateCreateTableSql(columns, settings, relationships), [isEditMode, originalColumns, columns, settings, initialTableData, relationships, originalRelationships], ) // ── Column operations ────────────────────────────────────────────────────── const addColumn = () => setColumns((prev) => [...prev, createEmptyColumn()]) const removeColumn = (id: string) => setColumns((prev) => prev.filter((c) => c.id !== id)) const moveColumn = (id: string, direction: 'up' | 'down') => { setColumns((prev) => { const idx = prev.findIndex((c) => c.id === id) if (idx < 0) return prev const next = [...prev] const swap = direction === 'up' ? idx - 1 : idx + 1 if (swap < 0 || swap >= next.length) return prev ;[next[idx], next[swap]] = [next[swap], next[idx]] return next }) } const updateColumn = useCallback( (id: string, field: K, value: ColumnDefinition[K]) => { setColumns((prev) => prev.map((c) => (c.id === id ? { ...c, [field]: value } : c))) }, [], ) // ── Settings helpers ─────────────────────────────────────────────────────── const buildTableName = (prefix: string, entity: string) => prefix && entity ? `${prefix}_D_${entity}` : '' const onMenuChange = (value: string) => { const opt = menuOptions.find((o) => o.value === value) const prefix = opt?.value ?? '' setSettings((s) => ({ ...s, menuValue: value, menuPrefix: prefix, tableName: buildTableName(prefix, s.entityName), })) } // Strip spaces and special chars — only alphanumeric + underscore allowed const onEntityNameChange = (value: string) => { const sanitized = value.replace(/[^A-Za-z0-9_]/g, '') setSettings((s) => ({ ...s, entityName: sanitized, tableName: buildTableName(s.menuPrefix, sanitized), displayName: sanitized, // always mirrors entity name })) } // ── FK Relationship handlers ─────────────────────────────────────────────── const loadTargetColumns = (tableName: string) => { if (!tableName || !dataSource) { setTargetTableColumns([]) return } const tbl = dbTables.find((t) => t.tableName === tableName) if (!tbl) return setTargetColsLoading(true) sqlObjectManagerService .getTableColumns(dataSource, tbl.schemaName, tbl.tableName) .then((res) => setTargetTableColumns((res.data ?? []).map((c) => c.columnName))) .catch(() => setTargetTableColumns([])) .finally(() => setTargetColsLoading(false)) } const openAddFk = () => { setEditingFkId(null) setFkForm(EMPTY_FK) setTargetTableColumns([]) setFkModalOpen(true) } const openEditFk = (rel: FkRelationshipDefinition) => { setEditingFkId(rel.id) const { id: _id, ...rest } = rel setFkForm(rest) loadTargetColumns(rest.referencedTable) setFkModalOpen(true) } const saveFk = () => { if (!fkForm.fkColumnName.trim() || !fkForm.referencedTable.trim()) return if (editingFkId) { setRelationships((prev) => prev.map((r) => (r.id === editingFkId ? { ...fkForm, id: editingFkId } : r)), ) } else { setRelationships((prev) => [...prev, { ...fkForm, id: crypto.randomUUID() }]) } setFkModalOpen(false) } const deleteFk = (id: string) => { setRelationships((prev) => prev.filter((r) => r.id !== id)) } // ── Navigation ───────────────────────────────────────────────────────────── // Compute duplicate column names (lowercased) const duplicateColumnNames = useMemo(() => { const names = columns.map((c) => c.columnName.trim().toLowerCase()).filter(Boolean) return new Set(names.filter((n, i) => names.indexOf(n) !== i)) }, [columns]) const canGoNext = (): boolean => { if (step === 0) { const hasNamed = columns.some((c) => c.columnName.trim().length > 0) return hasNamed && duplicateColumnNames.size === 0 } if (step === 1) { if (isEditMode) return true // table name is locked in edit mode const baseOk = !!settings.tableName.trim() && !!settings.entityName.trim() && !!settings.menuValue if (!baseOk) return false if (!settings.isFullAudited) { return columns.some((c) => c.columnName.trim().toLowerCase() === 'id') } return true } return true } const handleNext = () => { if (step < 3) setStep((s) => (s + 1) as Step) } const handleBack = () => { if (step > 0) setStep((s) => (s - 1) as Step) } // ── Deploy ───────────────────────────────────────────────────────────────── const syncSqlTableMetadata = async (deployedTableName: string) => { try { const namedCols = columns.filter((c) => c.columnName.trim()) // Find existing SqlTable record by table name const listResult = await developerKitService.getSqlTables() const existing = (listResult.items ?? []).find( (e) => e.tableName.toLowerCase() === deployedTableName.toLowerCase(), ) if (existing) { // Update: keep existing metadata, sync fields (match by name to preserve IDs) const fieldDtos: CreateUpdateSqlTableFieldDto[] = namedCols.map((col, i) => { const existingField = existing.fields?.find( (f) => f.name.toLowerCase() === col.columnName.trim().toLowerCase(), ) const maxLen = col.maxLength ? parseInt(col.maxLength, 10) || undefined : undefined return { id: existingField?.id, name: col.columnName.trim(), type: colTypeToEntityFieldType(col.dataType), isRequired: !col.isNullable, maxLength: maxLen, isUnique: false, defaultValue: col.defaultValue || undefined, description: col.description || undefined, displayOrder: i, } }) const updateDto: CreateUpdateSqlTableDto = { menu: existing.menu, name: existing.name, displayName: existing.displayName, tableName: existing.tableName, description: existing.description, isActive: existing.isActive, isFullAuditedEntity: existing.isFullAuditedEntity, isMultiTenant: existing.isMultiTenant, fields: fieldDtos, } await developerKitService.updateSqlTable(existing.id, updateDto) } else { // Create new SqlTable record const fieldDtos: CreateUpdateSqlTableFieldDto[] = namedCols.map((col, i) => { const maxLen = col.maxLength ? parseInt(col.maxLength, 10) || undefined : undefined return { name: col.columnName.trim(), type: colTypeToEntityFieldType(col.dataType), isRequired: !col.isNullable, maxLength: maxLen, isUnique: false, defaultValue: col.defaultValue || undefined, description: col.description || undefined, displayOrder: i, } }) const createDto: CreateUpdateSqlTableDto = { menu: settings.menuValue, name: settings.entityName || deployedTableName, displayName: settings.displayName || deployedTableName, tableName: deployedTableName, description: settings.description || undefined, isActive: settings.isActive, isFullAuditedEntity: settings.isFullAudited, isMultiTenant: settings.isMultiTenant, fields: fieldDtos, } await developerKitService.createSqlTable(createDto) } } catch { // Silent — SQL deploy already succeeded; metadata sync failure is non-critical } } const handleDeploy = async () => { if (!dataSource) { toast.push( Lutfen once bir veri kaynagi secin. , { placement: 'top-center' }, ) return } setIsDeploying(true) try { const result = await sqlObjectManagerService.executeQuery({ queryText: generatedSql, dataSourceCode: dataSource, }) if (result.data.success) { const deployedTable = settings.tableName || initialTableData?.tableName || '' // Sync entity metadata to SqlTable / SqlTableField tables await syncSqlTableMetadata(deployedTable) toast.push( Tablo basariyla {isEditMode ? 'güncellendi' : 'oluşturuldu'}: [dbo].[{deployedTable}] , { placement: 'top-center' }, ) onDeployed?.() handleClose() } else { toast.push( {result.data.message || 'Tablo olusturulamadi.'} , { placement: 'top-center' }, ) } } catch (error: any) { toast.push( {error.response?.data?.error?.message || 'Tablo deploy edilemedi.'} , { placement: 'top-center' }, ) } finally { setIsDeploying(false) } } const handleClose = () => { setStep(0) setColumns([createEmptyColumn()]) setOriginalColumns([]) setSettings(DEFAULT_SETTINGS) setRelationships([]) setOriginalRelationships([]) setDbTables([]) setTargetTableColumns([]) onClose() } // ── Step Indicator ───────────────────────────────────────────────────────── const STEP_LABELS = ['Sütun Tasarımı', 'Entity Ayarları', 'İlişkiler', 'T-SQL Önizleme'] const renderStepIndicator = () => (
{STEP_LABELS.map((label, i) => { const isDone = i < step const isActive = i === step return (
{isDone ? : i + 1}
{label}
{i < STEP_LABELS.length - 1 && (
)}
) })}
) // ── Step 0: Column Designer ──────────────────────────────────────────────── const renderColumnDesigner = () => (
{colsLoading && (
Tablo sütunları yükleniyor...
)} {!colsLoading && ( <>
{isEditMode ? (
📝 Mevcut sütunlar yüklendi. Değişiklikler T-SQL Önizleme adımında ALTER TABLE olarak gösterilecek.  | Yeni Ad Değişti (sp_rename) Tip/Null Değişti
) : (
Sonraki adımda seceğiniz{' '} Full Audited Entity / Multi-Tenant ayarlarina gore ek sutunlar da dahil edilecektir.
)}
{/* Header row */}
Sutun Adi *
Veri Tipi *
Max
Null
Varsayilan
Not
Islem
{/* Editable column rows */} {duplicateColumnNames.size > 0 && (
⚠️ Aynı isimde sütun tanımlanamaz:{' '} {[...duplicateColumnNames].map((n) => ( {n} ))}
)}
{columns.map((col, idx) => { const isDuplicate = col.columnName.trim() !== '' && duplicateColumnNames.has(col.columnName.trim().toLowerCase()) const isNewRow = isEditMode && !originalColumns.some((o) => o.id === col.id) const origRow = isEditMode ? originalColumns.find((o) => o.id === col.id) : undefined const isRenamedRow = isEditMode && !!origRow && origRow.columnName.trim().toLowerCase() !== col.columnName.trim().toLowerCase() const isModifiedRow = isEditMode && !!origRow && !isNewRow && (origRow.dataType !== col.dataType || origRow.maxLength !== col.maxLength || origRow.isNullable !== col.isNullable) const rowBg = isDuplicate ? 'outline outline-1 outline-red-400' : isNewRow ? 'bg-green-50 dark:bg-green-900/20 outline outline-1 outline-green-400' : isRenamedRow && isModifiedRow ? 'bg-purple-50 dark:bg-purple-900/20 outline outline-1 outline-purple-400' : isRenamedRow ? 'bg-blue-50 dark:bg-blue-900/20 outline outline-1 outline-blue-400' : isModifiedRow ? 'bg-yellow-50 dark:bg-yellow-900/20 outline outline-1 outline-yellow-400' : '' return (
updateColumn(col.id, 'columnName', e.target.value)} />
updateColumn(col.id, 'maxLength', e.target.value)} />
updateColumn(col.id, 'isNullable', checked as boolean)} />
updateColumn(col.id, 'defaultValue', e.target.value)} />
updateColumn(col.id, 'description', e.target.value)} />
) })}
{/* Id warning when Full Audited is off */} {!isEditMode && !settings.isFullAudited && !columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && (
⚠️ Full Audited Entity seçilmedi. Tablonun birincil anahtarı için{' '} Id sütununu elle eklemeniz gerekmektedir.
)} )}
) // ── Step 1: Entity Settings ──────────────────────────────────────────────── const renderEntitySettings = () => (
{/* Menu Name */}
{menuLoading &&

Yukleniyor...

}
{/* Entity Name */}
onEntityNameChange(e.target.value)} placeholder="e.g. Product, User, Order" />
{/* Table Name (readonly, auto-generated) */}
{isEditMode && (

Mevcut tablo — ad değiştirilemez.

)}
{/* Display Name */}
setSettings((s) => ({ ...s, displayName: e.target.value }))} placeholder="e.g. Ürün, Kullanıcı, Sipariş" />
{/* Description */}