ListFormWorkflowun Grid ve Tree üzerinden çalıştır
This commit is contained in:
parent
8f3932bc6e
commit
6262baa6f1
32 changed files with 1643 additions and 135 deletions
|
|
@ -394,6 +394,19 @@ public class GridOptionsDto : AuditedEntityDto<Guid>
|
|||
set { LayoutJson = JsonSerializer.Serialize(value); }
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string WorkflowJson { get; set; }
|
||||
public WorkflowDto WorkflowDto
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!string.IsNullOrEmpty(WorkflowJson))
|
||||
return JsonSerializer.Deserialize<WorkflowDto>(WorkflowJson);
|
||||
return new WorkflowDto();
|
||||
}
|
||||
set { WorkflowJson = JsonSerializer.Serialize(value); }
|
||||
}
|
||||
|
||||
/// <summary> Chart a yetkili kullanıcı. UserId ve RoleId boş ise herkes yetkilidir
|
||||
/// </summary>
|
||||
public string UserId { get; set; }
|
||||
|
|
|
|||
|
|
@ -123,18 +123,5 @@ public class GridOptionsEditDto : GridOptionsDto
|
|||
}
|
||||
set { ExtraFilterJson = JsonSerializer.Serialize(value); }
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string WorkflowJson { get; set; }
|
||||
public WorkflowDto WorkflowDto
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!string.IsNullOrEmpty(WorkflowJson))
|
||||
return JsonSerializer.Deserialize<WorkflowDto>(WorkflowJson);
|
||||
return new WorkflowDto();
|
||||
}
|
||||
set { WorkflowJson = JsonSerializer.Serialize(value); }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
namespace Sozsoft.Platform.ListForms;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
public class WorkflowDto
|
||||
{
|
||||
public string ApprovalUserFieldName { get; set; }
|
||||
public string ApprovalDateFieldName { get; set; }
|
||||
public string ApprovalStatusFieldName { get; set; }
|
||||
public string ApprovalDescriptionFieldName { get; set; }
|
||||
|
||||
public List<ListFormWorkflowCriteriaDto> Criteria { get; set; } = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Sozsoft.Platform.ListForms.Workflow;
|
||||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
public class CompareOutcomeDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Sozsoft.Platform.ListForms.Workflow;
|
||||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
public class CreateUpdateListFormWorkflowCriteriaDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
namespace Sozsoft.Platform.ListForms.Workflow;
|
||||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
public class DecisionWorkflowDto
|
||||
{
|
||||
public string ListFormCode { get; set; }
|
||||
public object[] Keys { get; set; }
|
||||
public string CurrentNodeId { get; set; }
|
||||
public bool Approved { get; set; }
|
||||
public string Note { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@ using System;
|
|||
using System.Threading.Tasks;
|
||||
using Volo.Abp.Application.Services;
|
||||
|
||||
namespace Sozsoft.Platform.ListForms.Workflow;
|
||||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
public interface IListFormWorkflowAppService : IApplicationService
|
||||
{
|
||||
Task<ListFormWorkflowStateDto> GetStateAsync(string listFormCode = null);
|
||||
Task<ListFormWorkflowStateDto> GetCriteriaAsync(string listFormCode = null);
|
||||
Task<ListFormWorkflowCriteriaDto> SaveCriteriaAsync(CreateUpdateListFormWorkflowCriteriaDto input);
|
||||
Task DeleteCriteriaAsync(string id);
|
||||
Task<ListFormWorkflowStateDto> ResetDemoAsync(string listFormCode = null);
|
||||
Task<WorkflowRunResultDto> StartAsync(StartWorkflowDto input);
|
||||
Task<WorkflowRunResultDto> DecisionAsync(DecisionWorkflowDto input);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
|
||||
namespace Sozsoft.Platform.ListForms.Workflow;
|
||||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
public class ListFormWorkflowCriteriaDto : AuditedEntityDto<string>
|
||||
public class ListFormWorkflowCriteriaDto : EntityDto<string>
|
||||
{
|
||||
public string ListFormCode { get; set; }
|
||||
public string Kind { get; set; }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Sozsoft.Platform.ListForms.Workflow;
|
||||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
public class ListFormWorkflowStateDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
public class StartWorkflowDto
|
||||
{
|
||||
public string ListFormCode { get; set; }
|
||||
public object[] Keys { get; set; }
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Sozsoft.Platform.ListForms.Workflow;
|
||||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
public class WorkflowConditionDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using System;
|
||||
|
||||
namespace Sozsoft.Platform.ListForms.Workflow;
|
||||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
public class WorkflowHistoryDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
namespace Sozsoft.Platform.ListForms;
|
||||
|
||||
public class WorkflowRunResultDto
|
||||
{
|
||||
public object[] Keys { get; set; }
|
||||
public string CurrentNodeId { get; set; }
|
||||
public string CurrentNodeTitle { get; set; }
|
||||
public string CurrentNodeKind { get; set; }
|
||||
public bool WaitingApproval { get; set; }
|
||||
public bool Completed { get; set; }
|
||||
}
|
||||
|
||||
|
|
@ -21,6 +21,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
|
|||
{
|
||||
private readonly IRepository<ListForm, Guid> listFormRepository;
|
||||
private readonly IRepository<ListFormField, Guid> listFormFieldRepository;
|
||||
private readonly IRepository<ListFormWorkflow, string> listFormWorkflowRepository;
|
||||
private readonly ICurrentUser currentUser;
|
||||
private readonly ISelectQueryManager selectQueryManager;
|
||||
private readonly IListFormAuthorizationManager authManager;
|
||||
|
|
@ -39,6 +40,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
|
|||
public ListFormSelectAppService(
|
||||
IRepository<ListForm, Guid> listFormRepository,
|
||||
IRepository<ListFormField, Guid> listFormFieldRepository,
|
||||
IRepository<ListFormWorkflow, string> listFormWorkflowRepository,
|
||||
ICurrentUser currentUser,
|
||||
ISelectQueryManager selectQueryManager,
|
||||
IListFormAuthorizationManager authManager,
|
||||
|
|
@ -54,6 +56,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
|
|||
{
|
||||
this.listFormRepository = listFormRepository;
|
||||
this.listFormFieldRepository = listFormFieldRepository;
|
||||
this.listFormWorkflowRepository = listFormWorkflowRepository;
|
||||
this.currentUser = currentUser;
|
||||
this.selectQueryManager = selectQueryManager;
|
||||
this.authManager = authManager;
|
||||
|
|
@ -165,6 +168,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
|
|||
var data = await dynamicDataRepository.QueryAsync(selectQueryManager.GroupQuery, connectionString, param);
|
||||
var dataQueryable = data.AsQueryable();
|
||||
|
||||
|
||||
var groups = new List<(string, int, List<dynamic>)>(selectQueryManager.GroupTuples.Count);
|
||||
for (int i = 0; i < selectQueryManager.GroupTuples.Count; i++)
|
||||
{
|
||||
|
|
@ -305,6 +309,11 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
|
|||
Widgets = await GetWidgetsAsync(listForm)
|
||||
};
|
||||
|
||||
//Workflow kriterlerini alıp GridOptionsDto içine atıyoruz ki frontend'de kullanabilelim
|
||||
var workflowDto = result.GridOptions.WorkflowDto;
|
||||
workflowDto.Criteria = await GetWorkflowCriteriaAsync(ListFormCode);
|
||||
result.GridOptions.WorkflowDto = workflowDto;
|
||||
|
||||
var queryParameters = httpContextAccessor.HttpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value);
|
||||
var defaultFields = await defaultValueManager.GenerateDefaultValuesAsync(listForm, fields, Enums.OperationEnum.Select, queryParameters: queryParameters);
|
||||
|
||||
|
|
@ -491,5 +500,153 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
|
|||
|
||||
return sql + " " + insertText;
|
||||
}
|
||||
|
||||
//Workflow kriterlerini alma
|
||||
private async Task<List<ListFormWorkflowCriteriaDto>> GetWorkflowCriteriaAsync(string listFormCode)
|
||||
{
|
||||
var criteria = await listFormWorkflowRepository.GetListAsync(x => x.ListFormCode == listFormCode);
|
||||
return OrderWorkflowCriteria(criteria.Select(MapWorkflowCriteria).ToList());
|
||||
}
|
||||
|
||||
private static List<ListFormWorkflowCriteriaDto> OrderWorkflowCriteria(List<ListFormWorkflowCriteriaDto> criteria)
|
||||
{
|
||||
if (criteria.Count <= 1)
|
||||
{
|
||||
return criteria;
|
||||
}
|
||||
|
||||
var criteriaById = criteria.ToDictionary(x => x.Id);
|
||||
var ordered = new List<ListFormWorkflowCriteriaDto>();
|
||||
var visited = new HashSet<string>();
|
||||
var visiting = new HashSet<string>();
|
||||
|
||||
var roots = criteria
|
||||
.Where(x => x.Kind == "Start")
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
if (roots.Count == 0)
|
||||
{
|
||||
var referencedIds = criteria
|
||||
.SelectMany(GetWorkflowTargetIds)
|
||||
.Where(criteriaById.ContainsKey)
|
||||
.ToHashSet();
|
||||
|
||||
roots = criteria
|
||||
.Where(x => !referencedIds.Contains(x.Id))
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
foreach (var root in roots)
|
||||
{
|
||||
VisitWorkflowCriteria(root);
|
||||
}
|
||||
|
||||
foreach (var item in criteria.OrderBy(x => x.Id))
|
||||
{
|
||||
VisitWorkflowCriteria(item);
|
||||
}
|
||||
|
||||
return ordered;
|
||||
|
||||
void VisitWorkflowCriteria(ListFormWorkflowCriteriaDto item)
|
||||
{
|
||||
if (visited.Contains(item.Id) || visiting.Contains(item.Id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
visiting.Add(item.Id);
|
||||
ordered.Add(item);
|
||||
|
||||
foreach (var targetId in GetWorkflowTargetIds(item))
|
||||
{
|
||||
if (criteriaById.TryGetValue(targetId, out var target))
|
||||
{
|
||||
VisitWorkflowCriteria(target);
|
||||
}
|
||||
}
|
||||
|
||||
visiting.Remove(item.Id);
|
||||
visited.Add(item.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetWorkflowTargetIds(ListFormWorkflowCriteriaDto criteria)
|
||||
{
|
||||
if (!criteria.NextOnStart.IsNullOrWhiteSpace())
|
||||
{
|
||||
yield return criteria.NextOnStart;
|
||||
}
|
||||
|
||||
foreach (var outcome in criteria.CompareOutcomes ?? [])
|
||||
{
|
||||
if (!outcome.TargetId.IsNullOrWhiteSpace())
|
||||
{
|
||||
yield return outcome.TargetId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!criteria.NextOnTrue.IsNullOrWhiteSpace())
|
||||
{
|
||||
yield return criteria.NextOnTrue;
|
||||
}
|
||||
|
||||
if (!criteria.NextOnFalse.IsNullOrWhiteSpace())
|
||||
{
|
||||
yield return criteria.NextOnFalse;
|
||||
}
|
||||
|
||||
if (!criteria.NextOnApprove.IsNullOrWhiteSpace())
|
||||
{
|
||||
yield return criteria.NextOnApprove;
|
||||
}
|
||||
|
||||
if (!criteria.NextOnReject.IsNullOrWhiteSpace())
|
||||
{
|
||||
yield return criteria.NextOnReject;
|
||||
}
|
||||
}
|
||||
|
||||
private static ListFormWorkflowCriteriaDto MapWorkflowCriteria(ListFormWorkflow criteria)
|
||||
{
|
||||
return new ListFormWorkflowCriteriaDto
|
||||
{
|
||||
Id = criteria.Id,
|
||||
ListFormCode = criteria.ListFormCode,
|
||||
Kind = criteria.Kind,
|
||||
Title = criteria.Title,
|
||||
CompareColumn = criteria.CompareColumn,
|
||||
CompareOperator = criteria.CompareOperator,
|
||||
CompareValue = criteria.CompareValue,
|
||||
Approver = criteria.Approver,
|
||||
NextOnStart = criteria.NextOnStart,
|
||||
NextOnTrue = criteria.NextOnTrue,
|
||||
NextOnFalse = criteria.NextOnFalse,
|
||||
NextOnApprove = criteria.NextOnApprove,
|
||||
NextOnReject = criteria.NextOnReject,
|
||||
PositionX = criteria.PositionX,
|
||||
PositionY = criteria.PositionY,
|
||||
CompareOutcomes = DeserializeCompareOutcomes(criteria.CompareOutcomesJson)
|
||||
};
|
||||
}
|
||||
|
||||
private static List<CompareOutcomeDto> DeserializeCompareOutcomes(string json)
|
||||
{
|
||||
if (json.IsNullOrWhiteSpace())
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<CompareOutcomeDto>>(json) ?? [];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Sozsoft.Platform.Entities;
|
||||
using Sozsoft.Platform.Enums;
|
||||
using Sozsoft.Platform.ListForms.Select;
|
||||
using Sozsoft.Platform.Queries;
|
||||
using Volo.Abp;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
|
||||
|
|
@ -15,36 +19,149 @@ namespace Sozsoft.Platform.ListForms.Workflow;
|
|||
[Route("api/app/list-form-workflow")]
|
||||
public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowAppService
|
||||
{
|
||||
private const string DefaultListFormCode = "workflow";
|
||||
private const string CriteriaIdPrefix = "N";
|
||||
private const int CriteriaIdPadding = 3;
|
||||
private const string SystemApprovalDescription = "Sistem tarafından otomatik olarak onaylandı.";
|
||||
|
||||
private readonly IRepository<ListFormWorkflow, string> criteriaRepository;
|
||||
private readonly IListFormManager listFormManager;
|
||||
private readonly IListFormAuthorizationManager authManager;
|
||||
private readonly IListFormSelectAppService listFormSelectAppService;
|
||||
private readonly IQueryManager queryManager;
|
||||
|
||||
public ListFormWorkflowAppService(IRepository<ListFormWorkflow, string> criteriaRepository)
|
||||
public ListFormWorkflowAppService(
|
||||
IRepository<ListFormWorkflow, string> criteriaRepository,
|
||||
IListFormManager listFormManager,
|
||||
IListFormAuthorizationManager authManager,
|
||||
IListFormSelectAppService listFormSelectAppService,
|
||||
IQueryManager queryManager)
|
||||
{
|
||||
this.criteriaRepository = criteriaRepository;
|
||||
this.listFormManager = listFormManager;
|
||||
this.authManager = authManager;
|
||||
this.listFormSelectAppService = listFormSelectAppService;
|
||||
this.queryManager = queryManager;
|
||||
}
|
||||
|
||||
[HttpGet("state")]
|
||||
public async Task<ListFormWorkflowStateDto> GetStateAsync(string listFormCode = null)
|
||||
[HttpGet("criteria")]
|
||||
public async Task<ListFormWorkflowStateDto> GetCriteriaAsync(string listFormCode = null)
|
||||
{
|
||||
var code = NormalizeListFormCode(listFormCode);
|
||||
var code = listFormCode;
|
||||
var criteria = (await criteriaRepository.GetListAsync(x => x.ListFormCode == code))
|
||||
.OrderBy(x => x.PositionX)
|
||||
.ThenBy(x => x.PositionY)
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
return new ListFormWorkflowStateDto
|
||||
{
|
||||
Criteria = criteria.Select(MapCriteria).ToList()
|
||||
Criteria = OrderWorkflowCriteria(criteria.Select(MapCriteria).ToList())
|
||||
};
|
||||
}
|
||||
|
||||
private static List<ListFormWorkflowCriteriaDto> OrderWorkflowCriteria(List<ListFormWorkflowCriteriaDto> criteria)
|
||||
{
|
||||
if (criteria.Count <= 1)
|
||||
{
|
||||
return criteria;
|
||||
}
|
||||
|
||||
var criteriaById = criteria.ToDictionary(x => x.Id);
|
||||
var ordered = new List<ListFormWorkflowCriteriaDto>();
|
||||
var visited = new HashSet<string>();
|
||||
var visiting = new HashSet<string>();
|
||||
|
||||
var roots = criteria
|
||||
.Where(x => x.Kind == "Start")
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
|
||||
if (roots.Count == 0)
|
||||
{
|
||||
var referencedIds = criteria
|
||||
.SelectMany(GetWorkflowTargetIds)
|
||||
.Where(criteriaById.ContainsKey)
|
||||
.ToHashSet();
|
||||
|
||||
roots = criteria
|
||||
.Where(x => !referencedIds.Contains(x.Id))
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
foreach (var root in roots)
|
||||
{
|
||||
VisitWorkflowCriteria(root);
|
||||
}
|
||||
|
||||
foreach (var item in criteria.OrderBy(x => x.Id))
|
||||
{
|
||||
VisitWorkflowCriteria(item);
|
||||
}
|
||||
|
||||
return ordered;
|
||||
|
||||
void VisitWorkflowCriteria(ListFormWorkflowCriteriaDto item)
|
||||
{
|
||||
if (visited.Contains(item.Id) || visiting.Contains(item.Id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
visiting.Add(item.Id);
|
||||
ordered.Add(item);
|
||||
|
||||
foreach (var targetId in GetWorkflowTargetIds(item))
|
||||
{
|
||||
if (criteriaById.TryGetValue(targetId, out var target))
|
||||
{
|
||||
VisitWorkflowCriteria(target);
|
||||
}
|
||||
}
|
||||
|
||||
visiting.Remove(item.Id);
|
||||
visited.Add(item.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetWorkflowTargetIds(ListFormWorkflowCriteriaDto criteria)
|
||||
{
|
||||
if (!criteria.NextOnStart.IsNullOrWhiteSpace())
|
||||
{
|
||||
yield return criteria.NextOnStart;
|
||||
}
|
||||
|
||||
foreach (var outcome in criteria.CompareOutcomes ?? [])
|
||||
{
|
||||
if (!outcome.TargetId.IsNullOrWhiteSpace())
|
||||
{
|
||||
yield return outcome.TargetId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!criteria.NextOnTrue.IsNullOrWhiteSpace())
|
||||
{
|
||||
yield return criteria.NextOnTrue;
|
||||
}
|
||||
|
||||
if (!criteria.NextOnFalse.IsNullOrWhiteSpace())
|
||||
{
|
||||
yield return criteria.NextOnFalse;
|
||||
}
|
||||
|
||||
if (!criteria.NextOnApprove.IsNullOrWhiteSpace())
|
||||
{
|
||||
yield return criteria.NextOnApprove;
|
||||
}
|
||||
|
||||
if (!criteria.NextOnReject.IsNullOrWhiteSpace())
|
||||
{
|
||||
yield return criteria.NextOnReject;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("criteria")]
|
||||
public async Task<ListFormWorkflowCriteriaDto> SaveCriteriaAsync(CreateUpdateListFormWorkflowCriteriaDto input)
|
||||
{
|
||||
var code = NormalizeListFormCode(input.ListFormCode);
|
||||
var code = input.ListFormCode;
|
||||
var isNew = input.Id.IsNullOrWhiteSpace();
|
||||
var criteria = isNew
|
||||
? new ListFormWorkflow(await GenerateNextCriteriaIdAsync())
|
||||
|
|
@ -58,7 +175,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
criteria.ListFormCode = code;
|
||||
criteria.Kind = NormalizeRequired(input.Kind, "Compare");
|
||||
criteria.Title = NormalizeRequired(input.Title, criteria.Kind);
|
||||
criteria.CompareColumn = NormalizeRequired(input.CompareColumn, "Tutar");
|
||||
criteria.CompareColumn = NormalizeRequired(input.CompareColumn, "Price");
|
||||
criteria.CompareOperator = NormalizeRequired(input.CompareOperator, ">");
|
||||
criteria.CompareValue = input.CompareValue;
|
||||
criteria.Approver = input.Approver ?? string.Empty;
|
||||
|
|
@ -110,7 +227,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
[HttpPost("reset-demo")]
|
||||
public async Task<ListFormWorkflowStateDto> ResetDemoAsync(string listFormCode = null)
|
||||
{
|
||||
var code = NormalizeListFormCode(listFormCode);
|
||||
var code = listFormCode;
|
||||
var existing = await criteriaRepository.GetListAsync(x => x.ListFormCode == code);
|
||||
foreach (var item in existing)
|
||||
{
|
||||
|
|
@ -121,7 +238,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
var compare = await CreateCriteriaAsync(code, "Compare", "Tutar kontrolü", 330, 130);
|
||||
var approval = await CreateCriteriaAsync(code, "Approval", "Yönetici Onayı", 590, 60, PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue);
|
||||
var inform = await CreateCriteriaAsync(code, "Inform", "Muhasebe Bilgilendirme", 590, 230, PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue);
|
||||
var end = await CreateCriteriaAsync(code, "End", "Akışı Bitir", 850, 150);
|
||||
var end = await CreateCriteriaAsync(code, "End", "İş Akışı Bitir", 850, 150);
|
||||
|
||||
start.NextOnStart = compare.Id;
|
||||
compare.NextOnTrue = approval.Id;
|
||||
|
|
@ -131,13 +248,13 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
{
|
||||
Label = "Onay gerekir",
|
||||
TargetId = approval.Id,
|
||||
Conditions = [new WorkflowConditionDto { CompareColumn = "Tutar", CompareOperator = ">", CompareValue = 5000 }]
|
||||
Conditions = [new WorkflowConditionDto { CompareColumn = "Price", CompareOperator = ">", CompareValue = 5000 }]
|
||||
},
|
||||
new CompareOutcomeDto
|
||||
{
|
||||
Label = "Bilgilendir",
|
||||
TargetId = inform.Id,
|
||||
Conditions = [new WorkflowConditionDto { CompareColumn = "Tutar", CompareOperator = "<=", CompareValue = 5000 }]
|
||||
Conditions = [new WorkflowConditionDto { CompareColumn = "Price", CompareOperator = "<=", CompareValue = 5000 }]
|
||||
}
|
||||
]);
|
||||
approval.NextOnApprove = inform.Id;
|
||||
|
|
@ -149,7 +266,422 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
await criteriaRepository.UpdateAsync(approval, autoSave: true);
|
||||
await criteriaRepository.UpdateAsync(inform, autoSave: true);
|
||||
|
||||
return await GetStateAsync(code);
|
||||
return await GetCriteriaAsync(code);
|
||||
}
|
||||
|
||||
[HttpPost("start")]
|
||||
public async Task<WorkflowRunResultDto> StartAsync(StartWorkflowDto input)
|
||||
{
|
||||
if (input.Keys?.Length > 1)
|
||||
{
|
||||
return await RunForEachKeyAsync(input.Keys, keys => StartSingleAsync(input.ListFormCode, keys));
|
||||
}
|
||||
|
||||
return await StartSingleAsync(input.ListFormCode, input.Keys);
|
||||
}
|
||||
|
||||
private async Task<WorkflowRunResultDto> StartSingleAsync(string listFormCode, object[] keys)
|
||||
{
|
||||
var context = await CreateRunContextAsync(listFormCode, keys);
|
||||
var start = context.Criteria.FirstOrDefault(x => x.Kind == "Start")
|
||||
?? throw new UserFriendlyException("Workflow başlangıç adımı bulunamadı.");
|
||||
|
||||
return await RunUntilWaitAsync(context, start);
|
||||
}
|
||||
|
||||
[HttpPost("decision")]
|
||||
public async Task<WorkflowRunResultDto> DecisionAsync(DecisionWorkflowDto input)
|
||||
{
|
||||
if (input.Keys?.Length > 1)
|
||||
{
|
||||
return await RunForEachKeyAsync(input.Keys, keys => DecisionSingleAsync(input, keys));
|
||||
}
|
||||
|
||||
return await DecisionSingleAsync(input, input.Keys);
|
||||
}
|
||||
|
||||
private async Task<WorkflowRunResultDto> DecisionSingleAsync(DecisionWorkflowDto input, object[] keys)
|
||||
{
|
||||
var context = await CreateRunContextAsync(input.ListFormCode, keys);
|
||||
var currentNodeId = GetRowValue(context.Row, context.Workflow.ApprovalStatusFieldName)?.ToString();
|
||||
var current = FindCurrentCriteria(context.Criteria, currentNodeId);
|
||||
|
||||
if (current == null && currentNodeId.IsNullOrWhiteSpace())
|
||||
{
|
||||
var start = context.Criteria.FirstOrDefault(x => x.Kind == "Start")
|
||||
?? throw new UserFriendlyException("Workflow başlangıç adımı bulunamadı.");
|
||||
|
||||
var started = await RunUntilWaitAsync(context, start);
|
||||
current = FindCurrentCriteria(context.Criteria, started.CurrentNodeId);
|
||||
}
|
||||
else if (current != null && current.Kind != "Approval" && current.Kind != "End")
|
||||
{
|
||||
var progressed = await RunUntilWaitAsync(context, current);
|
||||
current = FindCurrentCriteria(context.Criteria, progressed.CurrentNodeId);
|
||||
}
|
||||
|
||||
if (current?.Kind != "Approval")
|
||||
{
|
||||
throw new UserFriendlyException("Seçili kayıt onay adımında beklemiyor.");
|
||||
}
|
||||
if (!input.CurrentNodeId.IsNullOrWhiteSpace() && input.CurrentNodeId != current.Id)
|
||||
{
|
||||
throw new UserFriendlyException("Seçili kayıt bu onay adımında beklemiyor.");
|
||||
}
|
||||
|
||||
var update = new Dictionary<string, object>();
|
||||
if (!context.Workflow.ApprovalUserFieldName.IsNullOrWhiteSpace())
|
||||
{
|
||||
update[context.Workflow.ApprovalUserFieldName] = CurrentUser.UserName ?? CurrentUser.Name ?? string.Empty;
|
||||
}
|
||||
if (!context.Workflow.ApprovalDateFieldName.IsNullOrWhiteSpace())
|
||||
{
|
||||
update[context.Workflow.ApprovalDateFieldName] = Clock.Now;
|
||||
}
|
||||
if (!context.Workflow.ApprovalDescriptionFieldName.IsNullOrWhiteSpace())
|
||||
{
|
||||
update[context.Workflow.ApprovalDescriptionFieldName] = input.Note ?? string.Empty;
|
||||
context.UserUpdatedFields.Add(context.Workflow.ApprovalDescriptionFieldName);
|
||||
}
|
||||
|
||||
if (update.Count > 0)
|
||||
{
|
||||
await UpdateRowAsync(context, update);
|
||||
MergeRowValues(context.Row, update);
|
||||
}
|
||||
|
||||
var next = FindNextCriteria(context.Criteria, input.Approved ? current.NextOnApprove : current.NextOnReject);
|
||||
return await RunUntilWaitAsync(context, next);
|
||||
}
|
||||
|
||||
private async Task<WorkflowRunResultDto> RunForEachKeyAsync(
|
||||
object[] keys,
|
||||
Func<object[], Task<WorkflowRunResultDto>> action)
|
||||
{
|
||||
var results = new List<WorkflowRunResultDto>();
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
results.Add(await action([key]));
|
||||
}
|
||||
|
||||
var last = results.LastOrDefault();
|
||||
return new WorkflowRunResultDto
|
||||
{
|
||||
Keys = keys,
|
||||
CurrentNodeId = last?.CurrentNodeId,
|
||||
CurrentNodeTitle = last?.CurrentNodeTitle,
|
||||
CurrentNodeKind = last?.CurrentNodeKind,
|
||||
WaitingApproval = results.Any(x => x.WaitingApproval),
|
||||
Completed = results.All(x => x.Completed)
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<WorkflowRunContext> CreateRunContextAsync(string listFormCode, object[] keys)
|
||||
{
|
||||
var code = listFormCode;
|
||||
if (!await authManager.CanAccess(code, AuthorizationTypeEnum.Update))
|
||||
{
|
||||
throw new UserFriendlyException("Workflow işlemi için güncelleme yetkiniz yok.");
|
||||
}
|
||||
|
||||
var listForm = await listFormManager.GetUserListForm(code);
|
||||
var workflow = DeserializeWorkflow(listForm.WorkflowJson);
|
||||
if (workflow.ApprovalStatusFieldName.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new UserFriendlyException("Workflow durum alanı tanımlı değil.");
|
||||
}
|
||||
if (listForm.KeyFieldName.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new UserFriendlyException("Liste formu anahtar alanı tanımlı değil.");
|
||||
}
|
||||
if (keys == null || keys.Length == 0)
|
||||
{
|
||||
throw new UserFriendlyException("Workflow için kayıt anahtarı seçilmelidir.");
|
||||
}
|
||||
|
||||
var criteria = (await criteriaRepository.GetListAsync(x => x.ListFormCode == code))
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
var row = await GetRowAsync(code, listForm.KeyFieldName, keys[0]);
|
||||
|
||||
return new WorkflowRunContext(code, listForm.KeyFieldName, keys, workflow, criteria, row);
|
||||
}
|
||||
|
||||
private async Task<IDictionary<string, object>> GetRowAsync(string listFormCode, string keyFieldName, object key)
|
||||
{
|
||||
var filter = JsonSerializer.Serialize(new object[] { keyFieldName, "=", key });
|
||||
var result = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto
|
||||
{
|
||||
ListFormCode = listFormCode,
|
||||
Filter = filter,
|
||||
Skip = 0,
|
||||
Take = 1,
|
||||
RequireTotalCount = false,
|
||||
RequireGroupCount = false
|
||||
});
|
||||
|
||||
var row = ((result?.Data as System.Collections.IEnumerable)?.Cast<object>()?.FirstOrDefault())
|
||||
?? throw new UserFriendlyException("Workflow kaydı bulunamadı.");
|
||||
|
||||
if (row is IDictionary<string, object> dictionary)
|
||||
{
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
throw new UserFriendlyException("Workflow kaydı okunamadı.");
|
||||
}
|
||||
|
||||
private async Task<WorkflowRunResultDto> RunUntilWaitAsync(WorkflowRunContext context, ListFormWorkflow current)
|
||||
{
|
||||
var visited = new HashSet<string>();
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
if (!visited.Add(current.Id))
|
||||
{
|
||||
throw new UserFriendlyException("Workflow adımlarında döngü tespit edildi.");
|
||||
}
|
||||
|
||||
await ApplyNodeAsync(context, current);
|
||||
|
||||
if (current.Kind == "Approval")
|
||||
{
|
||||
return ToRunResult(context, current, waitingApproval: true, completed: false);
|
||||
}
|
||||
|
||||
if (current.Kind == "End")
|
||||
{
|
||||
return ToRunResult(context, current, waitingApproval: false, completed: true);
|
||||
}
|
||||
|
||||
current = FindNextCriteria(context.Criteria, ResolveNextNodeId(context, current));
|
||||
}
|
||||
|
||||
return ToRunResult(context, null, waitingApproval: false, completed: true);
|
||||
}
|
||||
|
||||
private async Task ApplyNodeAsync(WorkflowRunContext context, ListFormWorkflow node)
|
||||
{
|
||||
var update = new Dictionary<string, object>
|
||||
{
|
||||
[context.Workflow.ApprovalStatusFieldName] = node.Title
|
||||
};
|
||||
|
||||
if (node.Kind == "Approval")
|
||||
{
|
||||
if (!context.Workflow.ApprovalUserFieldName.IsNullOrWhiteSpace())
|
||||
{
|
||||
update[context.Workflow.ApprovalUserFieldName] = node.Approver ?? string.Empty;
|
||||
}
|
||||
}
|
||||
else if (node.Kind == "End")
|
||||
{
|
||||
if (!context.Workflow.ApprovalUserFieldName.IsNullOrWhiteSpace() &&
|
||||
IsRowValueEmpty(context.Row, context.Workflow.ApprovalUserFieldName))
|
||||
{
|
||||
update[context.Workflow.ApprovalUserFieldName] = PlatformConsts.AbpIdentity.User.AdminEmailDefaultValue;
|
||||
}
|
||||
if (!context.Workflow.ApprovalDateFieldName.IsNullOrWhiteSpace() &&
|
||||
IsRowValueEmpty(context.Row, context.Workflow.ApprovalDateFieldName))
|
||||
{
|
||||
update[context.Workflow.ApprovalDateFieldName] = Clock.Now;
|
||||
}
|
||||
if (!context.Workflow.ApprovalDescriptionFieldName.IsNullOrWhiteSpace() &&
|
||||
!context.UserUpdatedFields.Contains(context.Workflow.ApprovalDescriptionFieldName) &&
|
||||
IsRowValueEmpty(context.Row, context.Workflow.ApprovalDescriptionFieldName))
|
||||
{
|
||||
update[context.Workflow.ApprovalDescriptionFieldName] = SystemApprovalDescription;
|
||||
}
|
||||
}
|
||||
|
||||
await UpdateRowAsync(context, update);
|
||||
MergeRowValues(context.Row, update);
|
||||
}
|
||||
|
||||
private async Task UpdateRowAsync(WorkflowRunContext context, Dictionary<string, object> data)
|
||||
{
|
||||
await queryManager.GenerateAndRunQueryAsync<int>(
|
||||
context.ListFormCode,
|
||||
OperationEnum.Update,
|
||||
JsonSerializer.Serialize(data),
|
||||
context.Keys);
|
||||
}
|
||||
|
||||
private string ResolveNextNodeId(WorkflowRunContext context, ListFormWorkflow current)
|
||||
{
|
||||
if (current.Kind == "Start" || current.Kind == "Inform")
|
||||
{
|
||||
return current.NextOnStart;
|
||||
}
|
||||
|
||||
if (current.Kind == "Compare")
|
||||
{
|
||||
var outcomes = DeserializeCompareOutcomes(current.CompareOutcomesJson);
|
||||
var matched = outcomes.FirstOrDefault(outcome =>
|
||||
outcome.Conditions == null ||
|
||||
outcome.Conditions.Count == 0 ||
|
||||
outcome.Conditions.All(condition => EvaluateCondition(context.Row, condition, current.CompareColumn)));
|
||||
|
||||
if (matched != null && !matched.TargetId.IsNullOrWhiteSpace())
|
||||
{
|
||||
return matched.TargetId;
|
||||
}
|
||||
|
||||
return EvaluateCondition(context.Row, new WorkflowConditionDto
|
||||
{
|
||||
CompareColumn = current.CompareColumn,
|
||||
CompareOperator = current.CompareOperator,
|
||||
CompareValue = current.CompareValue
|
||||
}, current.CompareColumn)
|
||||
? current.NextOnTrue
|
||||
: current.NextOnFalse;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool EvaluateCondition(
|
||||
IDictionary<string, object> row,
|
||||
WorkflowConditionDto condition,
|
||||
string fallbackCompareColumn = null)
|
||||
{
|
||||
if (condition == null || condition.CompareColumn.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var compareColumn = ResolveCompareColumn(row, condition.CompareColumn, fallbackCompareColumn);
|
||||
if (!TryGetRowValue(row, compareColumn, out var rawValue))
|
||||
{
|
||||
throw new UserFriendlyException(
|
||||
$"Workflow karşılaştırma alanı bulunamadı: {condition.CompareColumn}. Mevcut alanlar: {string.Join(", ", row.Keys)}");
|
||||
}
|
||||
|
||||
if (rawValue == null ||
|
||||
(!decimal.TryParse(rawValue.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out var value) &&
|
||||
!decimal.TryParse(rawValue.ToString(), NumberStyles.Any, CultureInfo.CurrentCulture, out value)))
|
||||
{
|
||||
throw new UserFriendlyException(
|
||||
$"Workflow karşılaştırma alanı sayısal değer olarak okunamadı: {compareColumn} = {rawValue}");
|
||||
}
|
||||
|
||||
return condition.CompareOperator switch
|
||||
{
|
||||
">" => value > condition.CompareValue,
|
||||
">=" => value >= condition.CompareValue,
|
||||
"<" => value < condition.CompareValue,
|
||||
"<=" => value <= condition.CompareValue,
|
||||
"==" or "=" => value == condition.CompareValue,
|
||||
"!=" or "<>" => value != condition.CompareValue,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static string ResolveCompareColumn(
|
||||
IDictionary<string, object> row,
|
||||
string compareColumn,
|
||||
string fallbackCompareColumn)
|
||||
{
|
||||
if (TryGetRowValue(row, compareColumn, out _))
|
||||
{
|
||||
return compareColumn;
|
||||
}
|
||||
|
||||
if (!fallbackCompareColumn.IsNullOrWhiteSpace() &&
|
||||
TryGetRowValue(row, fallbackCompareColumn, out _))
|
||||
{
|
||||
return fallbackCompareColumn;
|
||||
}
|
||||
|
||||
var numericKeys = row
|
||||
.Where(item =>
|
||||
item.Value != null &&
|
||||
decimal.TryParse(item.Value.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out _))
|
||||
.Select(item => item.Key)
|
||||
.ToList();
|
||||
|
||||
if (numericKeys.Count == 1)
|
||||
{
|
||||
return numericKeys[0];
|
||||
}
|
||||
|
||||
throw new UserFriendlyException(
|
||||
$"Workflow karşılaştırma alanı bulunamadı: {compareColumn}. Mevcut alanlar: {string.Join(", ", row.Keys)}");
|
||||
}
|
||||
|
||||
private static object GetRowValue(IDictionary<string, object> row, string fieldName)
|
||||
{
|
||||
return TryGetRowValue(row, fieldName, out var value) ? value : null;
|
||||
}
|
||||
|
||||
private static bool IsRowValueEmpty(IDictionary<string, object> row, string fieldName)
|
||||
{
|
||||
if (!TryGetRowValue(row, fieldName, out var value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return value == null || value == DBNull.Value || value.ToString().IsNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
private static bool TryGetRowValue(IDictionary<string, object> row, string fieldName, out object value)
|
||||
{
|
||||
if (row.TryGetValue(fieldName, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var key = row.Keys.FirstOrDefault(x => string.Equals(x, fieldName, StringComparison.OrdinalIgnoreCase));
|
||||
if (key == null)
|
||||
{
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = row[key];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void MergeRowValues(IDictionary<string, object> row, Dictionary<string, object> values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
row[value.Key] = value.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private static ListFormWorkflow FindNextCriteria(List<ListFormWorkflow> criteria, string id)
|
||||
{
|
||||
return id.IsNullOrWhiteSpace() ? null : criteria.FirstOrDefault(x => x.Id == id);
|
||||
}
|
||||
|
||||
private static ListFormWorkflow FindCurrentCriteria(List<ListFormWorkflow> criteria, string currentNodeId)
|
||||
{
|
||||
if (currentNodeId.IsNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return criteria.FirstOrDefault(x => string.Equals(x.Id, currentNodeId, StringComparison.OrdinalIgnoreCase))
|
||||
?? criteria.FirstOrDefault(x => string.Equals(x.Title, currentNodeId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static WorkflowRunResultDto ToRunResult(
|
||||
WorkflowRunContext context,
|
||||
ListFormWorkflow node,
|
||||
bool waitingApproval,
|
||||
bool completed)
|
||||
{
|
||||
return new WorkflowRunResultDto
|
||||
{
|
||||
Keys = context.Keys,
|
||||
CurrentNodeId = node?.Id,
|
||||
CurrentNodeTitle = node?.Title,
|
||||
CurrentNodeKind = node?.Kind,
|
||||
WaitingApproval = waitingApproval,
|
||||
Completed = completed
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ListFormWorkflow> CreateCriteriaAsync(
|
||||
|
|
@ -165,7 +697,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
ListFormCode = listFormCode,
|
||||
Kind = kind,
|
||||
Title = title,
|
||||
CompareColumn = "Tutar",
|
||||
CompareColumn = "Price",
|
||||
CompareOperator = ">",
|
||||
CompareValue = 5000,
|
||||
Approver = approver,
|
||||
|
|
@ -287,11 +819,6 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
};
|
||||
}
|
||||
|
||||
private static string NormalizeListFormCode(string listFormCode)
|
||||
{
|
||||
return listFormCode.IsNullOrWhiteSpace() ? DefaultListFormCode : listFormCode.Trim();
|
||||
}
|
||||
|
||||
private static string NormalizeRequired(string value, string fallback)
|
||||
{
|
||||
return value.IsNullOrWhiteSpace() ? fallback : value.Trim();
|
||||
|
|
@ -318,4 +845,32 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static WorkflowDto DeserializeWorkflow(string json)
|
||||
{
|
||||
if (json.IsNullOrWhiteSpace())
|
||||
{
|
||||
return new WorkflowDto();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<WorkflowDto>(json) ?? new WorkflowDto();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new WorkflowDto();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record WorkflowRunContext(
|
||||
string ListFormCode,
|
||||
string KeyFieldName,
|
||||
object[] Keys,
|
||||
WorkflowDto Workflow,
|
||||
List<ListFormWorkflow> Criteria,
|
||||
IDictionary<string, object> Row)
|
||||
{
|
||||
public HashSet<string> UserUpdatedFields { get; } = [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3624,6 +3624,12 @@
|
|||
"en": "MENU",
|
||||
"tr": "MENÜ"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListForm.SelectRecord",
|
||||
"en": "Please select a row.",
|
||||
"tr": "Lütfen bir satır seçin."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListForm.ClearRedisCache",
|
||||
|
|
@ -16292,6 +16298,12 @@
|
|||
"en": "Status",
|
||||
"tr": "Durum"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Listform.ListformField.Connection",
|
||||
"en": "Connection",
|
||||
"tr": "Bağlantı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Listform.ListformField.FailureReason",
|
||||
|
|
@ -16580,6 +16592,48 @@
|
|||
"en": "Title",
|
||||
"tr": "Başlık"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Listform.ListformField.Approver",
|
||||
"en": "Approver",
|
||||
"tr": "Onayla"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Listform.ListformField.NextOnStart",
|
||||
"en": "Next on Start",
|
||||
"tr": "Sonraki Adım"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Listform.ListformField.NextOnApprove",
|
||||
"en": "Next on Approve",
|
||||
"tr": "Onay Adımı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Listform.ListformField.NextOnReject",
|
||||
"en": "Next on Reject",
|
||||
"tr": "Red Adımı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Listform.ListformField.AddCompareOutcome",
|
||||
"en": "Add Compare Outcome",
|
||||
"tr": "Karşılaştırma Adımı Ekle"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Listform.ListformField.CompareOutcomes",
|
||||
"en": "Compare Outcomes",
|
||||
"tr": "Karşılaştırma Adımları"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Listform.ListformField.LoadingColumns",
|
||||
"en": "Loading Columns...",
|
||||
"tr": "Sütunlar Yükleniyor..."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.Listform.ListformField.Total",
|
||||
|
|
@ -18745,6 +18799,12 @@
|
|||
"key": "ListForms.ListFormEdit.Workflow.ApprovalStatusFieldName",
|
||||
"en": "Approval Status Field Name",
|
||||
"tr": "Onay Durumu Alanı Adı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.Workflow.ApprovalDescriptionFieldName",
|
||||
"en": "Approval Description Field Name",
|
||||
"tr": "Onay Açıklaması Alanı Adı"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -135,6 +135,8 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
|
|||
List<ListFormCustomization> listFormCustomizations = null,
|
||||
QueryParameters queryParams = null)
|
||||
{
|
||||
ResetQueryState();
|
||||
|
||||
if (listform == null)
|
||||
{
|
||||
return;
|
||||
|
|
@ -213,6 +215,28 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
|
|||
#endregion
|
||||
}
|
||||
|
||||
private void ResetQueryState()
|
||||
{
|
||||
SelectCommand = null;
|
||||
TableName = null;
|
||||
KeyFieldName = null;
|
||||
SelectFields = [];
|
||||
JoinParts = [];
|
||||
WhereParts = [];
|
||||
SortParts = [];
|
||||
SelectQuery = null;
|
||||
SelectQueryParameters = [];
|
||||
TotalCountQuery = null;
|
||||
GroupQuery = null;
|
||||
DeleteQuery = null;
|
||||
ChartQuery = null;
|
||||
GroupTuples = [];
|
||||
GroupSummaryTuples = [];
|
||||
SummaryQueries = [];
|
||||
IsAppliedGridFilter = false;
|
||||
IsAppliedServerFilter = false;
|
||||
}
|
||||
|
||||
private List<SelectField> GetSelectAndJoinFields(List<ListFormField> listFormFields)
|
||||
{
|
||||
List<SelectField> selectFields = [];
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Sozsoft.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20260523104659_Initial")]
|
||||
[Migration("20260523160811_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -1,6 +1,17 @@
|
|||
{
|
||||
"commit": "e9d8f5e",
|
||||
"commit": "8f3932b",
|
||||
"releases": [
|
||||
{
|
||||
"version": "1.0.10",
|
||||
"buildDate": "2026-05-11",
|
||||
"commit": "414006204e324018be597a598d3dd102868c5cca",
|
||||
"changeLog": [
|
||||
"- Backup dosyalarını son 5 günün kalması",
|
||||
"- Grid için Fit Columns özelliği eklendi.",
|
||||
"- Notification Desktop, UiActivity, UiToast özelliği eklendi",
|
||||
"- Versiyon güncellemeleri için \"System Updating\" mesajı"
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.9",
|
||||
"buildDate": "2026-05-09",
|
||||
|
|
|
|||
|
|
@ -586,6 +586,7 @@ export interface GridOptionsDto extends AuditedEntityDto<string> {
|
|||
extraFilterJson?: string
|
||||
extraFilterDto: ExtraFilterDto[]
|
||||
layoutDto: LayoutDto
|
||||
workflowDto: WorkflowDto
|
||||
|
||||
//ChartEditDto
|
||||
userId?: string
|
||||
|
|
@ -663,7 +664,6 @@ export interface GridOptionsEditDto extends GridOptionsDto, Record<string, any>
|
|||
formFieldsDefaultValueDto: FieldsDefaultValueDto[]
|
||||
widgetsJson?: string
|
||||
widgetsDto: WidgetEditDto[]
|
||||
workflowDto: WorkflowDto
|
||||
extraFilterEditDto: ExtraFilterEditDto[]
|
||||
}
|
||||
|
||||
|
|
@ -910,6 +910,39 @@ export interface WorkflowDto {
|
|||
approvalUserFieldName: string
|
||||
approvalDateFieldName: string
|
||||
approvalStatusFieldName: string
|
||||
approvalDescriptionFieldName: string
|
||||
criteria: ListFormWorkflowCriteriaDto[]
|
||||
}
|
||||
|
||||
export interface WorkflowConditionDto {
|
||||
compareColumn: string
|
||||
compareOperator: string
|
||||
compareValue: number
|
||||
}
|
||||
|
||||
export interface CompareOutcomeDto {
|
||||
label: string
|
||||
targetId?: string | null
|
||||
conditions: WorkflowConditionDto[]
|
||||
}
|
||||
|
||||
export interface ListFormWorkflowCriteriaDto {
|
||||
id: string
|
||||
listFormCode: string
|
||||
kind: string
|
||||
title: string
|
||||
compareColumn: string
|
||||
compareOperator: string
|
||||
compareValue: number
|
||||
approver: string
|
||||
nextOnStart?: string | null
|
||||
nextOnTrue?: string | null
|
||||
nextOnFalse?: string | null
|
||||
nextOnApprove?: string | null
|
||||
nextOnReject?: string | null
|
||||
positionX: number
|
||||
positionY: number
|
||||
compareOutcomes: CompareOutcomeDto[]
|
||||
}
|
||||
|
||||
export interface LayoutDto {
|
||||
|
|
|
|||
|
|
@ -32,10 +32,19 @@ export interface WorkflowCriteriaDto {
|
|||
compareOutcomes: CompareOutcomeDto[]
|
||||
}
|
||||
|
||||
export interface WorkflowStateDto {
|
||||
export interface WorkflowCriteriasDto {
|
||||
criteria: WorkflowCriteriaDto[]
|
||||
}
|
||||
|
||||
export interface WorkflowRunResultDto {
|
||||
keys: unknown[]
|
||||
currentNodeId?: string | null
|
||||
currentNodeTitle?: string | null
|
||||
currentNodeKind?: string | null
|
||||
waitingApproval: boolean
|
||||
completed: boolean
|
||||
}
|
||||
|
||||
export type SaveCriteriaInput = Omit<Partial<WorkflowCriteriaDto>, 'id'> & {
|
||||
id?: string | null
|
||||
listFormCode: string
|
||||
|
|
@ -44,10 +53,10 @@ export type SaveCriteriaInput = Omit<Partial<WorkflowCriteriaDto>, 'id'> & {
|
|||
const baseUrl = '/api/app/list-form-workflow'
|
||||
|
||||
export const workflowService = {
|
||||
async getState(listFormCode?: string) {
|
||||
const response = await apiService.fetchData<WorkflowStateDto>({
|
||||
async getCriteria(listFormCode?: string) {
|
||||
const response = await apiService.fetchData<WorkflowCriteriasDto>({
|
||||
method: 'GET',
|
||||
url: `${baseUrl}/state`,
|
||||
url: `${baseUrl}/criteria`,
|
||||
params: { listFormCode },
|
||||
})
|
||||
|
||||
|
|
@ -72,7 +81,7 @@ export const workflowService = {
|
|||
},
|
||||
|
||||
async resetDemo(listFormCode?: string) {
|
||||
const response = await apiService.fetchData<WorkflowStateDto>({
|
||||
const response = await apiService.fetchData<WorkflowCriteriasDto>({
|
||||
method: 'POST',
|
||||
url: `${baseUrl}/reset-demo`,
|
||||
params: { listFormCode },
|
||||
|
|
@ -80,4 +89,30 @@ export const workflowService = {
|
|||
|
||||
return response.data
|
||||
},
|
||||
|
||||
async startWorkflow(listFormCode: string, keys: unknown[]) {
|
||||
const response = await apiService.fetchData<WorkflowRunResultDto>({
|
||||
method: 'POST',
|
||||
url: `${baseUrl}/start`,
|
||||
data: { listFormCode, keys },
|
||||
})
|
||||
|
||||
return response.data
|
||||
},
|
||||
|
||||
async decideWorkflow(
|
||||
listFormCode: string,
|
||||
keys: unknown[],
|
||||
approved: boolean,
|
||||
note?: string,
|
||||
currentNodeId?: string,
|
||||
) {
|
||||
const response = await apiService.fetchData<WorkflowRunResultDto>({
|
||||
method: 'POST',
|
||||
url: `${baseUrl}/decision`,
|
||||
data: { listFormCode, keys, approved, note, currentNodeId },
|
||||
})
|
||||
|
||||
return response.data
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useLocalization } from '../hooks/useLocalization'
|
||||
import { getNodeHeight, nodeSize } from './workflowConstants'
|
||||
import type {
|
||||
CompareOutcomeDto,
|
||||
|
|
@ -311,7 +312,7 @@ export function emptyCriteria(kind = 'Compare', listFormCode = ''): WorkflowCrit
|
|||
listFormCode,
|
||||
kind,
|
||||
title: defaultTitle(kind),
|
||||
compareColumn: 'Tutar',
|
||||
compareColumn: 'Price',
|
||||
compareOperator: '>',
|
||||
compareValue: 5000,
|
||||
approver: '',
|
||||
|
|
@ -347,19 +348,31 @@ export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm
|
|||
|
||||
export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput {
|
||||
const sharedPerson = item.approver || ''
|
||||
|
||||
return {
|
||||
...item,
|
||||
id: item.id || null,
|
||||
listFormCode: item.listFormCode || '',
|
||||
compareValue: Number(item.compareValue || 0),
|
||||
approver: sharedPerson,
|
||||
positionX: Number(item.positionX || 32),
|
||||
positionY: Number(item.positionY || 150),
|
||||
compareOutcomes: (item.compareOutcomes || [])
|
||||
const compareOutcomes = (item.compareOutcomes || [])
|
||||
.slice(0, 4)
|
||||
.filter((outcome) => outcome.label?.trim())
|
||||
.map(normalizeCompareOutcome),
|
||||
.map(normalizeCompareOutcome)
|
||||
const firstCompareColumn = compareOutcomes
|
||||
.flatMap((outcome) => outcome.conditions || [])
|
||||
.find((condition) => condition.compareColumn)?.compareColumn
|
||||
|
||||
return {
|
||||
id: item.id || null,
|
||||
listFormCode: item.listFormCode || '',
|
||||
kind: item.kind || 'Compare',
|
||||
title: item.title || defaultTitle(item.kind || 'Compare'),
|
||||
compareColumn: firstCompareColumn || item.compareColumn || 'Price',
|
||||
compareOperator: item.compareOperator || '>',
|
||||
compareValue: Number(item.compareValue || 0),
|
||||
approver: sharedPerson,
|
||||
nextOnStart: item.nextOnStart || '',
|
||||
nextOnTrue: compareOutcomes[0]?.targetId || item.nextOnTrue || '',
|
||||
nextOnFalse: compareOutcomes[1]?.targetId || item.nextOnFalse || '',
|
||||
nextOnApprove: item.nextOnApprove || '',
|
||||
nextOnReject: item.nextOnReject || '',
|
||||
positionX: Number(item.positionX || 32),
|
||||
positionY: Number(item.positionY || 150),
|
||||
compareOutcomes,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -367,27 +380,27 @@ export function defaultTitle(kind: string) {
|
|||
return (
|
||||
{
|
||||
Start: 'İş Akışı Başlat',
|
||||
Compare: 'Tutar > 5000 TL',
|
||||
Approval: 'Onaylanacak Kişi',
|
||||
Inform: 'Bilgilendirme Yapılacak Personel',
|
||||
End: 'Akışı Bitir',
|
||||
Compare: 'Karşılaştırma',
|
||||
Approval: 'Onay',
|
||||
Inform: 'Bilgilendirme',
|
||||
End: 'İş Akışı Bitir',
|
||||
}[kind] ?? 'İş Akışı Adımı'
|
||||
)
|
||||
}
|
||||
|
||||
export function emptyCompareOutcome1(label = 'Durum'): CompareOutcomeDto {
|
||||
export function emptyCompareOutcome1(label = 'Durum', compareColumn = 'Price'): CompareOutcomeDto {
|
||||
return {
|
||||
label,
|
||||
targetId: '',
|
||||
conditions: [{ compareColumn: 'Tutar', compareOperator: '>', compareValue: 5000 }],
|
||||
conditions: [{ compareColumn, compareOperator: '>', compareValue: 5000 }],
|
||||
}
|
||||
}
|
||||
|
||||
export function emptyCompareOutcome2(label = 'Durum'): CompareOutcomeDto {
|
||||
export function emptyCompareOutcome2(label = 'Durum', compareColumn = 'Price'): CompareOutcomeDto {
|
||||
return {
|
||||
label,
|
||||
targetId: '',
|
||||
conditions: [{ compareColumn: 'Tutar', compareOperator: '<=', compareValue: 5000 }],
|
||||
conditions: [{ compareColumn, compareOperator: '<=', compareValue: 5000 }],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -401,7 +414,7 @@ export function toCompareOutcomeForm(
|
|||
? outcome.conditions
|
||||
: [
|
||||
{
|
||||
compareColumn: outcome.compareColumn || 'Tutar',
|
||||
compareColumn: outcome.compareColumn || 'Price',
|
||||
compareOperator: outcome.compareOperator || '>',
|
||||
compareValue: outcome.compareValue || 0,
|
||||
},
|
||||
|
|
@ -411,7 +424,7 @@ export function toCompareOutcomeForm(
|
|||
label: outcome.label || '',
|
||||
targetId: outcome.targetId || '',
|
||||
conditions: conditions.map((condition) => ({
|
||||
compareColumn: condition.compareColumn || 'Tutar',
|
||||
compareColumn: condition.compareColumn || 'Price',
|
||||
compareOperator: condition.compareOperator || '>',
|
||||
compareValue: condition.compareValue ?? 0,
|
||||
})),
|
||||
|
|
@ -429,7 +442,7 @@ export function normalizeCompareOutcome(
|
|||
? outcome.conditions
|
||||
: [
|
||||
{
|
||||
compareColumn: outcome.compareColumn || 'Tutar',
|
||||
compareColumn: outcome.compareColumn || 'Price',
|
||||
compareOperator: outcome.compareOperator || '>',
|
||||
compareValue: outcome.compareValue || 0,
|
||||
},
|
||||
|
|
@ -437,7 +450,7 @@ export function normalizeCompareOutcome(
|
|||
)
|
||||
.filter((condition) => condition.compareOperator && String(condition.compareValue ?? '') !== '')
|
||||
.map((condition) => ({
|
||||
compareColumn: condition.compareColumn || 'Tutar',
|
||||
compareColumn: condition.compareColumn || 'Price',
|
||||
compareOperator: condition.compareOperator || '>',
|
||||
compareValue: Number(condition.compareValue || 0),
|
||||
}))
|
||||
|
|
@ -488,9 +501,10 @@ export function criteriaSummary(item: WorkflowCriteriaDto) {
|
|||
.join(' / ') || '-'
|
||||
)
|
||||
}
|
||||
if (item.kind === 'Approval') return item.approver || '-'
|
||||
if (item.kind === 'Inform') return item.approver || '-'
|
||||
return item.title
|
||||
if (item.kind === 'Approval' || item.kind === 'Inform') {
|
||||
return `${item.title} ${item.approver ? `- ${item.approver}` : ''}`
|
||||
}
|
||||
return `${item.title} ${item.approver ? `- ${item.approver}` : ''}`
|
||||
}
|
||||
|
||||
export function targetTitle(criteria: WorkflowCriteriaDto[], id?: string | null) {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export function FormTabWorkflow(
|
|||
)
|
||||
|
||||
const loadState = useCallback(async () => {
|
||||
const data = await workflowService.getState(props.listFormCode)
|
||||
const data = await workflowService.getCriteria(props.listFormCode)
|
||||
setCriteria(data.criteria)
|
||||
return data
|
||||
}, [props.listFormCode])
|
||||
|
|
@ -299,6 +299,7 @@ export function FormTabWorkflow(
|
|||
approvalUserFieldName: string(),
|
||||
approvalDateFieldName: string(),
|
||||
approvalStatusFieldName: string(),
|
||||
approvalDescriptionFieldName: string(),
|
||||
})
|
||||
|
||||
const initialValues = useStoreState((s) => s.admin.lists.values)
|
||||
|
|
@ -319,7 +320,7 @@ export function FormTabWorkflow(
|
|||
<Form>
|
||||
<FormContainer size="sm">
|
||||
<Card className="my-2">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-2">
|
||||
<FormItem
|
||||
label={translate('::ListForms.ListFormEdit.Workflow.ApprovalUserFieldName')}
|
||||
invalid={
|
||||
|
|
@ -391,6 +392,33 @@ export function FormTabWorkflow(
|
|||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label={translate(
|
||||
'::ListForms.ListFormEdit.Workflow.ApprovalDescriptionFieldName',
|
||||
)}
|
||||
invalid={
|
||||
errors.workflowDto?.approvalDescriptionFieldName &&
|
||||
touched.workflowDto?.approvalDescriptionFieldName
|
||||
}
|
||||
errorMessage={errors.workflowDto?.approvalDescriptionFieldName}
|
||||
>
|
||||
<Field type="text" name="workflowDto.approvalDescriptionFieldName">
|
||||
{({ field, form }: FieldProps<SelectBoxOption>) => (
|
||||
<Select
|
||||
field={field}
|
||||
form={form}
|
||||
options={columnOptions}
|
||||
isClearable={true}
|
||||
value={columnOptions.filter(
|
||||
(option) =>
|
||||
option.value === values.workflowDto.approvalDescriptionFieldName,
|
||||
)}
|
||||
onChange={(option) => form.setFieldValue(field.name, option?.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
<Button block variant="solid" loading={isSubmitting}>
|
||||
|
|
|
|||
|
|
@ -54,13 +54,55 @@ export function WorkflowCriteria({
|
|||
.filter((item) => item.id !== formValues.id)
|
||||
.map((item) => ({ value: item.id, label: `${item.id} - ${item.title}` })),
|
||||
]
|
||||
const numericCompareColumn = selectCommandColumns.find((column) =>
|
||||
isNumericDataType(column.dataType),
|
||||
)
|
||||
const compareColumnOptions = selectCommandColumns.length
|
||||
? selectCommandColumns.map((column) => ({
|
||||
value: column.columnName,
|
||||
label: `${column.columnName} (${column.dataType})`,
|
||||
}))
|
||||
: []
|
||||
const defaultCompareColumn = compareColumnOptions[0]?.value ?? 'Tutar'
|
||||
const defaultCompareColumn =
|
||||
numericCompareColumn?.columnName ?? compareColumnOptions[0]?.value ?? 'Price'
|
||||
|
||||
React.useEffect(() => {
|
||||
if (formValues.kind !== 'Compare' || !compareColumnOptions.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const validColumns = new Set(compareColumnOptions.map((option) => option.value))
|
||||
let changed = false
|
||||
const nextOutcomes = (formValues.compareOutcomes || []).map(
|
||||
(outcome: CompareOutcomeDto) => ({
|
||||
...outcome,
|
||||
conditions: (outcome.conditions || []).map((condition) => {
|
||||
if (validColumns.has(condition.compareColumn)) {
|
||||
return condition
|
||||
}
|
||||
|
||||
changed = true
|
||||
return { ...condition, compareColumn: defaultCompareColumn }
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
const nextCompareColumn = validColumns.has(formValues.compareColumn)
|
||||
? formValues.compareColumn
|
||||
: defaultCompareColumn
|
||||
|
||||
if (nextCompareColumn !== formValues.compareColumn) {
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
onChange({
|
||||
...formValues,
|
||||
compareColumn: nextCompareColumn,
|
||||
compareOutcomes: nextOutcomes,
|
||||
})
|
||||
}
|
||||
}, [compareColumnOptions, defaultCompareColumn, formValues, onChange])
|
||||
const updateCompareOutcome = (index: number, patch: Partial<CompareOutcomeDto>) => {
|
||||
const next = [...(formValues.compareOutcomes || [])]
|
||||
next[index] = { ...next[index], ...patch }
|
||||
|
|
@ -199,7 +241,7 @@ export function WorkflowCriteria({
|
|||
<div className="flex-1 min-h-0 overflow-y-auto pr-1">
|
||||
<FormContainer size="sm">
|
||||
<div className="grid grid-cols-2 gap-1">
|
||||
<FormItem label="Tip" asterisk>
|
||||
<FormItem label={translate('::App.Platform.Type')} asterisk>
|
||||
<SelectField
|
||||
required
|
||||
options={kindOptions}
|
||||
|
|
@ -207,7 +249,7 @@ export function WorkflowCriteria({
|
|||
onChange={(value) => setField('kind', value)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="Başlık" asterisk>
|
||||
<FormItem label={translate('::App.Listform.ListformField.Title')} asterisk>
|
||||
<Input
|
||||
required
|
||||
value={formValues.title}
|
||||
|
|
@ -215,11 +257,11 @@ export function WorkflowCriteria({
|
|||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label="Onaylayacak Kişi"
|
||||
label={translate('::App.Listform.ListformField.Approver')}
|
||||
asterisk={formValues.kind === 'Approval' || formValues.kind === 'Inform'}
|
||||
>
|
||||
<SelectField
|
||||
required
|
||||
required={formValues.kind === 'Approval' || formValues.kind === 'Inform'}
|
||||
options={userList}
|
||||
value={formValues.approver}
|
||||
onChange={(value) => setField('approver', value)}
|
||||
|
|
@ -227,7 +269,7 @@ export function WorkflowCriteria({
|
|||
</FormItem>
|
||||
|
||||
{(formValues.kind === 'Start' || formValues.kind === 'Inform') && (
|
||||
<FormItem label="Sonraki adım" asterisk>
|
||||
<FormItem label={translate('::App.Listform.ListformField.NextOnStart')} asterisk>
|
||||
{targetSelect(
|
||||
formValues.nextOnStart,
|
||||
(value) => setField('nextOnStart', value),
|
||||
|
|
@ -238,14 +280,14 @@ export function WorkflowCriteria({
|
|||
|
||||
{formValues.kind === 'Approval' && (
|
||||
<>
|
||||
<FormItem label="Onay adımı" asterisk>
|
||||
<FormItem label={translate('::App.Listform.ListformField.NextOnApprove')} asterisk>
|
||||
{targetSelect(
|
||||
formValues.nextOnApprove,
|
||||
(value) => setField('nextOnApprove', value),
|
||||
true,
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem label="Red adımı" asterisk>
|
||||
<FormItem label={translate('::App.Listform.ListformField.NextOnReject')} asterisk>
|
||||
{targetSelect(
|
||||
formValues.nextOnReject,
|
||||
(value) => setField('nextOnReject', value),
|
||||
|
|
@ -260,10 +302,10 @@ export function WorkflowCriteria({
|
|||
<div className="mb-4">
|
||||
<div className="mb-3 flex items-center justify-between gap-2">
|
||||
<h6>
|
||||
Karşılaştırma durumları
|
||||
{translate('::App.Listform.ListformField.CompareOutcomes')}
|
||||
{isLoadingSelectCommandColumns && (
|
||||
<span className="ml-2 text-xs font-normal text-gray-400">
|
||||
Sütunlar yükleniyor...
|
||||
{translate('::App.Listform.ListformField.LoadingColumns')}
|
||||
</span>
|
||||
)}
|
||||
</h6>
|
||||
|
|
@ -276,11 +318,12 @@ export function WorkflowCriteria({
|
|||
...(formValues.compareOutcomes || []),
|
||||
emptyCompareOutcome1(
|
||||
`Durum ${(formValues.compareOutcomes || []).length + 1}`,
|
||||
defaultCompareColumn,
|
||||
),
|
||||
])
|
||||
}
|
||||
>
|
||||
Karşılaştırma Ekle
|
||||
{translate('::App.Listform.ListformField.AddCompareOutcome')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -289,8 +332,8 @@ export function WorkflowCriteria({
|
|||
(outcome: CompareOutcomeDto, index: number) => (
|
||||
<div key={index} className="rounded border border-gray-200 p-3">
|
||||
<div className="flex flex-col-11 items-center gap-2 mb-2">
|
||||
<strong className="flex-[5]">Durum {index + 1}</strong>
|
||||
<strong className="flex-[5]">Bağlantı</strong>
|
||||
<strong className="flex-[5]">{translate('::App.Listform.ListformField.Status')} {index + 1}</strong>
|
||||
<strong className="flex-[5]">{translate('::App.Listform.ListformField.Connection')}</strong>
|
||||
<span className="flex-1" />
|
||||
</div>
|
||||
<div className="flex flex-col-11 items-center gap-2">
|
||||
|
|
@ -322,7 +365,7 @@ export function WorkflowCriteria({
|
|||
disabled={(formValues.compareOutcomes || []).length <= 2}
|
||||
onClick={() => removeCompareOutcome(index)}
|
||||
>
|
||||
Sil
|
||||
{translate('::Delete')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -381,7 +424,7 @@ export function WorkflowCriteria({
|
|||
onClick={() => addCompareCondition(index)}
|
||||
className="flex-[1]"
|
||||
>
|
||||
Ekle
|
||||
{translate('::Insert')}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -398,7 +441,7 @@ export function WorkflowCriteria({
|
|||
|
||||
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
|
||||
<Button type="button" variant="plain" disabled={busy} onClick={closeDialog}>
|
||||
Cancel
|
||||
{translate('::Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -406,10 +449,10 @@ export function WorkflowCriteria({
|
|||
disabled={busy || !formValues.id}
|
||||
onClick={() => onDelete(formValues.id)}
|
||||
>
|
||||
Sil
|
||||
{translate('::Delete')}
|
||||
</Button>
|
||||
<Button variant="solid" loading={busy} type="submit">
|
||||
Kaydet
|
||||
{translate('::Save')}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
|
|
@ -418,6 +461,20 @@ export function WorkflowCriteria({
|
|||
)
|
||||
}
|
||||
|
||||
function isNumericDataType(dataType?: string | null) {
|
||||
const normalized = (dataType || '').toLowerCase()
|
||||
return [
|
||||
'int',
|
||||
'decimal',
|
||||
'numeric',
|
||||
'money',
|
||||
'float',
|
||||
'real',
|
||||
'double',
|
||||
'number',
|
||||
].some((typeName) => normalized.includes(typeName))
|
||||
}
|
||||
|
||||
function SelectField({
|
||||
options,
|
||||
value,
|
||||
|
|
|
|||
|
|
@ -250,6 +250,23 @@ function colToSqlLine(col: ColumnDefinition, addComma = true): string {
|
|||
return ` [${col.columnName}] ${typeSql} ${nullPart}${defaultPart}${addComma ? ',' : ''}`
|
||||
}
|
||||
|
||||
function buildCreateIndexIfNotExistsSql(
|
||||
fullTableName: string,
|
||||
indexName: string,
|
||||
isClustered: boolean,
|
||||
colsSql: string,
|
||||
): string[] {
|
||||
const escapedFullTableName = fullTableName.replace(/'/g, "''")
|
||||
const escapedIndexName = indexName.replace(/'/g, "''")
|
||||
|
||||
return [
|
||||
`IF INDEXPROPERTY(OBJECT_ID(N'${escapedFullTableName}'), '${escapedIndexName}', 'IndexID') IS NULL`,
|
||||
`BEGIN`,
|
||||
` CREATE ${isClustered ? 'CLUSTERED ' : ''}INDEX [${indexName}] ON ${fullTableName} (${colsSql});`,
|
||||
`END`,
|
||||
]
|
||||
}
|
||||
|
||||
function generateCreateTableSql(
|
||||
columns: ColumnDefinition[],
|
||||
settings: TableSettings,
|
||||
|
|
@ -305,7 +322,12 @@ function generateCreateTableSql(
|
|||
} else {
|
||||
extraIndexLines.push(`-- 📋 Index: [${idx.indexName}]`)
|
||||
extraIndexLines.push(
|
||||
`CREATE ${idx.isClustered ? 'CLUSTERED ' : ''}INDEX [${idx.indexName}] ON ${fullTableName} (${colsSql});`,
|
||||
...buildCreateIndexIfNotExistsSql(
|
||||
fullTableName,
|
||||
idx.indexName,
|
||||
idx.isClustered,
|
||||
colsSql,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -715,7 +737,12 @@ function generateAlterTableSql(
|
|||
} else {
|
||||
lines.push(`-- 📋 Index: [${ix.indexName}]`)
|
||||
lines.push(
|
||||
`CREATE ${ix.isClustered ? 'CLUSTERED ' : ''}INDEX [${ix.indexName}] ON ${fullTableName} (${colsSql});`,
|
||||
...buildCreateIndexIfNotExistsSql(
|
||||
fullTableName,
|
||||
ix.indexName,
|
||||
ix.isClustered,
|
||||
colsSql,
|
||||
),
|
||||
)
|
||||
}
|
||||
lines.push('')
|
||||
|
|
|
|||
|
|
@ -253,7 +253,11 @@ const useGridData = (props: {
|
|||
name: i.dataField,
|
||||
editorType2: i.editorType2,
|
||||
editorType:
|
||||
i.editorType2 == PlatformEditorTypes.dxGridBox ? 'dxDropDownBox' : i.editorType2,
|
||||
i.editorType2 == PlatformEditorTypes.dxGridBox
|
||||
? 'dxDropDownBox'
|
||||
: i.editorType2 == PlatformEditorTypes.dxImageUpload
|
||||
? undefined
|
||||
: i.editorType2,
|
||||
colSpan: i.colSpan,
|
||||
isRequired: i.isRequired,
|
||||
editorOptions: {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
|
|||
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
|
||||
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
|
||||
import { useFilters } from './useFilters'
|
||||
import { useToolbar } from './useToolbar'
|
||||
import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
|
||||
import { ImportDashboard } from '@/components/importManager/ImportDashboard'
|
||||
import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
|
||||
import { GridExtraFilterToolbar } from './GridExtraFilterToolbar'
|
||||
|
|
@ -75,6 +75,7 @@ import { useListFormCustomDataSource } from './useListFormCustomDataSource'
|
|||
import { useListFormColumns } from './useListFormColumns'
|
||||
import { Loading } from '@/components/shared'
|
||||
import { useStoreState } from '@/store'
|
||||
import { workflowService } from '@/services/workflow.service'
|
||||
|
||||
interface GridProps {
|
||||
listFormCode: string
|
||||
|
|
@ -87,6 +88,24 @@ interface GridProps {
|
|||
|
||||
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' // kullanici tanimli gridState ile islem gormus gridin paneline ait renk
|
||||
|
||||
const isTemporaryDxKey = (key: unknown) => typeof key === 'string' && key.startsWith('_DX_KEY_')
|
||||
|
||||
const getPersistedInsertedKey = (
|
||||
e: DataGridTypes.RowInsertedEvent<any, any>,
|
||||
keyFieldName?: string,
|
||||
) => {
|
||||
const dataKey = keyFieldName ? e.data?.[keyFieldName] : undefined
|
||||
if (dataKey !== undefined && dataKey !== null && !isTemporaryDxKey(dataKey)) {
|
||||
return dataKey
|
||||
}
|
||||
|
||||
if (e.key !== undefined && e.key !== null && !isTemporaryDxKey(e.key)) {
|
||||
return e.key
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const Grid = (props: GridProps) => {
|
||||
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
|
||||
const { translate } = useLocalization()
|
||||
|
|
@ -208,9 +227,28 @@ const Grid = (props: GridProps) => {
|
|||
return grd.getSelectedRowsData()
|
||||
}, [])
|
||||
|
||||
const updateWorkflowApprovalButtons = useCallback(
|
||||
(component?: any, selectedRowsData?: Record<string, unknown>[]) => {
|
||||
const grd = component ?? gridRef.current?.instance()
|
||||
if (!grd) {
|
||||
return
|
||||
}
|
||||
|
||||
updateWorkflowApprovalToolbarItems(
|
||||
grd,
|
||||
gridDto?.gridOptions.workflowDto,
|
||||
selectedRowsData ?? grd.getSelectedRowsData(),
|
||||
config?.currentUser,
|
||||
)
|
||||
},
|
||||
[config?.currentUser, gridDto],
|
||||
)
|
||||
|
||||
const refreshData = useCallback(() => {
|
||||
gridRef.current?.instance()?.refresh()
|
||||
}, [])
|
||||
const grd = gridRef.current?.instance()
|
||||
const refreshResult = grd?.refresh()
|
||||
Promise.resolve(refreshResult).finally(() => updateWorkflowApprovalButtons(grd))
|
||||
}, [updateWorkflowApprovalButtons])
|
||||
|
||||
const getFilter = useCallback(() => {
|
||||
const grd = gridRef.current?.instance()
|
||||
|
|
@ -257,6 +295,8 @@ const Grid = (props: GridProps) => {
|
|||
}
|
||||
|
||||
// SubForm'ları gösterebilmek için secili satiri formData'ya at
|
||||
updateWorkflowApprovalButtons(grd, data.selectedRowsData)
|
||||
|
||||
if (data.selectedRowsData.length) {
|
||||
setFormData(data.selectedRowsData[0])
|
||||
}
|
||||
|
|
@ -401,12 +441,15 @@ const Grid = (props: GridProps) => {
|
|||
[gridDto, searchParams, extraFilters, getNextSequenceValue],
|
||||
)
|
||||
|
||||
const onRowInserting = useCallback((e: DataGridTypes.RowInsertingEvent<any, any>) => {
|
||||
const onRowInserting = useCallback(
|
||||
(e: DataGridTypes.RowInsertingEvent<any, any>) => {
|
||||
if (!gridDto?.columnFormats) {
|
||||
e.data = setFormEditingExtraItemValues(e.data)
|
||||
return
|
||||
}
|
||||
const allowedFields = gridDto.columnFormats.filter(f => f.allowAdding).map(f => f.fieldName)
|
||||
const allowedFields = gridDto.columnFormats
|
||||
.filter((f) => f.allowAdding)
|
||||
.map((f) => f.fieldName)
|
||||
const filteredData: any = {}
|
||||
for (const key of allowedFields) {
|
||||
if (e.data.hasOwnProperty(key) && key) {
|
||||
|
|
@ -414,12 +457,16 @@ const Grid = (props: GridProps) => {
|
|||
}
|
||||
}
|
||||
e.data = setFormEditingExtraItemValues(filteredData)
|
||||
}, [gridDto])
|
||||
},
|
||||
[gridDto],
|
||||
)
|
||||
|
||||
const onRowUpdating = useCallback(
|
||||
(e: DataGridTypes.RowUpdatingEvent<any, any>) => {
|
||||
if (!gridDto?.columnFormats) return
|
||||
const allowedFields = gridDto.columnFormats.filter(f => f.allowEditing).map(f => f.fieldName)
|
||||
const allowedFields = gridDto.columnFormats
|
||||
.filter((f) => f.allowEditing)
|
||||
.map((f) => f.fieldName)
|
||||
let newData = { ...e.oldData, ...e.newData }
|
||||
// Remove keys not allowed
|
||||
Object.keys(newData).forEach((key) => {
|
||||
|
|
@ -521,6 +568,19 @@ const Grid = (props: GridProps) => {
|
|||
const formItem = gridDto.gridOptions.editingFormDto
|
||||
.flatMap((group) => group.items || [])
|
||||
.find((i) => i.dataField === editor.dataField)
|
||||
const fieldName = editor.dataField.split(':')[0]
|
||||
const columnFormat = gridDto.columnFormats.find((column) => column.fieldName === fieldName)
|
||||
const isNewRow = Boolean((editor as any).row?.isNewRow) || mode === 'new'
|
||||
|
||||
if (
|
||||
(isNewRow && columnFormat?.allowAdding === false) ||
|
||||
(!isNewRow && columnFormat?.allowEditing === false)
|
||||
) {
|
||||
editor.editorOptions.readOnly = true
|
||||
} else if (isNewRow && columnFormat?.allowAdding === true) {
|
||||
editor.editorOptions.readOnly = false
|
||||
editor.editorOptions.disabled = false
|
||||
}
|
||||
|
||||
// Cascade mantığı
|
||||
const cascadeInfo = cascadeFieldsMap.get(editor.dataField)
|
||||
|
|
@ -1192,6 +1252,7 @@ const Grid = (props: GridProps) => {
|
|||
showColumnHeaders={gridDto.gridOptions.columnOptionDto?.showColumnHeaders}
|
||||
filterSyncEnabled={true}
|
||||
onSelectionChanged={onSelectionChanged}
|
||||
onContentReady={(e) => updateWorkflowApprovalButtons(e.component)}
|
||||
onInitNewRow={onInitNewRow}
|
||||
onCellPrepared={onCellPrepared}
|
||||
onRowInserting={onRowInserting}
|
||||
|
|
@ -1212,7 +1273,18 @@ const Grid = (props: GridProps) => {
|
|||
setMode('view')
|
||||
setIsPopupFullScreen(false)
|
||||
}}
|
||||
onRowInserted={() => {
|
||||
onRowInserted={(e) => {
|
||||
const insertedKey = getPersistedInsertedKey(e, gridDto.gridOptions.keyFieldName)
|
||||
|
||||
if (
|
||||
gridDto.gridOptions.workflowDto?.approvalStatusFieldName &&
|
||||
insertedKey !== undefined
|
||||
) {
|
||||
workflowService
|
||||
.startWorkflow(listFormCode, [insertedKey])
|
||||
.then(() => gridRef.current?.instance()?.refresh())
|
||||
.catch(console.error)
|
||||
}
|
||||
props.refreshData?.()
|
||||
}}
|
||||
onRowUpdated={() => {
|
||||
|
|
@ -1342,9 +1414,9 @@ const Grid = (props: GridProps) => {
|
|||
if (mode === 'view') {
|
||||
return a.canRead
|
||||
} else if (mode === 'new') {
|
||||
return (a.canCreate || a.canRead) && a.allowAdding
|
||||
return a.canCreate && a.allowAdding
|
||||
} else if (mode === 'edit') {
|
||||
return (a.canUpdate || a.canRead) && a.allowEditing
|
||||
return a.canUpdate && a.allowEditing
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
|
@ -1371,9 +1443,9 @@ const Grid = (props: GridProps) => {
|
|||
if (mode === 'view') {
|
||||
return a.canRead
|
||||
} else if (mode === 'new') {
|
||||
return (a.canCreate || a.canRead) && a.allowAdding
|
||||
return a.canCreate && a.allowAdding
|
||||
} else if (mode === 'edit') {
|
||||
return (a.canUpdate || a.canRead) && a.allowEditing
|
||||
return a.canUpdate && a.allowEditing
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ import {
|
|||
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
|
||||
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
|
||||
import { useFilters } from './useFilters'
|
||||
import { useToolbar } from './useToolbar'
|
||||
import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
|
||||
import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
|
||||
import { GridExtraFilterToolbar } from './GridExtraFilterToolbar'
|
||||
import { getList } from '@/services/form.service'
|
||||
|
|
@ -66,6 +66,7 @@ import { DataType } from 'devextreme/common'
|
|||
import { useStoreState } from '@/store/store'
|
||||
import SubForms from '../form/SubForms'
|
||||
import { ImportDashboard } from '@/components/importManager/ImportDashboard'
|
||||
import { workflowService } from '@/services/workflow.service'
|
||||
|
||||
interface TreeProps {
|
||||
listFormCode: string
|
||||
|
|
@ -78,6 +79,21 @@ interface TreeProps {
|
|||
|
||||
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)'
|
||||
|
||||
const isTemporaryDxKey = (key: unknown) => typeof key === 'string' && key.startsWith('_DX_KEY_')
|
||||
|
||||
const getPersistedInsertedKey = (e: any, keyFieldName?: string) => {
|
||||
const dataKey = keyFieldName ? e.data?.[keyFieldName] : undefined
|
||||
if (dataKey !== undefined && dataKey !== null && !isTemporaryDxKey(dataKey)) {
|
||||
return dataKey
|
||||
}
|
||||
|
||||
if (e.key !== undefined && e.key !== null && !isTemporaryDxKey(e.key)) {
|
||||
return e.key
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const Tree = (props: TreeProps) => {
|
||||
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
|
||||
const { translate } = useLocalization()
|
||||
|
|
@ -246,9 +262,28 @@ const Tree = (props: TreeProps) => {
|
|||
})
|
||||
}, [])
|
||||
|
||||
const updateWorkflowApprovalButtons = useCallback(
|
||||
(component?: any, selectedRowsData?: Record<string, unknown>[]) => {
|
||||
const tree = component ?? gridRef.current?.instance()
|
||||
if (!tree) {
|
||||
return
|
||||
}
|
||||
|
||||
updateWorkflowApprovalToolbarItems(
|
||||
tree,
|
||||
gridDto?.gridOptions.workflowDto,
|
||||
selectedRowsData ?? tree.getSelectedRowsData(),
|
||||
config?.currentUser,
|
||||
)
|
||||
},
|
||||
[config?.currentUser, gridDto],
|
||||
)
|
||||
|
||||
const refreshData = useCallback(() => {
|
||||
gridRef.current?.instance().refresh()
|
||||
}, [])
|
||||
const tree = gridRef.current?.instance()
|
||||
const refreshResult = tree?.refresh()
|
||||
Promise.resolve(refreshResult).finally(() => updateWorkflowApprovalButtons(tree))
|
||||
}, [updateWorkflowApprovalButtons])
|
||||
|
||||
const getFilter = useCallback(() => {
|
||||
const tree = gridRef.current?.instance()
|
||||
|
|
@ -300,6 +335,8 @@ const Tree = (props: TreeProps) => {
|
|||
}
|
||||
}
|
||||
|
||||
updateWorkflowApprovalButtons(tree, data.selectedRowsData)
|
||||
|
||||
if (data.selectedRowsData.length) {
|
||||
setFormData(data.selectedRowsData[0])
|
||||
}
|
||||
|
|
@ -480,6 +517,19 @@ const Tree = (props: TreeProps) => {
|
|||
const formItem = gridDto.gridOptions.editingFormDto
|
||||
.flatMap((group) => group.items || [])
|
||||
.find((i) => i.dataField === editor.dataField)
|
||||
const fieldName = editor.dataField.split(':')[0]
|
||||
const columnFormat = gridDto.columnFormats.find((column) => column.fieldName === fieldName)
|
||||
const isNewRow = Boolean((editor as any).row?.isNewRow) || mode === 'new'
|
||||
|
||||
if (
|
||||
(isNewRow && columnFormat?.allowAdding === false) ||
|
||||
(!isNewRow && columnFormat?.allowEditing === false)
|
||||
) {
|
||||
editor.editorOptions.readOnly = true
|
||||
} else if (isNewRow && columnFormat?.allowAdding === true) {
|
||||
editor.editorOptions.readOnly = false
|
||||
editor.editorOptions.disabled = false
|
||||
}
|
||||
|
||||
// Cascade disabled mantığı
|
||||
const colFormat = gridDto.columnFormats.find((c) => c.fieldName === editor.dataField)
|
||||
|
|
@ -878,7 +928,18 @@ const Tree = (props: TreeProps) => {
|
|||
setMode('view')
|
||||
setIsPopupFullScreen(false)
|
||||
}}
|
||||
onRowInserted={() => {
|
||||
onRowInserted={(e) => {
|
||||
const insertedKey = getPersistedInsertedKey(e, gridDto.gridOptions.keyFieldName)
|
||||
|
||||
if (
|
||||
gridDto.gridOptions.workflowDto?.approvalStatusFieldName &&
|
||||
insertedKey !== undefined
|
||||
) {
|
||||
workflowService
|
||||
.startWorkflow(listFormCode, [insertedKey])
|
||||
.then(() => gridRef.current?.instance()?.refresh())
|
||||
.catch(console.error)
|
||||
}
|
||||
props.refreshData?.()
|
||||
}}
|
||||
onRowUpdated={() => {
|
||||
|
|
@ -889,6 +950,8 @@ const Tree = (props: TreeProps) => {
|
|||
}}
|
||||
onEditorPreparing={onEditorPreparing}
|
||||
onContentReady={(e) => {
|
||||
updateWorkflowApprovalButtons(e.component)
|
||||
|
||||
// Restore expanded keys after data refresh (only if autoExpandAll is false)
|
||||
if (
|
||||
!gridDto.gridOptions.treeOptionDto?.autoExpandAll &&
|
||||
|
|
@ -1117,9 +1180,9 @@ const Tree = (props: TreeProps) => {
|
|||
if (mode === 'view') {
|
||||
return a.canRead
|
||||
} else if (mode === 'new') {
|
||||
return a.canCreate || a.canRead
|
||||
return a.canCreate && a.allowAdding
|
||||
} else if (mode === 'edit') {
|
||||
return a.canUpdate || a.canRead
|
||||
return a.canUpdate && a.allowEditing
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
|
@ -1146,9 +1209,9 @@ const Tree = (props: TreeProps) => {
|
|||
if (mode === 'view') {
|
||||
return a.canRead
|
||||
} else if (mode === 'new') {
|
||||
return a.canCreate || a.canRead
|
||||
return a.canCreate && a.allowAdding
|
||||
} else if (mode === 'edit') {
|
||||
return a.canUpdate || a.canRead
|
||||
return a.canUpdate && a.allowEditing
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -691,7 +691,7 @@ const useListFormColumns = ({
|
|||
}
|
||||
}
|
||||
|
||||
column.allowEditing = colData?.allowEditing
|
||||
column.allowEditing = colData?.allowEditing || colData?.allowAdding
|
||||
|
||||
// #region lookup ayarlari
|
||||
if (colData.lookupDto?.dataSourceType) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,34 @@ import { CardViewRef, CardViewTypes } from 'devextreme-react/cjs/card-view'
|
|||
|
||||
const filteredGridPanelColor = 'rgba(10, 200, 10, 0.5)' // kullanici tanimli filtre ile filtrelenmis gridin paneline ait renk
|
||||
|
||||
const toInsertedRowData = (values: any, responseData: any, keyFieldName?: string | null) => {
|
||||
if (!keyFieldName) {
|
||||
return responseData ?? values
|
||||
}
|
||||
|
||||
if (responseData && typeof responseData === 'object' && !Array.isArray(responseData)) {
|
||||
if (responseData[keyFieldName] !== undefined && responseData[keyFieldName] !== null) {
|
||||
return { ...values, ...responseData }
|
||||
}
|
||||
|
||||
if (
|
||||
responseData.data &&
|
||||
typeof responseData.data === 'object' &&
|
||||
!Array.isArray(responseData.data) &&
|
||||
responseData.data[keyFieldName] !== undefined &&
|
||||
responseData.data[keyFieldName] !== null
|
||||
) {
|
||||
return { ...values, ...responseData.data }
|
||||
}
|
||||
}
|
||||
|
||||
if (responseData !== undefined && responseData !== null) {
|
||||
return { ...values, [keyFieldName]: responseData }
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
const useListFormCustomDataSource = ({
|
||||
gridRef,
|
||||
}: {
|
||||
|
|
@ -372,7 +400,10 @@ const useListFormCustomDataSource = ({
|
|||
}
|
||||
const insertUrl = getServiceAddress(gridOptions.insertServiceAddress)
|
||||
|
||||
return dynamicFetch(insertUrl, 'POST', searchParams, { data: values, listFormCode })
|
||||
return dynamicFetch(insertUrl, 'POST', searchParams, { data: values, listFormCode }).then(
|
||||
(response: any) =>
|
||||
toInsertedRowData(values, response?.data, gridOptions.keyFieldName),
|
||||
)
|
||||
},
|
||||
errorHandler: (error: any) => {
|
||||
console.log(error.message)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Button, Notification, toast } from '@/components/ui'
|
||||
import { GridDto, UiCommandButtonPositionTypeEnum } from '@/proxy/form/models'
|
||||
import { GridDto, UiCommandButtonPositionTypeEnum, WorkflowDto } from '@/proxy/form/models'
|
||||
import { dynamicFetch } from '@/services/form.service'
|
||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||
import { usePermission } from '@/utils/hooks/usePermission'
|
||||
|
|
@ -10,6 +10,7 @@ import { useDialogContext } from '../shared/DialogContext'
|
|||
import { usePWA } from '@/utils/hooks/usePWA'
|
||||
import { layoutTypes, ListViewLayoutType } from '../admin/listForm/edit/types'
|
||||
import { useStoreState } from '@/store'
|
||||
import { workflowService } from '@/services/workflow.service'
|
||||
|
||||
type ToolbarModalData = {
|
||||
open: boolean
|
||||
|
|
@ -51,7 +52,6 @@ const useToolbar = ({
|
|||
|
||||
const [toolbarData, setToolbarData] = useState<ToolbarItem[]>([])
|
||||
const [toolbarModalData, setToolbarModalData] = useState<ToolbarModalData>()
|
||||
|
||||
const grdOpt = gridDto?.gridOptions
|
||||
|
||||
function getToolbarData() {
|
||||
|
|
@ -112,6 +112,136 @@ const useToolbar = ({
|
|||
location: 'after',
|
||||
})
|
||||
|
||||
const workflowOptions = grdOpt.workflowDto
|
||||
const approvalCriteria = workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
|
||||
if (
|
||||
workflowOptions?.approvalStatusFieldName &&
|
||||
approvalCriteria.length > 0 &&
|
||||
grdOpt.updateServiceAddress
|
||||
) {
|
||||
items.push({
|
||||
widget: 'dxButton',
|
||||
name: 'workflowStart',
|
||||
location: 'after',
|
||||
options: {
|
||||
icon: 'play',
|
||||
text: 'Workflow Start',
|
||||
hint: 'Workflow Start',
|
||||
visible: true,
|
||||
disabled: true,
|
||||
onClick: async () => {
|
||||
const keys = (await Promise.resolve(getSelectedRowKeys() as any)) as unknown[]
|
||||
if (!keys?.length) {
|
||||
toast.push(
|
||||
<Notification type="warning" duration={2000}>
|
||||
{translate('::ListForms.ListForm.SelectRecord')}
|
||||
</Notification>,
|
||||
{ placement: 'top-end' },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const selectedRows = ((await Promise.resolve(getSelectedRowsData() as any)) ||
|
||||
[]) as Record<string, unknown>[]
|
||||
if (
|
||||
selectedRows.length === 0 ||
|
||||
!selectedRows.every((row) => isWorkflowNotStarted(row, workflowOptions))
|
||||
) {
|
||||
toast.push(
|
||||
<Notification type="warning" duration={2500}>
|
||||
Secili kayit icin workflow zaten baslamis.
|
||||
</Notification>,
|
||||
{ placement: 'top-end' },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await workflowService.startWorkflow(listFormCode, keys)
|
||||
refreshData()
|
||||
} catch (error: any) {
|
||||
toast.push(
|
||||
<Notification type="danger" duration={3000}>
|
||||
{error?.response?.data?.error?.message ||
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
'Workflow baslatilamadi.'}
|
||||
</Notification>,
|
||||
{ placement: 'top-end' },
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
approvalCriteria.forEach((criteria) => {
|
||||
items.push({
|
||||
widget: 'dxButton',
|
||||
name: `workflowApproval_${criteria.id}`,
|
||||
location: 'after',
|
||||
options: {
|
||||
icon: 'check',
|
||||
text: criteria.title,
|
||||
hint: criteria.title,
|
||||
visible: true,
|
||||
disabled: true,
|
||||
onClick: async () => {
|
||||
const keys = (await Promise.resolve(getSelectedRowKeys() as any)) as unknown[]
|
||||
if (!keys?.length) {
|
||||
toast.push(
|
||||
<Notification type="warning" duration={2000}>
|
||||
{translate('::ListForms.ListForm.SelectRecord')}
|
||||
</Notification>,
|
||||
{ placement: 'top-end' },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const selectedRows = ((await Promise.resolve(getSelectedRowsData() as any)) ||
|
||||
[]) as Record<string, unknown>[]
|
||||
const activeRows = selectedRows.filter((row) =>
|
||||
isWorkflowApprovalCriteriaActive(
|
||||
row,
|
||||
workflowOptions,
|
||||
criteria.title,
|
||||
getCurrentUserWorkflowIdentities(config?.currentUser),
|
||||
),
|
||||
)
|
||||
|
||||
if (activeRows.length !== selectedRows.length) {
|
||||
toast.push(
|
||||
<Notification type="warning" duration={2500}>
|
||||
Secili kayit bu onay adiminda veya onay kullanicisinda beklemiyor.
|
||||
</Notification>,
|
||||
{ placement: 'top-end' },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setToolbarModalData({
|
||||
open: true,
|
||||
content: (
|
||||
<>
|
||||
<WorkflowApprovalDecisionDialog
|
||||
criteriaTitle={criteria.title}
|
||||
keys={keys}
|
||||
listFormCode={listFormCode}
|
||||
criteriaId={criteria.id}
|
||||
onCancel={() => setToolbarModalData(undefined)}
|
||||
onCompleted={() => {
|
||||
refreshData()
|
||||
setToolbarModalData(undefined)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Add Expand All button for TreeList
|
||||
if (layout === layoutTypes.tree && grdOpt.treeOptionDto?.parentIdExpr) {
|
||||
items.push({
|
||||
|
|
@ -365,4 +495,182 @@ const useToolbar = ({
|
|||
}
|
||||
}
|
||||
|
||||
function isWorkflowApprovalCriteriaActive(
|
||||
row: Record<string, unknown>,
|
||||
workflowOptions: WorkflowDto,
|
||||
criteriaTitle: string,
|
||||
currentUserIdentities: string[] = [],
|
||||
) {
|
||||
if (!workflowOptions.approvalStatusFieldName || !criteriaTitle) {
|
||||
return false
|
||||
}
|
||||
|
||||
const statusMatches =
|
||||
normalizeWorkflowValue(row?.[workflowOptions.approvalStatusFieldName]) ===
|
||||
normalizeWorkflowValue(criteriaTitle)
|
||||
|
||||
if (!statusMatches) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!workflowOptions.approvalUserFieldName) {
|
||||
return true
|
||||
}
|
||||
|
||||
const approver = normalizeWorkflowValue(row?.[workflowOptions.approvalUserFieldName])
|
||||
return currentUserIdentities.some((identity) => normalizeWorkflowValue(identity) === approver)
|
||||
}
|
||||
|
||||
function normalizeWorkflowValue(value: unknown) {
|
||||
return String(value ?? '').trim().toLocaleLowerCase('tr-TR')
|
||||
}
|
||||
|
||||
function isWorkflowNotStarted(row: Record<string, unknown>, workflowOptions: WorkflowDto) {
|
||||
return normalizeWorkflowValue(row?.[workflowOptions.approvalStatusFieldName]) === ''
|
||||
}
|
||||
|
||||
function getCurrentUserWorkflowIdentities(currentUser?: {
|
||||
userName?: string
|
||||
email?: string
|
||||
name?: string
|
||||
}) {
|
||||
return [currentUser?.email, currentUser?.userName, currentUser?.name].filter(Boolean) as string[]
|
||||
}
|
||||
|
||||
export function updateWorkflowApprovalToolbarItems(
|
||||
component: any,
|
||||
workflowOptions: WorkflowDto | undefined,
|
||||
selectedRowsData: Record<string, unknown>[] = [],
|
||||
currentUser?: {
|
||||
userName?: string
|
||||
email?: string
|
||||
name?: string
|
||||
},
|
||||
) {
|
||||
const approvalCriteria = workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
|
||||
if (!component || !workflowOptions?.approvalStatusFieldName || !approvalCriteria.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const toolbarOptions = component.option('toolbar')
|
||||
if (!toolbarOptions?.items || !Array.isArray(toolbarOptions.items)) {
|
||||
return
|
||||
}
|
||||
|
||||
const workflowStartItemIndex = toolbarOptions.items
|
||||
.map((item: any) => item.name)
|
||||
.indexOf('workflowStart')
|
||||
|
||||
if (workflowStartItemIndex >= 0) {
|
||||
const startEnabled =
|
||||
selectedRowsData.length > 0 &&
|
||||
selectedRowsData.every((row) => isWorkflowNotStarted(row, workflowOptions))
|
||||
const startOptionPath = `toolbar.items[${workflowStartItemIndex}].options.disabled`
|
||||
const nextStartDisabled = !startEnabled
|
||||
if (component.option(startOptionPath) !== nextStartDisabled) {
|
||||
component.option(startOptionPath, nextStartDisabled)
|
||||
}
|
||||
}
|
||||
|
||||
const currentUserIdentities = getCurrentUserWorkflowIdentities(currentUser)
|
||||
|
||||
approvalCriteria.forEach((criteria) => {
|
||||
const toolbarItemIndex = toolbarOptions.items
|
||||
.map((item: any) => item.name)
|
||||
.indexOf(`workflowApproval_${criteria.id}`)
|
||||
|
||||
if (toolbarItemIndex < 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const enabled =
|
||||
selectedRowsData.length > 0 &&
|
||||
selectedRowsData.every((row) =>
|
||||
isWorkflowApprovalCriteriaActive(row, workflowOptions, criteria.title, currentUserIdentities),
|
||||
)
|
||||
|
||||
const optionPath = `toolbar.items[${toolbarItemIndex}].options.disabled`
|
||||
const nextDisabled = !enabled
|
||||
if (component.option(optionPath) !== nextDisabled) {
|
||||
component.option(optionPath, nextDisabled)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function WorkflowApprovalDecisionDialog({
|
||||
criteriaTitle,
|
||||
keys,
|
||||
listFormCode,
|
||||
criteriaId,
|
||||
onCancel,
|
||||
onCompleted,
|
||||
}: {
|
||||
criteriaTitle: string
|
||||
keys: unknown[]
|
||||
listFormCode: string
|
||||
criteriaId: string
|
||||
onCancel: () => void
|
||||
onCompleted: () => void
|
||||
}) {
|
||||
const { translate } = useLocalization()
|
||||
const [note, setNote] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const decide = async (approved: boolean) => {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await Promise.all(
|
||||
keys.map((key) =>
|
||||
workflowService.decideWorkflow(
|
||||
listFormCode,
|
||||
[key],
|
||||
approved,
|
||||
note,
|
||||
criteriaId,
|
||||
),
|
||||
),
|
||||
)
|
||||
onCompleted()
|
||||
} catch (error: any) {
|
||||
toast.push(
|
||||
<Notification type="danger" duration={3000}>
|
||||
{error?.response?.data?.error?.message ||
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
'Workflow karari verilemedi.'}
|
||||
</Notification>,
|
||||
{ placement: 'top-end' },
|
||||
)
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h5 className="mb-4">{criteriaTitle}</h5>
|
||||
<p>{keys.length} kayit icin workflow karari verilecek.</p>
|
||||
<label className="mb-2 block font-semibold">Not</label>
|
||||
<textarea
|
||||
className="input input-textarea mb-4 min-h-[96px] w-full resize-y"
|
||||
rows={4}
|
||||
value={note}
|
||||
placeholder="Onay veya red aciklamasi"
|
||||
onChange={(event) => setNote(event.target.value)}
|
||||
/>
|
||||
<div className="text-right mt-6">
|
||||
<Button className="ltr:mr-2 rtl:ml-2" variant="plain" disabled={submitting} onClick={onCancel}>
|
||||
{translate('::Cancel')}
|
||||
</Button>
|
||||
<Button className="ltr:mr-2 rtl:ml-2" disabled={submitting} onClick={() => decide(false)}>
|
||||
Reddet
|
||||
</Button>
|
||||
<Button variant="solid" disabled={submitting} onClick={() => decide(true)}>
|
||||
Onayla
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export { useToolbar }
|
||||
|
|
|
|||
Loading…
Reference in a new issue