using System; using System.Collections.Generic; using System.Data; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Sozsoft.Languages.Entities; using Sozsoft.Languages.Languages; using Sozsoft.Platform.Entities; using Sozsoft.Platform.Enums; using Sozsoft.Platform.ListForms; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; using Volo.Abp.MultiTenancy; using Volo.Abp.PermissionManagement; using static Sozsoft.Platform.PlatformConsts; namespace Sozsoft.Platform.Data.Seeds; /// /// Wizard ile oluşturulan konfigürasyonları Seeds/{host|tenants/{tenantId}}/wizard/*.json /// dosyalarından okuyarak veritabanına aktarır (kapsam, seed edilen veritabanını izler). /// Wizard çalıştıktan sonra oluşturulan JSON dosyaları projeye commit edildikten sonra, /// veritabanı silinip yeniden oluşturulduğunda bu seeder tüm wizard konfigürasyonlarını geri yükler. /// public class WizardDataSeeder : IDataSeedContributor, ITransientDependency { private readonly IRepository _repoLangKey; private readonly IRepository _repoLangText; private readonly IRepository _repoPermGroup; private readonly IRepository _repoPerm; private readonly IRepository _repoMenu; private readonly IRepository _repoDataSource; private readonly IRepository _repoListForm; private readonly IRepository _repoListFormField; private readonly IRepository _repoListFormWorkflow; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly string _cultureNameDefault = PlatformConsts.DefaultLanguage; private readonly string _appName = PlatformConsts.AppName; public WizardDataSeeder( IRepository repoLangKey, IRepository repoLangText, IRepository repoPermGroup, IRepository repoPerm, IRepository repoMenu, IRepository repoDataSource, IRepository repoListForm, IRepository repoListFormField, IRepository repoListFormWorkflow, IConfiguration configuration, ILogger logger) { _repoLangKey = repoLangKey; _repoLangText = repoLangText; _repoPermGroup = repoPermGroup; _repoPerm = repoPerm; _repoMenu = repoMenu; _repoDataSource = repoDataSource; _repoListForm = repoListForm; _repoListFormField = repoListFormField; _repoListFormWorkflow = repoListFormWorkflow; _configuration = configuration; _logger = logger; } public async Task SeedAsync(DataSeedContext context) { var scopeFolderName = SeedPathResolver.GetScopeFolderName(context.TenantId); var wizardDataPath = SeedPathResolver.GetScopePath(_configuration, context.TenantId, SeedPathResolver.WizardFolder); if (!Directory.Exists(wizardDataPath)) { _logger.LogInformation("Seeds/{Scope}/wizard directory not found, skipping.", scopeFolderName); return; } var jsonFiles = Directory.GetFiles(wizardDataPath, "*.json").OrderBy(f => Path.GetFileName(f)).ToArray(); if (jsonFiles.Length == 0) { _logger.LogInformation("No JSON files found in Seeds/{Scope}/wizard directory, skipping.", scopeFolderName); return; } _logger.LogInformation("WizardDataSeeder started. {Count} files to be processed.", jsonFiles.Length); var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; foreach (var filePath in jsonFiles) { try { var json = await File.ReadAllTextAsync(filePath); var seedFile = JsonSerializer.Deserialize(json, options); if (seedFile?.Wizard == null) { _logger.LogWarning("Invalid file skipped: {FilePath}", filePath); continue; } var wizardName = seedFile.Wizard.WizardName?.Trim(); if (string.IsNullOrWhiteSpace(wizardName)) { _logger.LogWarning("WizardName is empty, skipped: {FilePath}", filePath); continue; } var fileName = Path.GetFileName(filePath); await BackfillMenuParentShortNameAsync(seedFile.Wizard); // Zaten seeded mi kontrol et (ListForm var mı?) if (await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode)) { _logger.LogInformation("[{File}] '{WizardName}' already exists, skipped.", fileName, wizardName); continue; } _logger.LogInformation("[{File}] '{WizardName}' is being applied...", fileName, wizardName); await ApplyWizardSeedAsync(seedFile); _logger.LogInformation("[{File}] '{WizardName}' has been successfully applied.", fileName, wizardName); } catch (Exception ex) { _logger.LogError(ex, $"Error - {filePath}: {ex.Message}"); } } } private async Task ApplyWizardSeedAsync(WizardSeedFileDto seedFile) { var input = seedFile.Wizard; var isDeleted = seedFile.IsDeletedField; var isCreated = seedFile.IsCreatedField; input.SubForms ??= new List(); input.Widgets ??= new List(); input.WorkflowDto ??= new WorkflowDto(); input.WorkflowDto.Criteria ??= []; EnsureUniqueWorkflowCriteriaTitles(input.WorkflowDto.Criteria); var workflow = input.WorkflowDto; var wizardName = input.WizardName.Trim(); var code = string.IsNullOrWhiteSpace(input.MenuCode) ? WizardConsts.WizardKey(wizardName) : input.MenuCode.Trim(); var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode) ? code : input.ListFormCode.Trim(); NormalizeWorkflowCriteriaIds(listFormCode, workflow.Criteria); var titleLangKey = $"{listFormCode}.Title"; var nameLangKey = code; var descLangKey = $"{listFormCode}.Desc"; var permCreateName = $"{code}.Create"; var permUpdateName = $"{code}.Update"; var permDeleteName = $"{code}.Delete"; var permExportName = $"{code}.Export"; var permImportName = $"{code}.Import"; var permNoteName = $"{code}.Note"; // Dil - Language Keys await CreateLangKeyAsync(nameLangKey, input.LanguageTextMenuEn, input.LanguageTextMenuTr); await CreateLangKeyAsync(titleLangKey, input.LanguageTextTitleEn, input.LanguageTextTitleTr); await CreateLangKeyAsync(descLangKey, input.LanguageTextDescEn, input.LanguageTextDescTr); // Permission Group var groupName = input.PermissionGroupName ?? AppName; if (!await _repoPermGroup.AnyAsync(a => a.Name == groupName)) { await _repoPermGroup.InsertAsync( new PermissionGroupDefinitionRecord(Guid.NewGuid(), groupName, groupName), autoSave: true); if (string.Equals(groupName, input.MenuParentCode, StringComparison.OrdinalIgnoreCase)) await EnsureLangKeyAsync(groupName); else await CreateLangKeyAsync(groupName, groupName, groupName); } // Permissions - tek seferde mevcut permission'ları çek, sonra her birini kontrol et var permQueryable = await _repoPerm.GetQueryableAsync(); var existingPerms = permQueryable.Where(a => a.GroupName == groupName).ToList(); var permRead = existingPerms.FirstOrDefault(a => a.Name == code); if (permRead == null) permRead = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( Guid.NewGuid(), groupName, code, null, nameLangKey, true, MultiTenancySides.Both), autoSave: true); var permCreate = existingPerms.FirstOrDefault(a => a.Name == permCreateName); if (permCreate == null) permCreate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( Guid.NewGuid(), groupName, permCreateName, permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: true); var permUpdate = existingPerms.FirstOrDefault(a => a.Name == permUpdateName); if (permUpdate == null) permUpdate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( Guid.NewGuid(), groupName, permUpdateName, permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: true); var permDelete = existingPerms.FirstOrDefault(a => a.Name == permDeleteName); if (permDelete == null) permDelete = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( Guid.NewGuid(), groupName, permDeleteName, permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: true); var permExport = existingPerms.FirstOrDefault(a => a.Name == permExportName); if (permExport == null) permExport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( Guid.NewGuid(), groupName, permExportName, permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: true); var permImport = existingPerms.FirstOrDefault(a => a.Name == permImportName); if (permImport == null) permImport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( Guid.NewGuid(), groupName, permImportName, permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: true); var permNote = existingPerms.FirstOrDefault(a => a.Name == permNoteName); if (permNote == null) permNote = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( Guid.NewGuid(), groupName, permNoteName, permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: true); // // Permission Grants - Admin role için, sadece eksik olanları ekle // var existingGrants = await _permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName); // var existingGrantNames = new HashSet(existingGrants.Select(g => g.Name)); // var grantsToInsert = new[] // { // permRead.Name, permCreate.Name, permUpdate.Name, // permDelete.Name, permExport.Name, permImport.Name, permNote.Name // } // .Where(name => !existingGrantNames.Contains(name)) // .Select(name => new PermissionGrant(Guid.NewGuid(), name, "R", PlatformConsts.AbpIdentity.User.AdminRoleName)) // .ToList(); // if (grantsToInsert.Count > 0) // { // await _permissionGrantRepository.InsertManyAsync(grantsToInsert, autoSave: true); // } // Menu Parent var menuQueryable = await _repoMenu.GetQueryableAsync(); var menuParent = await _repoMenu.FirstOrDefaultAsync(a => a.Code == input.MenuParentCode); if (menuParent == null) { var maxRootOrder = menuQueryable.Where(a => a.ParentCode == null || a.ParentCode == "").Select(a => (int?)a.Order).Max() ?? 0; var menuParentIcon = !string.IsNullOrWhiteSpace(input.MenuParentIcon) ? input.MenuParentIcon : !string.IsNullOrWhiteSpace(input.MenuIcon) ? input.MenuIcon : WizardConsts.MenuIcon; await CreateLangKeyAsync(input.MenuParentCode, input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr); menuParent = await _repoMenu.InsertAsync(new Menu { Code = input.MenuParentCode, DisplayName = input.MenuParentCode, IsDisabled = false, ShortName = input.MenuParentShortName, Icon = menuParentIcon, Order = maxRootOrder + 1, }, autoSave: true); } // Menu var maxChildOrder = menuQueryable.Where(a => a.ParentCode == menuParent.Code).Select(a => (int?)a.Order).Max() ?? 0; var menuOrder = input.MenuOrder > 0 ? input.MenuOrder : maxChildOrder + 1; input.MenuOrder = menuOrder; var existingMenu = await _repoMenu.FirstOrDefaultAsync(a => a.Code == code); if (existingMenu == null) { await _repoMenu.InsertAsync(new Menu { Code = code, DisplayName = nameLangKey, IsDisabled = false, ParentCode = menuParent.Code, Icon = input.MenuIcon ?? WizardConsts.MenuIcon, Target = null, ElementId = null, CssClass = null, Url = WizardConsts.MenuUrl(code), RequiredPermissionName = permRead.Name, Order = menuOrder, }, autoSave: true); } else if (existingMenu.Order != menuOrder) { existingMenu.Order = menuOrder; await _repoMenu.UpdateAsync(existingMenu, autoSave: true); } // DataSource var existingDataSource = await _repoDataSource.FirstOrDefaultAsync(a => a.Code == input.DataSourceCode); if (existingDataSource == null) { await _repoDataSource.InsertAsync(new DataSource { Code = input.DataSourceCode, DataSourceType = input.DataSourceConnectionString != null && input.DataSourceConnectionString.IndexOf("Server", StringComparison.OrdinalIgnoreCase) >= 0 ? DataSourceTypeEnum.Mssql : DataSourceTypeEnum.Postgresql, ConnectionString = input.DataSourceConnectionString }, autoSave: true); } // EditingFormJson var editingFormDtos = input.Groups .Select((g, gi) => new EditingFormDto { Order = gi + 1, Caption = g.Caption, ColCount = g.ColCount, ColSpan = g.ColCount, ItemType = "group" }) .Where((_, index) => input.Groups[index].Items.Any(i => i.IncludeInEditingForm && i.FieldName != input.KeyFieldName)) .ToList(); // ListForm - varsa sil, yeniden ekle var existingListForm = await _repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == input.ListFormCode); if (existingListForm != null) { await _repoListForm.DeleteAsync(existingListForm, autoSave: true); } var existingListFormFields = await _repoListFormField.GetListAsync(a => a.ListFormCode == input.ListFormCode); if (existingListFormFields.Count > 0) { await _repoListFormField.DeleteManyAsync(existingListFormFields, autoSave: true); } var existingWorkflowCriteria = await _repoListFormWorkflow.GetListAsync(a => a.ListFormCode == input.ListFormCode); if (existingWorkflowCriteria.Count > 0) { await _repoListFormWorkflow.DeleteManyAsync(existingWorkflowCriteria, autoSave: true); } // EditingForm'da required olan alanları tespit et, ValidationRuleJson eklemek için var editingFormFieldNames = input.Groups .SelectMany(g => g.Items) .Where(i => i.IncludeInEditingForm && i.FieldName != input.KeyFieldName) .Select(i => new { FieldName = i.FieldName, Isrequired = i.IsRequired }) .ToList(); // Heigth ve Width hesaplama var groupLayouts = input.Groups .Select(g => { var items = g.Items .Where(i => i.IncludeInEditingForm && i.FieldName != input.KeyFieldName) .ToList(); var columnCount = g.ColCount; if (columnCount <= 0) columnCount = 1; // 3 kolonu geçmesin columnCount = Math.Min(columnCount, WizardConsts.EditFormMaxColumnCount); // Örneğin 4 item / 3 kolon = 2 satır var rowCount = (int)Math.Ceiling(items.Count / (double)columnCount); return new { Group = g, Items = items, ColumnCount = columnCount, RowCount = rowCount }; }) .ToList(); // Form genelinde kullanılacak kolon sayısı var formColumnCount = groupLayouts.Any() ? groupLayouts.Max(x => x.ColumnCount) : 1; // Width hesabı var formWidth = (WizardConsts.EditFormMaxWidth / WizardConsts.EditFormMaxColumnCount) * formColumnCount; // Toplam satır sayısı var totalRowCount = groupLayouts.Sum(x => x.RowCount); // Height hesabı var formHeight = WizardConsts.EditFormHeightDefault + (totalRowCount * WizardConsts.EditFormHeightByRow); // ListForm await _repoListForm.InsertAsync(new ListForm { ListFormType = ListFormTypeEnum.List, PageSize = 10, ExportJson = WizardConsts.DefaultExportJson, IsSubForm = false, ShowNote = input.SubForms.Count > 0 || workflow.Criteria.Count > 0, LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Card, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler, input.Todo), CultureName = LanguageCodes.En, ListFormCode = input.ListFormCode, Name = nameLangKey, Title = titleLangKey, Description = descLangKey, DataSourceCode = input.DataSourceCode, IsTenant = input.IsTenant, IsBranch = input.IsBranch, IsOrganizationUnit = input.IsOrganizationUnit, SelectCommandType = input.SelectCommandType, SelectCommand = input.SelectCommand, KeyFieldName = input.KeyFieldName, KeyFieldDbSourceType = input.KeyFieldDbSourceType, DefaultFilter = isDeleted ? WizardConsts.DefaultFilterJson : null, SortMode = GridOptions.SortModeSingle, FilterRowJson = WizardConsts.DefaultFilterRowJson(), HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson(), SearchPanelJson = WizardConsts.DefaultSearchPanelJson(), GroupPanelJson = WizardConsts.DefaultGroupPanelJson(), SelectionJson = WizardConsts.DefaultSelectionSingleJson(workflow.Criteria.Count > 0 ? GridOptions.SelectionModeSingle : GridOptions.SelectionModeNone), ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(), PermissionJson = WizardConsts.DefaultPermissionJson(code), DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null, DeleteFieldsDefaultValueJson = isDeleted ? WizardConsts.DefaultDeleteFieldsDefaultValueJson(input.KeyFieldDbSourceType) : WizardConsts.DefaultDeleteFieldsJsonOnlyId(input.KeyFieldDbSourceType), InsertFieldsDefaultValueJson = isCreated ? WizardConsts.DefaultInsertFieldsDefaultValueJson(input.KeyFieldDbSourceType) : WizardConsts.DefaultInsertFieldsJsonOnlyId(input.KeyFieldDbSourceType), PagerOptionJson = WizardConsts.DefaultPagerOptionJson, EditingOptionJson = WizardConsts.DefaultEditingOptionJson(titleLangKey, formWidth, formHeight, input.AllowDeleting, input.AllowAdding, input.AllowUpdating, input.ConfirmDelete, false, input.AllowDetail), EditingFormJson = editingFormDtos.Count > 0 ? JsonSerializer.Serialize(editingFormDtos) : null, SubFormsJson = input.SubForms.Count > 0 ? JsonSerializer.Serialize(input.SubForms) : null, WidgetsJson = input.Widgets.Count > 0 ? JsonSerializer.Serialize(input.Widgets) : null, WorkflowJson = HasWorkflow(workflow) ? JsonSerializer.Serialize(workflow) : null, TreeOptionJson = (input.Tree || input.DefaultLayout == "tree") && !string.IsNullOrEmpty(input.TreeOptionDto?.ParentIdExpr) ? JsonSerializer.Serialize(input.TreeOptionDto) : null, GanttOptionJson = (input.Gantt || input.DefaultLayout == "gantt") && !string.IsNullOrEmpty(input.GanttOptionDto?.ParentIdExpr) ? JsonSerializer.Serialize(input.GanttOptionDto) : null, SchedulerOptionJson = (input.Scheduler || input.DefaultLayout == "scheduler") && !string.IsNullOrEmpty(input.SchedulerOptionDto?.TextExpr) ? JsonSerializer.Serialize(input.SchedulerOptionDto) : null, TodoOptionJson = (input.Todo || input.DefaultLayout == "todo") && !string.IsNullOrEmpty(input.TodoOptionDto?.TitleExpr) && !string.IsNullOrEmpty(input.TodoOptionDto?.StatusExpr) ? JsonSerializer.Serialize(input.TodoOptionDto) : null, }, autoSave: true); // ListFormFields var fieldOrder = 0; var editGroupOrder = 0; foreach (var group in input.Groups) { editGroupOrder++; var editOrder = 0; foreach (var item in group.Items) { fieldOrder++; if (item.IncludeInEditingForm && item.FieldName != input.KeyFieldName) { editOrder++; } await _repoListFormField.InsertAsync(new ListFormField { ListFormCode = input.ListFormCode, FieldName = item.FieldName, CaptionName = item.CaptionName, Visible = item.FieldName != input.KeyFieldName, IsActive = true, Width = 0, //Fit columns için hepsine sıfır veriyorum. AllowSearch = true, ListOrderNo = fieldOrder, EditGroupOrderNo = item.IncludeInEditingForm ? editGroupOrder : null, EditOrderNo = item.IncludeInEditingForm && item.FieldName != input.KeyFieldName ? editOrder : null, EditorType2 = item.EditorType, ColSpan = item.ColSpan, EditorOptions = string.IsNullOrWhiteSpace(item.EditorOptions) ? null : item.EditorOptions, EditorScript = string.IsNullOrWhiteSpace(item.EditorScript) ? null : item.EditorScript, SourceDbType = item.DbSourceType, CultureName = PlatformConsts.DefaultLanguage, PermissionJson = WizardConsts.DefaultFieldPermissionJson(code), ColumnCustomizationJson = WizardConsts.DefaultColumnCustomizationJson, ColumnFilterJson = WizardConsts.DefaultColumnFilteringJson, PivotSettingsJson = WizardConsts.DefaultPivotSettingsJson, LookupJson = !string.IsNullOrWhiteSpace(item.LookupQuery) ? WizardConsts.DefaultLookupJson(item.LookupDataSourceType, item.DisplayExpr, item.ValueExpr, item.LookupQuery) : null, ValidationRuleJson = editingFormFieldNames.Any(f => f.FieldName == item.FieldName && f.Isrequired) ? WizardConsts.DefaultValidationRuleRequiredJson : null }, autoSave: true); await CreateLangKeyAsync(item.CaptionName, item.EnglishCaption, item.TurkishCaption); } } foreach (var criteria in workflow.Criteria) { if (string.IsNullOrWhiteSpace(criteria.Id)) { _logger.LogWarning("Workflow criteria skipped because Id is empty. ListFormCode: {ListFormCode}, Title: {Title}", input.ListFormCode, criteria.Title); continue; } await _repoListFormWorkflow.InsertAsync(new ListFormWorkflow(criteria.Id) { ListFormCode = string.IsNullOrWhiteSpace(criteria.ListFormCode) ? input.ListFormCode : criteria.ListFormCode, Kind = criteria.Kind, Title = criteria.Title, CompareColumn = criteria.CompareColumn, CompareOperator = criteria.CompareOperator, CompareValue = criteria.CompareValue, Approver = criteria.Approver, NextOnStart = criteria.NextOnStart, NextOnTrue = criteria.NextOnTrue, NextOnFalse = criteria.NextOnFalse, NextOnApprove = criteria.NextOnApprove, NextOnReject = criteria.NextOnReject, PositionX = criteria.PositionX, PositionY = criteria.PositionY, CompareOutcomesJson = JsonSerializer.Serialize(criteria.CompareOutcomes ?? []), }, autoSave: true); } } private async Task BackfillMenuParentShortNameAsync(ListFormWizardDto input) { if (string.IsNullOrWhiteSpace(input.MenuParentCode) || string.IsNullOrWhiteSpace(input.MenuParentShortName)) return; var menuParent = await _repoMenu.FirstOrDefaultAsync(a => a.Code == input.MenuParentCode); if (menuParent == null || !string.IsNullOrWhiteSpace(menuParent.ShortName)) return; menuParent.ShortName = input.MenuParentShortName.Trim(); await _repoMenu.UpdateAsync(menuParent, autoSave: true); _logger.LogInformation( "Menu parent '{MenuParentCode}' ShortName backfilled as '{ShortName}'.", input.MenuParentCode, menuParent.ShortName); } private static bool HasWorkflow(WorkflowDto workflow) { return workflow != null && ( !string.IsNullOrWhiteSpace(workflow.ApprovalUserFieldName) || !string.IsNullOrWhiteSpace(workflow.ApprovalDateFieldName) || !string.IsNullOrWhiteSpace(workflow.ApprovalStatusFieldName) || !string.IsNullOrWhiteSpace(workflow.ApprovalDescriptionFieldName) || workflow.Criteria.Count > 0 ); } private static void EnsureUniqueWorkflowCriteriaTitles(List criteria) { if (criteria == null || criteria.Count == 0) { return; } var baseTitles = criteria .Select(x => string.IsNullOrWhiteSpace(x.Title) ? NormalizeWorkflowTitleFallback(x.Kind) : x.Title.Trim()) .ToList(); var duplicateTitles = baseTitles .GroupBy(x => x, StringComparer.OrdinalIgnoreCase) .Where(x => x.Count() > 1) .Select(x => x.Key) .ToHashSet(StringComparer.OrdinalIgnoreCase); var titleCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); var usedTitles = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var item in criteria) { var baseTitle = string.IsNullOrWhiteSpace(item.Title) ? NormalizeWorkflowTitleFallback(item.Kind) : item.Title.Trim(); var title = baseTitle; if (duplicateTitles.Contains(baseTitle)) { titleCounts.TryGetValue(baseTitle, out var count); count++; titleCounts[baseTitle] = count; title = $"{baseTitle}{count}"; } if (usedTitles.Contains(title)) { var index = 1; var candidate = $"{title}{index}"; while (usedTitles.Contains(candidate)) { index++; candidate = $"{title}{index}"; } title = candidate; } item.Title = title; usedTitles.Add(title); } } private static string NormalizeWorkflowTitleFallback(string kind) { return string.IsNullOrWhiteSpace(kind) ? "Step" : kind.Trim(); } private static void NormalizeWorkflowCriteriaIds(string listFormCode, List criteria) { if (string.IsNullOrWhiteSpace(listFormCode) || criteria == null || criteria.Count == 0) { return; } foreach (var item in criteria) { item.Id = NormalizeWorkflowCriteriaId(listFormCode, item.Id); item.ListFormCode = listFormCode; item.NextOnStart = NormalizeWorkflowTargetId(listFormCode, item.NextOnStart); item.NextOnTrue = NormalizeWorkflowTargetId(listFormCode, item.NextOnTrue); item.NextOnFalse = NormalizeWorkflowTargetId(listFormCode, item.NextOnFalse); item.NextOnApprove = NormalizeWorkflowTargetId(listFormCode, item.NextOnApprove); item.NextOnReject = NormalizeWorkflowTargetId(listFormCode, item.NextOnReject); foreach (var outcome in item.CompareOutcomes ?? []) { outcome.TargetId = NormalizeWorkflowTargetId(listFormCode, outcome.TargetId); } } } private static string NormalizeWorkflowTargetId(string listFormCode, string id) { return string.IsNullOrWhiteSpace(id) ? string.Empty : NormalizeWorkflowCriteriaId(listFormCode, id); } private static string NormalizeWorkflowCriteriaId(string listFormCode, string id) { var trimmed = id?.Trim(); if (string.IsNullOrWhiteSpace(trimmed)) { return string.Empty; } if (trimmed.StartsWith($"{listFormCode}-", StringComparison.OrdinalIgnoreCase)) { return trimmed; } return trimmed.StartsWith("N", StringComparison.OrdinalIgnoreCase) ? $"{listFormCode}-{trimmed}" : trimmed; } private async Task CreateLangKeyAsync(string key, string textEn, string textTr) { if (string.IsNullOrWhiteSpace(key)) return; var langKey = await _repoLangKey.FirstOrDefaultAsync(a => a.ResourceName == _appName && a.Key == key) ?? await _repoLangKey.InsertAsync(new LanguageKey { ResourceName = _appName, Key = key }, autoSave: true); var existingTexts = await _repoLangText.GetListAsync(a => a.ResourceName == _appName && a.Key == langKey.Key); var existingEn = existingTexts.FirstOrDefault(a => a.CultureName == _cultureNameDefault); if (existingEn == null) { await _repoLangText.InsertAsync(new LanguageText { ResourceName = _appName, Key = langKey.Key, CultureName = _cultureNameDefault, Value = textEn ?? key }, autoSave: true); } var existingTr = existingTexts.FirstOrDefault(a => a.CultureName == LanguageCodes.Tr); if (existingTr == null) { await _repoLangText.InsertAsync(new LanguageText { ResourceName = _appName, Key = langKey.Key, CultureName = LanguageCodes.Tr, Value = textTr ?? key }, autoSave: true); } } private async Task EnsureLangKeyAsync(string key) { if (string.IsNullOrWhiteSpace(key)) return; if (!await _repoLangKey.AnyAsync(a => a.ResourceName == _appName && a.Key == key)) { await _repoLangKey.InsertAsync(new LanguageKey { ResourceName = _appName, Key = key }, autoSave: true); } } }