using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Services; using Sozsoft.Platform; using Sozsoft.Platform.Entities; using Sozsoft.Platform.Enums; using Sozsoft.Platform.ListForms.Select; using Volo.Abp; using Volo.Abp.Domain.Repositories; namespace Sozsoft.Mcp; /// /// Rapor kategorisi ve DynamicReport sablonu uretimi. Okuma depodan, yazma ekranlarin /// C/U/D sozlesmesinden () gecer; boylece ekran yetkisi, /// kiraci filtresi ve seed senkronizasyonu ekrandaki kayitla ayni kalir. /// [Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)] public partial class McpReportAppService( IMcpServerConfigProvider serverConfigProvider, IListFormDataAppService listFormDataAppService, IMcpListFormAccessFilter accessFilter, IRepository categoryRepository, IRepository templateRepository, IRepository listFormRepository, IRepository listFormFieldRepository, IMcpDevRequestTimesheetRecorder timesheetRecorder) : McpAppService(serverConfigProvider), IMcpReportAppService { private const string CategoriesListFormCode = PlatformConsts.AppCodes.Reports.Categories; private const string TemplatesListFormCode = PlatformConsts.AppCodes.Reports.ReportTemplates; private const string ActiveStatus = "active"; private const string DefaultButtonText = "Sas.Platform.View"; [GeneratedRegex(@"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}")] private static partial Regex PlaceholderRegex(); public async Task> GetReportTemplatesAsync(string? search) { if (await EnsureCanReadAsync(McpToolNames.ListReportTemplates) is { } problem) { return problem; } var categories = await categoryRepository.GetListAsync(); var categoryNames = categories.ToDictionary(x => x.Id, x => x.Name); var templates = (await templateRepository.GetListAsync()) .Where(x => string.IsNullOrWhiteSpace(search) || x.Name.Contains(search, StringComparison.OrdinalIgnoreCase) || (x.Description?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false)) .OrderBy(x => x.Name) .Select(x => new McpReportTemplateSummaryDto { Id = x.Id, Name = x.Name, Description = x.Description, Category = categoryNames.GetValueOrDefault(x.CategoryId, string.Empty), Status = x.Status }) .ToList(); return new McpReportCatalogDto { Categories = [.. categories.OrderBy(x => x.Name).Select(ToDto)], Templates = templates }; } public async Task> GetReportTemplateAsync(string template) { if (await EnsureCanReadAsync(McpToolNames.GetReportTemplate) is { } problem) { return problem; } var entity = await FindTemplateAsync(template); if (entity is null) { return Problem(McpErrorCodes.ReportTemplateNotFound, template); } return await ToDtoAsync(entity, null, null); } public async Task> SaveReportCategoryAsync(McpReportCategorySaveRequestDto input) { Check.NotNull(input, nameof(input)); const string toolName = McpToolNames.SaveReportCategory; if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem }) { return problem; } if (string.IsNullOrWhiteSpace(input.Name)) { return Problem(McpErrorCodes.ReportNameRequired); } var name = input.Name.Trim(); var existing = await categoryRepository.FirstOrDefaultAsync(x => x.Name == name); var values = new Dictionary { ["Name"] = name, ["Description"] = input.Description ?? existing?.Description, ["Icon"] = input.Icon ?? existing?.Icon }; var saved = existing is null ? await InsertAsync(toolName, CategoriesListFormCode, values) : await UpdateAsync(toolName, CategoriesListFormCode, existing.Id, values); if (!saved.TryGetValue(out var id, out var rejected)) { return rejected; } await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveReportCategory, name); var category = await categoryRepository.FirstOrDefaultAsync(x => x.Id == id); return category is null ? new McpReportCategoryDto { Id = id, Name = name } : ToDto(category); } public async Task> SaveReportTemplateAsync(McpReportTemplateSaveRequestDto input) { Check.NotNull(input, nameof(input)); const string toolName = McpToolNames.SaveReportTemplate; if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem }) { return problem; } if (string.IsNullOrWhiteSpace(input.Name)) { return Problem(McpErrorCodes.ReportNameRequired); } var name = input.Name.Trim(); var existing = await templateRepository.FirstOrDefaultAsync(x => x.Name == name); var htmlContent = string.IsNullOrWhiteSpace(input.HtmlContent) ? existing?.HtmlContent : input.HtmlContent; if (string.IsNullOrWhiteSpace(htmlContent)) { return Problem(McpErrorCodes.ReportHtmlRequired); } var categoryId = existing?.CategoryId; if (!string.IsNullOrWhiteSpace(input.Category)) { categoryId = (await FindCategoryAsync(input.Category))?.Id; if (categoryId is null) { return Problem(McpErrorCodes.ReportCategoryNotFound, input.Category); } } if (categoryId is null) { return Problem(McpErrorCodes.ReportCategoryRequired); } if (!(await FindListFormAsync(input.ListFormCode)).TryGetValue(out var listForm, out var notFound)) { return notFound; } if (listForm is not null && await CheckPlaceholdersExistAsync(listForm.ListFormCode, htmlContent) is { } unknown) { return unknown; } var values = new Dictionary { ["Name"] = name, ["Description"] = input.Description ?? existing?.Description, ["HtmlContent"] = htmlContent, ["CategoryId"] = categoryId, ["Status"] = input.Status ?? existing?.Status ?? ActiveStatus }; var saved = existing is null ? await InsertAsync(toolName, TemplatesListFormCode, values) : await UpdateAsync(toolName, TemplatesListFormCode, existing.Id, values); if (!saved.TryGetValue(out var id, out var rejected)) { return rejected; } await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveReportTemplate, name); var template = await templateRepository.FirstOrDefaultAsync(x => x.Id == id); if (template is null) { return Problem(McpErrorCodes.ReportTemplateNotFound, name); } return await ToDtoAsync(template, listForm, input.ButtonText); } public async Task DeleteReportTemplateAsync(string template) { const string toolName = McpToolNames.DeleteReportTemplate; if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem }) { return problem; } var entity = await FindTemplateAsync(template); if (entity is null) { return Problem(McpErrorCodes.ReportTemplateNotFound, template); } if (!(await BuildRequestAsync(toolName, TemplatesListFormCode, AuthorizationTypeEnum.Delete, null, entity.Id)).TryGetValue(out var request, out var denied)) { return denied; } await listFormDataAppService.PostDeleteAsync(request); await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteReportTemplate, entity.Name); return null; } /// /// Okuma depodan yapildigi icin ekranin okuma yetkisi burada istenir; MCP kullanicinin /// uygulamada goremedigi sablonu gostermez. /// private async Task EnsureCanReadAsync(string toolName) { if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem }) { return problem; } var permission = await accessFilter.GetPermissionNameAsync(TemplatesListFormCode, AuthorizationTypeEnum.Read); return await CheckPermissionAsync(toolName, permission ?? TemplatesListFormCode); } private async Task> InsertAsync(string toolName, string listFormCode, Dictionary values) { if (!(await BuildRequestAsync(toolName, listFormCode, AuthorizationTypeEnum.Create, values, null)).TryGetValue(out var request, out var problem)) { return problem; } var key = await listFormDataAppService.PostInsertAsync(request); // Insert ucu her veri kaynaginda anahtari dondurmez; bu durumda tekil ad uzerinden bulunur. if (Guid.TryParse(((object?)key)?.ToString(), out var id)) { return id; } var name = (string)values["Name"]!; return listFormCode == CategoriesListFormCode ? (await categoryRepository.FirstAsync(x => x.Name == name)).Id : (await templateRepository.FirstAsync(x => x.Name == name)).Id; } private async Task> UpdateAsync(string toolName, string listFormCode, Guid id, Dictionary values) { if (!(await BuildRequestAsync(toolName, listFormCode, AuthorizationTypeEnum.Update, values, id)).TryGetValue(out var request, out var problem)) { return problem; } await listFormDataAppService.PostUpdateAsync(request); return id; } /// /// Ekranin kendi kontrolu reddi adsiz bir hatayla bildirir; yetki burada adiyla istenir ki /// istemci hangi yetkinin eksik oldugunu gorsun. /// private async Task> BuildRequestAsync( string toolName, string listFormCode, AuthorizationTypeEnum operation, Dictionary? values, Guid? key) { var permission = await accessFilter.GetPermissionNameAsync(listFormCode, operation); if (permission is not null && await CheckPermissionAsync(toolName, permission) is { } denied) { return denied; } return new DataRequestDto { ListFormCode = listFormCode, Data = JsonSerializer.Serialize(values ?? []), Keys = key is null ? [] : [key.Value] }; } private async Task FindTemplateAsync(string template) { if (string.IsNullOrWhiteSpace(template)) { return null; } var value = template.Trim(); return Guid.TryParse(value, out var id) ? await templateRepository.FirstOrDefaultAsync(x => x.Id == id) : await templateRepository.FirstOrDefaultAsync(x => x.Name == value); } private async Task FindCategoryAsync(string category) { var value = category.Trim(); return Guid.TryParse(value, out var id) ? await categoryRepository.FirstOrDefaultAsync(x => x.Id == id) : await categoryRepository.FirstOrDefaultAsync(x => x.Name == value); } /// Kod verilmediyse null; verilen kodun ekrani yoksa sorun. private async Task> FindListFormAsync(string? listFormCode) { if (string.IsNullOrWhiteSpace(listFormCode)) { return default(ListForm); } var code = listFormCode.Trim(); var listForm = await listFormRepository.FirstOrDefaultAsync(x => x.ListFormCode == code); return listForm is null ? Problem(McpErrorCodes.ReportListFormNotFound, code) : listForm; } /// /// Bilinmeyen yer tutucu raporda oldugu gibi basilir; sertifikada "{{AdSoyad}}" gorunmesin /// diye kayitta reddedilir. /// private async Task CheckPlaceholdersExistAsync(string listFormCode, string htmlContent) { var fieldNames = (await listFormFieldRepository.GetListAsync(x => x.ListFormCode == listFormCode)) .Select(x => x.FieldName) .ToHashSet(StringComparer.OrdinalIgnoreCase); var unknown = GetPlaceholders(htmlContent).Where(x => !fieldNames.Contains(x)).ToList(); return unknown.Count == 0 ? null : Problem(McpErrorCodes.ReportPlaceholdersUnknown, listFormCode, string.Join(", ", unknown)); } private static List GetPlaceholders(string htmlContent) => [.. PlaceholderRegex().Matches(htmlContent ?? string.Empty) .Select(x => x.Groups[1].Value) .Distinct(StringComparer.OrdinalIgnoreCase)]; private async Task ToDtoAsync(ReportTemplate entity, ListForm? listForm, string? buttonText) { var category = await categoryRepository.FirstOrDefaultAsync(x => x.Id == entity.CategoryId); var viewUrl = $"/admin/reports/DynamicReport/view/{entity.Id}"; return new McpReportTemplateDto { Id = entity.Id, Name = entity.Name, Description = entity.Description, Category = category?.Name ?? string.Empty, CategoryId = entity.CategoryId, Status = entity.Status, HtmlContent = entity.HtmlContent, Placeholders = GetPlaceholders(entity.HtmlContent), ViewUrl = viewUrl, RowButton = listForm is null ? null : new McpReportRowButtonDto { Text = string.IsNullOrWhiteSpace(buttonText) ? DefaultButtonText : buttonText, Hint = string.IsNullOrWhiteSpace(buttonText) ? DefaultButtonText : buttonText, Url = $"{viewUrl}/{listForm.ListFormCode}?recordId=@{listForm.KeyFieldName}", AuthName = listForm.ListFormCode } }; } private static McpReportCategoryDto ToDto(ReportCategory category) => new() { Id = category.Id, Name = category.Name, Description = category.Description, Icon = category.Icon }; }