325 lines
13 KiB
C#
325 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
|
||
|
|
};
|
||
|
|
}
|