diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260225082054_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260301173343_Initial.Designer.cs
similarity index 99%
rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260225082054_Initial.Designer.cs
rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260301173343_Initial.Designer.cs
index 0e7522b..1b02102 100644
--- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260225082054_Initial.Designer.cs
+++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260301173343_Initial.Designer.cs
@@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
- [Migration("20260225082054_Initial")]
+ [Migration("20260301173343_Initial")]
partial class Initial
{
///
diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260225082054_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260301173343_Initial.cs
similarity index 100%
rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260225082054_Initial.cs
rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260301173343_Initial.cs
diff --git a/ui/src/views/sqlQueryManager/SqlQueryManager.tsx b/ui/src/views/sqlQueryManager/SqlQueryManager.tsx
index 2496a1c..97456f4 100644
--- a/ui/src/views/sqlQueryManager/SqlQueryManager.tsx
+++ b/ui/src/views/sqlQueryManager/SqlQueryManager.tsx
@@ -13,7 +13,7 @@ import type {
} from '@/proxy/sql-query-manager/models'
import { SqlObjectType } from '@/proxy/sql-query-manager/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
-import { FaDatabase, FaPlay, FaSave, FaSyncAlt, FaCloudUploadAlt, FaCode } from 'react-icons/fa'
+import { FaDatabase, FaPlay, FaSave, FaCloudUploadAlt, FaCode, FaTable } from 'react-icons/fa'
import { FaCheckCircle } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import SqlObjectExplorer from './components/SqlObjectExplorer'
@@ -21,6 +21,7 @@ import SqlEditor, { SqlEditorRef } from './components/SqlEditor'
import SqlResultsGrid from './components/SqlResultsGrid'
import SqlObjectProperties from './components/SqlObjectProperties'
import TemplateDialog from './components/TemplateDialog'
+import TableDesignerDialog from './components/TableDesignerDialog'
import { Splitter } from '@/components/codeLayout/Splitter'
import { Helmet } from 'react-helmet'
import { useStoreState } from '@/store/store'
@@ -72,11 +73,18 @@ const SqlQueryManager = () => {
isExistingObject: false,
})
const [showTemplateDialog, setShowTemplateDialog] = useState(false)
+ const [showTableDesignerDialog, setShowTableDesignerDialog] = useState(false)
+ const [designTableData, setDesignTableData] = useState<{ schemaName: string; tableName: string } | null>(null)
const [showTemplateConfirmDialog, setShowTemplateConfirmDialog] = useState(false)
const [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>(
null,
)
+ const handleDesignTable = (schemaName: string, tableName: string) => {
+ setDesignTableData({ schemaName, tableName })
+ setShowTableDesignerDialog(true)
+ }
+
useEffect(() => {
loadDataSources()
}, [])
@@ -802,16 +810,6 @@ GO`,
- }
- onClick={() => setShowTemplateDialog(true)}
- className="shadow-sm"
- >
- {translate('::App.Platform.Templates')}
-
{
+ setDesignTableData(null)
+ setShowTableDesignerDialog(true)
+ }}
refreshTrigger={state.refreshTrigger}
/>
@@ -972,6 +975,20 @@ GO`,
+ {/* Table Designer Dialog */}
+ {
+ setShowTableDesignerDialog(false)
+ setDesignTableData(null)
+ }}
+ dataSource={state.selectedDataSource}
+ initialTableData={designTableData}
+ onDeployed={() => {
+ setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 }))
+ }}
+ />
+
{/* Template Dialog */}
void
onShowTableColumns?: (schemaName: string, tableName: string) => void
+ onDesignTable?: (schemaName: string, tableName: string) => void
+ onNewTable?: () => void
refreshTrigger?: number
}
@@ -51,6 +54,8 @@ const SqlObjectExplorer = ({
selectedObject,
onTemplateSelect,
onShowTableColumns,
+ onDesignTable,
+ onNewTable,
refreshTrigger,
}: SqlObjectExplorerProps) => {
const { translate } = useLocalization()
@@ -506,6 +511,22 @@ const SqlObjectExplorer = ({
className="fixed z-50 bg-white dark:bg-gray-800 shadow-lg rounded border border-gray-200 dark:border-gray-700 py-1"
style={{ top: contextMenu.y, left: contextMenu.x }}
>
+ {contextMenu.node?.id?.startsWith('table-') && (
+ {
+ const tableData = contextMenu.node?.data as any
+ if (tableData && onDesignTable) {
+ onDesignTable(tableData.schemaName, tableData.tableName)
+ }
+ setContextMenu({ show: false, x: 0, y: 0, node: null })
+ }}
+ >
+
+ Design
+
+ )}
+
{contextMenu.node?.type === 'object' && contextMenu.node?.objectType && contextMenu.node?.data?.isCustom && (
<>
{
- loadObjects()
- setContextMenu({ show: false, x: 0, y: 0, node: null })
- }}
- >
-
- {translate('::App.Platform.Refresh')}
-
+ <>
+ {/* Tables folder */}
+ {contextMenu.node.id === 'tables' && (
+ {
+ onNewTable?.()
+ setContextMenu({ show: false, x: 0, y: 0, node: null })
+ }}
+ >
+
+ New Table
+
+ )}
+
+ {/* Stored Procedures folder */}
+ {contextMenu.node.id === 'procedures' && (
+ {
+ onTemplateSelect?.('', 'create-procedure')
+ setContextMenu({ show: false, x: 0, y: 0, node: null })
+ }}
+ >
+
+ New Stored Procedure
+
+ )}
+
+ {/* Views folder */}
+ {contextMenu.node.id === 'views' && (
+ {
+ onTemplateSelect?.('', 'create-view')
+ setContextMenu({ show: false, x: 0, y: 0, node: null })
+ }}
+ >
+
+ New View
+
+ )}
+
+ {/* Functions folder */}
+ {contextMenu.node.id === 'functions' && (
+ {
+ onTemplateSelect?.('', 'create-scalar-function')
+ setContextMenu({ show: false, x: 0, y: 0, node: null })
+ }}
+ >
+
+ New Function
+
+ )}
+
+ {/* Queries folder */}
+ {contextMenu.node.id === 'queries' && (
+ {
+ onTemplateSelect?.('', 'select')
+ setContextMenu({ show: false, x: 0, y: 0, node: null })
+ }}
+ >
+
+ New Query
+
+ )}
+
+ {/* Separator */}
+ {contextMenu.node.id !== 'root' && (
+
+ )}
+
+ {/* Refresh — all folders */}
+ {
+ loadObjects()
+ setContextMenu({ show: false, x: 0, y: 0, node: null })
+ }}
+ >
+
+ {translate('::App.Platform.Refresh')}
+
+ >
)}
>
diff --git a/ui/src/views/sqlQueryManager/components/TableDesignerDialog.tsx b/ui/src/views/sqlQueryManager/components/TableDesignerDialog.tsx
new file mode 100644
index 0000000..50c7f28
--- /dev/null
+++ b/ui/src/views/sqlQueryManager/components/TableDesignerDialog.tsx
@@ -0,0 +1,1703 @@
+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,
+ FaCog,
+ FaDatabase,
+ FaLink,
+ FaEdit,
+ FaTimes,
+ FaArrowRight,
+ FaInfoCircle,
+ FaExclamationTriangle,
+} 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 { CreateUpdateCustomEntityDto, CreateUpdateCustomEntityFieldDto } 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 → CustomEntityField type
+function colTypeToEntityFieldType(
+ dt: SqlDataType,
+): CreateUpdateCustomEntityFieldDto['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 TableDesignerDialog = ({
+ 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 CustomEntity metadata (menu, entityName, displayName, flags…)
+ developerKitService
+ .getCustomEntities()
+ .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,
+ }))
+ }
+ })
+ .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 syncCustomEntityMetadata = async (deployedTableName: string) => {
+ try {
+ const namedCols = columns.filter((c) => c.columnName.trim())
+
+ // Find existing CustomEntity record by table name
+ const listResult = await developerKitService.getCustomEntities()
+ 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: CreateUpdateCustomEntityFieldDto[] = 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: CreateUpdateCustomEntityDto = {
+ 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.updateCustomEntity(existing.id, updateDto)
+ } else {
+ // Create new CustomEntity record
+ const fieldDtos: CreateUpdateCustomEntityFieldDto[] = 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: CreateUpdateCustomEntityDto = {
+ 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.createCustomEntity(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 CustomEntity / CustomEntityField tables
+ await syncCustomEntityMetadata(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.
+
+ )}
+
} onClick={addColumn}>
+ Ekle
+
+
+
+ {/* 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)}
+ />
+
+
+ {
+ const dt = e.target.value as SqlDataType
+ updateColumn(col.id, 'dataType', dt)
+ if (dt !== 'nvarchar') updateColumn(col.id, 'maxLength', '')
+ else if (!col.maxLength) updateColumn(col.id, 'maxLength', '100')
+ }}
+ >
+ {DATA_TYPES.map((t) => (
+
+ {t.label}
+
+ ))}
+
+
+
+ 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)}
+ />
+
+
+ moveColumn(col.id, 'up')}
+ disabled={idx === 0}
+ className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-30 text-gray-500"
+ title="Yukari tasi"
+ >
+
+
+ moveColumn(col.id, 'down')}
+ disabled={idx === columns.length - 1}
+ className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-30 text-gray-500"
+ title="Asagi tasi"
+ >
+
+
+ removeColumn(col.id)}
+ className="p-1 rounded hover:bg-red-50 dark:hover:bg-red-900/30 text-red-500"
+ title="Sil"
+ >
+
+
+
+
+ )
+ })}
+
+
+ {/* 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 */}
+
+
+ Menu Name *
+
+
onMenuChange(e.target.value)}
+ >
+
+ {menuOptions.map((opt) => (
+
+ {opt.label} ({opt.value})
+
+ ))}
+
+ {menuLoading &&
Yukleniyor...
}
+
+
+ {/* Entity Name */}
+
+
+ Entity Name *
+
+ onEntityNameChange(e.target.value)}
+ placeholder="e.g. Product, User, Order"
+ />
+
+
+ {/* Table Name (readonly, auto-generated) */}
+
+
Table Name
+
+ {isEditMode && (
+
Mevcut tablo — ad değiştirilemez.
+ )}
+
+
+ {/* Display Name */}
+
+ Display Name
+ setSettings((s) => ({ ...s, displayName: e.target.value }))}
+ placeholder="e.g. Ürün, Kullanıcı, Sipariş"
+ />
+
+
+ {/* Description */}
+
+ Description
+
+
+
+ {/* Feature flags */}
+
+
Entity Ozellikleri
+
+ {/* Active */}
+
+
setSettings((s) => ({ ...s, isActive: checked as boolean }))}
+ />
+
+
Active
+
Entity aktif mi olacak?
+
+
+
+ {/* Full Audited */}
+
+
+ setSettings((s) => ({ ...s, isFullAudited: checked as boolean }))
+ }
+ />
+
+
Full Audited Entity
+
+ {[
+ 'Id',
+ 'CreationTime',
+ 'CreatorId',
+ 'LastModificationTime',
+ 'LastModifierId',
+ 'IsDeleted',
+ 'DeletionTime',
+ 'DeleterId',
+ ].map((c) => (
+
+ {c}
+
+ ))}
+
+
+
+
+ {/* Multi-Tenant */}
+
+
+ setSettings((s) => ({ ...s, isMultiTenant: checked as boolean }))
+ }
+ />
+
+
Multi-Tenant
+
+ TenantId{' '}
+ (uniqueidentifier NULL)
+
+
+
+
+
+ {/* Warning: no Id column when Full Audited is off */}
+ {!isEditMode && !settings.isFullAudited &&
+ !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`}
+
+
+ İlişki Ekle
+
+
+
+ {/* 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}
+ )}
+
+
+
+ openEditFk(rel)}
+ title="Düzenle"
+ className="p-1.5 text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded transition-colors"
+ >
+
+
+ deleteFk(rel.id)}
+ title="Sil"
+ className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
+ >
+
+
+
+
+ ))}
+
+ >}
+
+ {/* FK Add/Edit Modal */}
+ {fkModalOpen && (
+
+
+ {/* Modal Header */}
+
+
+ {editingFkId ? 'İlişkiyi Düzenle' : 'Yeni İlişki Ekle'}
+
+ setFkModalOpen(false)}
+ className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
+ >
+
+
+
+
+ {/* Modal Body */}
+
+ {/* Relationship type */}
+
+
+ İlişki Tipi
+
+
+ {REL_TYPES.map((t) => (
+ setFkForm((f) => ({ ...f, relationshipType: t.value }))}
+ className={`flex-1 py-2 px-3 rounded-lg border text-sm font-medium transition-all ${
+ fkForm.relationshipType === t.value
+ ? 'bg-indigo-600 border-indigo-600 text-white'
+ : 'bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:border-indigo-300'
+ }`}
+ >
+ {t.label}
+ {t.desc}
+
+ ))}
+
+
+
+ {/* FK column / Referenced table & column */}
+
+
+
+ Bu Tablodaki FK Sütunu *
+
+ setFkForm((f) => ({ ...f, fkColumnName: e.target.value }))}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-mono dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
+ >
+ — Seçiniz —
+ {columns
+ .filter((c) => c.columnName.trim())
+ .map((c) => (
+
+ {c.columnName}
+
+ ))}
+
+
+
+
+ Hedef Tablo *
+
+ {
+ const val = e.target.value
+ setFkForm((f) => ({ ...f, referencedTable: val, referencedColumn: '' }))
+ loadTargetColumns(val)
+ }}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-mono dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
+ >
+ — Seçiniz —
+ {dbTables.map((t) => (
+
+ [{t.schemaName}].[{t.tableName}]
+
+ ))}
+
+
+
+
+
+
+ Hedef Sütun (PK){targetColsLoading ? ' — Yükleniyor...' : ''}
+
+ setFkForm((f) => ({ ...f, referencedColumn: e.target.value }))}
+ disabled={targetColsLoading || targetTableColumns.length === 0}
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-mono dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500 disabled:opacity-60"
+ >
+ — Önce hedef tablo seçin —
+ {targetTableColumns.map((col) => (
+
+ {col}
+
+ ))}
+
+
+
+ {/* Cascade */}
+
+
+
+ Cascade Delete
+
+
+ setFkForm((f) => ({ ...f, cascadeDelete: e.target.value as CascadeBehavior }))
+ }
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
+ >
+ {CASCADE_OPTIONS.map((o) => (
+ {o.label}
+ ))}
+
+
+
+
+ Cascade Update
+
+
+ setFkForm((f) => ({ ...f, cascadeUpdate: e.target.value as CascadeBehavior }))
+ }
+ className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
+ >
+ {CASCADE_OPTIONS.map((o) => (
+ {o.label}
+ ))}
+
+
+
+
+ {/* Options */}
+
+
+ setFkForm((f) => ({ ...f, isRequired: e.target.checked }))}
+ className="w-4 h-4 text-indigo-600 rounded"
+ />
+ Zorunlu
+
+
+
+ {/* Description */}
+
+
+ Açıklama
+
+
+
+
+ {/* Modal Footer */}
+
+ setFkModalOpen(false)}
+ className="px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
+ >
+ İptal
+
+
+ Kaydet
+
+
+
+
+ )}
+
+ )
+
+ // ── Step 3: T-SQL Preview ──────────────────────────────────────────────────
+
+ const renderSqlPreview = () => (
+
+
+
+ Olusturulan T-SQL kodu. Deploy Et ile veritabanina uygulayin.
+
+
navigator.clipboard.writeText(generatedSql)}
+ >
+ Kopyala
+
+
+
+ {generatedSql}
+
+
+ )
+
+ // ── Render ─────────────────────────────────────────────────────────────────
+
+ return (
+
+
+ {/* Header */}
+
+
+
+
+ {isEditMode
+ ? `Tablo Düzenle — ${initialTableData?.tableName}`
+ : 'Tablo Tasarımcısı'}
+
+
+ {isEditMode
+ ? 'Mevcut sütunları düzenleyin, yeni ekleyin veya ilişki tanımlayın — ALTER TABLE SQL üretilir'
+ : 'Sütunları tanımlayın, ayarları yapın ve T-SQL ile deploy edin'}
+
+
+
+
+ {/* Steps */}
+ {renderStepIndicator()}
+
+ {/* Content */}
+
+ {step === 0 && renderColumnDesigner()}
+ {step === 1 && renderEntitySettings()}
+ {step === 2 && renderRelationships()}
+ {step === 3 && renderSqlPreview()}
+
+
+ {/* Footer */}
+
+
+ Iptal
+
+
+ {step > 0 && (
+
+ Geri
+
+ )}
+ {step < 3 ? (
+
+ İleri
+
+ ) : (
+ }
+ onClick={handleDeploy}
+ loading={isDeploying}
+ disabled={!dataSource || isDeploying || (isEditMode && generatedSql.includes('Henüz değişiklik yapılmadı'))}
+ >
+ Deploy Et
+
+ )}
+
+
+
+
+ )
+}
+
+export default TableDesignerDialog