diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/CompareOutcomeDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/CompareOutcomeDto.cs new file mode 100644 index 0000000..3d54767 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/CompareOutcomeDto.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Sozsoft.Platform.ListForms.Workflow; + +public class CompareOutcomeDto +{ + public string Label { get; set; } + public string TargetId { get; set; } + public List Conditions { get; set; } = []; +} + diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/CreateUpdateListFormWorkflowCriteriaDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/CreateUpdateListFormWorkflowCriteriaDto.cs new file mode 100644 index 0000000..232dabe --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/CreateUpdateListFormWorkflowCriteriaDto.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace Sozsoft.Platform.ListForms.Workflow; + +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 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 List CompareOutcomes { get; set; } = []; +} + diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/CreateUpdateListFormWorkflowDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/CreateUpdateListFormWorkflowDto.cs new file mode 100644 index 0000000..501f5d2 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/CreateUpdateListFormWorkflowDto.cs @@ -0,0 +1,12 @@ +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; } +} + diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/DecisionWorkflowDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/DecisionWorkflowDto.cs new file mode 100644 index 0000000..1343de3 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/DecisionWorkflowDto.cs @@ -0,0 +1,8 @@ +namespace Sozsoft.Platform.ListForms.Workflow; + +public class DecisionWorkflowDto +{ + public bool Approved { get; set; } + public string Note { get; set; } +} + diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/IListFormWorkflowAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/IListFormWorkflowAppService.cs new file mode 100644 index 0000000..a276668 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/IListFormWorkflowAppService.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace Sozsoft.Platform.ListForms.Workflow; + +public interface IListFormWorkflowAppService : IApplicationService +{ + Task GetStateAsync(string listFormCode = null); + Task CreateWorkflowAsync(CreateUpdateListFormWorkflowDto input); + Task UpdateWorkflowAsync(Guid id, CreateUpdateListFormWorkflowDto input); + Task StartWorkflowAsync(Guid id); + Task DecideWorkflowAsync(Guid id, DecisionWorkflowDto input); + Task SaveCriteriaAsync(CreateUpdateListFormWorkflowCriteriaDto input); + Task DeleteCriteriaAsync(Guid id); + Task ResetDemoAsync(string listFormCode = null); +} + diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/ListFormWorkflowCriteriaDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/ListFormWorkflowCriteriaDto.cs new file mode 100644 index 0000000..db2a10e --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/ListFormWorkflowCriteriaDto.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Application.Dtos; + +namespace Sozsoft.Platform.ListForms.Workflow; + +public class ListFormWorkflowCriteriaDto : AuditedEntityDto +{ + 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 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 List CompareOutcomes { get; set; } = []; +} + diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/ListFormWorkflowDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/ListFormWorkflowDto.cs new file mode 100644 index 0000000..fa45215 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/ListFormWorkflowDto.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Application.Dtos; + +namespace Sozsoft.Platform.ListForms.Workflow; + +public class ListFormWorkflowDto : AuditedEntityDto +{ + 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 History { get; set; } = []; +} + diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/ListFormWorkflowStateDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/ListFormWorkflowStateDto.cs new file mode 100644 index 0000000..9eb3e03 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/ListFormWorkflowStateDto.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Sozsoft.Platform.ListForms.Workflow; + +public class ListFormWorkflowStateDto +{ + public List WorkflowItems { get; set; } = []; + public List Criteria { get; set; } = []; +} + diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/WorkflowConditionDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/WorkflowConditionDto.cs new file mode 100644 index 0000000..9d7e90d --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/WorkflowConditionDto.cs @@ -0,0 +1,9 @@ +namespace Sozsoft.Platform.ListForms.Workflow; + +public class WorkflowConditionDto +{ + public string Column { get; set; } + public string Operator { get; set; } + public decimal CompareValue { get; set; } +} + diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/WorkflowHistoryDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/WorkflowHistoryDto.cs new file mode 100644 index 0000000..b5cf770 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Workflow/WorkflowHistoryDto.cs @@ -0,0 +1,11 @@ +using System; + +namespace Sozsoft.Platform.ListForms.Workflow; + +public class WorkflowHistoryDto +{ + public DateTime Time { get; set; } + public string Action { get; set; } + public string Note { get; set; } +} + diff --git a/api/src/Sozsoft.Platform.Application/ListForms/Workflow/ListFormWorkflowAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/Workflow/ListFormWorkflowAppService.cs new file mode 100644 index 0000000..fe5e7a0 --- /dev/null +++ b/api/src/Sozsoft.Platform.Application/ListForms/Workflow/ListFormWorkflowAppService.cs @@ -0,0 +1,595 @@ +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 workflowRepository; + private readonly IRepository criteriaRepository; + + public ListFormWorkflowAppService( + IRepository workflowRepository, + IRepository criteriaRepository) + { + this.workflowRepository = workflowRepository; + this.criteriaRepository = criteriaRepository; + } + + [HttpGet("state")] + public async Task 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 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 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 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 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 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 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 CreateDefaultStartCriteriaAsync(ListFormWorkflow workflow) + { + return await CreateCriteriaAsync(workflow, "Start", "İş Akışı Başlat", 32, 150); + } + + private async Task 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> 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 criteria, + ListFormWorkflowCriteria current) + { + var visited = new HashSet(); + 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 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 outcomes) + { + return JsonSerializer.Serialize(outcomes ?? []); + } + + private static List DeserializeCompareOutcomes(string json) + { + if (json.IsNullOrWhiteSpace()) + { + return []; + } + + try + { + return JsonSerializer.Deserialize>(json) ?? []; + } + catch + { + return []; + } + } + + private static string SerializeHistory(List history) + { + return JsonSerializer.Serialize(history ?? []); + } + + private static List DeserializeHistory(string json) + { + if (json.IsNullOrWhiteSpace()) + { + return []; + } + + try + { + return JsonSerializer.Deserialize>(json) ?? []; + } + catch + { + return []; + } + } +} diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListFormWorkflow.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListFormWorkflow.cs index 002071e..788fd29 100644 --- a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListFormWorkflow.cs +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListFormWorkflow.cs @@ -6,6 +6,14 @@ namespace Sozsoft.Platform.Entities; public class ListFormWorkflow : FullAuditedEntity { + protected ListFormWorkflow() + { + } + + public ListFormWorkflow(Guid id) : base(id) + { + } + public string ListFormCode { get; set; } public int OrderNo { get; set; } public string Title { get; set; } @@ -17,4 +25,4 @@ public class ListFormWorkflow : FullAuditedEntity public string HistoryJson { get; set; } public ICollection Criteria { get; set; } -} \ No newline at end of file +} diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListFormWorkflowCriteria.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListFormWorkflowCriteria.cs index 044914e..4f2fe4f 100644 --- a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListFormWorkflowCriteria.cs +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/ListForm/ListFormWorkflowCriteria.cs @@ -5,6 +5,14 @@ namespace Sozsoft.Platform.Entities; public class ListFormWorkflowCriteria : FullAuditedEntity { + protected ListFormWorkflowCriteria() + { + } + + public ListFormWorkflowCriteria(Guid id) : base(id) + { + } + public string ListFormCode { get; set; } public Guid WorkflowItemId { get; set; } public ListFormWorkflow WorkflowItem { get; set; } @@ -24,4 +32,4 @@ public class ListFormWorkflowCriteria : FullAuditedEntity public int PositionX { get; set; } public int PositionY { get; set; } public string CompareOutcomesJson { get; set; } -} \ No newline at end of file +} diff --git a/ui/public/version.json b/ui/public/version.json index 519b447..9261f6b 100644 --- a/ui/public/version.json +++ b/ui/public/version.json @@ -93,4 +93,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/ui/src/services/workflow.service.ts b/ui/src/services/workflow.service.ts new file mode 100644 index 0000000..b1c9097 --- /dev/null +++ b/ui/src/services/workflow.service.ts @@ -0,0 +1,152 @@ +import apiService from './api.service' + +export interface WorkflowConditionDto { + column: string + operator: string + compareValue: number +} + +export interface CompareOutcomeDto { + label: string + targetId?: string | null + 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 + compareValue: number + approver: string + informPerson: string + nextOnStart?: string | null + nextOnTrue?: string | null + nextOnFalse?: string | null + nextOnApprove?: string | null + nextOnReject?: string | null + positionX: number + positionY: number + compareOutcomes: CompareOutcomeDto[] +} + +export interface WorkflowStateDto { + workflowItems: WorkflowItemDto[] + criteria: WorkflowCriteriaDto[] +} + +export type CreateUpdateWorkflowInput = Partial & { + listFormCode?: string + sorumlu: string + amount: number + tarih?: string +} + +export type SaveCriteriaInput = Partial & { + workflowItemId: string +} + +const baseUrl = '/api/app/list-form-workflow' + +export const workflowService = { + async getState(listFormCode?: string) { + const response = await apiService.fetchData({ + method: 'GET', + url: `${baseUrl}/state`, + params: { listFormCode }, + }) + + return response.data + }, + + async createWorkflow(payload: CreateUpdateWorkflowInput) { + const response = await apiService.fetchData({ + method: 'POST', + url: `${baseUrl}/workflows`, + data: payload, + }) + + return response.data + }, + + async updateWorkflow(id: string, payload: CreateUpdateWorkflowInput) { + const response = await apiService.fetchData({ + method: 'PUT', + url: `${baseUrl}/workflows/${id}`, + data: payload, + }) + + return response.data + }, + + async startWorkflow(id: string) { + const response = await apiService.fetchData({ + method: 'POST', + url: `${baseUrl}/workflows/${id}/start`, + }) + + return response.data + }, + + async decideWorkflow(id: string, payload: { approved: boolean; note?: string }) { + const response = await apiService.fetchData({ + method: 'POST', + url: `${baseUrl}/workflows/${id}/decision`, + data: payload, + }) + + return response.data + }, + + async saveCriteria(payload: SaveCriteriaInput) { + const response = await apiService.fetchData({ + method: 'POST', + url: `${baseUrl}/criteria`, + data: payload, + }) + + return response.data + }, + + async deleteCriteria(id: string) { + await apiService.fetchData({ + method: 'DELETE', + url: `${baseUrl}/criteria/${id}`, + }) + }, + + async resetDemo(listFormCode?: string) { + const response = await apiService.fetchData({ + method: 'POST', + url: `${baseUrl}/reset-demo`, + params: { listFormCode }, + }) + + return response.data + }, +} + diff --git a/ui/src/utils/workflow/workflowConstants.ts b/ui/src/utils/workflow/workflowConstants.ts new file mode 100644 index 0000000..4f02e3c --- /dev/null +++ b/ui/src/utils/workflow/workflowConstants.ts @@ -0,0 +1,38 @@ +import { FiBell, FiCheck, FiGitBranch, FiPlay, FiSlash } from "react-icons/fi"; + +export const kindOptions = [ + { value: "Start", label: "Başlat" }, + { value: "Compare", label: "Karşılaştırma" }, + { value: "Approval", label: "Onaylanacak kişi" }, + { value: "Inform", label: "Bilgilendirme" }, + { value: "End", label: "Akışı bitir" }, +]; + +export const operatorOptions = [">", ">=", "<", "<=", "=", "!="].map( + (value) => ({ + value, + label: value, + }), +); + +export const columnOptions = ["Tutar", "Id"].map((value) => ({ + value, + label: value, +})); + +export const kindIcon = { + Start: FiPlay as any, + Compare: FiGitBranch as any, + Approval: FiCheck as any, + Inform: FiBell as any, + End: FiSlash as any, +}; + +export const nodeSize = { + width: 176, + height: 128, +}; + +export function getNodeHeight(item: { kind: string }) { + return item?.kind === "Compare" ? 158 : nodeSize.height; +} diff --git a/ui/src/utils/workflow/workflowHelpers.ts b/ui/src/utils/workflow/workflowHelpers.ts new file mode 100644 index 0000000..3dae4e6 --- /dev/null +++ b/ui/src/utils/workflow/workflowHelpers.ts @@ -0,0 +1,475 @@ +import { getNodeHeight, nodeSize } from "./workflowConstants"; + +export function isPendingApproval(item, criteria) { + if (!item) return false; + + return criteria.some( + (candidate) => + candidate.workflowItemId === item.id && + candidate.id === item.currentNodeId && + candidate.kind === "Approval", + ); +} + +export function buildFitLayout(criteria) { + const links = collectLinks(criteria); + const rankById = buildTraversalRanks(criteria, links); + const groups = new Map(); + criteria.forEach((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 maxGroupHeight = Math.max( + 1, + ...[...groups.values()].map( + (items) => + 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(); + + sortedColumns.forEach((column, columnIndex) => { + 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); + + items.forEach((item) => { + positions.set(item.id, { + x: left + columnIndex * (nodeSize.width + xGap), + y: Math.round(y), + }); + y += getNodeHeight(item) + yGap; + }); + }); + + return positions; +} + +function fitColumn(item) { + const priority = { + Start: 0, + Compare: 1, + Approval: 2, + Inform: 3, + End: 4, + }; + + return priority[item.kind] ?? 2; +} + +function compareLayoutNodes(a, b, rankById = new Map()) { + return ( + (rankById.get(a.id) ?? 999) - (rankById.get(b.id) ?? 999) || + a.title.localeCompare(b.title, "tr") + ); +} + +function buildTraversalRanks(criteria, links) { + const rankById = new Map(); + const outgoing = new Map(criteria.map((item) => [item.id, []])); + links.forEach((link) => { + 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); + + while (queue.length) { + const id = queue.shift(); + if (rankById.has(id)) continue; + + 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); + }); + + return rankById; +} + +export function collectLinks(criteria) { + const links = []; + criteria.forEach((source) => { + 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, source.nextOnStart, "Sonraki", "next", { + index: 0, + count: 1, + 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", { + index: 1, + count: 2, + field: "nextOnFalse", + }); + addLink(links, criteria, source, source.nextOnApprove, "Onay", "approve", { + index: 0, + count: 2, + field: "nextOnApprove", + }); + addLink(links, criteria, source, source.nextOnReject, "Red", "reject", { + index: 1, + count: 2, + field: "nextOnReject", + }); + }); + return assignLinkSlots(links, criteria); +} + +export function assignLinkSlots(links, criteria) { + const endpointGroups = new Map(); + const addEndpoint = (nodeId, side, 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", + }); + addEndpoint(link.target.id, sideToward(link.target, link.source), { + link, + role: "target", + }); + }); + + endpointGroups.forEach((endpoints) => { + endpoints.forEach((endpoint, index) => { + 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; + } + }); + }); + + links.forEach((link) => { + link.sourcePort.routeIndex = link.sourcePort.targetSlotIndex ?? 0; + link.sourcePort.routeCount = link.sourcePort.targetSlotCount ?? 1; + }); + + return links; +} + +function sideToward(from, to) { + 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 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) { + if (item.kind === "Compare") { + const outcomes = item.compareOutcomes?.length + ? item.compareOutcomes + : [ + { 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") { + return [ + { field: "nextOnApprove", label: "Onay", targetId: item.nextOnApprove }, + { field: "nextOnReject", label: "Red", targetId: item.nextOnReject }, + ]; + } + + if (item.kind === "End") return []; + + return [ + { field: "nextOnStart", label: "Sonraki", targetId: item.nextOnStart }, + ]; +} + +export function outcomeLabel(field) { + if (field?.startsWith("compareOutcomes:")) return "Karşılaştırma durumu"; + + return { + nextOnStart: "Sonraki", + nextOnTrue: "Doğru", + nextOnFalse: "Yanlış", + nextOnApprove: "Onay", + nextOnReject: "Red", + }[field]; +} + +export function addLink( + links, + criteria, + source, + targetId, + label, + type, + sourcePort = null, +) { + if (!targetId) return; + const target = criteria.find((item) => item.id === targetId); + if (target) { + links.push({ + key: `${source.id}-${target.id}-${type}`, + source, + target, + label, + sourcePort, + }); + } +} + +export function emptyCriteria(kind = "Compare", workflowItemId = null) { + return { + id: "", + workflowItemId, + kind, + title: defaultTitle(kind), + column: "Tutar", + operator: ">", + compareValue: 5000, + approver: "", + informPerson: "", + nextOnStart: "", + nextOnTrue: "", + nextOnFalse: "", + nextOnApprove: "", + nextOnReject: "", + compareOutcomes: + kind === "Compare" + ? [emptyCompareOutcome("Durum 1"), emptyCompareOutcome("Durum 2")] + : [], + positionX: 32, + positionY: 150, + }; +} + +export function toCriteriaForm(item) { + const sharedPerson = item.approver || item.informPerson || ""; + + return { + ...emptyCriteria(item.kind), + ...item, + approver: sharedPerson, + informPerson: sharedPerson, + 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) { + const sharedPerson = item.approver || item.informPerson || ""; + + return { + ...item, + id: item.id || null, + workflowItemId: item.workflowItemId || null, + 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) { + return { + Start: "İş Akışı Başlat", + Compare: "Tutar > 5000 TL", + Approval: "Onaylanacak Kişi", + Inform: "Bilgilendirme Yapılacak Personel", + End: "Akışı Bitir", + }[kind]; +} + +export function emptyCompareOutcome(label = "Durum") { + return { + label, + targetId: "", + conditions: [{ column: "Tutar", operator: ">", compareValue: 5000 }], + }; +} + +export function toCompareOutcomeForm(outcome) { + const conditions = outcome.conditions?.length + ? outcome.conditions + : [ + { + column: outcome.column || "Tutar", + operator: outcome.operator || ">", + compareValue: outcome.compareValue || 0, + }, + ]; + + return { + label: outcome.label || "", + targetId: outcome.targetId || "", + conditions: conditions.map((condition) => ({ + column: condition.column || "Tutar", + operator: condition.operator || ">", + compareValue: condition.compareValue ?? 0, + })), + }; +} + +export function normalizeCompareOutcome(outcome) { + const conditions = ( + outcome.conditions?.length + ? outcome.conditions + : [ + { + column: outcome.column || "Tutar", + operator: outcome.operator || ">", + compareValue: outcome.compareValue || 0, + }, + ] + ) + .filter((condition) => condition.operator && condition.compareValue !== "") + .map((condition) => ({ + column: condition.column || "Tutar", + operator: condition.operator || ">", + compareValue: Number(condition.compareValue || 0), + })); + + return { + label: outcome.label.trim(), + targetId: outcome.targetId || null, + conditions, + }; +} + +export function compareOutcomeRuleText(outcome) { + const conditions = outcome.conditions?.length + ? outcome.conditions + : outcome.operator + ? [ + { + column: outcome.column || "Tutar", + operator: outcome.operator, + compareValue: outcome.compareValue, + }, + ] + : []; + + return conditions.length + ? conditions + .map( + (condition) => + `${condition.column} ${condition.operator} ${formatCompactValue(condition.compareValue)}`, + ) + .join(" ve ") + : "Kural yok"; +} + +export function formatCompactValue(value) { + return new Intl.NumberFormat("tr-TR", { + maximumFractionDigits: 2, + }).format(Number(value || 0)); +} + +export function criteriaSummary(item) { + if (item.kind === "Compare") { + return ( + (item.compareOutcomes || []) + .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; +} + +export function targetTitle(criteria, id) { + if (!id) return "-"; + const item = criteria.find((candidate) => candidate.id === id); + return item ? `${item.id} - ${item.title}` : id; +} + +export function statusClass(status) { + if (status === "Onay Bekliyor") return "pending"; + if (status === "Bitti") return "done"; + if (status === "Bilgilendirildi") return "info"; + return ""; +} + +export function formatMoney(value) { + return new Intl.NumberFormat("tr-TR", { + style: "currency", + currency: "TRY", + }).format(value || 0); +} diff --git a/ui/src/views/admin/listForm/workflow/ApprovalDialog.tsx b/ui/src/views/admin/listForm/workflow/ApprovalDialog.tsx new file mode 100644 index 0000000..42cce66 --- /dev/null +++ b/ui/src/views/admin/listForm/workflow/ApprovalDialog.tsx @@ -0,0 +1,151 @@ +import { formatMoney } from "@/utils/workflow/workflowHelpers"; +import { useState } from "react"; +import { FiCheck, FiSlash, FiX } from "react-icons/fi"; + +const CloseIcon = FiX as any; +const CheckIcon = FiCheck as any; +const SlashIcon = FiSlash as any; + +export function ApprovalDialog({ busy, criteria, items, onClose, onDecision }) { + if (!items.length) return null; + + return ( +
+
+
+
+

+ Bekleyen Onaylar +

+

+ Workflow onay adiminda bekliyor. +

+
+ +
+ +
+
+ ); +} + +function PendingApprovals({ + items, + criteria, + busy, + onDecision, + showChrome = true, +}) { + const [notes, setNotes] = useState({}); + + const content = ( + <> + {showChrome && ( +
+

Bekleyen Onaylar

+ + {items.length} bekleyen + +
+ )} +
+ {items.length === 0 && ( +

Bekleyen onay yok.

+ )} + {items.map((item) => { + const activeStep = criteria.find( + (candidate) => + candidate.workflowItemId === item.id && + candidate.id === item.currentNodeId, + ); + + return ( +
+
+ + #{item.id} {item.sorumlu} + + {activeStep?.title && ( + + {activeStep.title} + + )} + + {formatMoney(item.amount)} - Onaylayacak kişi:{" "} + {item.assignedApprover} + +
+