erp-platform/ui/src/components/developerKit/EntityEditor.tsx
Sedat ÖZTÜRK 8cc8ed07f9 Düzenleme
2025-08-11 09:34:44 +03:00

508 lines
22 KiB
TypeScript

import React, { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useEntities, EntityFieldType } from '../../contexts/EntityContext'
import { Save, ArrowLeft, Plus, Trash2, Database, HelpCircle } from 'lucide-react'
import { CreateUpdateCustomEntityFieldDto, CustomEntityField } from '@/proxy/developerKit/models'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
const EntityEditor: React.FC = () => {
const { id } = useParams()
const navigate = useNavigate()
const { translate } = useLocalization()
const { getEntity, addEntity, updateEntity } = useEntities()
const [name, setName] = useState('')
const [displayName, setDisplayName] = useState('')
const [tableName, setTableName] = useState('')
const [description, setDescription] = useState('')
const [fields, setFields] = useState<CustomEntityField[]>([])
const [isActive, setIsActive] = useState(true)
const [hasAuditFields, setHasAuditFields] = useState(true)
const [hasSoftDelete, setHasSoftDelete] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const isEditing = !!id
// Check if migration is applied to disable certain fields
const isMigrationApplied = Boolean(
isEditing && id && getEntity(id)?.migrationStatus === 'applied',
)
useEffect(() => {
if (isEditing && id) {
const entity = getEntity(id)
if (entity) {
setName(entity.name)
setDisplayName(entity.displayName)
setTableName(entity.tableName)
setDescription(entity.description || '')
setFields(entity.fields)
setIsActive(entity.isActive)
setHasAuditFields(entity.hasAuditFields)
setHasSoftDelete(entity.hasSoftDelete)
}
} else {
// Initialize with default field
setFields([
{
id: crypto.randomUUID(),
entityId: id || '',
name: 'Name',
type: 'string',
isRequired: true,
maxLength: 100,
description: 'Entity name',
},
])
}
}, [id, isEditing, getEntity])
const addField = () => {
const newField: CustomEntityField = {
id: crypto.randomUUID(),
entityId: id || '',
name: '',
type: 'string',
isRequired: false,
description: '',
}
setFields((prev) => [...prev, newField])
}
const updateField = (fieldKey: string, updates: Partial<CustomEntityField>) => {
setFields((prev) =>
prev.map((field) => (field.id === fieldKey ? { ...field, ...updates } : field)),
)
}
const removeField = (fieldKey: string) => {
setFields((prev) => prev.filter((field) => field.id !== fieldKey))
}
const handleSave = async () => {
if (!name.trim()) {
alert('Please enter an entity name')
return
}
if (!displayName.trim()) {
alert('Please enter a display name')
return
}
if (!tableName.trim()) {
alert('Please enter a table name')
return
}
if (fields.length === 0) {
alert('Please add at least one field')
return
}
const invalidFields = fields.filter((f) => !f.name.trim())
if (invalidFields.length > 0) {
alert('Please fill in all field names')
return
}
setIsSaving(true)
try {
const sanitizedFields = fields.map((f) => {
const sanitized: CreateUpdateCustomEntityFieldDto = {
...(f.id && isEditing ? { id: f.id } : {}), // sadece güncelleme modunda varsa gönder
name: f.name.trim(),
type: f.type,
isRequired: f.isRequired,
maxLength: f.maxLength,
isUnique: f.isUnique || false,
defaultValue: f.defaultValue,
description: f.description,
}
return sanitized
})
const entityData = {
name: name.trim(),
displayName: displayName.trim(),
tableName: tableName.trim(),
description: description.trim(),
fields: sanitizedFields,
isActive,
hasAuditFields,
hasSoftDelete,
}
if (isEditing && id) {
updateEntity(id, entityData)
} else {
addEntity(entityData)
}
navigate(ROUTES_ENUM.protected.saas.developerKitEntities)
} catch (error) {
console.error('Error saving entity:', error)
alert('Failed to save entity. Please try again.')
} finally {
setIsSaving(false)
}
}
const fieldTypes = [
{ value: 'string', label: 'String (Text)' },
{ value: 'number', label: 'Number (Integer)' },
{ value: 'decimal', label: 'Decimal' },
{ value: 'boolean', label: 'Boolean (True/False)' },
{ value: 'date', label: 'Date' },
{ value: 'guid', label: 'GUID (Unique ID)' },
]
return (
<div className="min-h-screen bg-slate-50">
<div className="w-full">
{/* Header */}
<div className="bg-white rounded-lg border border-slate-200 p-6 mb-6 shadow-sm">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
onClick={() => navigate(ROUTES_ENUM.protected.saas.developerKitEntities)}
className="flex items-center gap-2 text-slate-600 hover:text-slate-900 transition-colors"
>
<ArrowLeft className="w-4 h-4" />
{translate('::App.DeveloperKit.EntityEditor.Back')}
</button>
<div className="h-6 w-px bg-slate-300" />
<div className="flex items-center gap-2">
<Database className="w-6 h-6 text-blue-600" />
<h1 className="text-xl font-semibold text-slate-900">
{isEditing
? translate('::App.DeveloperKit.EntityEditor.Title.Edit')
: translate('::App.DeveloperKit.EntityEditor.Title.Create')}
</h1>
</div>
</div>
<div className="flex items-center gap-3">
{/* Help Tooltip */}
<div className="relative group">
<button className="p-2 text-slate-400 hover:text-slate-600 transition-colors">
<HelpCircle className="w-4 h-4" />
</button>
<div className="absolute right-0 top-full mt-2 w-96 bg-slate-900 text-white text-sm rounded-lg p-4 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50 shadow-xl">
<div className="absolute -top-1 right-4 w-2 h-2 bg-slate-900 rotate-45"></div>
<div className="flex items-center gap-2 mb-2">
<Database className="w-4 h-4 text-blue-400" />
<h4 className="font-semibold text-blue-200">{translate('::App.DeveloperKit.EntityEditor.Tooltip.Title')}</h4>
</div>
<p className="text-slate-300 leading-relaxed">
{translate('::App.DeveloperKit.EntityEditor.Tooltip.Content')}
</p>
</div>
</div>
<button
onClick={handleSave}
disabled={isSaving || !name.trim() || !displayName.trim() || !tableName.trim()}
className="flex items-center gap-2 bg-emerald-600 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
>
<Save className="w-4 h-4" />
{isSaving ? translate('::App.DeveloperKit.EntityEditor.Saving') : translate('::App.DeveloperKit.EntityEditor.Save')}
</button>
</div>
</div>
</div>
{/* Entity Basic Info */}
<div className="bg-white rounded-lg border border-slate-200 p-6 mb-6 shadow-sm">
<h2 className="text-lg font-semibold text-slate-900 mb-4">{translate('::App.DeveloperKit.EntityEditor.BasicInfo')}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">{translate('::App.DeveloperKit.EntityEditor.EntityName')}</label>
<input
autoFocus
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={() => {
if (!tableName) {
setTableName(name + 's')
}
if (!displayName) {
setDisplayName(name)
}
}}
disabled={isMigrationApplied}
className={`w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent ${
isMigrationApplied ? 'bg-slate-100 text-slate-500 cursor-not-allowed' : ''
}`}
placeholder="e.g., Product, User, Order"
/>
{isMigrationApplied && (
<p className="text-xs text-slate-500 mt-1">
{translate('::App.DeveloperKit.EntityEditor.CannotChange')}
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.EntityEditor.DisplayName')} *
</label>
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
placeholder="e.g., Product, User, Order"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">{translate('::App.DeveloperKit.EntityEditor.TableName')} *</label>
<input
type="text"
value={tableName}
onChange={(e) => setTableName(e.target.value)}
disabled={isMigrationApplied}
className={`w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent ${
isMigrationApplied ? 'bg-slate-100 text-slate-500 cursor-not-allowed' : ''
}`}
placeholder="e.g., Products, Users, Orders"
/>
{isMigrationApplied && (
<p className="text-xs text-slate-500 mt-1">
Cannot be changed after migration is applied
</p>
)}
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-slate-700 mb-1">{translate('::App.DeveloperKit.EntityEditor.Description')}</label>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
placeholder="Brief description of this entity"
/>
</div>
</div>
{isEditing && (
<div className="mb-4 p-4 bg-slate-50 rounded-lg border border-slate-200">
<h3 className="text-sm font-medium text-slate-700 mb-3">{translate('::App.DeveloperKit.EntityEditor.Status')}</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">{translate('::App.DeveloperKit.EntityEditor.Status.Migration')}</span>
<span
className={`text-xs px-2 py-1 rounded-full ${
getEntity(id!)?.migrationStatus === 'applied'
? 'bg-green-100 text-green-700'
: getEntity(id!)?.migrationStatus === 'pending'
? 'bg-yellow-100 text-yellow-700'
: 'bg-red-100 text-red-700'
}`}
>
{getEntity(id!)?.migrationStatus || 'pending'}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">{translate('::App.DeveloperKit.EntityEditor.Status.Endpoint')}</span>
<span
className={`text-xs px-2 py-1 rounded-full ${
getEntity(id!)?.endpointStatus === 'applied'
? 'bg-blue-100 text-blue-700'
: getEntity(id!)?.endpointStatus === 'pending'
? 'bg-orange-100 text-orange-700'
: 'bg-red-100 text-red-700'
}`}
>
{getEntity(id!)?.endpointStatus || 'pending'}
</span>
</div>
</div>
</div>
)}
<div className="flex gap-6">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={isActive}
onChange={(e) => setIsActive(e.target.checked)}
className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500"
/>
<span className="text-sm font-medium text-slate-700">{translate('::App.DeveloperKit.EntityEditor.Active')}</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={hasAuditFields}
onChange={(e) => setHasAuditFields(e.target.checked)}
className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500"
/>
<span className="text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.EntityEditor.Audit')}
<span className="text-slate-500 ml-1">
(creationTime, lastModificationTime, etc.)
</span>
</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={hasSoftDelete}
onChange={(e) => setHasSoftDelete(e.target.checked)}
className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500"
/>
<span className="text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.EntityEditor.SoftDelete')}
<span className="text-slate-500 ml-1">(IsDeleted field)</span>
</span>
</label>
</div>
</div>
{/* Entity Fields */}
<div className="bg-white rounded-lg border border-slate-200 p-6 shadow-sm">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-slate-900">{translate('::App.DeveloperKit.EntityEditor.Fields')}</h2>
<button
onClick={addField}
className="flex items-center gap-2 bg-blue-600 text-white px-3 py-2 rounded-lg hover:bg-blue-700 transition-colors text-sm"
>
<Plus className="w-4 h-4" />
{translate('::App.DeveloperKit.EntityEditor.AddField')}
</button>
</div>
<div className="space-y-4">
{fields.map((field, index) => (
<div
key={field.id || `new-${index}`}
className="border border-slate-200 rounded-lg p-4 bg-slate-50"
>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-3">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.EntityEditor.FieldName')} *
</label>
<input
type="text"
value={field.name}
onChange={(e) => updateField(field.id, { name: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
placeholder="e.g., Name, Price, IsActive"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">{translate('::App.DeveloperKit.EntityEditor.Type')} *</label>
<select
value={field.type}
onChange={(e) =>
updateField(field.id, {
type: e.target.value as EntityFieldType,
})
}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
>
{fieldTypes.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</div>
{field.type === 'string' && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.EntityEditor.MaxLength')}
</label>
<input
type="number"
value={field.maxLength || ''}
onChange={(e) =>
updateField(field.id, {
maxLength: e.target.value ? parseInt(e.target.value) : undefined,
})
}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
placeholder="e.g., 100"
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.EntityEditor.DefaultValue')}
</label>
<input
type="text"
value={field.defaultValue || ''}
onChange={(e) => updateField(field.id, { defaultValue: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
placeholder="Optional default value"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-3">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.EntityEditor.FieldDescription')}
</label>
<input
type="text"
value={field.description || ''}
onChange={(e) => updateField(field.id, { description: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
placeholder="Field description"
/>
</div>
<div className="flex items-end gap-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={field.isRequired}
onChange={(e) =>
updateField(field.id, {
isRequired: e.target.checked,
})
}
className="rounded border-slate-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm font-medium text-slate-700">{translate('::App.DeveloperKit.EntityEditor.Required')}</span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={field.isUnique || false}
onChange={(e) => updateField(field.id, { isUnique: e.target.checked })}
className="rounded border-slate-300 text-blue-600 focus:ring-blue-500"
/>
<span className="text-sm font-medium text-slate-700">{translate('::App.DeveloperKit.EntityEditor.Unique')}</span>
</label>
<button
onClick={() => removeField(field.id)}
className="p-2 text-slate-600 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
title={translate('::App.DeveloperKit.EntityEditor.RemoveField')}
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
</div>
))}
</div>
{fields.length === 0 && (
<div className="text-center py-8 text-slate-500">
<Database className="w-12 h-12 mx-auto mb-2 text-slate-300" />
<p>{translate('::App.DeveloperKit.EntityEditor.NoFields')}</p>
<p className="text-sm">{translate('::App.DeveloperKit.EntityEditor.NoFieldsDescription')}</p>
</div>
)}
</div>
</div>
</div>
)
}
export default EntityEditor