sozsoft-platform/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpReportAppService.cs

324 lines
13 KiB
C#

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;
/// <summary>
/// Rapor kategorisi ve <c>DynamicReport</c> sablonu uretimi. Okuma depodan, yazma ekranlarin
/// C/U/D sozlesmesinden (<see cref="IListFormDataAppService"/>) gecer; boylece ekran yetkisi,
/// kiraci filtresi ve seed senkronizasyonu ekrandaki kayitla ayni kalir.
/// </summary>
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)]
public partial class McpReportAppService(
IMcpServerConfigProvider serverConfigProvider,
IListFormDataAppService listFormDataAppService,
IMcpListFormAccessFilter accessFilter,
IRepository<ReportCategory, Guid> categoryRepository,
IRepository<ReportTemplate, Guid> templateRepository,
IRepository<ListForm, Guid> listFormRepository,
IRepository<ListFormField, Guid> 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<McpReportCatalogDto> 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<McpReportTemplateDto> 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<McpReportCategoryDto> 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<string, object?>
{
["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<McpReportTemplateDto> 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<string, object?>
{
["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);
}
/// <summary>
/// Okuma depodan yapildigi icin ekranin okuma yetkisi burada istenir; MCP kullanicinin
/// uygulamada goremedigi sablonu gostermez.
/// </summary>
private async Task EnsureCanReadAsync()
{
var permission = await accessFilter.GetPermissionNameAsync(TemplatesListFormCode, AuthorizationTypeEnum.Read);
await AuthorizationService.CheckAsync(permission ?? TemplatesListFormCode);
}
private async Task<Guid> InsertAsync(string listFormCode, Dictionary<string, object?> 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<Guid> UpdateAsync(string listFormCode, Guid id, Dictionary<string, object?> values)
{
await listFormDataAppService.PostUpdateAsync(await BuildRequestAsync(
listFormCode, AuthorizationTypeEnum.Update, values, id));
return id;
}
/// <summary>
/// Ekranin kendi kontrolu reddi adsiz bir hatayla bildirir; yetki burada adiyla istenir ki
/// istemci hangi yetkinin eksik oldugunu gorsun.
/// </summary>
private async Task<DataRequestDto> BuildRequestAsync(
string listFormCode,
AuthorizationTypeEnum operation,
Dictionary<string, object?>? 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<ReportTemplate?> 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<ReportCategory?> 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<ListForm?> 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]);
}
/// <summary>
/// Bilinmeyen yer tutucu raporda oldugu gibi basilir; sertifikada "{{AdSoyad}}" gorunmesin
/// diye kayitta reddedilir.
/// </summary>
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<string> GetPlaceholders(string htmlContent)
=> [.. PlaceholderRegex().Matches(htmlContent ?? string.Empty)
.Select(x => x.Groups[1].Value)
.Distinct(StringComparer.OrdinalIgnoreCase)];
private async Task<McpReportTemplateDto> 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
};
}