import { useState, useCallback, useMemo, useEffect } from 'react' import { createPortal } from 'react-dom' import { Button, Dialog, Notification, toast, Checkbox } from '@/components/ui' import { FaPlus, FaTrash, FaArrowUp, FaArrowDown, FaTable, FaCloudUploadAlt, FaCheck, FaLink, FaEdit, FaTimes, FaArrowRight, } from 'react-icons/fa' import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { MenuService } from '@/services/menu.service' import { useLocalization } from '@/utils/hooks/useLocalization' import { CascadeBehavior, SqlTableRelation, RelationshipType } from '@/proxy/developerKit/models' // ─── 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 } interface TableDesignerDialogProps { isOpen: boolean onClose: () => void dataSource: string | null onDeployed?: () => void initialTableData?: { schemaName: string; tableName: string } | null } // ─── 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: '__Id', columnName: 'Id', dataType: 'uniqueidentifier', maxLength: '', isNullable: false, defaultValue: 'NEWID()', description: 'Primary key', }, { 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: 'Cascade', 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: SqlTableRelation[], ): string { const tableName = settings.tableName || 'NewTable' const fullTableName = `[dbo].[${tableName}]` const userCols = columns.filter((c) => c.columnName.trim()) const allBodyCols = userCols const hasIdCol = userCols.some((c) => c.columnName.trim().toLowerCase() === 'id') const bodyLines = hasIdCol ? [ ...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} */`] : []), ...(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: SqlTableRelation[], originalRelationships: SqlTableRelation[], ): 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: SqlTableRelation) => { 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') } 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: '', } // ─── 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)) // Derive settings from table name (e.g. "Sas_D_EntityName" → menu: "Sas", entity: "EntityName") const parts = initialTableData.tableName.split('_') const derivedMenu = parts[0] ?? '' const derivedEntity = parts[parts.length - 1] ?? initialTableData.tableName setSettings((s) => ({ ...s, menuValue: derivedMenu, menuPrefix: derivedMenu, entityName: derivedEntity, displayName: derivedEntity, })) // 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: SqlTableRelation[] = 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 clearAllColumns = () => setColumns([createEmptyColumn()]) const addFullAuditedColumns = () => { const existingNames = new Set(columns.map((c) => c.columnName.trim().toLowerCase())) const toAdd = FULL_AUDIT_COLUMNS.filter( (c) => !existingNames.has(c.columnName.toLowerCase()), ) setColumns((prev) => { const nonEmpty = prev.filter((c) => c.columnName.trim() !== '') return [...nonEmpty, ...toAdd.map((c) => ({ ...c })), createEmptyColumn()] }) } const addMultiTenantColumns = () => { const existingNames = new Set(columns.map((c) => c.columnName.trim().toLowerCase())) if (!existingNames.has(TENANT_COLUMN.columnName.toLowerCase())) { setColumns((prev) => { const nonEmpty = prev.filter((c) => c.columnName.trim() !== '') return [...nonEmpty, { ...TENANT_COLUMN }, 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: SqlTableRelation) => { 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 return columns.some((c) => c.columnName.trim().toLowerCase() === 'id') } 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 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 || '' 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 && ( <>
{/* 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 */} {!isEditMode && !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) */}
{/* Warning: no Id column */} {!isEditMode && !columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && (
Full Audited Entity seçili değil. Geri dönüp{' '} Id sütununu{' '} uniqueidentifier{' '} tipinde manuel olarak eklemelisiniz.
)}
) // ── Step 2: Relationships ───────────────────────────────────────────────── const renderRelationships = () => (
{/* Loading indicator for edit mode */} {fksLoading && (
Mevcut FK kısıtlamaları yükleniyor...
)} {!fksLoading && <> {/* Header */}
{relationships.length === 0 ? 'İlişki tanımlanmamış' : `${relationships.length} ilişki`}
{/* Empty state */} {relationships.length === 0 && (

Bu tablo için henüz ilişki tanımlanmamış

Bu adım isteğe bağlıdır, atlayabilirsiniz

)} {/* Relationship cards */}
{relationships.map((rel) => (
[{rel.fkColumnName}] [dbo].[{rel.referencedTable}].[{rel.referencedColumn || 'Id'}] {REL_TYPES.find((t) => t.value === rel.relationshipType)?.label}
ON DELETE: {rel.cascadeDelete} ON UPDATE: {rel.cascadeUpdate} {rel.isRequired && ( Zorunlu )} {rel.description && ( {rel.description} )}
))}
} {/* FK Add/Edit Modal */} {fkModalOpen && createPortal(
{/* Modal Header */}

{editingFkId ? 'İlişkiyi Düzenle' : 'Yeni İlişki Ekle'}

{/* Modal Body */}
{/* Relationship type */}
{REL_TYPES.map((t) => ( ))}
{/* FK column / Referenced table & column */}
{/* Cascade */}
{/* Options */}
{/* Description */}