ListFormWorkflowun Grid ve Tree üzerinden çalıştır

This commit is contained in:
Sedat Öztürk 2026-05-24 00:12:01 +03:00
parent 8f3932bc6e
commit 6262baa6f1
32 changed files with 1643 additions and 135 deletions

View file

@ -394,6 +394,19 @@ public class GridOptionsDto : AuditedEntityDto<Guid>
set { LayoutJson = JsonSerializer.Serialize(value); } 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> Chart a yetkili kullanıcı. UserId ve RoleId boş ise herkes yetkilidir
/// </summary> /// </summary>
public string UserId { get; set; } public string UserId { get; set; }

View file

@ -123,18 +123,5 @@ public class GridOptionsEditDto : GridOptionsDto
} }
set { ExtraFilterJson = JsonSerializer.Serialize(value); } 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); }
}
} }

View file

@ -1,8 +1,13 @@
namespace Sozsoft.Platform.ListForms; using System.Collections.Generic;
namespace Sozsoft.Platform.ListForms;
public class WorkflowDto public class WorkflowDto
{ {
public string ApprovalUserFieldName { get; set; } public string ApprovalUserFieldName { get; set; }
public string ApprovalDateFieldName { get; set; } public string ApprovalDateFieldName { get; set; }
public string ApprovalStatusFieldName { get; set; } public string ApprovalStatusFieldName { get; set; }
public string ApprovalDescriptionFieldName { get; set; }
public List<ListFormWorkflowCriteriaDto> Criteria { get; set; } = [];
} }

View file

@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace Sozsoft.Platform.ListForms.Workflow; namespace Sozsoft.Platform.ListForms;
public class CompareOutcomeDto public class CompareOutcomeDto
{ {

View file

@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Sozsoft.Platform.ListForms.Workflow; namespace Sozsoft.Platform.ListForms;
public class CreateUpdateListFormWorkflowCriteriaDto public class CreateUpdateListFormWorkflowCriteriaDto
{ {

View file

@ -1,7 +1,10 @@
namespace Sozsoft.Platform.ListForms.Workflow; namespace Sozsoft.Platform.ListForms;
public class DecisionWorkflowDto public class DecisionWorkflowDto
{ {
public string ListFormCode { get; set; }
public object[] Keys { get; set; }
public string CurrentNodeId { get; set; }
public bool Approved { get; set; } public bool Approved { get; set; }
public string Note { get; set; } public string Note { get; set; }
} }

View file

@ -2,13 +2,14 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
namespace Sozsoft.Platform.ListForms.Workflow; namespace Sozsoft.Platform.ListForms;
public interface IListFormWorkflowAppService : IApplicationService public interface IListFormWorkflowAppService : IApplicationService
{ {
Task<ListFormWorkflowStateDto> GetStateAsync(string listFormCode = null); Task<ListFormWorkflowStateDto> GetCriteriaAsync(string listFormCode = null);
Task<ListFormWorkflowCriteriaDto> SaveCriteriaAsync(CreateUpdateListFormWorkflowCriteriaDto input); Task<ListFormWorkflowCriteriaDto> SaveCriteriaAsync(CreateUpdateListFormWorkflowCriteriaDto input);
Task DeleteCriteriaAsync(string id); Task DeleteCriteriaAsync(string id);
Task<ListFormWorkflowStateDto> ResetDemoAsync(string listFormCode = null); Task<ListFormWorkflowStateDto> ResetDemoAsync(string listFormCode = null);
Task<WorkflowRunResultDto> StartAsync(StartWorkflowDto input);
Task<WorkflowRunResultDto> DecisionAsync(DecisionWorkflowDto input);
} }

View file

@ -2,9 +2,9 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Volo.Abp.Application.Dtos; 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 ListFormCode { get; set; }
public string Kind { get; set; } public string Kind { get; set; }

View file

@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace Sozsoft.Platform.ListForms.Workflow; namespace Sozsoft.Platform.ListForms;
public class ListFormWorkflowStateDto public class ListFormWorkflowStateDto
{ {

View file

@ -0,0 +1,8 @@
namespace Sozsoft.Platform.ListForms;
public class StartWorkflowDto
{
public string ListFormCode { get; set; }
public object[] Keys { get; set; }
}

View file

@ -1,4 +1,4 @@
namespace Sozsoft.Platform.ListForms.Workflow; namespace Sozsoft.Platform.ListForms;
public class WorkflowConditionDto public class WorkflowConditionDto
{ {

View file

@ -1,6 +1,6 @@
using System; using System;
namespace Sozsoft.Platform.ListForms.Workflow; namespace Sozsoft.Platform.ListForms;
public class WorkflowHistoryDto public class WorkflowHistoryDto
{ {

View file

@ -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; }
}

View file

@ -21,6 +21,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
{ {
private readonly IRepository<ListForm, Guid> listFormRepository; private readonly IRepository<ListForm, Guid> listFormRepository;
private readonly IRepository<ListFormField, Guid> listFormFieldRepository; private readonly IRepository<ListFormField, Guid> listFormFieldRepository;
private readonly IRepository<ListFormWorkflow, string> listFormWorkflowRepository;
private readonly ICurrentUser currentUser; private readonly ICurrentUser currentUser;
private readonly ISelectQueryManager selectQueryManager; private readonly ISelectQueryManager selectQueryManager;
private readonly IListFormAuthorizationManager authManager; private readonly IListFormAuthorizationManager authManager;
@ -39,6 +40,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
public ListFormSelectAppService( public ListFormSelectAppService(
IRepository<ListForm, Guid> listFormRepository, IRepository<ListForm, Guid> listFormRepository,
IRepository<ListFormField, Guid> listFormFieldRepository, IRepository<ListFormField, Guid> listFormFieldRepository,
IRepository<ListFormWorkflow, string> listFormWorkflowRepository,
ICurrentUser currentUser, ICurrentUser currentUser,
ISelectQueryManager selectQueryManager, ISelectQueryManager selectQueryManager,
IListFormAuthorizationManager authManager, IListFormAuthorizationManager authManager,
@ -54,6 +56,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
{ {
this.listFormRepository = listFormRepository; this.listFormRepository = listFormRepository;
this.listFormFieldRepository = listFormFieldRepository; this.listFormFieldRepository = listFormFieldRepository;
this.listFormWorkflowRepository = listFormWorkflowRepository;
this.currentUser = currentUser; this.currentUser = currentUser;
this.selectQueryManager = selectQueryManager; this.selectQueryManager = selectQueryManager;
this.authManager = authManager; this.authManager = authManager;
@ -165,6 +168,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
var data = await dynamicDataRepository.QueryAsync(selectQueryManager.GroupQuery, connectionString, param); var data = await dynamicDataRepository.QueryAsync(selectQueryManager.GroupQuery, connectionString, param);
var dataQueryable = data.AsQueryable(); var dataQueryable = data.AsQueryable();
var groups = new List<(string, int, List<dynamic>)>(selectQueryManager.GroupTuples.Count); var groups = new List<(string, int, List<dynamic>)>(selectQueryManager.GroupTuples.Count);
for (int i = 0; i < selectQueryManager.GroupTuples.Count; i++) for (int i = 0; i < selectQueryManager.GroupTuples.Count; i++)
{ {
@ -231,7 +235,7 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
{ {
var widgetList = JsonSerializer.Deserialize<WidgetEditDto[]>(listForm.WidgetsJson) ?? []; var widgetList = JsonSerializer.Deserialize<WidgetEditDto[]>(listForm.WidgetsJson) ?? [];
var activeWidgets = widgetList.Where(w => w.IsActive && !string.IsNullOrWhiteSpace(w.SqlQuery)).ToList(); var activeWidgets = widgetList.Where(w => w.IsActive && !string.IsNullOrWhiteSpace(w.SqlQuery)).ToList();
if (activeWidgets.Count == 0) if (activeWidgets.Count == 0)
return Widgets; return Widgets;
@ -305,6 +309,11 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
Widgets = await GetWidgetsAsync(listForm) 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 queryParameters = httpContextAccessor.HttpContext.Request.Query.ToDictionary(x => x.Key, x => x.Value);
var defaultFields = await defaultValueManager.GenerateDefaultValuesAsync(listForm, fields, Enums.OperationEnum.Select, queryParameters: queryParameters); var defaultFields = await defaultValueManager.GenerateDefaultValuesAsync(listForm, fields, Enums.OperationEnum.Select, queryParameters: queryParameters);
@ -491,5 +500,153 @@ public class ListFormSelectAppService : PlatformAppService, IListFormSelectAppSe
return sql + " " + insertText; 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 [];
}
}
} }

View file

@ -1,11 +1,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.ListForms.Select;
using Sozsoft.Platform.Queries;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
@ -15,36 +19,149 @@ namespace Sozsoft.Platform.ListForms.Workflow;
[Route("api/app/list-form-workflow")] [Route("api/app/list-form-workflow")]
public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowAppService public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowAppService
{ {
private const string DefaultListFormCode = "workflow";
private const string CriteriaIdPrefix = "N"; private const string CriteriaIdPrefix = "N";
private const int CriteriaIdPadding = 3; private const int CriteriaIdPadding = 3;
private const string SystemApprovalDescription = "Sistem tarafından otomatik olarak onaylandı.";
private readonly IRepository<ListFormWorkflow, string> criteriaRepository; 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.criteriaRepository = criteriaRepository;
this.listFormManager = listFormManager;
this.authManager = authManager;
this.listFormSelectAppService = listFormSelectAppService;
this.queryManager = queryManager;
} }
[HttpGet("state")] [HttpGet("criteria")]
public async Task<ListFormWorkflowStateDto> GetStateAsync(string listFormCode = null) public async Task<ListFormWorkflowStateDto> GetCriteriaAsync(string listFormCode = null)
{ {
var code = NormalizeListFormCode(listFormCode); var code = listFormCode;
var criteria = (await criteriaRepository.GetListAsync(x => x.ListFormCode == code)) var criteria = (await criteriaRepository.GetListAsync(x => x.ListFormCode == code))
.OrderBy(x => x.PositionX) .OrderBy(x => x.Id)
.ThenBy(x => x.PositionY)
.ToList(); .ToList();
return new ListFormWorkflowStateDto 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")] [HttpPost("criteria")]
public async Task<ListFormWorkflowCriteriaDto> SaveCriteriaAsync(CreateUpdateListFormWorkflowCriteriaDto input) public async Task<ListFormWorkflowCriteriaDto> SaveCriteriaAsync(CreateUpdateListFormWorkflowCriteriaDto input)
{ {
var code = NormalizeListFormCode(input.ListFormCode); var code = input.ListFormCode;
var isNew = input.Id.IsNullOrWhiteSpace(); var isNew = input.Id.IsNullOrWhiteSpace();
var criteria = isNew var criteria = isNew
? new ListFormWorkflow(await GenerateNextCriteriaIdAsync()) ? new ListFormWorkflow(await GenerateNextCriteriaIdAsync())
@ -58,7 +175,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
criteria.ListFormCode = code; criteria.ListFormCode = code;
criteria.Kind = NormalizeRequired(input.Kind, "Compare"); criteria.Kind = NormalizeRequired(input.Kind, "Compare");
criteria.Title = NormalizeRequired(input.Title, criteria.Kind); 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.CompareOperator = NormalizeRequired(input.CompareOperator, ">");
criteria.CompareValue = input.CompareValue; criteria.CompareValue = input.CompareValue;
criteria.Approver = input.Approver ?? string.Empty; criteria.Approver = input.Approver ?? string.Empty;
@ -110,7 +227,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
[HttpPost("reset-demo")] [HttpPost("reset-demo")]
public async Task<ListFormWorkflowStateDto> ResetDemoAsync(string listFormCode = null) public async Task<ListFormWorkflowStateDto> ResetDemoAsync(string listFormCode = null)
{ {
var code = NormalizeListFormCode(listFormCode); var code = listFormCode;
var existing = await criteriaRepository.GetListAsync(x => x.ListFormCode == code); var existing = await criteriaRepository.GetListAsync(x => x.ListFormCode == code);
foreach (var item in existing) 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 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 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 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; start.NextOnStart = compare.Id;
compare.NextOnTrue = approval.Id; compare.NextOnTrue = approval.Id;
@ -131,13 +248,13 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
{ {
Label = "Onay gerekir", Label = "Onay gerekir",
TargetId = approval.Id, TargetId = approval.Id,
Conditions = [new WorkflowConditionDto { CompareColumn = "Tutar", CompareOperator = ">", CompareValue = 5000 }] Conditions = [new WorkflowConditionDto { CompareColumn = "Price", CompareOperator = ">", CompareValue = 5000 }]
}, },
new CompareOutcomeDto new CompareOutcomeDto
{ {
Label = "Bilgilendir", Label = "Bilgilendir",
TargetId = inform.Id, TargetId = inform.Id,
Conditions = [new WorkflowConditionDto { CompareColumn = "Tutar", CompareOperator = "<=", CompareValue = 5000 }] Conditions = [new WorkflowConditionDto { CompareColumn = "Price", CompareOperator = "<=", CompareValue = 5000 }]
} }
]); ]);
approval.NextOnApprove = inform.Id; approval.NextOnApprove = inform.Id;
@ -149,7 +266,422 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
await criteriaRepository.UpdateAsync(approval, autoSave: true); await criteriaRepository.UpdateAsync(approval, autoSave: true);
await criteriaRepository.UpdateAsync(inform, 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( private async Task<ListFormWorkflow> CreateCriteriaAsync(
@ -165,7 +697,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
ListFormCode = listFormCode, ListFormCode = listFormCode,
Kind = kind, Kind = kind,
Title = title, Title = title,
CompareColumn = "Tutar", CompareColumn = "Price",
CompareOperator = ">", CompareOperator = ">",
CompareValue = 5000, CompareValue = 5000,
Approver = approver, 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) private static string NormalizeRequired(string value, string fallback)
{ {
return value.IsNullOrWhiteSpace() ? fallback : value.Trim(); return value.IsNullOrWhiteSpace() ? fallback : value.Trim();
@ -318,4 +845,32 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
return []; 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; } = [];
}
} }

View file

@ -3624,6 +3624,12 @@
"en": "MENU", "en": "MENU",
"tr": "MENÜ" "tr": "MENÜ"
}, },
{
"resourceName": "Platform",
"key": "ListForms.ListForm.SelectRecord",
"en": "Please select a row.",
"tr": "Lütfen bir satır seçin."
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "ListForms.ListForm.ClearRedisCache", "key": "ListForms.ListForm.ClearRedisCache",
@ -16292,6 +16298,12 @@
"en": "Status", "en": "Status",
"tr": "Durum" "tr": "Durum"
}, },
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.Connection",
"en": "Connection",
"tr": "Bağlantı"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.Listform.ListformField.FailureReason", "key": "App.Listform.ListformField.FailureReason",
@ -16580,6 +16592,48 @@
"en": "Title", "en": "Title",
"tr": "Başlık" "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", "resourceName": "Platform",
"key": "App.Listform.ListformField.Total", "key": "App.Listform.ListformField.Total",
@ -18745,6 +18799,12 @@
"key": "ListForms.ListFormEdit.Workflow.ApprovalStatusFieldName", "key": "ListForms.ListFormEdit.Workflow.ApprovalStatusFieldName",
"en": "Approval Status Field Name", "en": "Approval Status Field Name",
"tr": "Onay Durumu Alanı Adı" "tr": "Onay Durumu Alanı Adı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.Workflow.ApprovalDescriptionFieldName",
"en": "Approval Description Field Name",
"tr": "Onay Açıklaması Alanı Adı"
} }
] ]
} }

View file

@ -135,6 +135,8 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
List<ListFormCustomization> listFormCustomizations = null, List<ListFormCustomization> listFormCustomizations = null,
QueryParameters queryParams = null) QueryParameters queryParams = null)
{ {
ResetQueryState();
if (listform == null) if (listform == null)
{ {
return; return;
@ -213,6 +215,28 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
#endregion #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) private List<SelectField> GetSelectAndJoinFields(List<ListFormField> listFormFields)
{ {
List<SelectField> selectFields = []; List<SelectField> selectFields = [];

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations namespace Sozsoft.Platform.Migrations
{ {
[DbContext(typeof(PlatformDbContext))] [DbContext(typeof(PlatformDbContext))]
[Migration("20260523104659_Initial")] [Migration("20260523160811_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />

View file

@ -1,6 +1,17 @@
{ {
"commit": "e9d8f5e", "commit": "8f3932b",
"releases": [ "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", "version": "1.0.9",
"buildDate": "2026-05-09", "buildDate": "2026-05-09",
@ -93,4 +104,4 @@
] ]
} }
] ]
} }

View file

@ -586,6 +586,7 @@ export interface GridOptionsDto extends AuditedEntityDto<string> {
extraFilterJson?: string extraFilterJson?: string
extraFilterDto: ExtraFilterDto[] extraFilterDto: ExtraFilterDto[]
layoutDto: LayoutDto layoutDto: LayoutDto
workflowDto: WorkflowDto
//ChartEditDto //ChartEditDto
userId?: string userId?: string
@ -663,7 +664,6 @@ export interface GridOptionsEditDto extends GridOptionsDto, Record<string, any>
formFieldsDefaultValueDto: FieldsDefaultValueDto[] formFieldsDefaultValueDto: FieldsDefaultValueDto[]
widgetsJson?: string widgetsJson?: string
widgetsDto: WidgetEditDto[] widgetsDto: WidgetEditDto[]
workflowDto: WorkflowDto
extraFilterEditDto: ExtraFilterEditDto[] extraFilterEditDto: ExtraFilterEditDto[]
} }
@ -910,6 +910,39 @@ export interface WorkflowDto {
approvalUserFieldName: string approvalUserFieldName: string
approvalDateFieldName: string approvalDateFieldName: string
approvalStatusFieldName: 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 { export interface LayoutDto {

View file

@ -32,10 +32,19 @@ export interface WorkflowCriteriaDto {
compareOutcomes: CompareOutcomeDto[] compareOutcomes: CompareOutcomeDto[]
} }
export interface WorkflowStateDto { export interface WorkflowCriteriasDto {
criteria: WorkflowCriteriaDto[] 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'> & { export type SaveCriteriaInput = Omit<Partial<WorkflowCriteriaDto>, 'id'> & {
id?: string | null id?: string | null
listFormCode: string listFormCode: string
@ -44,10 +53,10 @@ export type SaveCriteriaInput = Omit<Partial<WorkflowCriteriaDto>, 'id'> & {
const baseUrl = '/api/app/list-form-workflow' const baseUrl = '/api/app/list-form-workflow'
export const workflowService = { export const workflowService = {
async getState(listFormCode?: string) { async getCriteria(listFormCode?: string) {
const response = await apiService.fetchData<WorkflowStateDto>({ const response = await apiService.fetchData<WorkflowCriteriasDto>({
method: 'GET', method: 'GET',
url: `${baseUrl}/state`, url: `${baseUrl}/criteria`,
params: { listFormCode }, params: { listFormCode },
}) })
@ -72,7 +81,7 @@ export const workflowService = {
}, },
async resetDemo(listFormCode?: string) { async resetDemo(listFormCode?: string) {
const response = await apiService.fetchData<WorkflowStateDto>({ const response = await apiService.fetchData<WorkflowCriteriasDto>({
method: 'POST', method: 'POST',
url: `${baseUrl}/reset-demo`, url: `${baseUrl}/reset-demo`,
params: { listFormCode }, params: { listFormCode },
@ -80,4 +89,30 @@ export const workflowService = {
return response.data 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
},
} }

View file

@ -1,3 +1,4 @@
import { useLocalization } from '../hooks/useLocalization'
import { getNodeHeight, nodeSize } from './workflowConstants' import { getNodeHeight, nodeSize } from './workflowConstants'
import type { import type {
CompareOutcomeDto, CompareOutcomeDto,
@ -311,7 +312,7 @@ export function emptyCriteria(kind = 'Compare', listFormCode = ''): WorkflowCrit
listFormCode, listFormCode,
kind, kind,
title: defaultTitle(kind), title: defaultTitle(kind),
compareColumn: 'Tutar', compareColumn: 'Price',
compareOperator: '>', compareOperator: '>',
compareValue: 5000, compareValue: 5000,
approver: '', approver: '',
@ -347,19 +348,31 @@ export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm
export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput { export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput {
const sharedPerson = item.approver || '' const sharedPerson = item.approver || ''
const compareOutcomes = (item.compareOutcomes || [])
.slice(0, 4)
.filter((outcome) => outcome.label?.trim())
.map(normalizeCompareOutcome)
const firstCompareColumn = compareOutcomes
.flatMap((outcome) => outcome.conditions || [])
.find((condition) => condition.compareColumn)?.compareColumn
return { return {
...item,
id: item.id || null, id: item.id || null,
listFormCode: item.listFormCode || '', 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), compareValue: Number(item.compareValue || 0),
approver: sharedPerson, 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), positionX: Number(item.positionX || 32),
positionY: Number(item.positionY || 150), positionY: Number(item.positionY || 150),
compareOutcomes: (item.compareOutcomes || []) compareOutcomes,
.slice(0, 4)
.filter((outcome) => outcome.label?.trim())
.map(normalizeCompareOutcome),
} }
} }
@ -367,27 +380,27 @@ export function defaultTitle(kind: string) {
return ( return (
{ {
Start: 'İş Akışı Başlat', Start: 'İş Akışı Başlat',
Compare: 'Tutar > 5000 TL', Compare: 'Karşılaştırma',
Approval: 'Onaylanacak Kişi', Approval: 'Onay',
Inform: 'Bilgilendirme Yapılacak Personel', Inform: 'Bilgilendirme',
End: 'Akışı Bitir', End: 'İş Akışı Bitir',
}[kind] ?? 'İş Akışı Adımı' }[kind] ?? 'İş Akışı Adımı'
) )
} }
export function emptyCompareOutcome1(label = 'Durum'): CompareOutcomeDto { export function emptyCompareOutcome1(label = 'Durum', compareColumn = 'Price'): CompareOutcomeDto {
return { return {
label, label,
targetId: '', 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 { return {
label, label,
targetId: '', targetId: '',
conditions: [{ compareColumn: 'Tutar', compareOperator: '<=', compareValue: 5000 }], conditions: [{ compareColumn, compareOperator: '<=', compareValue: 5000 }],
} }
} }
@ -401,7 +414,7 @@ export function toCompareOutcomeForm(
? outcome.conditions ? outcome.conditions
: [ : [
{ {
compareColumn: outcome.compareColumn || 'Tutar', compareColumn: outcome.compareColumn || 'Price',
compareOperator: outcome.compareOperator || '>', compareOperator: outcome.compareOperator || '>',
compareValue: outcome.compareValue || 0, compareValue: outcome.compareValue || 0,
}, },
@ -411,7 +424,7 @@ export function toCompareOutcomeForm(
label: outcome.label || '', label: outcome.label || '',
targetId: outcome.targetId || '', targetId: outcome.targetId || '',
conditions: conditions.map((condition) => ({ conditions: conditions.map((condition) => ({
compareColumn: condition.compareColumn || 'Tutar', compareColumn: condition.compareColumn || 'Price',
compareOperator: condition.compareOperator || '>', compareOperator: condition.compareOperator || '>',
compareValue: condition.compareValue ?? 0, compareValue: condition.compareValue ?? 0,
})), })),
@ -429,7 +442,7 @@ export function normalizeCompareOutcome(
? outcome.conditions ? outcome.conditions
: [ : [
{ {
compareColumn: outcome.compareColumn || 'Tutar', compareColumn: outcome.compareColumn || 'Price',
compareOperator: outcome.compareOperator || '>', compareOperator: outcome.compareOperator || '>',
compareValue: outcome.compareValue || 0, compareValue: outcome.compareValue || 0,
}, },
@ -437,7 +450,7 @@ export function normalizeCompareOutcome(
) )
.filter((condition) => condition.compareOperator && String(condition.compareValue ?? '') !== '') .filter((condition) => condition.compareOperator && String(condition.compareValue ?? '') !== '')
.map((condition) => ({ .map((condition) => ({
compareColumn: condition.compareColumn || 'Tutar', compareColumn: condition.compareColumn || 'Price',
compareOperator: condition.compareOperator || '>', compareOperator: condition.compareOperator || '>',
compareValue: Number(condition.compareValue || 0), compareValue: Number(condition.compareValue || 0),
})) }))
@ -488,9 +501,10 @@ export function criteriaSummary(item: WorkflowCriteriaDto) {
.join(' / ') || '-' .join(' / ') || '-'
) )
} }
if (item.kind === 'Approval') return item.approver || '-' if (item.kind === 'Approval' || item.kind === 'Inform') {
if (item.kind === 'Inform') return item.approver || '-' return `${item.title} ${item.approver ? `- ${item.approver}` : ''}`
return item.title }
return `${item.title} ${item.approver ? `- ${item.approver}` : ''}`
} }
export function targetTitle(criteria: WorkflowCriteriaDto[], id?: string | null) { export function targetTitle(criteria: WorkflowCriteriaDto[], id?: string | null) {

View file

@ -70,7 +70,7 @@ export function FormTabWorkflow(
) )
const loadState = useCallback(async () => { const loadState = useCallback(async () => {
const data = await workflowService.getState(props.listFormCode) const data = await workflowService.getCriteria(props.listFormCode)
setCriteria(data.criteria) setCriteria(data.criteria)
return data return data
}, [props.listFormCode]) }, [props.listFormCode])
@ -299,6 +299,7 @@ export function FormTabWorkflow(
approvalUserFieldName: string(), approvalUserFieldName: string(),
approvalDateFieldName: string(), approvalDateFieldName: string(),
approvalStatusFieldName: string(), approvalStatusFieldName: string(),
approvalDescriptionFieldName: string(),
}) })
const initialValues = useStoreState((s) => s.admin.lists.values) const initialValues = useStoreState((s) => s.admin.lists.values)
@ -319,7 +320,7 @@ export function FormTabWorkflow(
<Form> <Form>
<FormContainer size="sm"> <FormContainer size="sm">
<Card className="my-2"> <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 <FormItem
label={translate('::ListForms.ListFormEdit.Workflow.ApprovalUserFieldName')} label={translate('::ListForms.ListFormEdit.Workflow.ApprovalUserFieldName')}
invalid={ invalid={
@ -391,6 +392,33 @@ export function FormTabWorkflow(
)} )}
</Field> </Field>
</FormItem> </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> </div>
<Button block variant="solid" loading={isSubmitting}> <Button block variant="solid" loading={isSubmitting}>

View file

@ -54,13 +54,55 @@ export function WorkflowCriteria({
.filter((item) => item.id !== formValues.id) .filter((item) => item.id !== formValues.id)
.map((item) => ({ value: item.id, label: `${item.id} - ${item.title}` })), .map((item) => ({ value: item.id, label: `${item.id} - ${item.title}` })),
] ]
const numericCompareColumn = selectCommandColumns.find((column) =>
isNumericDataType(column.dataType),
)
const compareColumnOptions = selectCommandColumns.length const compareColumnOptions = selectCommandColumns.length
? selectCommandColumns.map((column) => ({ ? selectCommandColumns.map((column) => ({
value: column.columnName, value: column.columnName,
label: `${column.columnName} (${column.dataType})`, 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 updateCompareOutcome = (index: number, patch: Partial<CompareOutcomeDto>) => {
const next = [...(formValues.compareOutcomes || [])] const next = [...(formValues.compareOutcomes || [])]
next[index] = { ...next[index], ...patch } next[index] = { ...next[index], ...patch }
@ -199,7 +241,7 @@ export function WorkflowCriteria({
<div className="flex-1 min-h-0 overflow-y-auto pr-1"> <div className="flex-1 min-h-0 overflow-y-auto pr-1">
<FormContainer size="sm"> <FormContainer size="sm">
<div className="grid grid-cols-2 gap-1"> <div className="grid grid-cols-2 gap-1">
<FormItem label="Tip" asterisk> <FormItem label={translate('::App.Platform.Type')} asterisk>
<SelectField <SelectField
required required
options={kindOptions} options={kindOptions}
@ -207,7 +249,7 @@ export function WorkflowCriteria({
onChange={(value) => setField('kind', value)} onChange={(value) => setField('kind', value)}
/> />
</FormItem> </FormItem>
<FormItem label="Başlık" asterisk> <FormItem label={translate('::App.Listform.ListformField.Title')} asterisk>
<Input <Input
required required
value={formValues.title} value={formValues.title}
@ -215,11 +257,11 @@ export function WorkflowCriteria({
/> />
</FormItem> </FormItem>
<FormItem <FormItem
label="Onaylayacak Kişi" label={translate('::App.Listform.ListformField.Approver')}
asterisk={formValues.kind === 'Approval' || formValues.kind === 'Inform'} asterisk={formValues.kind === 'Approval' || formValues.kind === 'Inform'}
> >
<SelectField <SelectField
required required={formValues.kind === 'Approval' || formValues.kind === 'Inform'}
options={userList} options={userList}
value={formValues.approver} value={formValues.approver}
onChange={(value) => setField('approver', value)} onChange={(value) => setField('approver', value)}
@ -227,7 +269,7 @@ export function WorkflowCriteria({
</FormItem> </FormItem>
{(formValues.kind === 'Start' || formValues.kind === 'Inform') && ( {(formValues.kind === 'Start' || formValues.kind === 'Inform') && (
<FormItem label="Sonraki adım" asterisk> <FormItem label={translate('::App.Listform.ListformField.NextOnStart')} asterisk>
{targetSelect( {targetSelect(
formValues.nextOnStart, formValues.nextOnStart,
(value) => setField('nextOnStart', value), (value) => setField('nextOnStart', value),
@ -238,14 +280,14 @@ export function WorkflowCriteria({
{formValues.kind === 'Approval' && ( {formValues.kind === 'Approval' && (
<> <>
<FormItem label="Onay adımı" asterisk> <FormItem label={translate('::App.Listform.ListformField.NextOnApprove')} asterisk>
{targetSelect( {targetSelect(
formValues.nextOnApprove, formValues.nextOnApprove,
(value) => setField('nextOnApprove', value), (value) => setField('nextOnApprove', value),
true, true,
)} )}
</FormItem> </FormItem>
<FormItem label="Red adımı" asterisk> <FormItem label={translate('::App.Listform.ListformField.NextOnReject')} asterisk>
{targetSelect( {targetSelect(
formValues.nextOnReject, formValues.nextOnReject,
(value) => setField('nextOnReject', value), (value) => setField('nextOnReject', value),
@ -260,10 +302,10 @@ export function WorkflowCriteria({
<div className="mb-4"> <div className="mb-4">
<div className="mb-3 flex items-center justify-between gap-2"> <div className="mb-3 flex items-center justify-between gap-2">
<h6> <h6>
Karşılaştırma durumları {translate('::App.Listform.ListformField.CompareOutcomes')}
{isLoadingSelectCommandColumns && ( {isLoadingSelectCommandColumns && (
<span className="ml-2 text-xs font-normal text-gray-400"> <span className="ml-2 text-xs font-normal text-gray-400">
Sütunlar yükleniyor... {translate('::App.Listform.ListformField.LoadingColumns')}
</span> </span>
)} )}
</h6> </h6>
@ -276,11 +318,12 @@ export function WorkflowCriteria({
...(formValues.compareOutcomes || []), ...(formValues.compareOutcomes || []),
emptyCompareOutcome1( emptyCompareOutcome1(
`Durum ${(formValues.compareOutcomes || []).length + 1}`, `Durum ${(formValues.compareOutcomes || []).length + 1}`,
defaultCompareColumn,
), ),
]) ])
} }
> >
Karşılaştırma Ekle {translate('::App.Listform.ListformField.AddCompareOutcome')}
</Button> </Button>
</div> </div>
@ -289,8 +332,8 @@ export function WorkflowCriteria({
(outcome: CompareOutcomeDto, index: number) => ( (outcome: CompareOutcomeDto, index: number) => (
<div key={index} className="rounded border border-gray-200 p-3"> <div key={index} className="rounded border border-gray-200 p-3">
<div className="flex flex-col-11 items-center gap-2 mb-2"> <div className="flex flex-col-11 items-center gap-2 mb-2">
<strong className="flex-[5]">Durum {index + 1}</strong> <strong className="flex-[5]">{translate('::App.Listform.ListformField.Status')} {index + 1}</strong>
<strong className="flex-[5]">Bağlantı</strong> <strong className="flex-[5]">{translate('::App.Listform.ListformField.Connection')}</strong>
<span className="flex-1" /> <span className="flex-1" />
</div> </div>
<div className="flex flex-col-11 items-center gap-2"> <div className="flex flex-col-11 items-center gap-2">
@ -322,7 +365,7 @@ export function WorkflowCriteria({
disabled={(formValues.compareOutcomes || []).length <= 2} disabled={(formValues.compareOutcomes || []).length <= 2}
onClick={() => removeCompareOutcome(index)} onClick={() => removeCompareOutcome(index)}
> >
Sil {translate('::Delete')}
</Button> </Button>
</div> </div>
@ -381,7 +424,7 @@ export function WorkflowCriteria({
onClick={() => addCompareCondition(index)} onClick={() => addCompareCondition(index)}
className="flex-[1]" className="flex-[1]"
> >
Ekle {translate('::Insert')}
</Button> </Button>
</div> </div>
))} ))}
@ -398,7 +441,7 @@ export function WorkflowCriteria({
<Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1"> <Dialog.Footer className="flex justify-end gap-2 border-t pt-3 mt-1">
<Button type="button" variant="plain" disabled={busy} onClick={closeDialog}> <Button type="button" variant="plain" disabled={busy} onClick={closeDialog}>
Cancel {translate('::Cancel')}
</Button> </Button>
<Button <Button
type="button" type="button"
@ -406,10 +449,10 @@ export function WorkflowCriteria({
disabled={busy || !formValues.id} disabled={busy || !formValues.id}
onClick={() => onDelete(formValues.id)} onClick={() => onDelete(formValues.id)}
> >
Sil {translate('::Delete')}
</Button> </Button>
<Button variant="solid" loading={busy} type="submit"> <Button variant="solid" loading={busy} type="submit">
Kaydet {translate('::Save')}
</Button> </Button>
</Dialog.Footer> </Dialog.Footer>
</form> </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({ function SelectField({
options, options,
value, value,

View file

@ -250,6 +250,23 @@ function colToSqlLine(col: ColumnDefinition, addComma = true): string {
return ` [${col.columnName}] ${typeSql} ${nullPart}${defaultPart}${addComma ? ',' : ''}` 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( function generateCreateTableSql(
columns: ColumnDefinition[], columns: ColumnDefinition[],
settings: TableSettings, settings: TableSettings,
@ -305,7 +322,12 @@ function generateCreateTableSql(
} else { } else {
extraIndexLines.push(`-- 📋 Index: [${idx.indexName}]`) extraIndexLines.push(`-- 📋 Index: [${idx.indexName}]`)
extraIndexLines.push( 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 { } else {
lines.push(`-- 📋 Index: [${ix.indexName}]`) lines.push(`-- 📋 Index: [${ix.indexName}]`)
lines.push( lines.push(
`CREATE ${ix.isClustered ? 'CLUSTERED ' : ''}INDEX [${ix.indexName}] ON ${fullTableName} (${colsSql});`, ...buildCreateIndexIfNotExistsSql(
fullTableName,
ix.indexName,
ix.isClustered,
colsSql,
),
) )
} }
lines.push('') lines.push('')

View file

@ -253,7 +253,11 @@ const useGridData = (props: {
name: i.dataField, name: i.dataField,
editorType2: i.editorType2, editorType2: i.editorType2,
editorType: editorType:
i.editorType2 == PlatformEditorTypes.dxGridBox ? 'dxDropDownBox' : i.editorType2, i.editorType2 == PlatformEditorTypes.dxGridBox
? 'dxDropDownBox'
: i.editorType2 == PlatformEditorTypes.dxImageUpload
? undefined
: i.editorType2,
colSpan: i.colSpan, colSpan: i.colSpan,
isRequired: i.isRequired, isRequired: i.isRequired,
editorOptions: { editorOptions: {

View file

@ -65,7 +65,7 @@ import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent' import { ImageUploadEditorComponent } from './editors/ImageUploadEditorComponent'
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent' import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { useFilters } from './useFilters' import { useFilters } from './useFilters'
import { useToolbar } from './useToolbar' import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
import { ImportDashboard } from '@/components/importManager/ImportDashboard' import { ImportDashboard } from '@/components/importManager/ImportDashboard'
import WidgetGroup from '@/components/ui/Widget/WidgetGroup' import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
import { GridExtraFilterToolbar } from './GridExtraFilterToolbar' import { GridExtraFilterToolbar } from './GridExtraFilterToolbar'
@ -75,6 +75,7 @@ import { useListFormCustomDataSource } from './useListFormCustomDataSource'
import { useListFormColumns } from './useListFormColumns' import { useListFormColumns } from './useListFormColumns'
import { Loading } from '@/components/shared' import { Loading } from '@/components/shared'
import { useStoreState } from '@/store' import { useStoreState } from '@/store'
import { workflowService } from '@/services/workflow.service'
interface GridProps { interface GridProps {
listFormCode: string 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 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 Grid = (props: GridProps) => {
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
const { translate } = useLocalization() const { translate } = useLocalization()
@ -208,9 +227,28 @@ const Grid = (props: GridProps) => {
return grd.getSelectedRowsData() 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(() => { 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 getFilter = useCallback(() => {
const grd = gridRef.current?.instance() const grd = gridRef.current?.instance()
@ -257,6 +295,8 @@ const Grid = (props: GridProps) => {
} }
// SubForm'ları gösterebilmek için secili satiri formData'ya at // SubForm'ları gösterebilmek için secili satiri formData'ya at
updateWorkflowApprovalButtons(grd, data.selectedRowsData)
if (data.selectedRowsData.length) { if (data.selectedRowsData.length) {
setFormData(data.selectedRowsData[0]) setFormData(data.selectedRowsData[0])
} }
@ -401,25 +441,32 @@ const Grid = (props: GridProps) => {
[gridDto, searchParams, extraFilters, getNextSequenceValue], [gridDto, searchParams, extraFilters, getNextSequenceValue],
) )
const onRowInserting = useCallback((e: DataGridTypes.RowInsertingEvent<any, any>) => { const onRowInserting = useCallback(
if (!gridDto?.columnFormats) { (e: DataGridTypes.RowInsertingEvent<any, any>) => {
e.data = setFormEditingExtraItemValues(e.data) if (!gridDto?.columnFormats) {
return e.data = setFormEditingExtraItemValues(e.data)
} return
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) {
filteredData[key] = e.data[key]
} }
} const allowedFields = gridDto.columnFormats
e.data = setFormEditingExtraItemValues(filteredData) .filter((f) => f.allowAdding)
}, [gridDto]) .map((f) => f.fieldName)
const filteredData: any = {}
for (const key of allowedFields) {
if (e.data.hasOwnProperty(key) && key) {
filteredData[key] = e.data[key]
}
}
e.data = setFormEditingExtraItemValues(filteredData)
},
[gridDto],
)
const onRowUpdating = useCallback( const onRowUpdating = useCallback(
(e: DataGridTypes.RowUpdatingEvent<any, any>) => { (e: DataGridTypes.RowUpdatingEvent<any, any>) => {
if (!gridDto?.columnFormats) return 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 } let newData = { ...e.oldData, ...e.newData }
// Remove keys not allowed // Remove keys not allowed
Object.keys(newData).forEach((key) => { Object.keys(newData).forEach((key) => {
@ -452,7 +499,7 @@ const Grid = (props: GridProps) => {
if (!e.data[field[0]]) { if (!e.data[field[0]]) {
return return
} }
const json = JSON.parse(e.data[field[0]]) const json = JSON.parse(e.data[field[0]])
e.data[col.dataField] = json[field[1]] e.data[col.dataField] = json[field[1]]
}) })
@ -521,6 +568,19 @@ const Grid = (props: GridProps) => {
const formItem = gridDto.gridOptions.editingFormDto const formItem = gridDto.gridOptions.editingFormDto
.flatMap((group) => group.items || []) .flatMap((group) => group.items || [])
.find((i) => i.dataField === editor.dataField) .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ığı // Cascade mantığı
const cascadeInfo = cascadeFieldsMap.get(editor.dataField) const cascadeInfo = cascadeFieldsMap.get(editor.dataField)
@ -1192,6 +1252,7 @@ const Grid = (props: GridProps) => {
showColumnHeaders={gridDto.gridOptions.columnOptionDto?.showColumnHeaders} showColumnHeaders={gridDto.gridOptions.columnOptionDto?.showColumnHeaders}
filterSyncEnabled={true} filterSyncEnabled={true}
onSelectionChanged={onSelectionChanged} onSelectionChanged={onSelectionChanged}
onContentReady={(e) => updateWorkflowApprovalButtons(e.component)}
onInitNewRow={onInitNewRow} onInitNewRow={onInitNewRow}
onCellPrepared={onCellPrepared} onCellPrepared={onCellPrepared}
onRowInserting={onRowInserting} onRowInserting={onRowInserting}
@ -1212,7 +1273,18 @@ const Grid = (props: GridProps) => {
setMode('view') setMode('view')
setIsPopupFullScreen(false) 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?.() props.refreshData?.()
}} }}
onRowUpdated={() => { onRowUpdated={() => {
@ -1342,9 +1414,9 @@ const Grid = (props: GridProps) => {
if (mode === 'view') { if (mode === 'view') {
return a.canRead return a.canRead
} else if (mode === 'new') { } else if (mode === 'new') {
return (a.canCreate || a.canRead) && a.allowAdding return a.canCreate && a.allowAdding
} else if (mode === 'edit') { } else if (mode === 'edit') {
return (a.canUpdate || a.canRead) && a.allowEditing return a.canUpdate && a.allowEditing
} else { } else {
return false return false
} }
@ -1371,9 +1443,9 @@ const Grid = (props: GridProps) => {
if (mode === 'view') { if (mode === 'view') {
return a.canRead return a.canRead
} else if (mode === 'new') { } else if (mode === 'new') {
return (a.canCreate || a.canRead) && a.allowAdding return a.canCreate && a.allowAdding
} else if (mode === 'edit') { } else if (mode === 'edit') {
return (a.canUpdate || a.canRead) && a.allowEditing return a.canUpdate && a.allowEditing
} else { } else {
return false return false
} }

View file

@ -55,7 +55,7 @@ import {
import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent' import { GridBoxEditorComponent } from './editors/GridBoxEditorComponent'
import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent' import { TagBoxEditorComponent } from './editors/TagBoxEditorComponent'
import { useFilters } from './useFilters' import { useFilters } from './useFilters'
import { useToolbar } from './useToolbar' import { updateWorkflowApprovalToolbarItems, useToolbar } from './useToolbar'
import WidgetGroup from '@/components/ui/Widget/WidgetGroup' import WidgetGroup from '@/components/ui/Widget/WidgetGroup'
import { GridExtraFilterToolbar } from './GridExtraFilterToolbar' import { GridExtraFilterToolbar } from './GridExtraFilterToolbar'
import { getList } from '@/services/form.service' import { getList } from '@/services/form.service'
@ -66,6 +66,7 @@ import { DataType } from 'devextreme/common'
import { useStoreState } from '@/store/store' import { useStoreState } from '@/store/store'
import SubForms from '../form/SubForms' import SubForms from '../form/SubForms'
import { ImportDashboard } from '@/components/importManager/ImportDashboard' import { ImportDashboard } from '@/components/importManager/ImportDashboard'
import { workflowService } from '@/services/workflow.service'
interface TreeProps { interface TreeProps {
listFormCode: string listFormCode: string
@ -78,6 +79,21 @@ interface TreeProps {
const statedGridPanelColor = 'rgba(50, 200, 200, 0.5)' 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 Tree = (props: TreeProps) => {
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
const { translate } = useLocalization() 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(() => { 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 getFilter = useCallback(() => {
const tree = gridRef.current?.instance() const tree = gridRef.current?.instance()
@ -300,6 +335,8 @@ const Tree = (props: TreeProps) => {
} }
} }
updateWorkflowApprovalButtons(tree, data.selectedRowsData)
if (data.selectedRowsData.length) { if (data.selectedRowsData.length) {
setFormData(data.selectedRowsData[0]) setFormData(data.selectedRowsData[0])
} }
@ -480,6 +517,19 @@ const Tree = (props: TreeProps) => {
const formItem = gridDto.gridOptions.editingFormDto const formItem = gridDto.gridOptions.editingFormDto
.flatMap((group) => group.items || []) .flatMap((group) => group.items || [])
.find((i) => i.dataField === editor.dataField) .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ığı // Cascade disabled mantığı
const colFormat = gridDto.columnFormats.find((c) => c.fieldName === editor.dataField) const colFormat = gridDto.columnFormats.find((c) => c.fieldName === editor.dataField)
@ -878,7 +928,18 @@ const Tree = (props: TreeProps) => {
setMode('view') setMode('view')
setIsPopupFullScreen(false) 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?.() props.refreshData?.()
}} }}
onRowUpdated={() => { onRowUpdated={() => {
@ -889,6 +950,8 @@ const Tree = (props: TreeProps) => {
}} }}
onEditorPreparing={onEditorPreparing} onEditorPreparing={onEditorPreparing}
onContentReady={(e) => { onContentReady={(e) => {
updateWorkflowApprovalButtons(e.component)
// Restore expanded keys after data refresh (only if autoExpandAll is false) // Restore expanded keys after data refresh (only if autoExpandAll is false)
if ( if (
!gridDto.gridOptions.treeOptionDto?.autoExpandAll && !gridDto.gridOptions.treeOptionDto?.autoExpandAll &&
@ -1117,9 +1180,9 @@ const Tree = (props: TreeProps) => {
if (mode === 'view') { if (mode === 'view') {
return a.canRead return a.canRead
} else if (mode === 'new') { } else if (mode === 'new') {
return a.canCreate || a.canRead return a.canCreate && a.allowAdding
} else if (mode === 'edit') { } else if (mode === 'edit') {
return a.canUpdate || a.canRead return a.canUpdate && a.allowEditing
} else { } else {
return false return false
} }
@ -1146,9 +1209,9 @@ const Tree = (props: TreeProps) => {
if (mode === 'view') { if (mode === 'view') {
return a.canRead return a.canRead
} else if (mode === 'new') { } else if (mode === 'new') {
return a.canCreate || a.canRead return a.canCreate && a.allowAdding
} else if (mode === 'edit') { } else if (mode === 'edit') {
return a.canUpdate || a.canRead return a.canUpdate && a.allowEditing
} else { } else {
return false return false
} }

View file

@ -691,7 +691,7 @@ const useListFormColumns = ({
} }
} }
column.allowEditing = colData?.allowEditing column.allowEditing = colData?.allowEditing || colData?.allowAdding
// #region lookup ayarlari // #region lookup ayarlari
if (colData.lookupDto?.dataSourceType) { if (colData.lookupDto?.dataSourceType) {

View file

@ -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 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 = ({ const useListFormCustomDataSource = ({
gridRef, gridRef,
}: { }: {
@ -372,7 +400,10 @@ const useListFormCustomDataSource = ({
} }
const insertUrl = getServiceAddress(gridOptions.insertServiceAddress) 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) => { errorHandler: (error: any) => {
console.log(error.message) console.log(error.message)

View file

@ -1,5 +1,5 @@
import { Button, Notification, toast } from '@/components/ui' 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 { dynamicFetch } from '@/services/form.service'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission' import { usePermission } from '@/utils/hooks/usePermission'
@ -10,6 +10,7 @@ import { useDialogContext } from '../shared/DialogContext'
import { usePWA } from '@/utils/hooks/usePWA' import { usePWA } from '@/utils/hooks/usePWA'
import { layoutTypes, ListViewLayoutType } from '../admin/listForm/edit/types' import { layoutTypes, ListViewLayoutType } from '../admin/listForm/edit/types'
import { useStoreState } from '@/store' import { useStoreState } from '@/store'
import { workflowService } from '@/services/workflow.service'
type ToolbarModalData = { type ToolbarModalData = {
open: boolean open: boolean
@ -51,7 +52,6 @@ const useToolbar = ({
const [toolbarData, setToolbarData] = useState<ToolbarItem[]>([]) const [toolbarData, setToolbarData] = useState<ToolbarItem[]>([])
const [toolbarModalData, setToolbarModalData] = useState<ToolbarModalData>() const [toolbarModalData, setToolbarModalData] = useState<ToolbarModalData>()
const grdOpt = gridDto?.gridOptions const grdOpt = gridDto?.gridOptions
function getToolbarData() { function getToolbarData() {
@ -112,6 +112,136 @@ const useToolbar = ({
location: 'after', 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 // Add Expand All button for TreeList
if (layout === layoutTypes.tree && grdOpt.treeOptionDto?.parentIdExpr) { if (layout === layoutTypes.tree && grdOpt.treeOptionDto?.parentIdExpr) {
items.push({ 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 } export { useToolbar }