LisformWorkflow düzenlemesi

This commit is contained in:
Sedat Öztürk 2026-05-22 23:41:36 +03:00
parent 49d82d6123
commit 7b0f4acced
34 changed files with 1495 additions and 3213 deletions

View file

@ -102,7 +102,7 @@ Driven by:
- ListFormFields
- ListFormCustomization (UserUiFilter, GridState, ServerJoin, ServerWhere)
- ListFormImport and ListFormImportLog
- ListFormWorkflow and ListFormWorkflowCriteria
- ListFormWorkflow
- ListFormJsonRow operations
Capabilities:

View file

@ -365,6 +365,19 @@ public class GridOptionsDto : AuditedEntityDto<Guid>
}
set { SubFormsJson = 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); }
}
[JsonIgnore]
public string ExtraFilterJson { get; set; } // Cagrilacak Extra Filters

View file

@ -0,0 +1,9 @@
using System;
namespace Sozsoft.Platform.ListForms;
public class WorkflowDto
{
public string ApprovalFieldName { get; set; }
public DateTime ApprovalDateFieldName { get; set; }
}

View file

@ -53,6 +53,7 @@ public class ListFormEditTabs
public const string StateForm = "state";
public const string SubFormJsonRow = "subForm";
public const string WidgetForm = "widget";
public const string WorkflowForm = "workflow";
public const string Fields = "fields";
public const string Customization = "customization";
public const string ExtraFilterForm = "extraFilter";

View file

@ -7,15 +7,12 @@ public class CreateUpdateListFormWorkflowCriteriaDto
{
public Guid? Id { get; set; }
public string ListFormCode { get; set; }
public Guid WorkflowItemId { get; set; }
public string NodeId { get; set; }
public string Kind { get; set; }
public string Title { get; set; }
public string Column { get; set; }
public string Operator { get; set; }
public string CompareColumn { get; set; }
public string CompareOperator { get; set; }
public decimal CompareValue { get; set; }
public string Approver { get; set; }
public string InformPerson { get; set; }
public string NextOnStart { get; set; }
public string NextOnTrue { get; set; }
public string NextOnFalse { get; set; }

View file

@ -1,12 +0,0 @@
using System;
namespace Sozsoft.Platform.ListForms.Workflow;
public class CreateUpdateListFormWorkflowDto
{
public string ListFormCode { get; set; }
public string Sorumlu { get; set; }
public DateTime? Tarih { get; set; }
public decimal Amount { get; set; }
}

View file

@ -7,10 +7,6 @@ namespace Sozsoft.Platform.ListForms.Workflow;
public interface IListFormWorkflowAppService : IApplicationService
{
Task<ListFormWorkflowStateDto> GetStateAsync(string listFormCode = null);
Task<ListFormWorkflowDto> CreateWorkflowAsync(CreateUpdateListFormWorkflowDto input);
Task<ListFormWorkflowDto> UpdateWorkflowAsync(Guid id, CreateUpdateListFormWorkflowDto input);
Task<ListFormWorkflowDto> StartWorkflowAsync(Guid id);
Task<ListFormWorkflowDto> DecideWorkflowAsync(Guid id, DecisionWorkflowDto input);
Task<ListFormWorkflowCriteriaDto> SaveCriteriaAsync(CreateUpdateListFormWorkflowCriteriaDto input);
Task DeleteCriteriaAsync(Guid id);
Task<ListFormWorkflowStateDto> ResetDemoAsync(string listFormCode = null);

View file

@ -7,15 +7,12 @@ namespace Sozsoft.Platform.ListForms.Workflow;
public class ListFormWorkflowCriteriaDto : AuditedEntityDto<Guid>
{
public string ListFormCode { get; set; }
public Guid WorkflowItemId { get; set; }
public string NodeId { get; set; }
public string Kind { get; set; }
public string Title { get; set; }
public string Column { get; set; }
public string Operator { get; set; }
public string CompareColumn { get; set; }
public string CompareOperator { get; set; }
public decimal CompareValue { get; set; }
public string Approver { get; set; }
public string InformPerson { get; set; }
public string NextOnStart { get; set; }
public string NextOnTrue { get; set; }
public string NextOnFalse { get; set; }

View file

@ -1,22 +0,0 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.ListForms.Workflow;
public class ListFormWorkflowDto : AuditedEntityDto<Guid>
{
public string ListFormCode { get; set; }
public int OrderNo { get; set; }
public string Title { get; set; }
public string Sorumlu { get; set; }
public DateTime Tarih { get; set; }
public string Status { get; set; }
public string Durum { get; set; }
public decimal Amount { get; set; }
public string CurrentNodeId { get; set; }
public string AssignedApprover { get; set; }
public string InformedPerson { get; set; }
public List<WorkflowHistoryDto> History { get; set; } = [];
}

View file

@ -4,7 +4,6 @@ namespace Sozsoft.Platform.ListForms.Workflow;
public class ListFormWorkflowStateDto
{
public List<ListFormWorkflowDto> WorkflowItems { get; set; } = [];
public List<ListFormWorkflowCriteriaDto> Criteria { get; set; } = [];
}

View file

@ -2,8 +2,8 @@ namespace Sozsoft.Platform.ListForms.Workflow;
public class WorkflowConditionDto
{
public string Column { get; set; }
public string Operator { get; set; }
public string CompareColumn { get; set; }
public string CompareOperator { get; set; }
public decimal CompareValue { get; set; }
}

View file

@ -179,6 +179,10 @@ public class ListFormsAppService : CrudAppService<
{
item.SubFormsJson = JsonSerializer.Serialize(input.SubFormsDto);
}
else if (input.EditType == ListFormEditTabs.WorkflowForm)
{
item.WorkflowJson = JsonSerializer.Serialize(input.WorkflowDto);
}
/*Chart*/
else if (input.EditType == ListFormEditTabs.ChartCommonForm)

View file

@ -0,0 +1,282 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Sozsoft.Platform.Entities;
using Volo.Abp;
using Volo.Abp.Domain.Repositories;
namespace Sozsoft.Platform.ListForms.Workflow;
[Authorize]
[Route("api/app/list-form-workflow")]
public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowAppService
{
private const string DefaultListFormCode = "workflow";
private readonly IRepository<ListFormWorkflow, Guid> criteriaRepository;
public ListFormWorkflowAppService(IRepository<ListFormWorkflow, Guid> criteriaRepository)
{
this.criteriaRepository = criteriaRepository;
}
[HttpGet("state")]
public async Task<ListFormWorkflowStateDto> GetStateAsync(string listFormCode = null)
{
var code = NormalizeListFormCode(listFormCode);
var criteria = (await criteriaRepository.GetListAsync(x => x.ListFormCode == code))
.OrderBy(x => x.PositionX)
.ThenBy(x => x.PositionY)
.ToList();
return new ListFormWorkflowStateDto
{
Criteria = criteria.Select(MapCriteria).ToList()
};
}
[HttpPost("criteria")]
public async Task<ListFormWorkflowCriteriaDto> SaveCriteriaAsync(CreateUpdateListFormWorkflowCriteriaDto input)
{
var code = NormalizeListFormCode(input.ListFormCode);
var isNew = !input.Id.HasValue || input.Id.Value == Guid.Empty;
var criteria = isNew
? new ListFormWorkflow(GuidGenerator.Create())
: await criteriaRepository.GetAsync(input.Id.Value);
if (!isNew && criteria.ListFormCode != code)
{
throw new UserFriendlyException("Workflow adımı seçili liste formuna ait değil.");
}
criteria.ListFormCode = code;
criteria.Kind = NormalizeRequired(input.Kind, "Compare");
criteria.Title = NormalizeRequired(input.Title, criteria.Kind);
criteria.CompareColumn = NormalizeRequired(input.CompareColumn, "Tutar");
criteria.CompareOperator = NormalizeRequired(input.CompareOperator, ">");
criteria.CompareValue = input.CompareValue;
criteria.Approver = input.Approver ?? string.Empty;
criteria.NextOnStart = input.NextOnStart ?? string.Empty;
criteria.NextOnTrue = input.NextOnTrue ?? string.Empty;
criteria.NextOnFalse = input.NextOnFalse ?? string.Empty;
criteria.NextOnApprove = input.NextOnApprove ?? string.Empty;
criteria.NextOnReject = input.NextOnReject ?? string.Empty;
criteria.PositionX = input.PositionX <= 0 ? 32 : input.PositionX;
criteria.PositionY = input.PositionY <= 0 ? 150 : input.PositionY;
criteria.CompareOutcomesJson = SerializeCompareOutcomes(input.CompareOutcomes);
var outcomes = input.CompareOutcomes ?? [];
if (criteria.Kind == "Compare" && outcomes.Count > 0)
{
criteria.NextOnTrue = outcomes.ElementAtOrDefault(0)?.TargetId ?? criteria.NextOnTrue;
criteria.NextOnFalse = outcomes.ElementAtOrDefault(1)?.TargetId ?? criteria.NextOnFalse;
}
if (isNew)
{
await criteriaRepository.InsertAsync(criteria, autoSave: true);
}
else
{
await criteriaRepository.UpdateAsync(criteria, autoSave: true);
}
return MapCriteria(criteria);
}
[HttpDelete("criteria/{id}")]
public async Task DeleteCriteriaAsync(Guid id)
{
var criteria = await criteriaRepository.GetAsync(id);
await criteriaRepository.DeleteAsync(criteria, autoSave: true);
var remaining = await criteriaRepository.GetListAsync(x => x.ListFormCode == criteria.ListFormCode);
foreach (var item in remaining)
{
var changed = ClearDeletedTarget(item, id.ToString());
if (changed)
{
await criteriaRepository.UpdateAsync(item, autoSave: true);
}
}
}
[HttpPost("reset-demo")]
public async Task<ListFormWorkflowStateDto> ResetDemoAsync(string listFormCode = null)
{
var code = NormalizeListFormCode(listFormCode);
var existing = await criteriaRepository.GetListAsync(x => x.ListFormCode == code);
foreach (var item in existing)
{
await criteriaRepository.DeleteAsync(item, autoSave: true);
}
var start = await CreateCriteriaAsync(code, "Start", "İş Akışı Başlat", 80, 150);
var compare = await CreateCriteriaAsync(code, "Compare", "Tutar kontrolü", 330, 130);
var approval = await CreateCriteriaAsync(code, "Approval", "Yönetici Onayı", 590, 60, "ayse.yilmaz");
var inform = await CreateCriteriaAsync(code, "Inform", "Muhasebe Bilgilendirme", 590, 230, "muhasebe");
var end = await CreateCriteriaAsync(code, "End", "Akışı Bitir", 850, 150);
start.NextOnStart = compare.Id.ToString();
compare.NextOnTrue = approval.Id.ToString();
compare.NextOnFalse = inform.Id.ToString();
compare.CompareOutcomesJson = SerializeCompareOutcomes([
new CompareOutcomeDto
{
Label = "Onay gerekir",
TargetId = approval.Id.ToString(),
Conditions = [new WorkflowConditionDto { CompareColumn = "Tutar", CompareOperator = ">", CompareValue = 5000 }]
},
new CompareOutcomeDto
{
Label = "Bilgilendir",
TargetId = inform.Id.ToString(),
Conditions = [new WorkflowConditionDto { CompareColumn = "Tutar", CompareOperator = "<=", CompareValue = 5000 }]
}
]);
approval.NextOnApprove = inform.Id.ToString();
approval.NextOnReject = end.Id.ToString();
inform.NextOnStart = end.Id.ToString();
await criteriaRepository.UpdateAsync(start, autoSave: true);
await criteriaRepository.UpdateAsync(compare, autoSave: true);
await criteriaRepository.UpdateAsync(approval, autoSave: true);
await criteriaRepository.UpdateAsync(inform, autoSave: true);
return await GetStateAsync(code);
}
private async Task<ListFormWorkflow> CreateCriteriaAsync(
string listFormCode,
string kind,
string title,
int positionX,
int positionY,
string approver = "")
{
var criteria = new ListFormWorkflow(GuidGenerator.Create())
{
ListFormCode = listFormCode,
Kind = kind,
Title = title,
CompareColumn = "Tutar",
CompareOperator = ">",
CompareValue = 5000,
Approver = approver,
NextOnStart = string.Empty,
NextOnTrue = string.Empty,
NextOnFalse = string.Empty,
NextOnApprove = string.Empty,
NextOnReject = string.Empty,
PositionX = positionX,
PositionY = positionY,
CompareOutcomesJson = SerializeCompareOutcomes([])
};
await criteriaRepository.InsertAsync(criteria, autoSave: true);
return criteria;
}
private static bool ClearDeletedTarget(ListFormWorkflow criteria, string deletedId)
{
var changed = false;
if (criteria.NextOnStart == deletedId)
{
criteria.NextOnStart = string.Empty;
changed = true;
}
if (criteria.NextOnTrue == deletedId)
{
criteria.NextOnTrue = string.Empty;
changed = true;
}
if (criteria.NextOnFalse == deletedId)
{
criteria.NextOnFalse = string.Empty;
changed = true;
}
if (criteria.NextOnApprove == deletedId)
{
criteria.NextOnApprove = string.Empty;
changed = true;
}
if (criteria.NextOnReject == deletedId)
{
criteria.NextOnReject = string.Empty;
changed = true;
}
var outcomes = DeserializeCompareOutcomes(criteria.CompareOutcomesJson);
foreach (var outcome in outcomes.Where(x => x.TargetId == deletedId))
{
outcome.TargetId = null;
changed = true;
}
if (changed)
{
criteria.CompareOutcomesJson = SerializeCompareOutcomes(outcomes);
}
return changed;
}
private static ListFormWorkflowCriteriaDto MapCriteria(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 string NormalizeListFormCode(string listFormCode)
{
return listFormCode.IsNullOrWhiteSpace() ? DefaultListFormCode : listFormCode.Trim();
}
private static string NormalizeRequired(string value, string fallback)
{
return value.IsNullOrWhiteSpace() ? fallback : value.Trim();
}
private static string SerializeCompareOutcomes(List<CompareOutcomeDto> outcomes)
{
return JsonSerializer.Serialize(outcomes ?? []);
}
private static List<CompareOutcomeDto> DeserializeCompareOutcomes(string json)
{
if (json.IsNullOrWhiteSpace())
{
return [];
}
try
{
return JsonSerializer.Deserialize<List<CompareOutcomeDto>>(json) ?? [];
}
catch
{
return [];
}
}
}

View file

@ -1,595 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Sozsoft.Platform.Entities;
using Volo.Abp;
using Volo.Abp.Domain.Repositories;
namespace Sozsoft.Platform.ListForms.Workflow;
[Authorize]
[Route("api/app/list-form-workflow")]
public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowAppService
{
private const string DefaultListFormCode = "workflow";
private const string StatusNew = "Yeni";
private const string StatusPending = "Onay Bekliyor";
private const string StatusFinished = "Bitti";
private const string StatusInformed = "Bilgilendirildi";
private readonly IRepository<ListFormWorkflow, Guid> workflowRepository;
private readonly IRepository<ListFormWorkflowCriteria, Guid> criteriaRepository;
public ListFormWorkflowAppService(
IRepository<ListFormWorkflow, Guid> workflowRepository,
IRepository<ListFormWorkflowCriteria, Guid> criteriaRepository)
{
this.workflowRepository = workflowRepository;
this.criteriaRepository = criteriaRepository;
}
[HttpGet("state")]
public async Task<ListFormWorkflowStateDto> GetStateAsync(string listFormCode = null)
{
var code = NormalizeListFormCode(listFormCode);
var workflows = (await workflowRepository.GetListAsync(x => x.ListFormCode == code))
.OrderBy(x => x.OrderNo)
.ThenBy(x => x.CreationTime)
.ToList();
var criteria = (await criteriaRepository.GetListAsync(x => x.ListFormCode == code))
.OrderBy(x => x.WorkflowItemId)
.ThenBy(x => x.PositionX)
.ThenBy(x => x.PositionY)
.ToList();
return new ListFormWorkflowStateDto
{
WorkflowItems = workflows.Select(MapWorkflow).ToList(),
Criteria = criteria.Select(MapCriteria).ToList()
};
}
[HttpPost("workflows")]
public async Task<ListFormWorkflowDto> CreateWorkflowAsync(CreateUpdateListFormWorkflowDto input)
{
var code = NormalizeListFormCode(input.ListFormCode);
var nextOrderNo = (await workflowRepository.GetListAsync(x => x.ListFormCode == code))
.Select(x => x.OrderNo)
.DefaultIfEmpty()
.Max() + 1;
var workflow = new ListFormWorkflow(GuidGenerator.Create())
{
ListFormCode = code,
OrderNo = nextOrderNo,
Title = NormalizeRequired(input.Sorumlu, "Yeni Workflow"),
Status = StatusNew,
Amount = input.Amount,
CurrentNodeId = string.Empty,
AssignedApprover = string.Empty,
InformedPerson = string.Empty,
HistoryJson = SerializeHistory([])
};
await workflowRepository.InsertAsync(workflow, autoSave: true);
var start = await CreateDefaultStartCriteriaAsync(workflow);
workflow.CurrentNodeId = start.Id.ToString();
await workflowRepository.UpdateAsync(workflow, autoSave: true);
return MapWorkflow(workflow);
}
[HttpPut("workflows/{id}")]
public async Task<ListFormWorkflowDto> UpdateWorkflowAsync(Guid id, CreateUpdateListFormWorkflowDto input)
{
var workflow = await workflowRepository.GetAsync(id);
workflow.ListFormCode = NormalizeListFormCode(input.ListFormCode ?? workflow.ListFormCode);
workflow.Title = NormalizeRequired(input.Sorumlu, workflow.Title);
workflow.Amount = input.Amount;
await workflowRepository.UpdateAsync(workflow, autoSave: true);
return MapWorkflow(workflow);
}
[HttpPost("workflows/{id}/start")]
public async Task<ListFormWorkflowDto> StartWorkflowAsync(Guid id)
{
var workflow = await workflowRepository.GetAsync(id);
var criteria = await GetCriteriaForWorkflowAsync(workflow);
var current = criteria.FirstOrDefault(x => x.Id.ToString() == workflow.CurrentNodeId)
?? criteria.FirstOrDefault(x => x.Kind == "Start")
?? throw new UserFriendlyException("Bu workflow için başlangıç adımı bulunamadı.");
workflow.CurrentNodeId = current.Id.ToString();
AddHistory(workflow, "Başlatıldı", "Workflow başlatıldı.");
await AdvanceWorkflowAsync(workflow, criteria, current);
await workflowRepository.UpdateAsync(workflow, autoSave: true);
return MapWorkflow(workflow);
}
[HttpPost("workflows/{id}/decision")]
public async Task<ListFormWorkflowDto> DecideWorkflowAsync(Guid id, DecisionWorkflowDto input)
{
var workflow = await workflowRepository.GetAsync(id);
var criteria = await GetCriteriaForWorkflowAsync(workflow);
var current = criteria.FirstOrDefault(x => x.Id.ToString() == workflow.CurrentNodeId)
?? throw new UserFriendlyException("Aktif workflow adımı bulunamadı.");
if (current.Kind != "Approval")
{
throw new UserFriendlyException("Workflow aktif olarak onay adımında değil.");
}
var nextId = input.Approved ? current.NextOnApprove : current.NextOnReject;
AddHistory(
workflow,
input.Approved ? "Onaylandı" : "Reddedildi",
NormalizeRequired(input.Note, input.Approved ? "Onay verildi." : "Red edildi."));
var next = criteria.FirstOrDefault(x => x.Id.ToString() == nextId);
if (next == null)
{
workflow.Status = StatusFinished;
workflow.CurrentNodeId = current.Id.ToString();
}
else
{
await AdvanceWorkflowAsync(workflow, criteria, next);
}
await workflowRepository.UpdateAsync(workflow, autoSave: true);
return MapWorkflow(workflow);
}
[HttpPost("criteria")]
public async Task<ListFormWorkflowCriteriaDto> SaveCriteriaAsync(CreateUpdateListFormWorkflowCriteriaDto input)
{
var workflow = await workflowRepository.GetAsync(input.WorkflowItemId);
var isNew = !input.Id.HasValue || input.Id.Value == Guid.Empty;
var criteria = isNew
? new ListFormWorkflowCriteria(GuidGenerator.Create()) { WorkflowItemId = workflow.Id }
: await criteriaRepository.GetAsync(input.Id.Value);
criteria.ListFormCode = NormalizeListFormCode(input.ListFormCode ?? workflow.ListFormCode);
criteria.WorkflowItemId = workflow.Id;
criteria.NodeId = NormalizeRequired(input.NodeId, criteria.Id.ToString());
criteria.Kind = NormalizeRequired(input.Kind, "Compare");
criteria.Title = NormalizeRequired(input.Title, criteria.Kind);
criteria.Column = NormalizeRequired(input.Column, "Tutar");
criteria.Operator = NormalizeRequired(input.Operator, ">");
criteria.CompareValue = input.CompareValue;
criteria.Approver = input.Approver ?? string.Empty;
criteria.InformPerson = input.InformPerson ?? string.Empty;
criteria.NextOnStart = input.NextOnStart ?? string.Empty;
criteria.NextOnTrue = input.NextOnTrue ?? string.Empty;
criteria.NextOnFalse = input.NextOnFalse ?? string.Empty;
criteria.NextOnApprove = input.NextOnApprove ?? string.Empty;
criteria.NextOnReject = input.NextOnReject ?? string.Empty;
criteria.PositionX = input.PositionX <= 0 ? 32 : input.PositionX;
criteria.PositionY = input.PositionY <= 0 ? 150 : input.PositionY;
criteria.CompareOutcomesJson = SerializeCompareOutcomes(input.CompareOutcomes);
var outcomes = input.CompareOutcomes ?? [];
if (criteria.Kind == "Compare" && outcomes.Count > 0)
{
criteria.NextOnTrue = outcomes.ElementAtOrDefault(0)?.TargetId ?? criteria.NextOnTrue;
criteria.NextOnFalse = outcomes.ElementAtOrDefault(1)?.TargetId ?? criteria.NextOnFalse;
}
if (isNew)
{
await criteriaRepository.InsertAsync(criteria, autoSave: true);
if (workflow.CurrentNodeId.IsNullOrWhiteSpace() || criteria.Kind == "Start")
{
workflow.CurrentNodeId = criteria.Id.ToString();
await workflowRepository.UpdateAsync(workflow, autoSave: true);
}
}
else
{
await criteriaRepository.UpdateAsync(criteria, autoSave: true);
}
return MapCriteria(criteria);
}
[HttpDelete("criteria/{id}")]
public async Task DeleteCriteriaAsync(Guid id)
{
var criteria = await criteriaRepository.GetAsync(id);
var workflow = await workflowRepository.GetAsync(criteria.WorkflowItemId);
await criteriaRepository.DeleteAsync(criteria, autoSave: true);
var remaining = await GetCriteriaForWorkflowAsync(workflow);
foreach (var item in remaining)
{
var changed = ClearDeletedTarget(item, id.ToString());
if (changed)
{
await criteriaRepository.UpdateAsync(item, autoSave: true);
}
}
if (workflow.CurrentNodeId == id.ToString())
{
workflow.CurrentNodeId = remaining.FirstOrDefault(x => x.Kind == "Start")?.Id.ToString() ?? string.Empty;
await workflowRepository.UpdateAsync(workflow, autoSave: true);
}
}
[HttpPost("reset-demo")]
public async Task<ListFormWorkflowStateDto> ResetDemoAsync(string listFormCode = null)
{
var code = NormalizeListFormCode(listFormCode);
var workflows = await workflowRepository.GetListAsync(x => x.ListFormCode == code);
var workflowIds = workflows.Select(x => x.Id).ToList();
var criteria = await criteriaRepository.GetListAsync(x => x.ListFormCode == code && workflowIds.Contains(x.WorkflowItemId));
foreach (var item in criteria)
{
await criteriaRepository.DeleteAsync(item, autoSave: true);
}
foreach (var workflow in workflows)
{
await workflowRepository.DeleteAsync(workflow, autoSave: true);
}
var demo = new ListFormWorkflow(GuidGenerator.Create())
{
ListFormCode = code,
OrderNo = 1,
Title = "Üretim Süreci",
Status = StatusNew,
Amount = 7200,
CurrentNodeId = string.Empty,
AssignedApprover = string.Empty,
InformedPerson = string.Empty,
HistoryJson = SerializeHistory([])
};
await workflowRepository.InsertAsync(demo, autoSave: true);
var start = await CreateCriteriaAsync(demo, "Start", "İş Akışı Başlat", 80, 150);
var compare = await CreateCriteriaAsync(demo, "Compare", "Tutar kontrolü", 330, 130);
var approval = await CreateCriteriaAsync(demo, "Approval", "Yönetici Onayı", 590, 60, "ayse.yilmaz");
var inform = await CreateCriteriaAsync(demo, "Inform", "Muhasebe Bilgilendirme", 590, 230, "muhasebe");
var end = await CreateCriteriaAsync(demo, "End", "Akışı Bitir", 850, 150);
start.NextOnStart = compare.Id.ToString();
compare.NextOnTrue = approval.Id.ToString();
compare.NextOnFalse = inform.Id.ToString();
compare.CompareOutcomesJson = SerializeCompareOutcomes([
new CompareOutcomeDto
{
Label = "Onay gerekir",
TargetId = approval.Id.ToString(),
Conditions = [new WorkflowConditionDto { Column = "Tutar", Operator = ">", CompareValue = 5000 }]
},
new CompareOutcomeDto
{
Label = "Bilgilendir",
TargetId = inform.Id.ToString(),
Conditions = [new WorkflowConditionDto { Column = "Tutar", Operator = "<=", CompareValue = 5000 }]
}
]);
approval.NextOnApprove = inform.Id.ToString();
approval.NextOnReject = end.Id.ToString();
inform.NextOnStart = end.Id.ToString();
await criteriaRepository.UpdateAsync(start, autoSave: true);
await criteriaRepository.UpdateAsync(compare, autoSave: true);
await criteriaRepository.UpdateAsync(approval, autoSave: true);
await criteriaRepository.UpdateAsync(inform, autoSave: true);
demo.CurrentNodeId = start.Id.ToString();
await workflowRepository.UpdateAsync(demo, autoSave: true);
return await GetStateAsync(code);
}
private async Task<ListFormWorkflowCriteria> CreateDefaultStartCriteriaAsync(ListFormWorkflow workflow)
{
return await CreateCriteriaAsync(workflow, "Start", "İş Akışı Başlat", 32, 150);
}
private async Task<ListFormWorkflowCriteria> CreateCriteriaAsync(
ListFormWorkflow workflow,
string kind,
string title,
int positionX,
int positionY,
string approver = "")
{
var criteria = new ListFormWorkflowCriteria(GuidGenerator.Create())
{
ListFormCode = workflow.ListFormCode,
WorkflowItemId = workflow.Id,
NodeId = GuidGenerator.Create().ToString(),
Kind = kind,
Title = title,
Column = "Tutar",
Operator = ">",
CompareValue = 5000,
Approver = approver,
InformPerson = approver,
NextOnStart = string.Empty,
NextOnTrue = string.Empty,
NextOnFalse = string.Empty,
NextOnApprove = string.Empty,
NextOnReject = string.Empty,
PositionX = positionX,
PositionY = positionY,
CompareOutcomesJson = SerializeCompareOutcomes([])
};
await criteriaRepository.InsertAsync(criteria, autoSave: true);
return criteria;
}
private async Task<List<ListFormWorkflowCriteria>> GetCriteriaForWorkflowAsync(ListFormWorkflow workflow)
{
return (await criteriaRepository.GetListAsync(x => x.WorkflowItemId == workflow.Id))
.OrderBy(x => x.PositionX)
.ThenBy(x => x.PositionY)
.ToList();
}
private async Task AdvanceWorkflowAsync(
ListFormWorkflow workflow,
List<ListFormWorkflowCriteria> criteria,
ListFormWorkflowCriteria current)
{
var visited = new HashSet<Guid>();
var step = current;
while (step != null && visited.Add(step.Id))
{
workflow.CurrentNodeId = step.Id.ToString();
if (step.Kind == "Approval")
{
workflow.Status = StatusPending;
workflow.AssignedApprover = step.Approver;
workflow.InformedPerson = step.InformPerson;
return;
}
if (step.Kind == "End")
{
workflow.Status = StatusFinished;
AddHistory(workflow, "Tamamlandı", "Workflow tamamlandı.");
return;
}
if (step.Kind == "Inform")
{
workflow.Status = StatusInformed;
workflow.InformedPerson = step.InformPerson;
AddHistory(workflow, "Bilgilendirildi", $"{step.InformPerson} bilgilendirildi.");
step = FindNext(criteria, step.NextOnStart);
continue;
}
if (step.Kind == "Compare")
{
step = FindNext(criteria, ResolveCompareTarget(workflow, step));
continue;
}
workflow.Status = StatusNew;
step = FindNext(criteria, step.NextOnStart);
}
await Task.CompletedTask;
}
private string ResolveCompareTarget(ListFormWorkflow workflow, ListFormWorkflowCriteria criteria)
{
var outcomes = DeserializeCompareOutcomes(criteria.CompareOutcomesJson);
foreach (var outcome in outcomes)
{
if (outcome.Conditions.Count > 0 && outcome.Conditions.All(condition => EvaluateCondition(workflow, condition)))
{
return outcome.TargetId;
}
}
return EvaluateCondition(workflow, new WorkflowConditionDto
{
Column = criteria.Column,
Operator = criteria.Operator,
CompareValue = criteria.CompareValue
})
? criteria.NextOnTrue
: criteria.NextOnFalse;
}
private static bool EvaluateCondition(ListFormWorkflow workflow, WorkflowConditionDto condition)
{
var value = condition.Column == "Id" ? workflow.OrderNo : workflow.Amount;
return condition.Operator switch
{
">" => value > condition.CompareValue,
">=" => value >= condition.CompareValue,
"<" => value < condition.CompareValue,
"<=" => value <= condition.CompareValue,
"=" => value == condition.CompareValue,
"!=" => value != condition.CompareValue,
_ => false
};
}
private static ListFormWorkflowCriteria FindNext(List<ListFormWorkflowCriteria> criteria, string id)
{
return id.IsNullOrWhiteSpace() ? null : criteria.FirstOrDefault(x => x.Id.ToString() == id);
}
private static bool ClearDeletedTarget(ListFormWorkflowCriteria criteria, string deletedId)
{
var changed = false;
if (criteria.NextOnStart == deletedId)
{
criteria.NextOnStart = string.Empty;
changed = true;
}
if (criteria.NextOnTrue == deletedId)
{
criteria.NextOnTrue = string.Empty;
changed = true;
}
if (criteria.NextOnFalse == deletedId)
{
criteria.NextOnFalse = string.Empty;
changed = true;
}
if (criteria.NextOnApprove == deletedId)
{
criteria.NextOnApprove = string.Empty;
changed = true;
}
if (criteria.NextOnReject == deletedId)
{
criteria.NextOnReject = string.Empty;
changed = true;
}
var outcomes = DeserializeCompareOutcomes(criteria.CompareOutcomesJson);
foreach (var outcome in outcomes.Where(x => x.TargetId == deletedId))
{
outcome.TargetId = null;
changed = true;
}
if (changed)
{
criteria.CompareOutcomesJson = SerializeCompareOutcomes(outcomes);
}
return changed;
}
private static void AddHistory(ListFormWorkflow workflow, string action, string note)
{
var history = DeserializeHistory(workflow.HistoryJson);
history.Add(new WorkflowHistoryDto
{
Time = DateTime.Now,
Action = action,
Note = note
});
workflow.HistoryJson = SerializeHistory(history);
}
private static ListFormWorkflowDto MapWorkflow(ListFormWorkflow workflow)
{
return new ListFormWorkflowDto
{
Id = workflow.Id,
CreationTime = workflow.CreationTime,
CreatorId = workflow.CreatorId,
LastModificationTime = workflow.LastModificationTime,
LastModifierId = workflow.LastModifierId,
ListFormCode = workflow.ListFormCode,
OrderNo = workflow.OrderNo,
Title = workflow.Title,
Sorumlu = workflow.Title,
Tarih = workflow.CreationTime == default ? DateTime.Now : workflow.CreationTime,
Status = workflow.Status,
Durum = workflow.Status,
Amount = workflow.Amount,
CurrentNodeId = workflow.CurrentNodeId,
AssignedApprover = workflow.AssignedApprover,
InformedPerson = workflow.InformedPerson,
History = DeserializeHistory(workflow.HistoryJson)
};
}
private static ListFormWorkflowCriteriaDto MapCriteria(ListFormWorkflowCriteria criteria)
{
return new ListFormWorkflowCriteriaDto
{
Id = criteria.Id,
CreationTime = criteria.CreationTime,
CreatorId = criteria.CreatorId,
LastModificationTime = criteria.LastModificationTime,
LastModifierId = criteria.LastModifierId,
ListFormCode = criteria.ListFormCode,
WorkflowItemId = criteria.WorkflowItemId,
NodeId = criteria.NodeId,
Kind = criteria.Kind,
Title = criteria.Title,
Column = criteria.Column,
Operator = criteria.Operator,
CompareValue = criteria.CompareValue,
Approver = criteria.Approver,
InformPerson = criteria.InformPerson,
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 string NormalizeListFormCode(string listFormCode)
{
return listFormCode.IsNullOrWhiteSpace() ? DefaultListFormCode : listFormCode.Trim();
}
private static string NormalizeRequired(string value, string fallback)
{
return value.IsNullOrWhiteSpace() ? fallback : value.Trim();
}
private static string SerializeCompareOutcomes(List<CompareOutcomeDto> outcomes)
{
return JsonSerializer.Serialize(outcomes ?? []);
}
private static List<CompareOutcomeDto> DeserializeCompareOutcomes(string json)
{
if (json.IsNullOrWhiteSpace())
{
return [];
}
try
{
return JsonSerializer.Deserialize<List<CompareOutcomeDto>>(json) ?? [];
}
catch
{
return [];
}
}
private static string SerializeHistory(List<WorkflowHistoryDto> history)
{
return JsonSerializer.Serialize(history ?? []);
}
private static List<WorkflowHistoryDto> DeserializeHistory(string json)
{
if (json.IsNullOrWhiteSpace())
{
return [];
}
try
{
return JsonSerializer.Deserialize<List<WorkflowHistoryDto>>(json) ?? [];
}
catch
{
return [];
}
}
}

View file

@ -59,6 +59,8 @@ public class ListFormSeeder_Utils
{
ListFormCode = newListFormCode,
SubFormsJson = listForm.SubFormsJson,
WidgetsJson = listForm.WidgetsJson,
WorkflowJson = listForm.WorkflowJson,
ListFormType = listForm.ListFormType,
IsSubForm = listForm.IsSubForm,
ShowNote = listForm.ShowNote,

View file

@ -29,7 +29,6 @@ public enum TableNameEnum
ListFormImport,
ListFormImportLog,
ListFormWorkflow,
ListFormWorkflowCriteria,
Note,
ForumCategory,
ForumTopic,

View file

@ -39,7 +39,6 @@ public static class TableNameResolver
{ nameof(TableNameEnum.ListFormImport), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.ListFormImportLog), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.ListFormWorkflow), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.ListFormWorkflowCriteria), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.Notification), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.NotificationRule), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.NotificationType), (TablePrefix.PlatformByName, MenuPrefix.Saas) },

View file

@ -59,7 +59,7 @@ public class PlatformBackgroundWorker : PlatformDomainService, IPlatformBackgrou
{
using var uow = LazyServiceProvider.LazyGetRequiredService<IUnitOfWorkManager>().Begin(requiresNew: true, isTransactional: false);
var Worker = await Repository.FirstOrDefaultAsync(a => a.Id == WorkerId);
var Worker = await Repository.FirstOrDefaultAsync(a => a.Id == WorkerId, cancellationToken: cancellationToken);
var LogPrefix = $"{Clock.Now:s}_{Worker.Name}: {{0}}";
var DistributedLockName = Worker.Name;

View file

@ -137,10 +137,13 @@ public class ListForm : Entity<Guid>
/// <summary>bu listform'un üstünde yer alan widgetların listesidir</summary>
public string WidgetsJson { get; set; }
/// <summary>bu listform'un üstünde yer alan widgetların listesidir</summary>
/// <summary>bu listform'un üstünde yer alan workflowların listesidir</summary>
public string WorkflowJson { get; set; }
/// <summary>bu listform'un üstünde yer alan extra filterların listesidir</summary>
public string ExtraFilterJson { get; set; }
/// <summary>bu listform'un üstünde yer alan widgetların listesidir</summary>
/// <summary>bu listform'un üstünde yer alan layoutların listesidir</summary>
public string LayoutJson { get; set; }
/*

View file

@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.Domain.Entities;
namespace Sozsoft.Platform.Entities;
public class ListFormWorkflow : FullAuditedEntity<Guid>
public class ListFormWorkflow : Entity<Guid>
{
protected ListFormWorkflow()
{
@ -15,14 +14,18 @@ public class ListFormWorkflow : FullAuditedEntity<Guid>
}
public string ListFormCode { get; set; }
public int OrderNo { get; set; }
public string Kind { get; set; }
public string Title { get; set; }
public string Status { get; set; }
public decimal Amount { get; set; }
public string CurrentNodeId { get; set; }
public string AssignedApprover { get; set; }
public string InformedPerson { get; set; }
public string HistoryJson { get; set; }
public ICollection<ListFormWorkflowCriteria> Criteria { get; set; }
public string CompareColumn { get; set; }
public string CompareOperator { get; set; }
public decimal CompareValue { get; set; }
public string Approver { get; set; }
public string NextOnStart { get; set; }
public string NextOnTrue { get; set; }
public string NextOnFalse { get; set; }
public string NextOnApprove { get; set; }
public string NextOnReject { get; set; }
public int PositionX { get; set; }
public int PositionY { get; set; }
public string CompareOutcomesJson { get; set; }
}

View file

@ -1,35 +0,0 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
namespace Sozsoft.Platform.Entities;
public class ListFormWorkflowCriteria : FullAuditedEntity<Guid>
{
protected ListFormWorkflowCriteria()
{
}
public ListFormWorkflowCriteria(Guid id) : base(id)
{
}
public string ListFormCode { get; set; }
public Guid WorkflowItemId { get; set; }
public ListFormWorkflow WorkflowItem { get; set; }
public string NodeId { get; set; }
public string Kind { get; set; }
public string Title { get; set; }
public string Column { get; set; }
public string Operator { get; set; }
public decimal CompareValue { get; set; }
public string Approver { get; set; }
public string InformPerson { get; set; }
public string NextOnStart { get; set; }
public string NextOnTrue { get; set; }
public string NextOnFalse { get; set; }
public string NextOnApprove { get; set; }
public string NextOnReject { get; set; }
public int PositionX { get; set; }
public int PositionY { get; set; }
public string CompareOutcomesJson { get; set; }
}

View file

@ -55,8 +55,7 @@ public class PlatformDbContext :
public DbSet<ListFormCustomization> ListFormCustomization { get; set; }
public DbSet<ListFormImport> ListFormImports { get; set; }
public DbSet<ListFormImportLog> ListFormImportLogs { get; set; }
public DbSet<ListFormWorkflow> ListFormWorkflows { get; set; }
public DbSet<ListFormWorkflowCriteria> ListFormWorkflowCriteria { get; set; }
public DbSet<ListFormWorkflow> ListFormWorkflow { get; set; }
public DbSet<BackgroundWorker> BackgroundWorkers { get; set; }
public DbSet<ForumCategory> ForumCategories { get; set; }
public DbSet<ForumTopic> ForumTopics { get; set; }
@ -365,6 +364,7 @@ public class PlatformDbContext :
b.Property(a => a.FormFieldsDefaultValueJson).HasColumnType("text");
b.Property(a => a.SubFormsJson).HasColumnType("text");
b.Property(a => a.WidgetsJson).HasColumnType("text");
b.Property(a => a.WorkflowJson).HasColumnType("text");
b.Property(a => a.ExtraFilterJson).HasColumnType("text");
b.Property(a => a.LayoutJson).HasColumnType("text");
b.Property(a => a.CommonJson).HasColumnType("text");
@ -488,36 +488,12 @@ public class PlatformDbContext :
b.ConfigureByConvention();
b.Property(x => x.ListFormCode).IsRequired().HasMaxLength(64);
b.Property(x => x.OrderNo).IsRequired();
b.Property(x => x.Title).IsRequired().HasMaxLength(150);
b.Property(x => x.Status).IsRequired().HasMaxLength(100);
b.Property(x => x.Amount).HasPrecision(18, 2);
b.Property(x => x.CurrentNodeId).IsRequired().HasMaxLength(50);
b.Property(x => x.AssignedApprover).IsRequired().HasMaxLength(250);
b.Property(x => x.InformedPerson).IsRequired().HasMaxLength(250);
b.Property(x => x.HistoryJson).HasColumnType("text");
b.HasMany(x => x.Criteria)
.WithOne(x => x.WorkflowItem)
.HasForeignKey(x => x.WorkflowItemId)
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<ListFormWorkflowCriteria>(b =>
{
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.ListFormWorkflowCriteria)), Prefix.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.ListFormCode).IsRequired().HasMaxLength(64);
b.Property(x => x.WorkflowItemId).IsRequired();
b.Property(x => x.NodeId).IsRequired().HasMaxLength(50);
b.Property(x => x.Kind).IsRequired().HasMaxLength(50);
b.Property(x => x.Title).IsRequired().HasMaxLength(250);
b.Property(x => x.Column).IsRequired().HasMaxLength(100);
b.Property(x => x.Operator).IsRequired().HasMaxLength(20);
b.Property(x => x.CompareColumn).IsRequired().HasMaxLength(100);
b.Property(x => x.CompareOperator).IsRequired().HasMaxLength(20);
b.Property(x => x.CompareValue).HasPrecision(18, 2);
b.Property(x => x.Approver).IsRequired().HasMaxLength(250);
b.Property(x => x.InformPerson).IsRequired().HasMaxLength(250);
b.Property(x => x.NextOnStart).IsRequired().HasMaxLength(50);
b.Property(x => x.NextOnTrue).IsRequired().HasMaxLength(50);
b.Property(x => x.NextOnFalse).IsRequired().HasMaxLength(50);
@ -526,13 +502,6 @@ public class PlatformDbContext :
b.Property(x => x.PositionX).IsRequired();
b.Property(x => x.PositionY).IsRequired();
b.Property(x => x.CompareOutcomesJson).HasColumnType("text");
b.HasIndex(x => new
{
x.ListFormCode,
x.WorkflowItemId,
x.NodeId
});
});
builder.Entity<Note>(b =>

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20260522085648_Initial")]
[Migration("20260522200739_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -3053,6 +3053,9 @@ namespace Sozsoft.Platform.Migrations
b.Property<int?>("Width")
.HasColumnType("int");
b.Property<string>("WorkflowJson")
.HasColumnType("text");
b.Property<string>("ZoomAndPanJson")
.HasColumnType("text");
@ -3410,86 +3413,6 @@ namespace Sozsoft.Platform.Migrations
});
modelBuilder.Entity("Sozsoft.Platform.Entities.ListFormWorkflow", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("AssignedApprover")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<string>("CurrentNodeId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("HistoryJson")
.HasColumnType("text");
b.Property<string>("InformedPerson")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("ListFormCode")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<int>("OrderNo")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("nvarchar(150)");
b.HasKey("Id");
b.ToTable("Sas_H_ListFormWorkflow", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.ListFormWorkflowCriteria", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
@ -3499,11 +3422,16 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.Property<string>("Column")
b.Property<string>("CompareColumn")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("CompareOperator")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("CompareOutcomesJson")
.HasColumnType("text");
@ -3511,46 +3439,11 @@ namespace Sozsoft.Platform.Migrations
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("InformPerson")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<string>("Kind")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("ListFormCode")
.IsRequired()
.HasMaxLength(64)
@ -3581,16 +3474,6 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("NodeId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Operator")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<int>("PositionX")
.HasColumnType("int");
@ -3602,16 +3485,9 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.Property<Guid>("WorkflowItemId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("WorkflowItemId");
b.HasIndex("ListFormCode", "WorkflowItemId", "NodeId");
b.ToTable("Sas_H_ListFormWorkflowCriteria", (string)null);
b.ToTable("Sas_H_ListFormWorkflow", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.LogEntry", b =>
@ -8344,17 +8220,6 @@ namespace Sozsoft.Platform.Migrations
.IsRequired();
});
modelBuilder.Entity("Sozsoft.Platform.Entities.ListFormWorkflowCriteria", b =>
{
b.HasOne("Sozsoft.Platform.Entities.ListFormWorkflow", "WorkflowItem")
.WithMany("Criteria")
.HasForeignKey("WorkflowItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("WorkflowItem");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.OrderItem", b =>
{
b.HasOne("Sozsoft.Platform.Entities.Order", "Order")
@ -8790,11 +8655,6 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("Events");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.ListFormWorkflow", b =>
{
b.Navigation("Criteria");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Order", b =>
{
b.Navigation("Items");

View file

@ -1355,6 +1355,7 @@ namespace Sozsoft.Platform.Migrations
ShowNote = table.Column<bool>(type: "bit", nullable: false),
SubFormsJson = table.Column<string>(type: "text", nullable: true),
WidgetsJson = table.Column<string>(type: "text", nullable: true),
WorkflowJson = table.Column<string>(type: "text", nullable: true),
ExtraFilterJson = table.Column<string>(type: "text", nullable: true),
LayoutJson = table.Column<string>(type: "text", nullable: true),
UserId = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
@ -1394,21 +1395,20 @@ namespace Sozsoft.Platform.Migrations
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ListFormCode = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
OrderNo = table.Column<int>(type: "int", nullable: false),
Title = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: false),
Status = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Amount = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
CurrentNodeId = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
AssignedApprover = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false),
InformedPerson = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false),
HistoryJson = table.Column<string>(type: "text", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
Kind = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Title = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false),
CompareColumn = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
CompareOperator = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
CompareValue = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
Approver = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false),
NextOnStart = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
NextOnTrue = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
NextOnFalse = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
NextOnApprove = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
NextOnReject = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
PositionX = table.Column<int>(type: "int", nullable: false),
PositionY = table.Column<int>(type: "int", nullable: false),
CompareOutcomesJson = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
@ -2661,48 +2661,6 @@ namespace Sozsoft.Platform.Migrations
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Sas_H_ListFormWorkflowCriteria",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
ListFormCode = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
WorkflowItemId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
NodeId = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Kind = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
Title = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false),
Column = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Operator = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
CompareValue = table.Column<decimal>(type: "decimal(18,2)", precision: 18, scale: 2, nullable: false),
Approver = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false),
InformPerson = table.Column<string>(type: "nvarchar(250)", maxLength: 250, nullable: false),
NextOnStart = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
NextOnTrue = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
NextOnFalse = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
NextOnApprove = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
NextOnReject = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
PositionX = table.Column<int>(type: "int", nullable: false),
PositionY = table.Column<int>(type: "int", nullable: false),
CompareOutcomesJson = table.Column<string>(type: "text", nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Sas_H_ListFormWorkflowCriteria", x => x.Id);
table.ForeignKey(
name: "FK_Sas_H_ListFormWorkflowCriteria_Sas_H_ListFormWorkflow_WorkflowItemId",
column: x => x.WorkflowItemId,
principalTable: "Sas_H_ListFormWorkflow",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Sas_H_NotificationRule",
columns: table => new
@ -3933,16 +3891,6 @@ namespace Sozsoft.Platform.Migrations
table: "Sas_H_ListFormImportLog",
column: "ImportId");
migrationBuilder.CreateIndex(
name: "IX_Sas_H_ListFormWorkflowCriteria_ListFormCode_WorkflowItemId_NodeId",
table: "Sas_H_ListFormWorkflowCriteria",
columns: new[] { "ListFormCode", "WorkflowItemId", "NodeId" });
migrationBuilder.CreateIndex(
name: "IX_Sas_H_ListFormWorkflowCriteria_WorkflowItemId",
table: "Sas_H_ListFormWorkflowCriteria",
column: "WorkflowItemId");
migrationBuilder.CreateIndex(
name: "IX_Sas_H_Menu_Code",
table: "Sas_H_Menu",
@ -4293,7 +4241,7 @@ namespace Sozsoft.Platform.Migrations
name: "Sas_H_ListFormImportLog");
migrationBuilder.DropTable(
name: "Sas_H_ListFormWorkflowCriteria");
name: "Sas_H_ListFormWorkflow");
migrationBuilder.DropTable(
name: "Sas_H_LogEntry");
@ -4400,9 +4348,6 @@ namespace Sozsoft.Platform.Migrations
migrationBuilder.DropTable(
name: "Sas_H_ListFormImport");
migrationBuilder.DropTable(
name: "Sas_H_ListFormWorkflow");
migrationBuilder.DropTable(
name: "Sas_H_NotificationRule");

View file

@ -3050,6 +3050,9 @@ namespace Sozsoft.Platform.Migrations
b.Property<int?>("Width")
.HasColumnType("int");
b.Property<string>("WorkflowJson")
.HasColumnType("text");
b.Property<string>("ZoomAndPanJson")
.HasColumnType("text");
@ -3407,86 +3410,6 @@ namespace Sozsoft.Platform.Migrations
});
modelBuilder.Entity("Sozsoft.Platform.Entities.ListFormWorkflow", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<string>("AssignedApprover")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<string>("CurrentNodeId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("HistoryJson")
.HasColumnType("text");
b.Property<string>("InformedPerson")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("ListFormCode")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<int>("OrderNo")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(150)
.HasColumnType("nvarchar(150)");
b.HasKey("Id");
b.ToTable("Sas_H_ListFormWorkflow", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.ListFormWorkflowCriteria", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
@ -3496,11 +3419,16 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.Property<string>("Column")
b.Property<string>("CompareColumn")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("CompareOperator")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("CompareOutcomesJson")
.HasColumnType("text");
@ -3508,46 +3436,11 @@ namespace Sozsoft.Platform.Migrations
.HasPrecision(18, 2)
.HasColumnType("decimal(18,2)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("InformPerson")
.IsRequired()
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<string>("Kind")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("ListFormCode")
.IsRequired()
.HasMaxLength(64)
@ -3578,16 +3471,6 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("NodeId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Operator")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<int>("PositionX")
.HasColumnType("int");
@ -3599,16 +3482,9 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(250)
.HasColumnType("nvarchar(250)");
b.Property<Guid>("WorkflowItemId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id");
b.HasIndex("WorkflowItemId");
b.HasIndex("ListFormCode", "WorkflowItemId", "NodeId");
b.ToTable("Sas_H_ListFormWorkflowCriteria", (string)null);
b.ToTable("Sas_H_ListFormWorkflow", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.LogEntry", b =>
@ -8341,17 +8217,6 @@ namespace Sozsoft.Platform.Migrations
.IsRequired();
});
modelBuilder.Entity("Sozsoft.Platform.Entities.ListFormWorkflowCriteria", b =>
{
b.HasOne("Sozsoft.Platform.Entities.ListFormWorkflow", "WorkflowItem")
.WithMany("Criteria")
.HasForeignKey("WorkflowItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("WorkflowItem");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.OrderItem", b =>
{
b.HasOne("Sozsoft.Platform.Entities.Order", "Order")
@ -8787,11 +8652,6 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("Events");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.ListFormWorkflow", b =>
{
b.Navigation("Criteria");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Order", b =>
{
b.Navigation("Items");

View file

@ -1,8 +1,8 @@
import apiService from './api.service'
export interface WorkflowConditionDto {
column: string
operator: string
compareColumn: string
compareOperator: string
compareValue: number
}
@ -12,38 +12,16 @@ export interface CompareOutcomeDto {
conditions: WorkflowConditionDto[]
}
export interface WorkflowItemDto {
id: string
listFormCode: string
orderNo: number
title: string
sorumlu: string
tarih: string
status: string
durum: string
amount: number
currentNodeId: string
assignedApprover: string
informedPerson: string
history: Array<{
time: string
action: string
note: string
}>
}
export interface WorkflowCriteriaDto {
id: string
listFormCode: string
workflowItemId: string
nodeId: string
kind: string
title: string
column: string
operator: string
compareColumn: string
compareOperator: string
compareValue: number
approver: string
informPerson: string
nextOnStart?: string | null
nextOnTrue?: string | null
nextOnFalse?: string | null
@ -55,20 +33,12 @@ export interface WorkflowCriteriaDto {
}
export interface WorkflowStateDto {
workflowItems: WorkflowItemDto[]
criteria: WorkflowCriteriaDto[]
}
export type CreateUpdateWorkflowInput = Partial<WorkflowItemDto> & {
listFormCode?: string
sorumlu: string
amount: number
tarih?: string
}
export type SaveCriteriaInput = Omit<Partial<WorkflowCriteriaDto>, 'id'> & {
id?: string | null
workflowItemId: string
listFormCode: string
}
const baseUrl = '/api/app/list-form-workflow'
@ -84,45 +54,6 @@ export const workflowService = {
return response.data
},
async createWorkflow(payload: CreateUpdateWorkflowInput) {
const response = await apiService.fetchData<WorkflowItemDto>({
method: 'POST',
url: `${baseUrl}/workflows`,
data: payload,
})
return response.data
},
async updateWorkflow(id: string, payload: CreateUpdateWorkflowInput) {
const response = await apiService.fetchData<WorkflowItemDto>({
method: 'PUT',
url: `${baseUrl}/workflows/${id}`,
data: payload,
})
return response.data
},
async startWorkflow(id: string) {
const response = await apiService.fetchData<WorkflowItemDto>({
method: 'POST',
url: `${baseUrl}/workflows/${id}/start`,
})
return response.data
},
async decideWorkflow(id: string, payload: { approved: boolean; note?: string }) {
const response = await apiService.fetchData<WorkflowItemDto>({
method: 'POST',
url: `${baseUrl}/workflows/${id}/decision`,
data: payload,
})
return response.data
},
async saveCriteria(payload: SaveCriteriaInput) {
const response = await apiService.fetchData<WorkflowCriteriaDto>({
method: 'POST',

View file

@ -1,75 +1,60 @@
import { getNodeHeight, nodeSize } from "./workflowConstants";
import { getNodeHeight, nodeSize } from './workflowConstants'
import type {
CompareOutcomeDto,
SaveCriteriaInput,
WorkflowConditionDto,
WorkflowCriteriaDto,
WorkflowItemDto,
} from "@/services/workflow.service";
} from '@/services/workflow.service'
export type WorkflowCriteriaForm = Partial<WorkflowCriteriaDto> & {
id?: string | null;
workflowItemId: string;
compareOutcomes: CompareOutcomeDto[];
};
id?: string | null
listFormCode: string
compareOutcomes: CompareOutcomeDto[]
}
export type WorkflowOutcome = {
field: string;
label: string;
targetId?: string | null;
};
field: string
label: string
targetId?: string | null
}
export type WorkflowLinkPort = {
field?: string;
index?: number;
count?: number;
sourceSlotIndex?: number;
sourceSlotCount?: number;
targetSlotIndex?: number;
targetSlotCount?: number;
routeIndex?: number;
routeCount?: number;
};
field?: string
index?: number
count?: number
sourceSlotIndex?: number
sourceSlotCount?: number
targetSlotIndex?: number
targetSlotCount?: number
routeIndex?: number
routeCount?: number
}
export type WorkflowLink = {
key: string;
source: WorkflowCriteriaDto;
target: WorkflowCriteriaDto;
label: string;
sourcePort: WorkflowLinkPort;
};
key: string
source: WorkflowCriteriaDto
target: WorkflowCriteriaDto
label: string
sourcePort: WorkflowLinkPort
}
type Endpoint = {
link: WorkflowLink;
role: "source" | "target";
};
export function isPendingApproval(
item: WorkflowItemDto | undefined | null,
criteria: WorkflowCriteriaDto[],
) {
if (!item) return false;
return criteria.some(
(candidate) =>
candidate.workflowItemId === item.id &&
candidate.id === item.currentNodeId &&
candidate.kind === "Approval",
);
link: WorkflowLink
role: 'source' | 'target'
}
export function buildFitLayout(criteria: WorkflowCriteriaDto[]) {
const links = collectLinks(criteria);
const rankById = buildTraversalRanks(criteria, links);
const groups = new Map<number, WorkflowCriteriaDto[]>();
const links = collectLinks(criteria)
const rankById = buildTraversalRanks(criteria, links)
const groups = new Map<number, WorkflowCriteriaDto[]>()
criteria.forEach((item) => {
const column = fitColumn(item);
if (!groups.has(column)) groups.set(column, []);
groups.get(column)?.push(item);
});
const column = fitColumn(item)
if (!groups.has(column)) groups.set(column, [])
groups.get(column)?.push(item)
})
const sortedColumns = [...groups.keys()].sort((a, b) => a - b);
const yGap = 74;
const sortedColumns = [...groups.keys()].sort((a, b) => a - b)
const yGap = 74
const maxGroupHeight = Math.max(
1,
...[...groups.values()].map(
@ -77,31 +62,29 @@ export function buildFitLayout(criteria: WorkflowCriteriaDto[]) {
items.reduce((sum, item) => sum + getNodeHeight(item), 0) +
Math.max(0, items.length - 1) * yGap,
),
);
const top = 72;
const left = 72;
const xGap = 128;
const positions = new Map<string, { x: number; y: number }>();
)
const top = 72
const left = 72
const xGap = 128
const positions = new Map<string, { x: number; y: number }>()
sortedColumns.forEach((column, columnIndex) => {
const items = (groups.get(column) || []).sort((a, b) =>
compareLayoutNodes(a, b, rankById),
);
const items = (groups.get(column) || []).sort((a, b) => compareLayoutNodes(a, b, rankById))
const groupHeight =
items.reduce((sum, item) => sum + getNodeHeight(item), 0) +
Math.max(0, items.length - 1) * yGap;
let y = top + Math.max(0, (maxGroupHeight - groupHeight) / 2);
Math.max(0, items.length - 1) * yGap
let y = top + Math.max(0, (maxGroupHeight - groupHeight) / 2)
items.forEach((item) => {
positions.set(item.id, {
x: left + columnIndex * (nodeSize.width + xGap),
y: Math.round(y),
});
y += getNodeHeight(item) + yGap;
});
});
})
y += getNodeHeight(item) + yGap
})
})
return positions;
return positions
}
function fitColumn(item: WorkflowCriteriaDto) {
@ -111,9 +94,9 @@ function fitColumn(item: WorkflowCriteriaDto) {
Approval: 2,
Inform: 3,
End: 4,
};
}
return priority[item.kind] ?? 2;
return priority[item.kind] ?? 2
}
function compareLayoutNodes(
@ -123,201 +106,181 @@ function compareLayoutNodes(
) {
return (
(rankById.get(a.id) ?? 999) - (rankById.get(b.id) ?? 999) ||
a.title.localeCompare(b.title, "tr")
);
a.title.localeCompare(b.title, 'tr')
)
}
function buildTraversalRanks(
criteria: WorkflowCriteriaDto[],
links: WorkflowLink[],
) {
const rankById = new Map<string, number>();
const outgoing = new Map<string, string[]>(
criteria.map((item) => [item.id, []]),
);
function buildTraversalRanks(criteria: WorkflowCriteriaDto[], links: WorkflowLink[]) {
const rankById = new Map<string, number>()
const outgoing = new Map<string, string[]>(criteria.map((item) => [item.id, []]))
links.forEach((link) => {
outgoing.get(link.source.id)?.push(link.target.id);
});
outgoing.get(link.source.id)?.push(link.target.id)
})
const roots = criteria.filter((item) => item.kind === "Start");
const queue = roots.length
? roots.map((item) => item.id)
: criteria.map((item) => item.id);
const roots = criteria.filter((item) => item.kind === 'Start')
const queue = roots.length ? roots.map((item) => item.id) : criteria.map((item) => item.id)
while (queue.length) {
const id = queue.shift();
if (!id) continue;
if (rankById.has(id)) continue;
const id = queue.shift()
if (!id) continue
if (rankById.has(id)) continue
rankById.set(id, rankById.size);
(outgoing.get(id) || []).forEach((targetId) => {
if (targetId && !rankById.has(targetId)) queue.push(targetId);
});
rankById.set(id, rankById.size)
;(outgoing.get(id) || []).forEach((targetId) => {
if (targetId && !rankById.has(targetId)) queue.push(targetId)
})
}
criteria.forEach((item) => {
if (!rankById.has(item.id)) rankById.set(item.id, rankById.size);
});
if (!rankById.has(item.id)) rankById.set(item.id, rankById.size)
})
return rankById;
return rankById
}
export function collectLinks(criteria: WorkflowCriteriaDto[]) {
const links: WorkflowLink[] = [];
const links: WorkflowLink[] = []
criteria.forEach((source) => {
if (source.kind === "Compare" && source.compareOutcomes?.length) {
if (source.kind === 'Compare' && source.compareOutcomes?.length) {
source.compareOutcomes.forEach((outcome, index) => {
addLink(
links,
criteria,
source,
outcome.targetId,
outcome.label,
`compare-${index}`,
{
index,
count: source.compareOutcomes.length,
field: `compareOutcomes:${index}`,
},
);
});
return;
addLink(links, criteria, source, outcome.targetId, outcome.label, `compare-${index}`, {
index,
count: source.compareOutcomes.length,
field: `compareOutcomes:${index}`,
})
})
return
}
addLink(links, criteria, source, source.nextOnStart, "Sonraki", "next", {
addLink(links, criteria, source, source.nextOnStart, 'Sonraki', 'next', {
index: 0,
count: 1,
field: "nextOnStart",
});
addLink(links, criteria, source, source.nextOnTrue, "Doğru", "true", {
field: 'nextOnStart',
})
addLink(links, criteria, source, source.nextOnTrue, 'Doğru', 'true', {
index: 0,
count: 2,
field: "nextOnTrue",
});
addLink(links, criteria, source, source.nextOnFalse, "Yanlış", "false", {
field: 'nextOnTrue',
})
addLink(links, criteria, source, source.nextOnFalse, 'Yanlış', 'false', {
index: 1,
count: 2,
field: "nextOnFalse",
});
addLink(links, criteria, source, source.nextOnApprove, "Onay", "approve", {
field: 'nextOnFalse',
})
addLink(links, criteria, source, source.nextOnApprove, 'Onay', 'approve', {
index: 0,
count: 2,
field: "nextOnApprove",
});
addLink(links, criteria, source, source.nextOnReject, "Red", "reject", {
field: 'nextOnApprove',
})
addLink(links, criteria, source, source.nextOnReject, 'Red', 'reject', {
index: 1,
count: 2,
field: "nextOnReject",
});
});
return assignLinkSlots(links, criteria);
field: 'nextOnReject',
})
})
return assignLinkSlots(links, criteria)
}
export function assignLinkSlots(
links: WorkflowLink[],
criteria: WorkflowCriteriaDto[],
) {
const endpointGroups = new Map<string, Endpoint[]>();
export function assignLinkSlots(links: WorkflowLink[], criteria: WorkflowCriteriaDto[]) {
const endpointGroups = new Map<string, Endpoint[]>()
const addEndpoint = (nodeId: string, side: string, endpoint: Endpoint) => {
const key = `${nodeId}:${side}`;
if (!endpointGroups.has(key)) endpointGroups.set(key, []);
endpointGroups.get(key)?.push(endpoint);
};
const key = `${nodeId}:${side}`
if (!endpointGroups.has(key)) endpointGroups.set(key, [])
endpointGroups.get(key)?.push(endpoint)
}
links.forEach((link) => {
addEndpoint(link.source.id, sideToward(link.source, link.target), {
link,
role: "source",
});
role: 'source',
})
addEndpoint(link.target.id, sideToward(link.target, link.source), {
link,
role: "target",
});
});
role: 'target',
})
})
endpointGroups.forEach((endpoints) => {
endpoints.forEach((endpoint, index) => {
if (endpoint.role === "source") {
endpoint.link.sourcePort.sourceSlotIndex = index;
endpoint.link.sourcePort.sourceSlotCount = endpoints.length;
if (endpoint.role === 'source') {
endpoint.link.sourcePort.sourceSlotIndex = index
endpoint.link.sourcePort.sourceSlotCount = endpoints.length
} else {
endpoint.link.sourcePort.targetSlotIndex = index;
endpoint.link.sourcePort.targetSlotCount = endpoints.length;
endpoint.link.sourcePort.targetSlotIndex = index
endpoint.link.sourcePort.targetSlotCount = endpoints.length
}
});
});
})
})
links.forEach((link) => {
link.sourcePort.routeIndex = link.sourcePort.targetSlotIndex ?? 0;
link.sourcePort.routeCount = link.sourcePort.targetSlotCount ?? 1;
});
link.sourcePort.routeIndex = link.sourcePort.targetSlotIndex ?? 0
link.sourcePort.routeCount = link.sourcePort.targetSlotCount ?? 1
})
return links;
return links
}
function sideToward(from: WorkflowCriteriaDto, to: WorkflowCriteriaDto) {
const fromLeft = Number(from.positionX || 0);
const fromTop = Number(from.positionY || 0);
const fromLeft = Number(from.positionX || 0)
const fromTop = Number(from.positionY || 0)
const fromCenter = {
x: fromLeft + nodeSize.width / 2,
y: fromTop + getNodeHeight(from) / 2,
};
}
const toCenter = {
x: Number(to.positionX || 0) + nodeSize.width / 2,
y: Number(to.positionY || 0) + getNodeHeight(to) / 2,
};
}
const dx = toCenter.x - fromCenter.x;
const dy = toCenter.y - fromCenter.y;
const dx = toCenter.x - fromCenter.x
const dy = toCenter.y - fromCenter.y
const horizontalDistance = Math.abs(dx) / (nodeSize.width / 2);
const verticalDistance = Math.abs(dy) / (getNodeHeight(from) / 2);
if (horizontalDistance >= verticalDistance) return dx >= 0 ? "right" : "left";
return dy >= 0 ? "bottom" : "top";
const horizontalDistance = Math.abs(dx) / (nodeSize.width / 2)
const verticalDistance = Math.abs(dy) / (getNodeHeight(from) / 2)
if (horizontalDistance >= verticalDistance) return dx >= 0 ? 'right' : 'left'
return dy >= 0 ? 'bottom' : 'top'
}
export function getNodeOutcomes(item: WorkflowCriteriaDto): WorkflowOutcome[] {
if (item.kind === "Compare") {
if (item.kind === 'Compare') {
const outcomes = item.compareOutcomes?.length
? item.compareOutcomes
: [
{ label: "Doğru", targetId: item.nextOnTrue },
{ label: "Yanlış", targetId: item.nextOnFalse },
];
{ label: 'Doğru', targetId: item.nextOnTrue },
{ label: 'Yanlış', targetId: item.nextOnFalse },
]
return outcomes.slice(0, 4).map((outcome, index) => ({
field: `compareOutcomes:${index}`,
label: outcome.label || `Durum ${index + 1}`,
targetId: outcome.targetId,
}));
}))
}
if (item.kind === "Approval") {
if (item.kind === 'Approval') {
return [
{ field: "nextOnApprove", label: "Onay", targetId: item.nextOnApprove },
{ field: "nextOnReject", label: "Red", targetId: item.nextOnReject },
];
{ field: 'nextOnApprove', label: 'Onay', targetId: item.nextOnApprove },
{ field: 'nextOnReject', label: 'Red', targetId: item.nextOnReject },
]
}
if (item.kind === "End") return [];
if (item.kind === 'End') return []
return [
{ field: "nextOnStart", label: "Sonraki", targetId: item.nextOnStart },
];
return [{ field: 'nextOnStart', label: 'Sonraki', targetId: item.nextOnStart }]
}
export function outcomeLabel(field?: string) {
if (field?.startsWith("compareOutcomes:")) return "Karşılaştırma durumu";
if (field?.startsWith('compareOutcomes:')) return 'Karşılaştırma durumu'
const labels: Record<string, string> = {
nextOnStart: "Sonraki",
nextOnTrue: "Doğru",
nextOnFalse: "Yanlış",
nextOnApprove: "Onay",
nextOnReject: "Red",
};
nextOnStart: 'Sonraki',
nextOnTrue: 'Doğru',
nextOnFalse: 'Yanlış',
nextOnApprove: 'Onay',
nextOnReject: 'Red',
}
return field ? labels[field] : undefined;
return field ? labels[field] : undefined
}
export function addLink(
@ -329,8 +292,8 @@ export function addLink(
type: string,
sourcePort: WorkflowLinkPort = {},
) {
if (!targetId) return;
const target = criteria.find((item) => item.id === targetId);
if (!targetId) return
const target = criteria.find((item) => item.id === targetId)
if (target) {
links.push({
key: `${source.id}-${target.id}-${type}`,
@ -338,125 +301,119 @@ export function addLink(
target,
label,
sourcePort,
});
})
}
}
export function emptyCriteria(
kind = "Compare",
workflowItemId = "",
): WorkflowCriteriaForm {
export function emptyCriteria(kind = 'Compare', listFormCode = ''): WorkflowCriteriaForm {
return {
id: "",
workflowItemId,
id: '',
listFormCode,
kind,
title: defaultTitle(kind),
column: "Tutar",
operator: ">",
compareColumn: 'Tutar',
compareOperator: '>',
compareValue: 5000,
approver: "",
informPerson: "",
nextOnStart: "",
nextOnTrue: "",
nextOnFalse: "",
nextOnApprove: "",
nextOnReject: "",
approver: '',
nextOnStart: '',
nextOnTrue: '',
nextOnFalse: '',
nextOnApprove: '',
nextOnReject: '',
compareOutcomes:
kind === "Compare"
? [emptyCompareOutcome("Durum 1"), emptyCompareOutcome("Durum 2")]
: [],
kind === 'Compare' ? [emptyCompareOutcome('Durum 1'), emptyCompareOutcome('Durum 2')] : [],
positionX: 32,
positionY: 150,
};
}
}
export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm {
const sharedPerson = item.approver || item.informPerson || "";
const sharedPerson = item.approver || ''
return {
...emptyCriteria(item.kind),
...item,
approver: sharedPerson,
informPerson: sharedPerson,
nextOnStart: item.nextOnStart || "",
nextOnTrue: item.nextOnTrue || "",
nextOnFalse: item.nextOnFalse || "",
nextOnApprove: item.nextOnApprove || "",
nextOnReject: item.nextOnReject || "",
nextOnStart: item.nextOnStart || '',
nextOnTrue: item.nextOnTrue || '',
nextOnFalse: item.nextOnFalse || '',
nextOnApprove: item.nextOnApprove || '',
nextOnReject: item.nextOnReject || '',
compareOutcomes: item.compareOutcomes?.length
? item.compareOutcomes.map(toCompareOutcomeForm)
: emptyCriteria(item.kind).compareOutcomes,
};
}
}
export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput {
const sharedPerson = item.approver || item.informPerson || "";
const sharedPerson = item.approver || ''
return {
...item,
id: item.id || null,
workflowItemId: item.workflowItemId || "",
listFormCode: item.listFormCode || '',
compareValue: Number(item.compareValue || 0),
approver: sharedPerson,
informPerson: sharedPerson,
positionX: Number(item.positionX || 32),
positionY: Number(item.positionY || 150),
compareOutcomes: (item.compareOutcomes || [])
.slice(0, 4)
.filter((outcome) => outcome.label?.trim())
.map(normalizeCompareOutcome),
};
}
}
export function defaultTitle(kind: string) {
return {
Start: "İş Akışı Başlat",
Compare: "Tutar > 5000 TL",
Approval: "Onaylanacak Kişi",
Inform: "Bilgilendirme Yapılacak Personel",
End: "Akışı Bitir",
}[kind] ?? "İş Akışı Adımı";
return (
{
Start: 'İş Akışı Başlat',
Compare: 'Tutar > 5000 TL',
Approval: 'Onaylanacak Kişi',
Inform: 'Bilgilendirme Yapılacak Personel',
End: 'Akışı Bitir',
}[kind] ?? 'İş Akışı Adımı'
)
}
export function emptyCompareOutcome(label = "Durum"): CompareOutcomeDto {
export function emptyCompareOutcome(label = 'Durum'): CompareOutcomeDto {
return {
label,
targetId: "",
conditions: [{ column: "Tutar", operator: ">", compareValue: 5000 }],
};
targetId: '',
conditions: [{ compareColumn: 'Tutar', compareOperator: '>', compareValue: 5000 }],
}
}
export function toCompareOutcomeForm(
outcome: Partial<CompareOutcomeDto> &
Partial<WorkflowConditionDto> & {
conditions?: Partial<WorkflowConditionDto>[];
conditions?: Partial<WorkflowConditionDto>[]
},
): CompareOutcomeDto {
const conditions = outcome.conditions?.length
? outcome.conditions
: [
{
column: outcome.column || "Tutar",
operator: outcome.operator || ">",
compareColumn: outcome.compareColumn || 'Tutar',
compareOperator: outcome.compareOperator || '>',
compareValue: outcome.compareValue || 0,
},
];
]
return {
label: outcome.label || "",
targetId: outcome.targetId || "",
label: outcome.label || '',
targetId: outcome.targetId || '',
conditions: conditions.map((condition) => ({
column: condition.column || "Tutar",
operator: condition.operator || ">",
compareColumn: condition.compareColumn || 'Tutar',
compareOperator: condition.compareOperator || '>',
compareValue: condition.compareValue ?? 0,
})),
};
}
}
export function normalizeCompareOutcome(
outcome: Partial<CompareOutcomeDto> &
Partial<WorkflowConditionDto> & {
conditions?: Partial<WorkflowConditionDto>[];
conditions?: Partial<WorkflowConditionDto>[]
},
): CompareOutcomeDto {
const conditions = (
@ -464,27 +421,24 @@ export function normalizeCompareOutcome(
? outcome.conditions
: [
{
column: outcome.column || "Tutar",
operator: outcome.operator || ">",
compareColumn: outcome.compareColumn || 'Tutar',
compareOperator: outcome.compareOperator || '>',
compareValue: outcome.compareValue || 0,
},
]
)
.filter(
(condition) =>
condition.operator && String(condition.compareValue ?? "") !== "",
)
.filter((condition) => condition.compareOperator && String(condition.compareValue ?? '') !== '')
.map((condition) => ({
column: condition.column || "Tutar",
operator: condition.operator || ">",
compareColumn: condition.compareColumn || 'Tutar',
compareOperator: condition.compareOperator || '>',
compareValue: Number(condition.compareValue || 0),
}));
}))
return {
label: (outcome.label || "").trim(),
label: (outcome.label || '').trim(),
targetId: outcome.targetId || null,
conditions,
};
}
}
export function compareOutcomeRuleText(
@ -492,67 +446,61 @@ export function compareOutcomeRuleText(
) {
const conditions = outcome.conditions?.length
? outcome.conditions
: outcome.operator
: outcome.compareOperator
? [
{
column: outcome.column || "Tutar",
operator: outcome.operator,
compareColumn: outcome.compareColumn,
compareOperator: outcome.compareOperator,
compareValue: outcome.compareValue,
},
]
: [];
: []
return conditions.length
? conditions
.map(
(condition) =>
`${condition.column} ${condition.operator} ${formatCompactValue(condition.compareValue)}`,
`${condition.compareColumn} ${condition.compareOperator} ${formatCompactValue(condition.compareValue)}`,
)
.join(" ve ")
: "Kural yok";
.join(' ve ')
: 'Kural yok'
}
export function formatCompactValue(value: number | string | null | undefined) {
return new Intl.NumberFormat("tr-TR", {
return new Intl.NumberFormat('tr-TR', {
maximumFractionDigits: 2,
}).format(Number(value || 0));
}).format(Number(value || 0))
}
export function criteriaSummary(item: WorkflowCriteriaDto) {
if (item.kind === "Compare") {
if (item.kind === 'Compare') {
return (
(item.compareOutcomes || [])
.map(
(outcome) => `${outcome.label}: ${compareOutcomeRuleText(outcome)}`,
)
.join(" / ") || "-"
);
.map((outcome) => `${outcome.label}: ${compareOutcomeRuleText(outcome)}`)
.join(' / ') || '-'
)
}
if (item.kind === "Approval")
return item.approver || item.informPerson || "-";
if (item.kind === "Inform") return item.approver || item.informPerson || "-";
return item.title;
if (item.kind === 'Approval') return item.approver || '-'
if (item.kind === 'Inform') return item.approver || '-'
return item.title
}
export function targetTitle(
criteria: WorkflowCriteriaDto[],
id?: string | null,
) {
if (!id) return "-";
const item = criteria.find((candidate) => candidate.id === id);
return item ? `${item.id} - ${item.title}` : id;
export function targetTitle(criteria: WorkflowCriteriaDto[], id?: string | null) {
if (!id) return '-'
const item = criteria.find((candidate) => candidate.id === id)
return item ? `${item.id} - ${item.title}` : id
}
export function statusClass(status?: string) {
if (status === "Onay Bekliyor") return "pending";
if (status === "Bitti") return "done";
if (status === "Bilgilendirildi") return "info";
return "";
if (status === 'Onay Bekliyor') return 'pending'
if (status === 'Bitti') return 'done'
if (status === 'Bilgilendirildi') return 'info'
return ''
}
export function formatMoney(value?: number | string | null) {
return new Intl.NumberFormat("tr-TR", {
style: "currency",
currency: "TRY",
}).format(Number(value || 0));
return new Intl.NumberFormat('tr-TR', {
style: 'currency',
currency: 'TRY',
}).format(Number(value || 0))
}

View file

@ -1,464 +1,299 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { FormEvent } from "react";
import dayjs from "dayjs";
import "dayjs/locale/tr";
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { FormEvent } from 'react'
import {
buildFitLayout,
emptyCriteria,
isPendingApproval,
normalizeCriteria,
toCriteriaForm,
type WorkflowCriteriaForm,
} from "@/utils/workflow/workflowHelpers";
import {
workflowService,
type WorkflowCriteriaDto,
type WorkflowItemDto,
} from "@/services/workflow.service";
import { DashboardShell } from "../workflow/DashboardShell";
dayjs.locale("tr");
type WorkflowForm = {
sorumlu: string;
amount: number | string;
};
type WorkflowEditForm = WorkflowForm & {
tarih: string;
};
} from '@/utils/workflow/workflowHelpers'
import { workflowService, type WorkflowCriteriaDto } from '@/services/workflow.service'
import { DashboardShell } from '../workflow/DashboardShell'
type PendingLink = {
sourceId: string;
outcome: string;
} | null;
sourceId: string
outcome: string
} | null
type DragPreview = {
id: string;
delta: { x: number; y: number };
} | null;
id: string
delta: { x: number; y: number }
} | null
type DragEndEvent = {
active: { id: string };
delta: { x: number; y: number };
};
active: { id: string }
delta: { x: number; y: number }
}
export function FormTabWorkflow(props: { listFormCode: string }) {
const [workflowItems, setWorkflowItems] = useState<WorkflowItemDto[]>([]);
const [criteria, setCriteria] = useState<WorkflowCriteriaDto[]>([]);
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | null>(
null,
);
const [selectedId, setSelectedId] = useState("");
const [pendingLink, setPendingLink] = useState<PendingLink>(null);
const [workflowForm, setWorkflowForm] = useState<WorkflowForm>({
sorumlu: "",
amount: 7200,
});
const [editingWorkflowId, setEditingWorkflowId] = useState<string | null>(
null,
);
const [workflowEditForm, setWorkflowEditForm] = useState<WorkflowEditForm>({
sorumlu: "",
tarih: "",
amount: 0,
});
export function FormTabWorkflow({ listFormCode }: { listFormCode: string }) {
const [criteria, setCriteria] = useState<WorkflowCriteriaDto[]>([])
const [selectedId, setSelectedId] = useState('')
const [pendingLink, setPendingLink] = useState<PendingLink>(null)
const [criteriaForm, setCriteriaForm] = useState<WorkflowCriteriaForm>(
emptyCriteria(),
);
const [dragPreview, setDragPreview] = useState<DragPreview>(null);
const [canvasZoom, setCanvasZoom] = useState(1);
const [designerTab, setDesignerTab] = useState("flow");
const [busy, setBusy] = useState(false);
const [approvalDialogWorkflowId, setApprovalDialogWorkflowId] =
useState<string | null>(null);
const canvasRef = useRef<HTMLDivElement | null>(null);
emptyCriteria('Start', listFormCode),
)
const [dragPreview, setDragPreview] = useState<DragPreview>(null)
const [canvasZoom, setCanvasZoom] = useState(1)
const [designerTab, setDesignerTab] = useState('flow')
const [busy, setBusy] = useState(false)
const canvasRef = useRef<HTMLDivElement | null>(null)
const currentCriteria = useMemo(
() => criteria.filter((item) => item.workflowItemId === selectedWorkflowId),
[criteria, selectedWorkflowId],
);
const selectedWorkflow = useMemo(
() => workflowItems.find((item) => item.id === selectedWorkflowId),
[selectedWorkflowId, workflowItems],
);
const currentCriteria = useMemo(() => criteria, [criteria])
const selectedCriteria = useMemo(
() => currentCriteria.find((item) => item.id === selectedId) ?? null,
[currentCriteria, selectedId],
);
const pendingItems = useMemo(
() => workflowItems.filter((item) => isPendingApproval(item, criteria)),
[criteria, workflowItems],
);
const dialogPendingItems = useMemo(
() => pendingItems.filter((item) => item.id === approvalDialogWorkflowId),
[approvalDialogWorkflowId, pendingItems],
);
)
const loadState = useCallback(async () => {
const data = await workflowService.getState();
setWorkflowItems(data.workflowItems);
setCriteria(data.criteria);
setSelectedWorkflowId(
(current) => current || data.workflowItems[0]?.id || null,
);
return data;
}, []);
const data = await workflowService.getState(listFormCode)
setCriteria(data.criteria)
return data
}, [listFormCode])
const runAction = useCallback(
async (action: () => Promise<unknown>) => {
setBusy(true);
setBusy(true)
try {
await action();
await loadState();
await action()
await loadState()
} finally {
setBusy(false);
setBusy(false)
}
},
[loadState],
);
)
useEffect(() => {
loadState();
}, [loadState]);
loadState()
}, [loadState])
useEffect(() => {
if (selectedCriteria) {
setCriteriaForm(toCriteriaForm(selectedCriteria));
} else if (selectedWorkflowId) {
setCriteriaForm(emptyCriteria("Start", selectedWorkflowId));
setCriteriaForm(toCriteriaForm(selectedCriteria))
} else {
setCriteriaForm(emptyCriteria('Start', listFormCode))
}
}, [selectedCriteria, selectedWorkflowId]);
}, [listFormCode, selectedCriteria])
useEffect(() => {
if (!selectedWorkflowId || !selectedId) return;
if (!selectedId) return
const selectedStillBelongs = currentCriteria.some(
(item) => item.id === selectedId,
);
if (!selectedStillBelongs) {
setSelectedId("");
const selectedStillExists = currentCriteria.some((item) => item.id === selectedId)
if (!selectedStillExists) {
setSelectedId('')
}
}, [currentCriteria, selectedId, selectedWorkflowId]);
useEffect(() => {
if (!approvalDialogWorkflowId) return;
const stillPending = pendingItems.some(
(item) => item.id === approvalDialogWorkflowId,
);
if (!stillPending) {
setApprovalDialogWorkflowId(null);
}
}, [approvalDialogWorkflowId, pendingItems]);
const createWorkflow = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
runAction(async () => {
const created = await workflowService.createWorkflow({
sorumlu: workflowForm.sorumlu,
amount: Number(workflowForm.amount),
});
setWorkflowForm({ sorumlu: "", amount: 7200 });
setSelectedWorkflowId(created.id);
setSelectedId("");
setCriteriaForm(emptyCriteria("Start", created.id));
});
};
const beginWorkflowEdit = (item: WorkflowItemDto) => {
setSelectedWorkflowId(item.id);
setPendingLink(null);
setSelectedId("");
setEditingWorkflowId(item.id);
setWorkflowEditForm({
sorumlu: item.sorumlu,
tarih: dayjs(item.tarih).format("YYYY-MM-DD"),
amount: item.amount,
});
};
const cancelWorkflowEdit = () => {
setEditingWorkflowId(null);
setWorkflowEditForm({ sorumlu: "", tarih: "", amount: 0 });
};
const saveWorkflowEdit = (id: string) => {
runAction(async () => {
await workflowService.updateWorkflow(id, {
sorumlu: workflowEditForm.sorumlu,
tarih: workflowEditForm.tarih,
amount: Number(workflowEditForm.amount),
});
cancelWorkflowEdit();
});
};
const startWorkflow = useCallback(
(id: string) => {
runAction(async () => {
await workflowService.startWorkflow(id);
const data = await loadState();
const startedWorkflow = data.workflowItems.find(
(item) => item.id === id,
);
setApprovalDialogWorkflowId(
isPendingApproval(startedWorkflow, data.criteria) ? id : null,
);
});
},
[loadState, runAction],
);
}, [currentCriteria, selectedId])
const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
event.preventDefault()
runAction(async () => {
await workflowService.saveCriteria(normalizeCriteria(criteriaForm));
setSelectedId("");
});
};
await workflowService.saveCriteria({
...normalizeCriteria(criteriaForm),
listFormCode,
})
setSelectedId('')
})
}
const addCriteria = (kind: string) => {
if (!selectedWorkflowId) return;
setDesignerTab("flow");
setDesignerTab('flow')
runAction(async () => {
const saved = await workflowService.saveCriteria({
...normalizeCriteria(emptyCriteria(kind, selectedWorkflowId)),
...normalizeCriteria(emptyCriteria(kind, listFormCode)),
listFormCode,
positionX: 80 + (currentCriteria.length % 5) * 230,
positionY: 220 + Math.floor(currentCriteria.length / 5) * 140,
});
setSelectedId(saved.id);
});
};
})
setSelectedId(saved.id)
})
}
const deleteSelectedCriteria = useCallback(
(criteriaId: string = selectedId) => {
if (!criteriaId || busy) return;
if (!criteriaId || busy) return
runAction(async () => {
await workflowService.deleteCriteria(criteriaId);
setSelectedId("");
});
await workflowService.deleteCriteria(criteriaId)
setSelectedId('')
})
},
[busy, runAction, selectedId],
);
)
const disconnectLink = useCallback(
(sourceId: string, outcome: string) => {
if (!sourceId || !outcome || busy) return;
if (!sourceId || !outcome || busy) return
const source = currentCriteria.find((item) => item.id === sourceId);
if (!source) return;
const source = currentCriteria.find((item) => item.id === sourceId)
if (!source) return
const next: WorkflowCriteriaForm = toCriteriaForm(source);
if (outcome.startsWith("compareOutcomes:")) {
const outcomeIndex = Number(outcome.split(":")[1]);
next.compareOutcomes = [...(source.compareOutcomes || [])];
const next: WorkflowCriteriaForm = toCriteriaForm(source)
if (outcome.startsWith('compareOutcomes:')) {
const outcomeIndex = Number(outcome.split(':')[1])
next.compareOutcomes = [...(source.compareOutcomes || [])]
if (next.compareOutcomes?.[outcomeIndex]) {
next.compareOutcomes[outcomeIndex] = {
...next.compareOutcomes[outcomeIndex],
targetId: null,
};
}
}
if (outcomeIndex === 0) next.nextOnTrue = null;
if (outcomeIndex === 1) next.nextOnFalse = null;
if (outcomeIndex === 0) next.nextOnTrue = null
if (outcomeIndex === 1) next.nextOnFalse = null
} else {
(next as Record<string, unknown>)[outcome] = null;
;(next as Record<string, unknown>)[outcome] = null
}
runAction(async () => {
await workflowService.saveCriteria(normalizeCriteria(next));
setPendingLink(null);
setSelectedId(sourceId);
});
await workflowService.saveCriteria({ ...normalizeCriteria(next), listFormCode })
setPendingLink(null)
setSelectedId(sourceId)
})
},
[busy, currentCriteria, runAction],
);
[busy, currentCriteria, listFormCode, runAction],
)
useEffect(() => {
const deleteWithKeyboard = (event: globalThis.KeyboardEvent) => {
const activeTag = document.activeElement?.tagName?.toLowerCase();
const activeTag = document.activeElement?.tagName?.toLowerCase()
const isEditing =
Boolean(activeTag && ["input", "textarea", "select"].includes(activeTag)) ||
(document.activeElement instanceof HTMLElement &&
document.activeElement.isContentEditable);
Boolean(activeTag && ['input', 'textarea', 'select'].includes(activeTag)) ||
(document.activeElement instanceof HTMLElement && document.activeElement.isContentEditable)
if (event.key !== "Delete" || isEditing) return;
if (event.key !== 'Delete' || isEditing) return
event.preventDefault();
event.preventDefault()
if (pendingLink) {
disconnectLink(pendingLink.sourceId, pendingLink.outcome);
return;
disconnectLink(pendingLink.sourceId, pendingLink.outcome)
return
}
deleteSelectedCriteria();
};
deleteSelectedCriteria()
}
window.addEventListener("keydown", deleteWithKeyboard);
return () => window.removeEventListener("keydown", deleteWithKeyboard);
}, [deleteSelectedCriteria, disconnectLink, pendingLink]);
window.addEventListener('keydown', deleteWithKeyboard)
return () => window.removeEventListener('keydown', deleteWithKeyboard)
}, [deleteSelectedCriteria, disconnectLink, pendingLink])
const updateNodePosition = ({ active, delta }: DragEndEvent) => {
setDragPreview(null);
setDragPreview(null)
const item = currentCriteria.find(
(candidate) => candidate.id === active.id,
);
if (!item) return;
const item = currentCriteria.find((candidate) => candidate.id === active.id)
if (!item) return
setSelectedId(item.id);
if (delta.x === 0 && delta.y === 0) return;
setSelectedId(item.id)
if (delta.x === 0 && delta.y === 0) return
const next = {
...item,
positionX: Math.max(12, Math.round(item.positionX + delta.x)),
positionY: Math.max(12, Math.round(item.positionY + delta.y)),
};
}
runAction(async () => {
await workflowService.saveCriteria(next);
setSelectedId(next.id);
});
};
await workflowService.saveCriteria({ ...normalizeCriteria(next), listFormCode })
setSelectedId(next.id)
})
}
const connectNodes = (sourceId: string, outcome: string, targetId: string) => {
const source = currentCriteria.find((item) => item.id === sourceId);
if (!source || source.id === targetId) return;
const source = currentCriteria.find((item) => item.id === sourceId)
if (!source || source.id === targetId) return
const next: WorkflowCriteriaForm = toCriteriaForm(source);
if (outcome.startsWith("compareOutcomes:")) {
const outcomeIndex = Number(outcome.split(":")[1]);
next.compareOutcomes = [...(source.compareOutcomes || [])];
const next: WorkflowCriteriaForm = toCriteriaForm(source)
if (outcome.startsWith('compareOutcomes:')) {
const outcomeIndex = Number(outcome.split(':')[1])
next.compareOutcomes = [...(source.compareOutcomes || [])]
next.compareOutcomes[outcomeIndex] = {
...next.compareOutcomes[outcomeIndex],
targetId,
};
if (outcomeIndex === 0) next.nextOnTrue = targetId;
if (outcomeIndex === 1) next.nextOnFalse = targetId;
}
if (outcomeIndex === 0) next.nextOnTrue = targetId
if (outcomeIndex === 1) next.nextOnFalse = targetId
} else {
(next as Record<string, unknown>)[outcome] = targetId;
;(next as Record<string, unknown>)[outcome] = targetId
}
setPendingLink(null);
setPendingLink(null)
runAction(async () => {
await workflowService.saveCriteria(normalizeCriteria(next));
setSelectedId("");
});
};
await workflowService.saveCriteria({ ...normalizeCriteria(next), listFormCode })
setSelectedId('')
})
}
const fitFlowLayout = () => {
if (!currentCriteria.length || busy) return;
if (!currentCriteria.length || busy) return
const nextPositions = buildFitLayout(currentCriteria);
setDesignerTab("flow");
setCanvasZoom(1);
const nextPositions = buildFitLayout(currentCriteria)
setDesignerTab('flow')
setCanvasZoom(1)
runAction(async () => {
for (const item of currentCriteria) {
const position = nextPositions.get(item.id);
if (!position) continue;
const position = nextPositions.get(item.id)
if (!position) continue
await workflowService.saveCriteria({
...normalizeCriteria(item),
listFormCode,
positionX: position.x,
positionY: position.y,
});
})
}
requestAnimationFrame(() => {
canvasRef.current?.scrollTo({ left: 0, top: 0, behavior: "smooth" });
});
});
};
const selectWorkflow = (item: WorkflowItemDto) => {
setSelectedWorkflowId(item.id);
setPendingLink(null);
setSelectedId("");
};
canvasRef.current?.scrollTo({ left: 0, top: 0, behavior: 'smooth' })
})
})
}
const openCriteriaDetails = (id: string) => {
setSelectedId(id);
setPendingLink(null);
setDesignerTab("criteria");
};
setSelectedId(id)
setPendingLink(null)
setDesignerTab('criteria')
}
const clearCanvasSelection = () => {
setPendingLink(null);
setSelectedId("");
};
setPendingLink(null)
setSelectedId('')
}
const beginLink = (sourceId: string, outcome: string) => {
setPendingLink({ sourceId, outcome });
setSelectedId(sourceId);
};
setPendingLink({ sourceId, outcome })
setSelectedId(sourceId)
}
return (
<DashboardShell
busy={busy}
canvasRef={canvasRef}
canvasZoom={canvasZoom}
criteria={criteria}
criteriaForm={criteriaForm}
currentCriteria={currentCriteria}
designerTab={designerTab}
dialogPendingItems={dialogPendingItems}
dragPreview={dragPreview}
editingWorkflowId={editingWorkflowId}
pendingLink={pendingLink}
selectedId={selectedId}
selectedWorkflow={selectedWorkflow}
selectedWorkflowId={selectedWorkflowId}
showApprovalDialog={Boolean(approvalDialogWorkflowId)}
workflowEditForm={workflowEditForm}
workflowForm={workflowForm}
workflowItems={workflowItems}
onAddCriteria={addCriteria}
onBeginLink={beginLink}
onBeginWorkflowEdit={beginWorkflowEdit}
onCancelWorkflowEdit={cancelWorkflowEdit}
onChangeCriteriaForm={setCriteriaForm}
onClearCanvasSelection={clearCanvasSelection}
onCloseApprovalDialog={() => setApprovalDialogWorkflowId(null)}
onConnectNodes={connectNodes}
onCreateWorkflow={createWorkflow}
onDecision={(id: string, approved: boolean, note: string) =>
runAction(() => workflowService.decideWorkflow(id, { approved, note }))
}
onDeleteSelectedCriteria={deleteSelectedCriteria}
onDisconnectLink={disconnectLink}
onDragMove={(event: DragEndEvent | null) =>
setDragPreview(
event ? { id: event.active.id, delta: event.delta } : null,
)
setDragPreview(event ? { id: event.active.id, delta: event.delta } : null)
}
onFitFlowLayout={fitFlowLayout}
onOpenCriteriaDetails={openCriteriaDetails}
onResetDemo={() => runAction(workflowService.resetDemo)}
onResetDemo={() => runAction(() => workflowService.resetDemo(listFormCode))}
onSaveCriteria={saveCriteria}
onSaveWorkflowEdit={saveWorkflowEdit}
onSelectCriteria={setSelectedId}
onSelectWorkflow={selectWorkflow}
onSetDesignerTab={setDesignerTab}
onStartWorkflow={startWorkflow}
onUpdateNodePosition={updateNodePosition}
onWorkflowEditFormChange={setWorkflowEditForm}
onWorkflowFormChange={setWorkflowForm}
onZoomIn={() =>
setCanvasZoom((current) =>
Math.min(1.5, Number((current + 0.1).toFixed(2))),
)
}
onZoomIn={() => setCanvasZoom((current) => Math.min(1.5, Number((current + 0.1).toFixed(2))))}
onZoomOut={() =>
setCanvasZoom((current) =>
Math.max(0.6, Number((current - 0.1).toFixed(2))),
)
setCanvasZoom((current) => Math.max(0.6, Number((current - 0.1).toFixed(2))))
}
/>
);
)
}

View file

@ -1,169 +0,0 @@
import { formatMoney } from "@/utils/workflow/workflowHelpers";
import { useState } from "react";
import { FiCheck, FiSlash, FiX } from "react-icons/fi";
import type {
WorkflowCriteriaDto,
WorkflowItemDto,
} from "@/services/workflow.service";
const CloseIcon = FiX as any;
const CheckIcon = FiCheck as any;
const SlashIcon = FiSlash as any;
type ApprovalDialogProps = {
busy: boolean;
criteria: WorkflowCriteriaDto[];
items: WorkflowItemDto[];
onClose: () => void;
onDecision: (id: string, approved: boolean, note: string) => void;
};
export function ApprovalDialog({
busy,
criteria,
items,
onClose,
onDecision,
}: ApprovalDialogProps) {
if (!items.length) return null;
return (
<div
className="fixed inset-0 z-50 grid place-items-center bg-slate-900/40 p-[18px]"
role="presentation"
>
<section
className="max-h-[calc(100vh-36px)] w-[min(560px,100%)] overflow-auto rounded-lg border border-app-line bg-app-surface p-4 shadow-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="approval-dialog-title"
>
<div className="mb-3 flex items-start justify-between gap-4 max-[720px]:flex-col max-[720px]:items-stretch">
<div>
<h2
id="approval-dialog-title"
className="m-0 text-lg tracking-normal"
>
Bekleyen Onaylar
</h2>
<p className="mb-0 mt-1 text-app-muted">
Workflow onay adiminda bekliyor.
</p>
</div>
<button
type="button"
className="w-[38px] justify-center border-app-primary bg-white p-0 text-app-primary"
title="Kapat"
onClick={onClose}
>
<CloseIcon />
</button>
</div>
<PendingApprovals
items={items}
criteria={criteria}
busy={busy}
showChrome={false}
onDecision={onDecision}
/>
</section>
</div>
);
}
function PendingApprovals({
items,
criteria,
busy,
onDecision,
showChrome = true,
}: Omit<ApprovalDialogProps, "onClose"> & { showChrome?: boolean }) {
const [notes, setNotes] = useState<Record<string, string>>({});
const content = (
<>
{showChrome && (
<div className="mb-3 flex items-center justify-between gap-4 max-[720px]:flex-col max-[720px]:items-stretch">
<h2 className="m-0 text-lg tracking-normal">Bekleyen Onaylar</h2>
<span className="text-sm text-app-muted">
{items.length} bekleyen
</span>
</div>
)}
<div className="grid gap-2.5">
{items.length === 0 && (
<p className="m-0 text-app-muted">Bekleyen onay yok.</p>
)}
{items.map((item) => {
const activeStep = criteria.find(
(candidate) =>
candidate.workflowItemId === item.id &&
candidate.id === item.currentNodeId,
);
return (
<article
key={item.id}
className="grid gap-2.5 rounded-lg border border-app-line p-3"
>
<div>
<strong>
#{item.id} {item.sorumlu}
</strong>
{activeStep?.title && (
<span className="mt-1 block text-sm font-bold text-app-text">
{activeStep.title}
</span>
)}
<span className="mt-1 block text-app-muted">
{formatMoney(item.amount)} - Onaylayacak kişi:{" "}
{item.assignedApprover}
</span>
</div>
<textarea
rows={2}
placeholder="Onay/red notu"
value={notes[item.id] || ""}
onChange={(event) =>
setNotes({ ...notes, [item.id]: event.target.value })
}
/>
<div className="flex flex-wrap gap-2">
<button
type="button"
className="border-app-green bg-app-green text-white"
disabled={busy}
onClick={() =>
onDecision(item.id, true, notes[item.id] || "Onay verildi.")
}
>
<CheckIcon />
Onayla
</button>
<button
type="button"
className="border-app-red bg-app-red text-white"
disabled={busy}
onClick={() =>
onDecision(item.id, false, notes[item.id] || "Red edildi.")
}
>
<SlashIcon />
Reddet
</button>
</div>
</article>
);
})}
</div>
</>
);
if (!showChrome) return content;
return (
<section className="min-w-0 rounded-lg border border-app-line bg-app-surface p-4">
{content}
</section>
);
}

View file

@ -1,105 +1,95 @@
import React from "react";
import { FiSave, FiTrash2 } from "react-icons/fi";
import classNames from "classnames";
import React from 'react'
import { FiSave, FiTrash2 } from 'react-icons/fi'
import classNames from 'classnames'
import {
columnOptions,
kindIcon,
kindOptions,
operatorOptions,
} from "@/utils/workflow/workflowConstants";
} from '@/utils/workflow/workflowConstants'
import {
compareOutcomeRuleText,
criteriaSummary,
emptyCompareOutcome,
targetTitle,
} from "@/utils/workflow/workflowHelpers";
import type {
CompareOutcomeDto,
WorkflowCriteriaDto,
WorkflowItemDto,
} from "@/services/workflow.service";
} from '@/utils/workflow/workflowHelpers'
import type { CompareOutcomeDto, WorkflowCriteriaDto } from '@/services/workflow.service'
const SaveIcon = FiSave as any;
const TrashIcon = FiTrash2 as any;
const SaveIcon = FiSave as any
const TrashIcon = FiTrash2 as any
const tableButtonClass =
'inline-flex min-h-8 items-center justify-center gap-1.5 rounded-md border px-2.5 py-1 text-[13px] font-medium leading-none transition-colors disabled:cursor-not-allowed disabled:opacity-50'
type CriteriaTableProps = {
criteria: WorkflowCriteriaDto[];
selectedWorkflow?: WorkflowItemDto | null;
selectedId: string;
activeNodeId?: string;
form: any;
busy: boolean;
onSelect: (id: string) => void;
onChange: (form: any) => void;
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
onDelete: (id: string) => void;
onAddCriteria?: (kind: string) => void;
};
criteria: WorkflowCriteriaDto[]
selectedId: string
form: any
busy: boolean
onSelect: (id: string) => void
onChange: (form: any) => void
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
onDelete: (id: string) => void
}
export function CriteriaTable({
criteria,
selectedWorkflow,
selectedId,
activeNodeId,
form,
busy,
onSelect,
onChange,
onSubmit,
onDelete,
onAddCriteria,
}: CriteriaTableProps) {
const setField = (name: string, value: unknown) =>
onChange({ ...form, [name]: value });
const setField = (name: string, value: unknown) => onChange({ ...form, [name]: value })
const targetOptions = [
{ value: "", label: "Bağlantı yok" },
{ value: '', label: 'Bağlantı yok' },
...criteria
.filter((item) => item.id !== form.id)
.map((item) => ({ value: item.id, label: `${item.id} - ${item.title}` })),
];
]
const updateCompareOutcome = (index: number, patch: Partial<CompareOutcomeDto>) => {
const next = [...(form.compareOutcomes || [])];
next[index] = { ...next[index], ...patch };
setField("compareOutcomes", next);
};
const next = [...(form.compareOutcomes || [])]
next[index] = { ...next[index], ...patch }
setField('compareOutcomes', next)
}
const updateCompareCondition = (
outcomeIndex: number,
conditionIndex: number,
patch: Record<string, unknown>,
) => {
const next = [...(form.compareOutcomes || [])];
const conditions = [...(next[outcomeIndex]?.conditions || [])];
conditions[conditionIndex] = { ...conditions[conditionIndex], ...patch };
next[outcomeIndex] = { ...next[outcomeIndex], conditions };
setField("compareOutcomes", next);
};
const next = [...(form.compareOutcomes || [])]
const conditions = [...(next[outcomeIndex]?.conditions || [])]
conditions[conditionIndex] = { ...conditions[conditionIndex], ...patch }
next[outcomeIndex] = { ...next[outcomeIndex], conditions }
setField('compareOutcomes', next)
}
const addCompareCondition = (outcomeIndex: number) => {
const next = [...(form.compareOutcomes || [])];
const next = [...(form.compareOutcomes || [])]
next[outcomeIndex] = {
...next[outcomeIndex],
conditions: [
...(next[outcomeIndex]?.conditions || []),
{ column: "Tutar", operator: ">", compareValue: 0 },
{ compareColumn: 'Tutar', compareOperator: '>', compareValue: 0 },
],
};
setField("compareOutcomes", next);
};
}
setField('compareOutcomes', next)
}
const removeCompareCondition = (outcomeIndex: number, conditionIndex: number) => {
const next = [...(form.compareOutcomes || [])];
const next = [...(form.compareOutcomes || [])]
const conditions = (next[outcomeIndex]?.conditions || []).filter(
(_: unknown, index: number) => index !== conditionIndex,
);
next[outcomeIndex] = { ...next[outcomeIndex], conditions };
setField("compareOutcomes", next);
};
)
next[outcomeIndex] = { ...next[outcomeIndex], conditions }
setField('compareOutcomes', next)
}
const removeCompareOutcome = (index: number) => {
setField(
"compareOutcomes",
(form.compareOutcomes || []).filter(
(_: unknown, itemIndex: number) => itemIndex !== index,
),
);
};
'compareOutcomes',
(form.compareOutcomes || []).filter((_: unknown, itemIndex: number) => itemIndex !== index),
)
}
const targetSelect = (
value: string | null | undefined,
onSelectTarget: (value: string) => void,
@ -107,7 +97,7 @@ export function CriteriaTable({
) => (
<select
required={required}
value={value || ""}
value={value || ''}
onChange={(event) => onSelectTarget(event.target.value)}
>
{targetOptions.map((option) => (
@ -116,360 +106,304 @@ export function CriteriaTable({
</option>
))}
</select>
);
const toggleRow = (id: string) => onSelect(id === selectedId ? "" : id);
)
const toggleRow = (id: string) => onSelect(id === selectedId ? '' : id)
return (
<section className="min-w-0 rounded-lg">
<form className="block" onSubmit={onSubmit}>
<div className="overflow-auto rounded-md border border-app-line">
<table className="[&_button]:text-[13px] [&_input]:text-[13px] [&_select]:text-[13px] [&_td]:text-[13px] [&_th]:text-[13px]">
<thead>
<tr>
<th>Id</th>
<th>Tip</th>
<th>Başlık / Kural</th>
<th>Bağlantılar</th>
<th>İşlem</th>
</tr>
</thead>
<tbody>
{criteria.map((item) => {
const isSelected = item.id === selectedId;
const isActive = item.id === activeNodeId;
const connectionSummary = criteriaConnectionSummary(
item,
criteria,
);
<div className="overflow-auto rounded-md border border-gray-200 text-sm">
<div className="min-w-[920px]">
<div className="grid grid-cols-[minmax(110px,0.8fr)_120px_minmax(220px,1.4fr)_minmax(220px,1.2fr)_110px] bg-slate-50 font-semibold text-slate-700">
<div className="px-4 py-3">Id</div>
<div className="px-4 py-3">Tip</div>
<div className="px-4 py-3">Başlık / Kural</div>
<div className="px-4 py-3">Bağlantılar</div>
<div className="px-4 py-3">İşlem</div>
</div>
return (
<React.Fragment key={item.id}>
<tr
className={classNames({
"[&>td]:bg-[#eef5ff]": isSelected,
"[&>td]:bg-[#f0fdf4] [&>td]:shadow-[inset_0_1px_0_#bbf7d0,inset_0_-1px_0_#bbf7d0]":
isActive,
"[&>td]:bg-[#e8f7ff]": isSelected && isActive,
})}
onClick={() => toggleRow(item.id)}
>
<td>
<strong>{item.id}</strong>
{isActive && (
<span className="ml-2 inline-flex rounded-full bg-[#dcfce7] px-2 py-0.5 text-[11px] font-bold text-[#166534]">
Aktif
</span>
{criteria.length === 0 && (
<div className="border-t border-gray-100 px-4 py-6 text-center text-slate-500">
Seçili akışı için ıklama kaydı yok.
</div>
)}
{criteria.map((item) => {
const isSelected = item.id === selectedId
const connectionSummary = criteriaConnectionSummary(item, criteria)
return (
<React.Fragment key={item.id}>
<div
className={classNames(
'grid cursor-pointer grid-cols-[minmax(110px,0.8fr)_120px_minmax(220px,1.4fr)_minmax(220px,1.2fr)_110px] border-t border-gray-100',
{
'bg-blue-50': isSelected,
},
)}
onClick={() => toggleRow(item.id)}
>
<div className="min-w-0 px-4 py-3">
<strong className="break-words">{item.id}</strong>
</div>
<div className="px-4 py-3">
{kindOptions.find((option) => option.value === item.kind)?.label}
</div>
<div className="min-w-0 break-words px-4 py-3">
{criteriaSummaryContent(item)}
</div>
<div className="min-w-0 break-words px-4 py-3">{connectionSummary}</div>
<div className="px-4 py-3">
<button
type="button"
className={classNames(
tableButtonClass,
'ml-1.5 border-gray-300 bg-white text-slate-700',
)}
</td>
<td>
{
kindOptions.find(
(option) => option.value === item.kind,
)?.label
}
</td>
<td>{criteriaSummaryContent(item)}</td>
<td>{connectionSummary}</td>
<td>
<button
type="button"
className="ml-1.5 min-h-8 border-[#cfd6e2] bg-white px-2.5 text-[#344054]"
onClick={(event) => {
event.stopPropagation();
toggleRow(item.id);
}}
onClick={(event) => {
event.stopPropagation()
toggleRow(item.id)
}}
>
{isSelected ? 'Kapat' : 'Düzenle'}
</button>
</div>
</div>
{isSelected && (
<div className="grid gap-3.5 border-t border-gray-100 bg-slate-50 p-3.5">
<div className="grid grid-cols-3 gap-2.5 max-[720px]:grid-cols-1">
<Field label="Tip" required>
<select
value={form.kind}
onChange={(event) => setField('kind', event.target.value)}
>
{kindOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</Field>
<Field label="Başlık" required>
<input
required
value={form.title}
onChange={(event) => setField('title', event.target.value)}
/>
</Field>
<Field
label="Onaylayacak Kişi"
required={form.kind === 'Approval' || form.kind === 'Inform'}
>
{isSelected ? "Kapat" : "Düzenle"}
</button>
</td>
</tr>
{isSelected && (
<tr className="[&>td]:bg-slate-50 [&>td]:p-3.5">
<td colSpan={5}>
<div className="grid grid-cols-3 gap-2.5 max-[720px]:grid-cols-1">
<Field label="Tip" required>
<select
value={form.kind}
onChange={(event) =>
setField("kind", event.target.value)
}
>
{kindOptions.map((option) => (
<option
key={option.value}
value={option.value}
>
{option.label}
</option>
))}
</select>
<input
required={form.kind === 'Approval' || form.kind === 'Inform'}
value={form.approver}
onChange={(event) => setField('approver', event.target.value)}
/>
</Field>
{(form.kind === 'Start' || form.kind === 'Inform') && (
<Field label="Sonraki adım" required>
{targetSelect(
form.nextOnStart,
(value) => setField('nextOnStart', value),
true,
)}
</Field>
)}
{form.kind === 'Approval' && (
<>
<Field label="Onay adımı" required>
{targetSelect(
form.nextOnApprove,
(value) => setField('nextOnApprove', value),
true,
)}
</Field>
<Field label="Başlık" required>
<input
required
value={form.title}
onChange={(event) =>
setField("title", event.target.value)
}
/>
<Field label="Red adımı" required>
{targetSelect(
form.nextOnReject,
(value) => setField('nextOnReject', value),
true,
)}
</Field>
<Field
label="Onaylayacak Kişi"
required={
form.kind === "Approval" ||
form.kind === "Inform"
</>
)}
</div>
{form.kind === 'Compare' && (
<div className="grid gap-2.5 rounded-lg border border-gray-200 bg-slate-50 p-2.5">
<div className="flex items-center justify-between gap-2 text-[13px] font-bold text-slate-700">
<span>Karşılaştırma durumları</span>
<button
type="button"
className={classNames(
tableButtonClass,
'ml-1.5 border-gray-300 bg-white text-slate-700',
)}
disabled={(form.compareOutcomes || []).length >= 4}
onClick={() =>
setField('compareOutcomes', [
...(form.compareOutcomes || []),
emptyCompareOutcome(
`Durum ${(form.compareOutcomes || []).length + 1}`,
),
])
}
>
<input
required={
form.kind === "Approval" ||
form.kind === "Inform"
}
value={form.approver}
onChange={(event) =>
onChange({
...form,
approver: event.target.value,
informPerson: event.target.value,
})
}
/>
</Field>
{false && (
<Field label="Bilgilendirme personeli">
<input
value={form.informPerson}
onChange={(event) =>
onChange({
...form,
approver: event.target.value,
informPerson: event.target.value,
})
}
/>
</Field>
)}
{(form.kind === "Start" ||
form.kind === "Inform") && (
<Field label="Sonraki adım" required>
{targetSelect(
form.nextOnStart,
(value) => setField("nextOnStart", value),
true,
)}
</Field>
)}
{form.kind === "Approval" && (
<>
<Field label="Onay adımı" required>
{targetSelect(
form.nextOnApprove,
(value) => setField("nextOnApprove", value),
true,
)}
</Field>
<Field label="Red adımı" required>
{targetSelect(
form.nextOnReject,
(value) => setField("nextOnReject", value),
true,
)}
</Field>
</>
)}
Ekle
</button>
</div>
{form.kind === "Compare" && (
<div className="grid gap-2.5 rounded-lg border border-app-line bg-slate-50 p-2.5">
<div className="flex items-center justify-between gap-2 text-[13px] font-bold text-[#344054]">
<span>Karşılaştırma durumları</span>
<button
type="button"
className="ml-1.5 min-h-8 border-[#cfd6e2] bg-white px-2.5 text-[#344054]"
disabled={
(form.compareOutcomes || []).length >= 4
}
onClick={() =>
setField("compareOutcomes", [
...(form.compareOutcomes || []),
emptyCompareOutcome(
`Durum ${(form.compareOutcomes || []).length + 1}`,
),
])
}
>
Ekle
</button>
</div>
{(form.compareOutcomes || []).map(
(outcome: CompareOutcomeDto, index: number) => (
<div
key={index}
className="grid gap-2 border-t border-[#e4e7ec] pt-2 first:border-t-0 first:pt-0"
{(form.compareOutcomes || []).map(
(outcome: CompareOutcomeDto, index: number) => (
<div
key={index}
className="grid gap-2 border-t border-gray-200 pt-2 first:border-t-0 first:pt-0"
>
<div className="grid grid-cols-[minmax(130px,0.8fr)_minmax(200px,1.4fr)_auto] items-center gap-2 max-[720px]:grid-cols-1">
<input
required
value={outcome.label}
aria-label="Durum adı zorunlu"
onChange={(event) =>
updateCompareOutcome(index, {
label: event.target.value,
})
}
/>
{targetSelect(
outcome.targetId,
(targetId) =>
updateCompareOutcome(index, {
targetId,
}),
true,
)}
<button
type="button"
className={classNames(
tableButtonClass,
'ml-1.5 border-gray-300 bg-white text-slate-700',
)}
disabled={(form.compareOutcomes || []).length <= 2}
onClick={() => removeCompareOutcome(index)}
>
<div className="grid grid-cols-[minmax(130px,0.8fr)_minmax(200px,1.4fr)_auto] items-center gap-2 max-[720px]:grid-cols-1">
Sil
</button>
</div>
<div className="grid gap-2.5">
{(outcome.conditions || []).map((condition, conditionIndex) => (
<div
key={conditionIndex}
className="grid grid-cols-[minmax(100px,0.7fr)_82px_minmax(110px,0.8fr)_auto] items-center gap-1.5 max-[720px]:grid-cols-1"
>
<select
value={condition.compareColumn}
onChange={(event) =>
updateCompareCondition(index, conditionIndex, {
column: event.target.value,
})
}
>
{columnOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<select
value={condition.compareOperator}
onChange={(event) =>
updateCompareCondition(index, conditionIndex, {
operator: event.target.value,
})
}
>
{operatorOptions.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<input
required
value={outcome.label}
aria-label="Durum adı zorunlu"
type="number"
step="0.01"
value={condition.compareValue}
onChange={(event) =>
updateCompareOutcome(index, {
label: event.target.value,
updateCompareCondition(index, conditionIndex, {
compareValue: event.target.value,
})
}
/>
{targetSelect(
outcome.targetId,
(targetId) =>
updateCompareOutcome(index, {
targetId,
}),
true,
)}
<button
type="button"
className="ml-1.5 min-h-8 border-[#cfd6e2] bg-white px-2.5 text-[#344054]"
disabled={
(form.compareOutcomes || []).length <=
2
}
className={classNames(
tableButtonClass,
'ml-1.5 border-gray-300 bg-white text-slate-700',
)}
disabled={(outcome.conditions || []).length <= 1}
onClick={() =>
removeCompareOutcome(index)
removeCompareCondition(index, conditionIndex)
}
>
Sil
Koşulu sil
</button>
</div>
<div className="grid gap-2.5">
{(outcome.conditions || []).map(
(condition, conditionIndex) => (
<div
key={conditionIndex}
className="grid grid-cols-[minmax(100px,0.7fr)_82px_minmax(110px,0.8fr)_auto] items-center gap-1.5 max-[720px]:grid-cols-1"
>
<select
value={condition.column}
onChange={(event) =>
updateCompareCondition(
index,
conditionIndex,
{
column: event.target.value,
},
)
}
>
{columnOptions.map((option) => (
<option
key={option.value}
value={option.value}
>
{option.label}
</option>
))}
</select>
<select
value={condition.operator}
onChange={(event) =>
updateCompareCondition(
index,
conditionIndex,
{
operator:
event.target.value,
},
)
}
>
{operatorOptions.map((option) => (
<option
key={option.value}
value={option.value}
>
{option.label}
</option>
))}
</select>
<input
required
type="number"
step="0.01"
value={condition.compareValue}
onChange={(event) =>
updateCompareCondition(
index,
conditionIndex,
{
compareValue:
event.target.value,
},
)
}
/>
<button
type="button"
className="ml-1.5 min-h-8 border-[#cfd6e2] bg-white px-2.5 text-[#344054]"
disabled={
(outcome.conditions || [])
.length <= 1
}
onClick={() =>
removeCompareCondition(
index,
conditionIndex,
)
}
>
Koşulu sil
</button>
</div>
),
)}
<button
type="button"
className="ml-1.5 min-h-8 border-[#cfd6e2] bg-white px-2.5 text-[#344054]"
onClick={() =>
addCompareCondition(index)
}
>
Koşul ekle
</button>
</div>
</div>
),
)}
</div>
))}
<button
type="button"
className={classNames(
tableButtonClass,
'ml-1.5 border-gray-300 bg-white text-slate-700',
)}
onClick={() => addCompareCondition(index)}
>
Koşul ekle
</button>
</div>
</div>
),
)}
</div>
)}
<div className="flex flex-wrap gap-2">
<button type="submit" disabled={busy}>
<SaveIcon />
Kaydet
</button>
<button
type="button"
className="border-app-red bg-app-red text-white"
disabled={busy || !form.id}
onClick={() => onDelete(form.id)}
>
<TrashIcon />
Sil
</button>
</div>
</td>
</tr>
)}
</React.Fragment>
);
})}
</tbody>
</table>
<div className="flex flex-wrap gap-2">
<button
type="submit"
className={classNames(
tableButtonClass,
'border-blue-600 bg-blue-600 text-white',
)}
disabled={busy}
>
<SaveIcon />
Kaydet
</button>
<button
type="button"
className={classNames(
tableButtonClass,
'border-red-600 bg-red-600 text-white',
)}
disabled={busy || !form.id}
onClick={() => onDelete(form.id)}
>
<TrashIcon />
Sil
</button>
</div>
</div>
)}
</React.Fragment>
)
})}
</div>
</div>
</form>
</section>
);
)
}
function Field({
@ -477,64 +411,61 @@ function Field({
children,
required = false,
}: {
label: string;
children: React.ReactNode;
required?: boolean;
label: string
children: React.ReactNode
required?: boolean
}) {
return (
<label className="grid gap-1.5 text-[12px] text-[#344054]">
<label className="grid gap-1.5 text-[12px] text-slate-700">
<span>
{label}
{required && <span className="font-bold text-app-red"> *</span>}
{required && <span className="font-bold text-red-600"> *</span>}
</span>
{children}
</label>
);
)
}
function criteriaSummaryContent(item: WorkflowCriteriaDto) {
if (item.kind === "Compare") {
const outcomes = item.compareOutcomes || [];
if (!outcomes.length) return "-";
if (item.kind === 'Compare') {
const outcomes = item.compareOutcomes || []
if (!outcomes.length) return '-'
return (
<ul className="m-0 grid gap-1 pl-[18px] [&_li]:pl-0.5">
<ul className="m-0 grid gap-1">
{outcomes.map((outcome, index: number) => (
<li key={`${outcome.label || "outcome"}-${index}`}>
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{" "}
<li key={`${outcome.label || 'outcome'}-${index}`}>
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{' '}
{compareOutcomeRuleText(outcome)}
</li>
))}
</ul>
);
)
}
return criteriaSummary(item);
return criteriaSummary(item)
}
function criteriaConnectionSummary(
item: WorkflowCriteriaDto,
criteria: WorkflowCriteriaDto[],
) {
if (item.kind === "Compare") {
const outcomes = item.compareOutcomes || [];
if (!outcomes.length) return "-";
function criteriaConnectionSummary(item: WorkflowCriteriaDto, criteria: WorkflowCriteriaDto[]) {
if (item.kind === 'Compare') {
const outcomes = item.compareOutcomes || []
if (!outcomes.length) return '-'
return (
<ul className="m-0 grid gap-1 pl-[18px] [&_li]:pl-0.5">
<ul className="m-0 grid gap-1">
{outcomes.map((outcome, index: number) => (
<li key={`${outcome.label || "target"}-${index}`}>
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{" "}
<li key={`${outcome.label || 'target'}-${index}`}>
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{' '}
{targetTitle(criteria, outcome.targetId)}
</li>
))}
</ul>
);
)
}
if (item.kind === "Approval") {
if (item.kind === 'Approval') {
return (
<ul className="m-0 grid gap-1 pl-[18px] [&_li]:pl-0.5">
<ul className="m-0 grid gap-1">
<li>
<strong>Onay:</strong> {targetTitle(criteria, item.nextOnApprove)}
</li>
@ -542,12 +473,12 @@ function criteriaConnectionSummary(
<strong>Red:</strong> {targetTitle(criteria, item.nextOnReject)}
</li>
</ul>
);
)
}
if (item.kind === "Start" || item.kind === "Inform") {
return targetTitle(criteria, item.nextOnStart);
if (item.kind === 'Start' || item.kind === 'Inform') {
return targetTitle(criteria, item.nextOnStart)
}
return "-";
return '-'
}

View file

@ -1,89 +1,53 @@
import { ApprovalDialog } from "./ApprovalDialog";
import { WorkflowDesigner } from "./WorkflowDesigner";
import { WorkflowTable } from "./WorkflowTable";
import type { FormEvent, RefObject } from "react";
import type {
WorkflowCriteriaDto,
WorkflowItemDto,
} from "@/services/workflow.service";
import { WorkflowDesigner } from './WorkflowDesigner'
import { useEffect, useState, type FormEvent, type RefObject } from 'react'
import type { WorkflowCriteriaDto } from '@/services/workflow.service'
import { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
type DashboardShellProps = {
busy: boolean;
canvasRef: RefObject<HTMLDivElement>;
canvasZoom: number;
criteria: WorkflowCriteriaDto[];
criteriaForm: any;
currentCriteria: WorkflowCriteriaDto[];
designerTab: string;
dialogPendingItems: WorkflowItemDto[];
dragPreview: any;
editingWorkflowId: string | null;
pendingLink: any;
selectedId: string;
selectedWorkflow?: WorkflowItemDto | null;
selectedWorkflowId: string | null;
showApprovalDialog: boolean;
workflowEditForm: any;
workflowForm: any;
workflowItems: WorkflowItemDto[];
onAddCriteria: (kind: string) => void;
onBeginLink: (sourceId: string, outcome: string) => void;
onBeginWorkflowEdit: (item: WorkflowItemDto) => void;
onCancelWorkflowEdit: () => void;
onChangeCriteriaForm: (form: any) => void;
onClearCanvasSelection: () => void;
onCloseApprovalDialog: () => void;
onConnectNodes: (sourceId: string, outcome: string, targetId: string) => void;
onCreateWorkflow: (event: FormEvent<HTMLFormElement>) => void;
onDecision: (id: string, approved: boolean, note: string) => void;
onDeleteSelectedCriteria: (criteriaId?: string) => void;
onDisconnectLink: (sourceId: string, outcome: string) => void;
onDragMove: (event: any) => void;
onFitFlowLayout: () => void;
onOpenCriteriaDetails: (id: string) => void;
onResetDemo: () => void;
onSaveCriteria: (event: FormEvent<HTMLFormElement>) => void;
onSaveWorkflowEdit: (id: string) => void;
onSelectCriteria: (id: string) => void;
onSelectWorkflow: (item: WorkflowItemDto) => void;
onSetDesignerTab: (tab: string) => void;
onStartWorkflow: (id: string) => void;
onUpdateNodePosition: (event: any) => void;
onWorkflowEditFormChange: (form: any) => void;
onWorkflowFormChange: (form: any) => void;
onZoomIn: () => void;
onZoomOut: () => void;
};
busy: boolean
canvasRef: RefObject<HTMLDivElement>
canvasZoom: number
criteriaForm: any
currentCriteria: WorkflowCriteriaDto[]
designerTab: string
dragPreview: any
pendingLink: any
selectedId: string
onAddCriteria: (kind: string) => void
onBeginLink: (sourceId: string, outcome: string) => void
onChangeCriteriaForm: (form: any) => void
onClearCanvasSelection: () => void
onConnectNodes: (sourceId: string, outcome: string, targetId: string) => void
onDeleteSelectedCriteria: (criteriaId?: string) => void
onDisconnectLink: (sourceId: string, outcome: string) => void
onDragMove: (event: any) => void
onFitFlowLayout: () => void
onOpenCriteriaDetails: (id: string) => void
onResetDemo: () => void
onSaveCriteria: (event: FormEvent<HTMLFormElement>) => void
onSelectCriteria: (id: string) => void
onSetDesignerTab: (tab: string) => void
onUpdateNodePosition: (event: any) => void
onZoomIn: () => void
onZoomOut: () => void
}
export function DashboardShell({
busy,
canvasRef,
canvasZoom,
criteria,
criteriaForm,
currentCriteria,
designerTab,
dialogPendingItems,
dragPreview,
editingWorkflowId,
pendingLink,
selectedId,
selectedWorkflow,
selectedWorkflowId,
showApprovalDialog,
workflowEditForm,
workflowForm,
workflowItems,
onAddCriteria,
onBeginLink,
onBeginWorkflowEdit,
onCancelWorkflowEdit,
onChangeCriteriaForm,
onClearCanvasSelection,
onCloseApprovalDialog,
onConnectNodes,
onCreateWorkflow,
onDecision,
onDeleteSelectedCriteria,
onDisconnectLink,
onDragMove,
@ -91,41 +55,34 @@ export function DashboardShell({
onOpenCriteriaDetails,
onResetDemo,
onSaveCriteria,
onSaveWorkflowEdit,
onSelectCriteria,
onSelectWorkflow,
onSetDesignerTab,
onStartWorkflow,
onUpdateNodePosition,
onWorkflowEditFormChange,
onWorkflowFormChange,
onZoomIn,
onZoomOut,
}: DashboardShellProps) {
const [selectCommandColumns, setSelectCommandColumns] = useState<DatabaseColumnDto[]>([])
const [isLoadingColumns, setIsLoadingColumns] = useState(false)
const loadColumns = async (dsCode: string, schema: string, name: string) => {
if (!dsCode || !name) {
setSelectCommandColumns([])
return
}
setIsLoadingColumns(true)
try {
const res = await sqlObjectManagerService.getTableColumns(dsCode, schema, name)
setSelectCommandColumns(res.data ?? [])
} catch {
setSelectCommandColumns([])
} finally {
setIsLoadingColumns(false)
}
}
return (
<div className="min-h-screen">
<main className="grid gap-[18px] p-[18px]">
<section className="grid grid-cols-1 gap-[18px]">
<WorkflowTable
items={workflowItems}
criteria={criteria}
selectedWorkflowId={selectedWorkflowId}
form={workflowForm}
busy={busy}
onFormChange={onWorkflowFormChange}
onSubmit={onCreateWorkflow}
editingId={editingWorkflowId}
editForm={workflowEditForm}
onEditFormChange={onWorkflowEditFormChange}
onEdit={onBeginWorkflowEdit}
onCancelEdit={onCancelWorkflowEdit}
onSaveEdit={onSaveWorkflowEdit}
onSelect={onSelectWorkflow}
onStart={onStartWorkflow}
onResetDemo={onResetDemo}
/>
</section>
<main className="grid">
<WorkflowDesigner
busy={busy}
canvasRef={canvasRef}
@ -136,7 +93,6 @@ export function DashboardShell({
dragPreview={dragPreview}
pendingLink={pendingLink}
selectedCriteriaId={selectedId}
selectedWorkflow={selectedWorkflow}
onAddCriteria={onAddCriteria}
onBeginLink={onBeginLink}
onChangeCriteriaForm={onChangeCriteriaForm}
@ -147,6 +103,7 @@ export function DashboardShell({
onDragMove={onDragMove}
onFitLayout={onFitFlowLayout}
onOpenDetails={onOpenCriteriaDetails}
onResetDemo={onResetDemo}
onSaveCriteria={onSaveCriteria}
onSelectCriteria={onSelectCriteria}
onSetDesignerTab={onSetDesignerTab}
@ -155,16 +112,6 @@ export function DashboardShell({
onZoomOut={onZoomOut}
/>
</main>
{showApprovalDialog && (
<ApprovalDialog
busy={busy}
criteria={criteria}
items={dialogPendingItems}
onClose={onCloseApprovalDialog}
onDecision={onDecision}
/>
)}
</div>
);
)
}

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,11 @@
import { DndContext } from '@dnd-kit/core'
import classNames from 'classnames'
import dayjs from 'dayjs'
import { FiMaximize2, FiZoomIn, FiZoomOut } from 'react-icons/fi'
import { FiMaximize2, FiRefreshCw, FiZoomIn, FiZoomOut } from 'react-icons/fi'
import { kindIcon, kindOptions } from '@/utils/workflow/workflowConstants'
import { CriteriaTable } from './CriteriaTable'
import { FlowCanvas } from './FlowCanvas'
import type { FormEvent, RefObject } from 'react'
import type {
WorkflowCriteriaDto,
WorkflowItemDto,
} from '@/services/workflow.service'
const MaximizeIcon = FiMaximize2 as any
const ZoomInIcon = FiZoomIn as any
const ZoomOutIcon = FiZoomOut as any
import type { WorkflowCriteriaDto } from '@/services/workflow.service'
type WorkflowDesignerProps = {
busy: boolean
@ -25,7 +17,6 @@ type WorkflowDesignerProps = {
dragPreview: any
pendingLink: any
selectedCriteriaId: string
selectedWorkflow?: WorkflowItemDto | null
onAddCriteria: (kind: string) => void
onBeginLink: (sourceId: string, outcome: string) => void
onChangeCriteriaForm: (form: any) => void
@ -36,6 +27,7 @@ type WorkflowDesignerProps = {
onDragMove: (event: any) => void
onFitLayout: () => void
onOpenDetails: (id: string) => void
onResetDemo: () => void
onSaveCriteria: (event: FormEvent<HTMLFormElement>) => void
onSelectCriteria: (id: string) => void
onSetDesignerTab: (tab: string) => void
@ -44,6 +36,14 @@ type WorkflowDesignerProps = {
onZoomOut: () => void
}
const designerButtonClass =
'inline-flex min-h-9 items-center justify-center gap-1.5 rounded-md border px-3 py-1.5 text-sm font-medium leading-none transition-colors disabled:cursor-not-allowed disabled:opacity-50'
const designerIconButtonClass =
'inline-flex min-h-9 w-9 items-center justify-center rounded-md border p-0 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50'
const designerTabClass = 'min-h-8 rounded-md border px-3 py-1.5 transition-colors'
export function WorkflowDesigner({
busy,
canvasRef,
@ -54,7 +54,6 @@ export function WorkflowDesigner({
dragPreview,
pendingLink,
selectedCriteriaId,
selectedWorkflow,
onAddCriteria,
onBeginLink,
onChangeCriteriaForm,
@ -65,6 +64,7 @@ export function WorkflowDesigner({
onDragMove,
onFitLayout,
onOpenDetails,
onResetDemo,
onSaveCriteria,
onSelectCriteria,
onSetDesignerTab,
@ -73,7 +73,7 @@ export function WorkflowDesigner({
onZoomOut,
}: WorkflowDesignerProps) {
return (
<section className="relative min-w-0 rounded-lg border border-app-line bg-app-surface p-4 max-[1080px]:pr-4">
<section className="relative min-w-0 rounded-lg border border-gray-200 bg-white p-4 max-[1080px]:pr-4">
<div className="mb-3.5 flex items-center justify-between gap-4 max-[720px]:flex-col max-[720px]:items-stretch">
<DesignerTabs activeTab={designerTab} onChange={onSetDesignerTab} />
@ -84,6 +84,7 @@ export function WorkflowDesigner({
zoom={canvasZoom}
onAddCriteria={onAddCriteria}
onFitLayout={onFitLayout}
onResetDemo={onResetDemo}
onZoomIn={onZoomIn}
onZoomOut={onZoomOut}
/>
@ -101,7 +102,6 @@ export function WorkflowDesigner({
currentCriteria={currentCriteria}
dragPreview={dragPreview}
zoom={canvasZoom}
activeNodeId={selectedWorkflow?.currentNodeId}
selectedId={selectedCriteriaId}
pendingLink={pendingLink}
canvasRef={canvasRef}
@ -120,20 +120,15 @@ export function WorkflowDesigner({
{designerTab === 'criteria' && (
<CriteriaTable
criteria={currentCriteria}
selectedWorkflow={selectedWorkflow}
selectedId={selectedCriteriaId}
activeNodeId={selectedWorkflow?.currentNodeId}
form={criteriaForm}
busy={busy}
onSelect={onSelectCriteria}
onChange={onChangeCriteriaForm}
onSubmit={onSaveCriteria}
onDelete={onDeleteCriteria}
onAddCriteria={onAddCriteria}
/>
)}
{designerTab === 'history' && <ApprovalHistoryTable selectedWorkflow={selectedWorkflow} />}
</section>
)
}
@ -144,6 +139,7 @@ function DesignerToolbar({
zoom,
onAddCriteria,
onFitLayout,
onResetDemo,
onZoomIn,
onZoomOut,
}: {
@ -152,6 +148,7 @@ function DesignerToolbar({
zoom: number
onAddCriteria: (kind: string) => void
onFitLayout: () => void
onResetDemo: () => void
onZoomIn: () => void
onZoomOut: () => void
}) {
@ -159,31 +156,41 @@ function DesignerToolbar({
<div className="flex flex-wrap justify-end gap-2">
<button
type="button"
className="border-app-primary bg-white text-app-primary"
className={classNames(designerButtonClass, 'border-gray-300 bg-white text-slate-700')}
disabled={busy}
title="Demo akışı yükle"
onClick={onResetDemo}
>
<FiRefreshCw />
Demo
</button>
<button
type="button"
className={classNames(designerButtonClass, 'border-blue-600 bg-white text-blue-600')}
disabled={busy || currentCriteria.length === 0}
title="Düğümleri okunabilir şekilde yerleştir"
onClick={onFitLayout}
>
<MaximizeIcon />
<FiMaximize2 />
Fit
</button>
<button
type="button"
className="w-[38px] justify-center border-app-primary bg-white p-0 text-app-primary"
className={classNames(designerIconButtonClass, 'border-blue-600 bg-white text-blue-600')}
title="Yakınlaştır"
onClick={onZoomIn}
>
<ZoomInIcon />
<FiZoomIn />
</button>
<button
type="button"
className="w-[38px] justify-center border-app-primary bg-white p-0 text-app-primary"
className={classNames(designerIconButtonClass, 'border-blue-600 bg-white text-blue-600')}
title="Uzaklaştır"
onClick={onZoomOut}
>
<ZoomOutIcon />
<FiZoomOut />
</button>
<span className="inline-flex min-w-12 items-center justify-center text-[13px] font-bold text-app-muted">
<span className="inline-flex min-w-12 items-center justify-center text-[13px] font-bold text-slate-500">
{Math.round(zoom * 100)}%
</span>
{kindOptions.map((option) => {
@ -192,7 +199,7 @@ function DesignerToolbar({
<button
key={option.value}
type="button"
className="border-app-primary bg-white text-app-primary"
className={classNames(designerButtonClass, 'border-blue-600 bg-white text-blue-600')}
disabled={busy}
onClick={() => onAddCriteria(option.value)}
>
@ -218,10 +225,10 @@ function DesignerTabs({
type="button"
role="tab"
className={classNames(
'min-h-8 rounded-md border px-3 py-1.5 bg-transparent text-[#475467] transition-colors',
{
'border-[#1d4ed8] bg-[#1d4ed8] text-white shadow-sm': activeTab === 'flow',
},
designerTabClass,
activeTab === 'flow'
? 'border-blue-700 bg-blue-700 text-white shadow-sm'
: 'border-gray-200 bg-white text-slate-600',
)}
onClick={() => onChange('flow')}
>
@ -231,66 +238,15 @@ function DesignerTabs({
type="button"
role="tab"
className={classNames(
'min-h-8 rounded-md border px-3 py-1.5 bg-transparent text-[#475467] transition-colors',
{
'border-[#1d4ed8] bg-[#1d4ed8] text-white shadow-sm': activeTab === 'criteria',
},
designerTabClass,
activeTab === 'criteria'
? 'border-blue-700 bg-blue-700 text-white shadow-sm'
: 'border-gray-200 bg-white text-slate-600',
)}
onClick={() => onChange('criteria')}
>
Adımlar
</button>
<button
type="button"
role="tab"
className={classNames(
'min-h-8 rounded-md border px-3 py-1.5 bg-transparent text-[#475467] transition-colors',
{
'border-[#1d4ed8] bg-[#1d4ed8] text-white shadow-sm': activeTab === 'history',
},
)}
onClick={() => onChange('history')}
>
Akış Geçmişi
</button>
</div>
)
}
function ApprovalHistoryTable({
selectedWorkflow,
}: {
selectedWorkflow?: WorkflowItemDto | null
}) {
const history = selectedWorkflow?.history || []
return (
<section className="min-w-0 rounded-lg">
<div className="overflow-auto rounded-md border border-app-line">
<table>
<thead>
<tr>
<th>Tarih</th>
<th>İşlem</th>
<th>ıklama</th>
</tr>
</thead>
<tbody>
{history.length === 0 && (
<tr>
<td colSpan={3}>Seçili akışı için ıklama kaydı yok.</td>
</tr>
)}
{history.map((item: WorkflowItemDto['history'][number], index: number) => (
<tr key={`${item.time}-${index}`}>
<td>{dayjs(item.time).format('DD MMM YYYY HH:mm')}</td>
<td>{item.action}</td>
<td>{item.note}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
)
}

View file

@ -1,282 +0,0 @@
import { FiCheck, FiEdit2, FiPlay, FiPlus, FiRefreshCw, FiX } from 'react-icons/fi'
import classNames from 'classnames'
import dayjs from 'dayjs'
import { formatMoney, statusClass } from '@/utils/workflow/workflowHelpers'
import type { FormEvent } from 'react'
import type {
WorkflowCriteriaDto,
WorkflowItemDto,
} from '@/services/workflow.service'
const CheckIcon = FiCheck as any
const EditIcon = FiEdit2 as any
const PlayIcon = FiPlay as any
const PlusIcon = FiPlus as any
const RefreshIcon = FiRefreshCw as any
const CloseIcon = FiX as any
type WorkflowTableProps = {
items: WorkflowItemDto[]
criteria: WorkflowCriteriaDto[]
selectedWorkflowId: string | null
form: { sorumlu: string; amount: number | string }
busy: boolean
onFormChange: (form: { sorumlu: string; amount: number | string }) => void
onSubmit: (event: FormEvent<HTMLFormElement>) => void
editingId: string | null
editForm: { sorumlu: string; tarih: string; amount: number | string }
onEditFormChange: (form: { sorumlu: string; tarih: string; amount: number | string }) => void
onEdit: (item: WorkflowItemDto) => void
onCancelEdit: () => void
onSaveEdit: (id: string) => void
onSelect: (item: WorkflowItemDto) => void
onStart: (id: string) => void
onResetDemo: () => void
}
export function WorkflowTable({
items,
criteria,
selectedWorkflowId,
form,
busy,
onFormChange,
onSubmit,
editingId,
editForm,
onEditFormChange,
onEdit,
onCancelEdit,
onSaveEdit,
onSelect,
onStart,
onResetDemo,
}: WorkflowTableProps) {
return (
<section className="min-w-0 rounded-lg border border-app-line bg-app-surface p-4">
<div className="mb-3 flex items-start justify-between gap-4 max-[720px]:flex-col max-[720px]:items-stretch">
<div className="flex items-center gap-3">
<h2 className="m-0 text-lg tracking-normal">WorkflowItems Tablosu</h2>
<span className="rounded-full bg-blue-50 px-2.5 py-1 text-xs font-medium text-blue-700">
{items.length} kayıt
</span>
</div>
<button
type="button"
className="min-h-10 rounded-md border-[#c7d7f4] bg-[#f8fbff] px-3.5 text-sm font-bold text-app-primary shadow-[0_1px_2px_rgba(16,24,40,0.06)] transition hover:-translate-y-px hover:border-app-primary hover:bg-white hover:shadow-[0_8px_18px_rgba(37,99,235,0.14)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-app-primary/35 disabled:hover:translate-y-0 disabled:hover:border-[#c7d7f4] disabled:hover:bg-[#f8fbff] disabled:hover:shadow-[0_1px_2px_rgba(16,24,40,0.06)]"
disabled={busy}
onClick={onResetDemo}
title="Demo verisini yenile"
>
<RefreshIcon className={busy ? 'animate-spin' : undefined} />
<span>Demo Verisini Yenile</span>
</button>
</div>
<form
className="mb-3 grid grid-cols-[minmax(420px,2fr)_minmax(160px,0.6fr)_max-content] items-end gap-2.5 max-[720px]:grid-cols-1"
onSubmit={onSubmit}
>
<label>
Başlık
<input
required
value={form.sorumlu}
placeholder="Örn. Üretim Süreci"
onChange={(event) => onFormChange({ ...form, sorumlu: event.target.value })}
/>
</label>
<label>
Fiyat
<input
required
min="0"
step="0.01"
type="number"
value={form.amount}
onChange={(event) => onFormChange({ ...form, amount: event.target.value })}
/>
</label>
<button type="submit" disabled={busy}>
<PlusIcon />
Yeni Akışı Ekle
</button>
</form>
<div className="overflow-auto rounded-md border border-app-line">
<table>
<thead>
<tr>
<th>Id</th>
<th>Başlık</th>
<th>Tarih</th>
<th>Fiyat</th>
<th>Durum</th>
<th>İşlem</th>
</tr>
</thead>
<tbody>
{items.map((item) => {
const currentStep = criteria.find(
(candidate) =>
candidate.workflowItemId === item.id && candidate.id === item.currentNodeId,
)
const statusTitle = currentStep?.title || item.durum
const canStart = currentStep?.kind === 'Start' || currentStep?.kind === 'Compare'
const isEditing = item.id === editingId
return (
<tr
key={item.id}
className={classNames({
'[&>td]:bg-[#eef5ff]': item.id === selectedWorkflowId,
})}
onClick={() => {
if (!isEditing) onSelect(item)
}}
>
<td>{item.id}</td>
<td>
{isEditing ? (
<input
required
value={editForm.sorumlu}
onClick={(event) => event.stopPropagation()}
onChange={(event) =>
onEditFormChange({
...editForm,
sorumlu: event.target.value,
})
}
/>
) : (
item.sorumlu
)}
</td>
<td>
{isEditing ? (
<input
required
type="date"
value={editForm.tarih}
onClick={(event) => event.stopPropagation()}
onChange={(event) =>
onEditFormChange({
...editForm,
tarih: event.target.value,
})
}
/>
) : (
dayjs(item.tarih).format('DD MMM YYYY')
)}
</td>
<td>
{isEditing ? (
<input
required
min="0"
step="0.01"
type="number"
value={editForm.amount}
onClick={(event) => event.stopPropagation()}
onChange={(event) =>
onEditFormChange({
...editForm,
amount: event.target.value,
})
}
/>
) : (
formatMoney(item.amount)
)}
</td>
<td>
<StatusPill status={statusTitle} />
</td>
<td>
<div className="flex flex-wrap gap-2">
{isEditing ? (
<>
<button
type="button"
className="border-app-green bg-app-green text-white"
disabled={busy || !editForm.sorumlu?.trim() || !editForm.tarih}
onClick={(event) => {
event.stopPropagation()
onSaveEdit(item.id)
}}
>
<CheckIcon />
Kaydet
</button>
<button
type="button"
className="border-app-primary bg-white text-app-primary"
disabled={busy}
onClick={(event) => {
event.stopPropagation()
onCancelEdit()
}}
>
<CloseIcon />
İptal
</button>
</>
) : (
<>
<button
type="button"
className="border-app-primary bg-white text-app-primary"
disabled={busy}
onClick={(event) => {
event.stopPropagation()
onEdit(item)
}}
>
<EditIcon />
Edit
</button>
<button
type="button"
className="border-app-primary bg-white text-app-primary"
disabled={busy || !canStart}
onClick={(event) => {
event.stopPropagation()
onStart(item.id)
}}
>
<PlayIcon />
Başlat
</button>
</>
)}
</div>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</section>
)
}
function StatusPill({ status }: { status: string }) {
return (
<span
className={classNames(
'inline-flex min-h-3 items-center whitespace-nowrap rounded-full bg-[#eef2f7] px-1.5 py-0.5 text-[#344054]',
{
'bg-[#fff4df] text-app-amber': statusClass(status) === 'pending',
'bg-[#e8f5ee] text-app-green': statusClass(status) === 'done',
'bg-[#e7f7f5] text-app-teal': statusClass(status) === 'info',
},
)}
>
{status}
</span>
)
}