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 = "App.Platform.View"; [GeneratedRegex(@"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}")] private static partial Regex PlaceholderRegex(); public async Task GetReportTemplatesAsync(string? search) { await EnsureToolAllowedAsync(McpToolNames.ListReportTemplates); await EnsureCanReadAsync(); 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) { await EnsureToolAllowedAsync(McpToolNames.GetReportTemplate); await EnsureCanReadAsync(); var entity = await FindTemplateAsync(template) ?? throw new UserFriendlyException(L[McpErrorCodes.ReportTemplateNotFound, template]); return await ToDtoAsync(entity, null, null); } public async Task SaveReportCategoryAsync(McpReportCategorySaveRequestDto input) { Check.NotNull(input, nameof(input)); await EnsureToolAllowedAsync(McpToolNames.SaveReportCategory); if (string.IsNullOrWhiteSpace(input.Name)) { throw new UserFriendlyException(L[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 id = existing is null ? await InsertAsync(CategoriesListFormCode, values) : await UpdateAsync(CategoriesListFormCode, existing.Id, values); await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveReportCategory, name); var saved = await categoryRepository.FirstOrDefaultAsync(x => x.Id == id); return saved is null ? new McpReportCategoryDto { Id = id, Name = name } : ToDto(saved); } public async Task SaveReportTemplateAsync(McpReportTemplateSaveRequestDto input) { Check.NotNull(input, nameof(input)); await EnsureToolAllowedAsync(McpToolNames.SaveReportTemplate); if (string.IsNullOrWhiteSpace(input.Name)) { throw new UserFriendlyException(L[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)) { throw new UserFriendlyException(L[McpErrorCodes.ReportHtmlRequired]); } var categoryId = string.IsNullOrWhiteSpace(input.Category) ? existing?.CategoryId : (await FindCategoryAsync(input.Category))?.Id ?? throw new UserFriendlyException(L[McpErrorCodes.ReportCategoryNotFound, input.Category]); if (categoryId is null) { throw new UserFriendlyException(L[McpErrorCodes.ReportCategoryRequired]); } var listForm = await FindListFormAsync(input.ListFormCode); if (listForm is not null) { await EnsurePlaceholdersExistAsync(listForm.ListFormCode, htmlContent); } var values = new Dictionary { ["Name"] = name, ["Description"] = input.Description ?? existing?.Description, ["HtmlContent"] = htmlContent, ["CategoryId"] = categoryId, ["Status"] = input.Status ?? existing?.Status ?? ActiveStatus }; var id = existing is null ? await InsertAsync(TemplatesListFormCode, values) : await UpdateAsync(TemplatesListFormCode, existing.Id, values); await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveReportTemplate, name); var saved = await templateRepository.FirstOrDefaultAsync(x => x.Id == id) ?? throw new UserFriendlyException(L[McpErrorCodes.ReportTemplateNotFound, name]); return await ToDtoAsync(saved, listForm, input.ButtonText); } public async Task DeleteReportTemplateAsync(string template) { await EnsureToolAllowedAsync(McpToolNames.DeleteReportTemplate); var entity = await FindTemplateAsync(template) ?? throw new UserFriendlyException(L[McpErrorCodes.ReportTemplateNotFound, template]); await listFormDataAppService.PostDeleteAsync(await BuildRequestAsync( TemplatesListFormCode, AuthorizationTypeEnum.Delete, null, entity.Id)); await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteReportTemplate, entity.Name); } /// /// Okuma depodan yapildigi icin ekranin okuma yetkisi burada istenir; MCP kullanicinin /// uygulamada goremedigi sablonu gostermez. /// private async Task EnsureCanReadAsync() { var permission = await accessFilter.GetPermissionNameAsync(TemplatesListFormCode, AuthorizationTypeEnum.Read); await AuthorizationService.CheckAsync(permission ?? TemplatesListFormCode); } private async Task InsertAsync(string listFormCode, Dictionary values) { var key = await listFormDataAppService.PostInsertAsync(await BuildRequestAsync( listFormCode, AuthorizationTypeEnum.Create, values, null)); // 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 listFormCode, Guid id, Dictionary values) { await listFormDataAppService.PostUpdateAsync(await BuildRequestAsync( listFormCode, AuthorizationTypeEnum.Update, values, id)); 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 listFormCode, AuthorizationTypeEnum operation, Dictionary? values, Guid? key) { var permission = await accessFilter.GetPermissionNameAsync(listFormCode, operation); if (permission is not null) { await AuthorizationService.CheckAsync(permission); } 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); } private async Task FindListFormAsync(string? listFormCode) { if (string.IsNullOrWhiteSpace(listFormCode)) { return null; } var code = listFormCode.Trim(); return await listFormRepository.FirstOrDefaultAsync(x => x.ListFormCode == code) ?? throw new UserFriendlyException(L[McpErrorCodes.ReportListFormNotFound, code]); } /// /// Bilinmeyen yer tutucu raporda oldugu gibi basilir; sertifikada "{{AdSoyad}}" gorunmesin /// diye kayitta reddedilir. /// private async Task EnsurePlaceholdersExistAsync(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(); if (unknown.Count > 0) { throw new UserFriendlyException(L[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 }; }