2026-10-10 22:03:36 +00:00
|
|
|
|
using System;
|
2026-10-01 12:01:57 +00:00
|
|
|
|
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";
|
2026-10-19 14:35:08 +00:00
|
|
|
|
private const string DefaultButtonText = "Sas.Platform.View";
|
2026-10-01 12:01:57 +00:00
|
|
|
|
|
|
|
|
|
|
[GeneratedRegex(@"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}")]
|
|
|
|
|
|
private static partial Regex PlaceholderRegex();
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<McpReportCatalogDto>> GetReportTemplatesAsync(string? search)
|
2026-10-01 12:01:57 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureCanReadAsync(McpToolNames.ListReportTemplates) is { } problem)
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-10-01 12:01:57 +00:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<McpReportTemplateDto>> GetReportTemplateAsync(string template)
|
2026-10-01 12:01:57 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureCanReadAsync(McpToolNames.GetReportTemplate) is { } problem)
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-10-01 12:01:57 +00:00
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
var entity = await FindTemplateAsync(template);
|
|
|
|
|
|
if (entity is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Problem(McpErrorCodes.ReportTemplateNotFound, template);
|
|
|
|
|
|
}
|
2026-10-01 12:01:57 +00:00
|
|
|
|
|
|
|
|
|
|
return await ToDtoAsync(entity, null, null);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<McpReportCategoryDto>> SaveReportCategoryAsync(McpReportCategorySaveRequestDto input)
|
2026-10-01 12:01:57 +00:00
|
|
|
|
{
|
|
|
|
|
|
Check.NotNull(input, nameof(input));
|
2026-10-10 22:03:36 +00:00
|
|
|
|
const string toolName = McpToolNames.SaveReportCategory;
|
|
|
|
|
|
|
|
|
|
|
|
if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-10-01 12:01:57 +00:00
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(input.Name))
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.ReportNameRequired);
|
2026-10-01 12:01:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-10-01 12:01:57 +00:00
|
|
|
|
|
|
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveReportCategory, name);
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
var category = await categoryRepository.FirstOrDefaultAsync(x => x.Id == id);
|
|
|
|
|
|
return category is null ? new McpReportCategoryDto { Id = id, Name = name } : ToDto(category);
|
2026-10-01 12:01:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpOutcome<McpReportTemplateDto>> SaveReportTemplateAsync(McpReportTemplateSaveRequestDto input)
|
2026-10-01 12:01:57 +00:00
|
|
|
|
{
|
|
|
|
|
|
Check.NotNull(input, nameof(input));
|
2026-10-10 22:03:36 +00:00
|
|
|
|
const string toolName = McpToolNames.SaveReportTemplate;
|
|
|
|
|
|
|
|
|
|
|
|
if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
2026-10-01 12:01:57 +00:00
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(input.Name))
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.ReportNameRequired);
|
2026-10-01 12:01:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-10-01 12:01:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (categoryId is null)
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return Problem(McpErrorCodes.ReportCategoryRequired);
|
2026-10-01 12:01:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (!(await FindListFormAsync(input.ListFormCode)).TryGetValue(out var listForm, out var notFound))
|
2026-10-01 12:01:57 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return notFound;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (listForm is not null && await CheckPlaceholdersExistAsync(listForm.ListFormCode, htmlContent) is { } unknown)
|
|
|
|
|
|
{
|
|
|
|
|
|
return unknown;
|
2026-10-01 12:01:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var values = new Dictionary<string, object?>
|
|
|
|
|
|
{
|
|
|
|
|
|
["Name"] = name,
|
|
|
|
|
|
["Description"] = input.Description ?? existing?.Description,
|
|
|
|
|
|
["HtmlContent"] = htmlContent,
|
|
|
|
|
|
["CategoryId"] = categoryId,
|
|
|
|
|
|
["Status"] = input.Status ?? existing?.Status ?? ActiveStatus
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-10-01 12:01:57 +00:00
|
|
|
|
|
|
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveReportTemplate, name);
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
var template = await templateRepository.FirstOrDefaultAsync(x => x.Id == id);
|
|
|
|
|
|
if (template is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Problem(McpErrorCodes.ReportTemplateNotFound, name);
|
|
|
|
|
|
}
|
2026-10-01 12:01:57 +00:00
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return await ToDtoAsync(template, listForm, input.ButtonText);
|
2026-10-01 12:01:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
public async Task<McpProblem?> DeleteReportTemplateAsync(string template)
|
2026-10-01 12:01:57 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-10-01 12:01:57 +00:00
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (!(await BuildRequestAsync(toolName, TemplatesListFormCode, AuthorizationTypeEnum.Delete, null, entity.Id)).TryGetValue(out var request, out var denied))
|
|
|
|
|
|
{
|
|
|
|
|
|
return denied;
|
|
|
|
|
|
}
|
2026-10-01 12:01:57 +00:00
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
await listFormDataAppService.PostDeleteAsync(request);
|
2026-10-01 12:01:57 +00:00
|
|
|
|
await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteReportTemplate, entity.Name);
|
2026-10-10 22:03:36 +00:00
|
|
|
|
|
|
|
|
|
|
return null;
|
2026-10-01 12:01:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Okuma depodan yapildigi icin ekranin okuma yetkisi burada istenir; MCP kullanicinin
|
|
|
|
|
|
/// uygulamada goremedigi sablonu gostermez.
|
|
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
private async Task<McpProblem?> EnsureCanReadAsync(string toolName)
|
2026-10-01 12:01:57 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem })
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-01 12:01:57 +00:00
|
|
|
|
var permission = await accessFilter.GetPermissionNameAsync(TemplatesListFormCode, AuthorizationTypeEnum.Read);
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return await CheckPermissionAsync(toolName, permission ?? TemplatesListFormCode);
|
2026-10-01 12:01:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
private async Task<McpOutcome<Guid>> InsertAsync(string toolName, string listFormCode, Dictionary<string, object?> values)
|
2026-10-01 12:01:57 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (!(await BuildRequestAsync(toolName, listFormCode, AuthorizationTypeEnum.Create, values, null)).TryGetValue(out var request, out var problem))
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var key = await listFormDataAppService.PostInsertAsync(request);
|
2026-10-01 12:01:57 +00:00
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
private async Task<McpOutcome<Guid>> UpdateAsync(string toolName, string listFormCode, Guid id, Dictionary<string, object?> values)
|
2026-10-01 12:01:57 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (!(await BuildRequestAsync(toolName, listFormCode, AuthorizationTypeEnum.Update, values, id)).TryGetValue(out var request, out var problem))
|
|
|
|
|
|
{
|
|
|
|
|
|
return problem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await listFormDataAppService.PostUpdateAsync(request);
|
2026-10-01 12:01:57 +00:00
|
|
|
|
return id;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Ekranin kendi kontrolu reddi adsiz bir hatayla bildirir; yetki burada adiyla istenir ki
|
|
|
|
|
|
/// istemci hangi yetkinin eksik oldugunu gorsun.
|
|
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
private async Task<McpOutcome<DataRequestDto>> BuildRequestAsync(
|
|
|
|
|
|
string toolName,
|
2026-10-01 12:01:57 +00:00
|
|
|
|
string listFormCode,
|
|
|
|
|
|
AuthorizationTypeEnum operation,
|
|
|
|
|
|
Dictionary<string, object?>? values,
|
|
|
|
|
|
Guid? key)
|
|
|
|
|
|
{
|
|
|
|
|
|
var permission = await accessFilter.GetPermissionNameAsync(listFormCode, operation);
|
2026-10-10 22:03:36 +00:00
|
|
|
|
if (permission is not null && await CheckPermissionAsync(toolName, permission) is { } denied)
|
2026-10-01 12:01:57 +00:00
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return denied;
|
2026-10-01 12:01:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 22:03:36 +00:00
|
|
|
|
/// <summary>Kod verilmediyse null; verilen kodun ekrani yoksa sorun.</summary>
|
|
|
|
|
|
private async Task<McpOutcome<ListForm?>> FindListFormAsync(string? listFormCode)
|
2026-10-01 12:01:57 +00:00
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(listFormCode))
|
|
|
|
|
|
{
|
2026-10-10 22:03:36 +00:00
|
|
|
|
return default(ListForm);
|
2026-10-01 12:01:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var code = listFormCode.Trim();
|
2026-10-10 22:03:36 +00:00
|
|
|
|
var listForm = await listFormRepository.FirstOrDefaultAsync(x => x.ListFormCode == code);
|
|
|
|
|
|
|
|
|
|
|
|
return listForm is null ? Problem(McpErrorCodes.ReportListFormNotFound, code) : listForm;
|
2026-10-01 12:01:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Bilinmeyen yer tutucu raporda oldugu gibi basilir; sertifikada "{{AdSoyad}}" gorunmesin
|
|
|
|
|
|
/// diye kayitta reddedilir.
|
|
|
|
|
|
/// </summary>
|
2026-10-10 22:03:36 +00:00
|
|
|
|
private async Task<McpProblem?> CheckPlaceholdersExistAsync(string listFormCode, string htmlContent)
|
2026-10-01 12:01:57 +00:00
|
|
|
|
{
|
|
|
|
|
|
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();
|
2026-10-10 22:03:36 +00:00
|
|
|
|
|
|
|
|
|
|
return unknown.Count == 0
|
|
|
|
|
|
? null
|
|
|
|
|
|
: Problem(McpErrorCodes.ReportPlaceholdersUnknown, listFormCode, string.Join(", ", unknown));
|
2026-10-01 12:01:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|