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 - ListFormFields
- ListFormCustomization (UserUiFilter, GridState, ServerJoin, ServerWhere) - ListFormCustomization (UserUiFilter, GridState, ServerJoin, ServerWhere)
- ListFormImport and ListFormImportLog - ListFormImport and ListFormImportLog
- ListFormWorkflow and ListFormWorkflowCriteria - ListFormWorkflow
- ListFormJsonRow operations - ListFormJsonRow operations
Capabilities: Capabilities:

View file

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

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 StateForm = "state";
public const string SubFormJsonRow = "subForm"; public const string SubFormJsonRow = "subForm";
public const string WidgetForm = "widget"; public const string WidgetForm = "widget";
public const string WorkflowForm = "workflow";
public const string Fields = "fields"; public const string Fields = "fields";
public const string Customization = "customization"; public const string Customization = "customization";
public const string ExtraFilterForm = "extraFilter"; public const string ExtraFilterForm = "extraFilter";

View file

@ -7,15 +7,12 @@ public class CreateUpdateListFormWorkflowCriteriaDto
{ {
public Guid? Id { get; set; } public Guid? Id { get; set; }
public string ListFormCode { get; set; } public string ListFormCode { get; set; }
public Guid WorkflowItemId { get; set; }
public string NodeId { get; set; }
public string Kind { get; set; } public string Kind { get; set; }
public string Title { get; set; } public string Title { get; set; }
public string Column { get; set; } public string CompareColumn { get; set; }
public string Operator { get; set; } public string CompareOperator { get; set; }
public decimal CompareValue { get; set; } public decimal CompareValue { get; set; }
public string Approver { get; set; } public string Approver { get; set; }
public string InformPerson { get; set; }
public string NextOnStart { get; set; } public string NextOnStart { get; set; }
public string NextOnTrue { get; set; } public string NextOnTrue { get; set; }
public string NextOnFalse { 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 public interface IListFormWorkflowAppService : IApplicationService
{ {
Task<ListFormWorkflowStateDto> GetStateAsync(string listFormCode = null); 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<ListFormWorkflowCriteriaDto> SaveCriteriaAsync(CreateUpdateListFormWorkflowCriteriaDto input);
Task DeleteCriteriaAsync(Guid id); Task DeleteCriteriaAsync(Guid id);
Task<ListFormWorkflowStateDto> ResetDemoAsync(string listFormCode = null); Task<ListFormWorkflowStateDto> ResetDemoAsync(string listFormCode = null);

View file

@ -7,15 +7,12 @@ namespace Sozsoft.Platform.ListForms.Workflow;
public class ListFormWorkflowCriteriaDto : AuditedEntityDto<Guid> public class ListFormWorkflowCriteriaDto : AuditedEntityDto<Guid>
{ {
public string ListFormCode { get; set; } public string ListFormCode { get; set; }
public Guid WorkflowItemId { get; set; }
public string NodeId { get; set; }
public string Kind { get; set; } public string Kind { get; set; }
public string Title { get; set; } public string Title { get; set; }
public string Column { get; set; } public string CompareColumn { get; set; }
public string Operator { get; set; } public string CompareOperator { get; set; }
public decimal CompareValue { get; set; } public decimal CompareValue { get; set; }
public string Approver { get; set; } public string Approver { get; set; }
public string InformPerson { get; set; }
public string NextOnStart { get; set; } public string NextOnStart { get; set; }
public string NextOnTrue { get; set; } public string NextOnTrue { get; set; }
public string NextOnFalse { 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 class ListFormWorkflowStateDto
{ {
public List<ListFormWorkflowDto> WorkflowItems { get; set; } = [];
public List<ListFormWorkflowCriteriaDto> Criteria { get; set; } = []; public List<ListFormWorkflowCriteriaDto> Criteria { get; set; } = [];
} }

View file

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

View file

@ -179,6 +179,10 @@ public class ListFormsAppService : CrudAppService<
{ {
item.SubFormsJson = JsonSerializer.Serialize(input.SubFormsDto); item.SubFormsJson = JsonSerializer.Serialize(input.SubFormsDto);
} }
else if (input.EditType == ListFormEditTabs.WorkflowForm)
{
item.WorkflowJson = JsonSerializer.Serialize(input.WorkflowDto);
}
/*Chart*/ /*Chart*/
else if (input.EditType == ListFormEditTabs.ChartCommonForm) 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, ListFormCode = newListFormCode,
SubFormsJson = listForm.SubFormsJson, SubFormsJson = listForm.SubFormsJson,
WidgetsJson = listForm.WidgetsJson,
WorkflowJson = listForm.WorkflowJson,
ListFormType = listForm.ListFormType, ListFormType = listForm.ListFormType,
IsSubForm = listForm.IsSubForm, IsSubForm = listForm.IsSubForm,
ShowNote = listForm.ShowNote, ShowNote = listForm.ShowNote,

View file

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

View file

@ -39,7 +39,6 @@ public static class TableNameResolver
{ nameof(TableNameEnum.ListFormImport), (TablePrefix.PlatformByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.ListFormImport), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.ListFormImportLog), (TablePrefix.PlatformByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.ListFormImportLog), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.ListFormWorkflow), (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.Notification), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.NotificationRule), (TablePrefix.PlatformByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.NotificationRule), (TablePrefix.PlatformByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.NotificationType), (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); 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 LogPrefix = $"{Clock.Now:s}_{Worker.Name}: {{0}}";
var DistributedLockName = Worker.Name; 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> /// <summary>bu listform'un üstünde yer alan widgetların listesidir</summary>
public string WidgetsJson { get; set; } 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; } 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; } public string LayoutJson { get; set; }
/* /*

View file

@ -1,10 +1,9 @@
using System; using System;
using System.Collections.Generic; using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Entities.Auditing;
namespace Sozsoft.Platform.Entities; namespace Sozsoft.Platform.Entities;
public class ListFormWorkflow : FullAuditedEntity<Guid> public class ListFormWorkflow : Entity<Guid>
{ {
protected ListFormWorkflow() protected ListFormWorkflow()
{ {
@ -15,14 +14,18 @@ public class ListFormWorkflow : FullAuditedEntity<Guid>
} }
public string ListFormCode { get; set; } public string ListFormCode { get; set; }
public int OrderNo { get; set; } public string Kind { get; set; }
public string Title { get; set; } public string Title { get; set; }
public string Status { get; set; } public string CompareColumn { get; set; }
public decimal Amount { get; set; } public string CompareOperator { get; set; }
public string CurrentNodeId { get; set; } public decimal CompareValue { get; set; }
public string AssignedApprover { get; set; } public string Approver { get; set; }
public string InformedPerson { get; set; } public string NextOnStart { get; set; }
public string HistoryJson { get; set; } public string NextOnTrue { get; set; }
public string NextOnFalse { get; set; }
public ICollection<ListFormWorkflowCriteria> Criteria { 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<ListFormCustomization> ListFormCustomization { get; set; }
public DbSet<ListFormImport> ListFormImports { get; set; } public DbSet<ListFormImport> ListFormImports { get; set; }
public DbSet<ListFormImportLog> ListFormImportLogs { get; set; } public DbSet<ListFormImportLog> ListFormImportLogs { get; set; }
public DbSet<ListFormWorkflow> ListFormWorkflows { get; set; } public DbSet<ListFormWorkflow> ListFormWorkflow { get; set; }
public DbSet<ListFormWorkflowCriteria> ListFormWorkflowCriteria { get; set; }
public DbSet<BackgroundWorker> BackgroundWorkers { get; set; } public DbSet<BackgroundWorker> BackgroundWorkers { get; set; }
public DbSet<ForumCategory> ForumCategories { get; set; } public DbSet<ForumCategory> ForumCategories { get; set; }
public DbSet<ForumTopic> ForumTopics { 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.FormFieldsDefaultValueJson).HasColumnType("text");
b.Property(a => a.SubFormsJson).HasColumnType("text"); b.Property(a => a.SubFormsJson).HasColumnType("text");
b.Property(a => a.WidgetsJson).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.ExtraFilterJson).HasColumnType("text");
b.Property(a => a.LayoutJson).HasColumnType("text"); b.Property(a => a.LayoutJson).HasColumnType("text");
b.Property(a => a.CommonJson).HasColumnType("text"); b.Property(a => a.CommonJson).HasColumnType("text");
@ -488,36 +488,12 @@ public class PlatformDbContext :
b.ConfigureByConvention(); b.ConfigureByConvention();
b.Property(x => x.ListFormCode).IsRequired().HasMaxLength(64); 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.Kind).IsRequired().HasMaxLength(50);
b.Property(x => x.Title).IsRequired().HasMaxLength(250); b.Property(x => x.Title).IsRequired().HasMaxLength(250);
b.Property(x => x.Column).IsRequired().HasMaxLength(100); b.Property(x => x.CompareColumn).IsRequired().HasMaxLength(100);
b.Property(x => x.Operator).IsRequired().HasMaxLength(20); b.Property(x => x.CompareOperator).IsRequired().HasMaxLength(20);
b.Property(x => x.CompareValue).HasPrecision(18, 2); b.Property(x => x.CompareValue).HasPrecision(18, 2);
b.Property(x => x.Approver).IsRequired().HasMaxLength(250); 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.NextOnStart).IsRequired().HasMaxLength(50);
b.Property(x => x.NextOnTrue).IsRequired().HasMaxLength(50); b.Property(x => x.NextOnTrue).IsRequired().HasMaxLength(50);
b.Property(x => x.NextOnFalse).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.PositionX).IsRequired();
b.Property(x => x.PositionY).IsRequired(); b.Property(x => x.PositionY).IsRequired();
b.Property(x => x.CompareOutcomesJson).HasColumnType("text"); b.Property(x => x.CompareOutcomesJson).HasColumnType("text");
b.HasIndex(x => new
{
x.ListFormCode,
x.WorkflowItemId,
x.NodeId
});
}); });
builder.Entity<Note>(b => builder.Entity<Note>(b =>

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations namespace Sozsoft.Platform.Migrations
{ {
[DbContext(typeof(PlatformDbContext))] [DbContext(typeof(PlatformDbContext))]
[Migration("20260522085648_Initial")] [Migration("20260522200739_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -3053,6 +3053,9 @@ namespace Sozsoft.Platform.Migrations
b.Property<int?>("Width") b.Property<int?>("Width")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("WorkflowJson")
.HasColumnType("text");
b.Property<string>("ZoomAndPanJson") b.Property<string>("ZoomAndPanJson")
.HasColumnType("text"); .HasColumnType("text");
@ -3410,86 +3413,6 @@ namespace Sozsoft.Platform.Migrations
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.ListFormWorkflow", b => 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") b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
@ -3499,11 +3422,16 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(250) .HasMaxLength(250)
.HasColumnType("nvarchar(250)"); .HasColumnType("nvarchar(250)");
b.Property<string>("Column") b.Property<string>("CompareColumn")
.IsRequired() .IsRequired()
.HasMaxLength(100) .HasMaxLength(100)
.HasColumnType("nvarchar(100)"); .HasColumnType("nvarchar(100)");
b.Property<string>("CompareOperator")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("CompareOutcomesJson") b.Property<string>("CompareOutcomesJson")
.HasColumnType("text"); .HasColumnType("text");
@ -3511,46 +3439,11 @@ namespace Sozsoft.Platform.Migrations
.HasPrecision(18, 2) .HasPrecision(18, 2)
.HasColumnType("decimal(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") b.Property<string>("Kind")
.IsRequired() .IsRequired()
.HasMaxLength(50) .HasMaxLength(50)
.HasColumnType("nvarchar(50)"); .HasColumnType("nvarchar(50)");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("ListFormCode") b.Property<string>("ListFormCode")
.IsRequired() .IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
@ -3581,16 +3474,6 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(50) .HasMaxLength(50)
.HasColumnType("nvarchar(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") b.Property<int>("PositionX")
.HasColumnType("int"); .HasColumnType("int");
@ -3602,16 +3485,9 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(250) .HasMaxLength(250)
.HasColumnType("nvarchar(250)"); .HasColumnType("nvarchar(250)");
b.Property<Guid>("WorkflowItemId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("WorkflowItemId"); b.ToTable("Sas_H_ListFormWorkflow", (string)null);
b.HasIndex("ListFormCode", "WorkflowItemId", "NodeId");
b.ToTable("Sas_H_ListFormWorkflowCriteria", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.LogEntry", b => modelBuilder.Entity("Sozsoft.Platform.Entities.LogEntry", b =>
@ -8344,17 +8220,6 @@ namespace Sozsoft.Platform.Migrations
.IsRequired(); .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 => modelBuilder.Entity("Sozsoft.Platform.Entities.OrderItem", b =>
{ {
b.HasOne("Sozsoft.Platform.Entities.Order", "Order") b.HasOne("Sozsoft.Platform.Entities.Order", "Order")
@ -8790,11 +8655,6 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("Events"); b.Navigation("Events");
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.ListFormWorkflow", b =>
{
b.Navigation("Criteria");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Order", b => modelBuilder.Entity("Sozsoft.Platform.Entities.Order", b =>
{ {
b.Navigation("Items"); b.Navigation("Items");

View file

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

View file

@ -3050,6 +3050,9 @@ namespace Sozsoft.Platform.Migrations
b.Property<int?>("Width") b.Property<int?>("Width")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("WorkflowJson")
.HasColumnType("text");
b.Property<string>("ZoomAndPanJson") b.Property<string>("ZoomAndPanJson")
.HasColumnType("text"); .HasColumnType("text");
@ -3407,86 +3410,6 @@ namespace Sozsoft.Platform.Migrations
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.ListFormWorkflow", b => 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") b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier"); .HasColumnType("uniqueidentifier");
@ -3496,11 +3419,16 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(250) .HasMaxLength(250)
.HasColumnType("nvarchar(250)"); .HasColumnType("nvarchar(250)");
b.Property<string>("Column") b.Property<string>("CompareColumn")
.IsRequired() .IsRequired()
.HasMaxLength(100) .HasMaxLength(100)
.HasColumnType("nvarchar(100)"); .HasColumnType("nvarchar(100)");
b.Property<string>("CompareOperator")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("CompareOutcomesJson") b.Property<string>("CompareOutcomesJson")
.HasColumnType("text"); .HasColumnType("text");
@ -3508,46 +3436,11 @@ namespace Sozsoft.Platform.Migrations
.HasPrecision(18, 2) .HasPrecision(18, 2)
.HasColumnType("decimal(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") b.Property<string>("Kind")
.IsRequired() .IsRequired()
.HasMaxLength(50) .HasMaxLength(50)
.HasColumnType("nvarchar(50)"); .HasColumnType("nvarchar(50)");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("ListFormCode") b.Property<string>("ListFormCode")
.IsRequired() .IsRequired()
.HasMaxLength(64) .HasMaxLength(64)
@ -3578,16 +3471,6 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(50) .HasMaxLength(50)
.HasColumnType("nvarchar(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") b.Property<int>("PositionX")
.HasColumnType("int"); .HasColumnType("int");
@ -3599,16 +3482,9 @@ namespace Sozsoft.Platform.Migrations
.HasMaxLength(250) .HasMaxLength(250)
.HasColumnType("nvarchar(250)"); .HasColumnType("nvarchar(250)");
b.Property<Guid>("WorkflowItemId")
.HasColumnType("uniqueidentifier");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("WorkflowItemId"); b.ToTable("Sas_H_ListFormWorkflow", (string)null);
b.HasIndex("ListFormCode", "WorkflowItemId", "NodeId");
b.ToTable("Sas_H_ListFormWorkflowCriteria", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.LogEntry", b => modelBuilder.Entity("Sozsoft.Platform.Entities.LogEntry", b =>
@ -8341,17 +8217,6 @@ namespace Sozsoft.Platform.Migrations
.IsRequired(); .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 => modelBuilder.Entity("Sozsoft.Platform.Entities.OrderItem", b =>
{ {
b.HasOne("Sozsoft.Platform.Entities.Order", "Order") b.HasOne("Sozsoft.Platform.Entities.Order", "Order")
@ -8787,11 +8652,6 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("Events"); b.Navigation("Events");
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.ListFormWorkflow", b =>
{
b.Navigation("Criteria");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Order", b => modelBuilder.Entity("Sozsoft.Platform.Entities.Order", b =>
{ {
b.Navigation("Items"); b.Navigation("Items");

View file

@ -1,8 +1,8 @@
import apiService from './api.service' import apiService from './api.service'
export interface WorkflowConditionDto { export interface WorkflowConditionDto {
column: string compareColumn: string
operator: string compareOperator: string
compareValue: number compareValue: number
} }
@ -12,38 +12,16 @@ export interface CompareOutcomeDto {
conditions: WorkflowConditionDto[] 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 { export interface WorkflowCriteriaDto {
id: string id: string
listFormCode: string listFormCode: string
workflowItemId: string
nodeId: string nodeId: string
kind: string kind: string
title: string title: string
column: string compareColumn: string
operator: string compareOperator: string
compareValue: number compareValue: number
approver: string approver: string
informPerson: string
nextOnStart?: string | null nextOnStart?: string | null
nextOnTrue?: string | null nextOnTrue?: string | null
nextOnFalse?: string | null nextOnFalse?: string | null
@ -55,20 +33,12 @@ export interface WorkflowCriteriaDto {
} }
export interface WorkflowStateDto { export interface WorkflowStateDto {
workflowItems: WorkflowItemDto[]
criteria: WorkflowCriteriaDto[] criteria: WorkflowCriteriaDto[]
} }
export type CreateUpdateWorkflowInput = Partial<WorkflowItemDto> & {
listFormCode?: string
sorumlu: string
amount: number
tarih?: string
}
export type SaveCriteriaInput = Omit<Partial<WorkflowCriteriaDto>, 'id'> & { export type SaveCriteriaInput = Omit<Partial<WorkflowCriteriaDto>, 'id'> & {
id?: string | null id?: string | null
workflowItemId: string listFormCode: string
} }
const baseUrl = '/api/app/list-form-workflow' const baseUrl = '/api/app/list-form-workflow'
@ -84,45 +54,6 @@ export const workflowService = {
return response.data 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) { async saveCriteria(payload: SaveCriteriaInput) {
const response = await apiService.fetchData<WorkflowCriteriaDto>({ const response = await apiService.fetchData<WorkflowCriteriaDto>({
method: 'POST', method: 'POST',

View file

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

View file

@ -1,464 +1,299 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { FormEvent } from "react"; import type { FormEvent } from 'react'
import dayjs from "dayjs";
import "dayjs/locale/tr";
import { import {
buildFitLayout, buildFitLayout,
emptyCriteria, emptyCriteria,
isPendingApproval,
normalizeCriteria, normalizeCriteria,
toCriteriaForm, toCriteriaForm,
type WorkflowCriteriaForm, type WorkflowCriteriaForm,
} from "@/utils/workflow/workflowHelpers"; } from '@/utils/workflow/workflowHelpers'
import { import { workflowService, type WorkflowCriteriaDto } from '@/services/workflow.service'
workflowService, import { DashboardShell } from '../workflow/DashboardShell'
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;
};
type PendingLink = { type PendingLink = {
sourceId: string; sourceId: string
outcome: string; outcome: string
} | null; } | null
type DragPreview = { type DragPreview = {
id: string; id: string
delta: { x: number; y: number }; delta: { x: number; y: number }
} | null; } | null
type DragEndEvent = { type DragEndEvent = {
active: { id: string }; active: { id: string }
delta: { x: number; y: number }; delta: { x: number; y: number }
}; }
export function FormTabWorkflow(props: { listFormCode: string }) { export function FormTabWorkflow({ listFormCode }: { listFormCode: string }) {
const [workflowItems, setWorkflowItems] = useState<WorkflowItemDto[]>([]); const [criteria, setCriteria] = useState<WorkflowCriteriaDto[]>([])
const [criteria, setCriteria] = useState<WorkflowCriteriaDto[]>([]); const [selectedId, setSelectedId] = useState('')
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | null>( const [pendingLink, setPendingLink] = useState<PendingLink>(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,
});
const [criteriaForm, setCriteriaForm] = useState<WorkflowCriteriaForm>( const [criteriaForm, setCriteriaForm] = useState<WorkflowCriteriaForm>(
emptyCriteria(), emptyCriteria('Start', listFormCode),
); )
const [dragPreview, setDragPreview] = useState<DragPreview>(null); const [dragPreview, setDragPreview] = useState<DragPreview>(null)
const [canvasZoom, setCanvasZoom] = useState(1); const [canvasZoom, setCanvasZoom] = useState(1)
const [designerTab, setDesignerTab] = useState("flow"); const [designerTab, setDesignerTab] = useState('flow')
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false)
const [approvalDialogWorkflowId, setApprovalDialogWorkflowId] = const canvasRef = useRef<HTMLDivElement | null>(null)
useState<string | null>(null);
const canvasRef = useRef<HTMLDivElement | null>(null);
const currentCriteria = useMemo( const currentCriteria = useMemo(() => criteria, [criteria])
() => criteria.filter((item) => item.workflowItemId === selectedWorkflowId),
[criteria, selectedWorkflowId],
);
const selectedWorkflow = useMemo(
() => workflowItems.find((item) => item.id === selectedWorkflowId),
[selectedWorkflowId, workflowItems],
);
const selectedCriteria = useMemo( const selectedCriteria = useMemo(
() => currentCriteria.find((item) => item.id === selectedId) ?? null, () => currentCriteria.find((item) => item.id === selectedId) ?? null,
[currentCriteria, selectedId], [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 loadState = useCallback(async () => {
const data = await workflowService.getState(); const data = await workflowService.getState(listFormCode)
setWorkflowItems(data.workflowItems); setCriteria(data.criteria)
setCriteria(data.criteria); return data
setSelectedWorkflowId( }, [listFormCode])
(current) => current || data.workflowItems[0]?.id || null,
);
return data;
}, []);
const runAction = useCallback( const runAction = useCallback(
async (action: () => Promise<unknown>) => { async (action: () => Promise<unknown>) => {
setBusy(true); setBusy(true)
try { try {
await action(); await action()
await loadState(); await loadState()
} finally { } finally {
setBusy(false); setBusy(false)
} }
}, },
[loadState], [loadState],
); )
useEffect(() => { useEffect(() => {
loadState(); loadState()
}, [loadState]); }, [loadState])
useEffect(() => { useEffect(() => {
if (selectedCriteria) { if (selectedCriteria) {
setCriteriaForm(toCriteriaForm(selectedCriteria)); setCriteriaForm(toCriteriaForm(selectedCriteria))
} else if (selectedWorkflowId) { } else {
setCriteriaForm(emptyCriteria("Start", selectedWorkflowId)); setCriteriaForm(emptyCriteria('Start', listFormCode))
} }
}, [selectedCriteria, selectedWorkflowId]); }, [listFormCode, selectedCriteria])
useEffect(() => { useEffect(() => {
if (!selectedWorkflowId || !selectedId) return; if (!selectedId) return
const selectedStillBelongs = currentCriteria.some( const selectedStillExists = currentCriteria.some((item) => item.id === selectedId)
(item) => item.id === selectedId, if (!selectedStillExists) {
); setSelectedId('')
if (!selectedStillBelongs) {
setSelectedId("");
} }
}, [currentCriteria, selectedId, selectedWorkflowId]); }, [currentCriteria, selectedId])
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],
);
const saveCriteria = (event: FormEvent<HTMLFormElement>) => { const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault()
runAction(async () => { runAction(async () => {
await workflowService.saveCriteria(normalizeCriteria(criteriaForm)); await workflowService.saveCriteria({
setSelectedId(""); ...normalizeCriteria(criteriaForm),
}); listFormCode,
}; })
setSelectedId('')
})
}
const addCriteria = (kind: string) => { const addCriteria = (kind: string) => {
if (!selectedWorkflowId) return; setDesignerTab('flow')
setDesignerTab("flow");
runAction(async () => { runAction(async () => {
const saved = await workflowService.saveCriteria({ const saved = await workflowService.saveCriteria({
...normalizeCriteria(emptyCriteria(kind, selectedWorkflowId)), ...normalizeCriteria(emptyCriteria(kind, listFormCode)),
listFormCode,
positionX: 80 + (currentCriteria.length % 5) * 230, positionX: 80 + (currentCriteria.length % 5) * 230,
positionY: 220 + Math.floor(currentCriteria.length / 5) * 140, positionY: 220 + Math.floor(currentCriteria.length / 5) * 140,
}); })
setSelectedId(saved.id); setSelectedId(saved.id)
}); })
}; }
const deleteSelectedCriteria = useCallback( const deleteSelectedCriteria = useCallback(
(criteriaId: string = selectedId) => { (criteriaId: string = selectedId) => {
if (!criteriaId || busy) return; if (!criteriaId || busy) return
runAction(async () => { runAction(async () => {
await workflowService.deleteCriteria(criteriaId); await workflowService.deleteCriteria(criteriaId)
setSelectedId(""); setSelectedId('')
}); })
}, },
[busy, runAction, selectedId], [busy, runAction, selectedId],
); )
const disconnectLink = useCallback( const disconnectLink = useCallback(
(sourceId: string, outcome: string) => { (sourceId: string, outcome: string) => {
if (!sourceId || !outcome || busy) return; if (!sourceId || !outcome || busy) return
const source = currentCriteria.find((item) => item.id === sourceId); const source = currentCriteria.find((item) => item.id === sourceId)
if (!source) return; if (!source) return
const next: WorkflowCriteriaForm = toCriteriaForm(source); const next: WorkflowCriteriaForm = toCriteriaForm(source)
if (outcome.startsWith("compareOutcomes:")) { if (outcome.startsWith('compareOutcomes:')) {
const outcomeIndex = Number(outcome.split(":")[1]); const outcomeIndex = Number(outcome.split(':')[1])
next.compareOutcomes = [...(source.compareOutcomes || [])]; next.compareOutcomes = [...(source.compareOutcomes || [])]
if (next.compareOutcomes?.[outcomeIndex]) { if (next.compareOutcomes?.[outcomeIndex]) {
next.compareOutcomes[outcomeIndex] = { next.compareOutcomes[outcomeIndex] = {
...next.compareOutcomes[outcomeIndex], ...next.compareOutcomes[outcomeIndex],
targetId: null, targetId: null,
}; }
} }
if (outcomeIndex === 0) next.nextOnTrue = null; if (outcomeIndex === 0) next.nextOnTrue = null
if (outcomeIndex === 1) next.nextOnFalse = null; if (outcomeIndex === 1) next.nextOnFalse = null
} else { } else {
(next as Record<string, unknown>)[outcome] = null; ;(next as Record<string, unknown>)[outcome] = null
} }
runAction(async () => { runAction(async () => {
await workflowService.saveCriteria(normalizeCriteria(next)); await workflowService.saveCriteria({ ...normalizeCriteria(next), listFormCode })
setPendingLink(null); setPendingLink(null)
setSelectedId(sourceId); setSelectedId(sourceId)
}); })
}, },
[busy, currentCriteria, runAction], [busy, currentCriteria, listFormCode, runAction],
); )
useEffect(() => { useEffect(() => {
const deleteWithKeyboard = (event: globalThis.KeyboardEvent) => { const deleteWithKeyboard = (event: globalThis.KeyboardEvent) => {
const activeTag = document.activeElement?.tagName?.toLowerCase(); const activeTag = document.activeElement?.tagName?.toLowerCase()
const isEditing = const isEditing =
Boolean(activeTag && ["input", "textarea", "select"].includes(activeTag)) || Boolean(activeTag && ['input', 'textarea', 'select'].includes(activeTag)) ||
(document.activeElement instanceof HTMLElement && (document.activeElement instanceof HTMLElement && document.activeElement.isContentEditable)
document.activeElement.isContentEditable);
if (event.key !== "Delete" || isEditing) return; if (event.key !== 'Delete' || isEditing) return
event.preventDefault(); event.preventDefault()
if (pendingLink) { if (pendingLink) {
disconnectLink(pendingLink.sourceId, pendingLink.outcome); disconnectLink(pendingLink.sourceId, pendingLink.outcome)
return; return
} }
deleteSelectedCriteria(); deleteSelectedCriteria()
}; }
window.addEventListener("keydown", deleteWithKeyboard); window.addEventListener('keydown', deleteWithKeyboard)
return () => window.removeEventListener("keydown", deleteWithKeyboard); return () => window.removeEventListener('keydown', deleteWithKeyboard)
}, [deleteSelectedCriteria, disconnectLink, pendingLink]); }, [deleteSelectedCriteria, disconnectLink, pendingLink])
const updateNodePosition = ({ active, delta }: DragEndEvent) => { const updateNodePosition = ({ active, delta }: DragEndEvent) => {
setDragPreview(null); setDragPreview(null)
const item = currentCriteria.find( const item = currentCriteria.find((candidate) => candidate.id === active.id)
(candidate) => candidate.id === active.id, if (!item) return
);
if (!item) return;
setSelectedId(item.id); setSelectedId(item.id)
if (delta.x === 0 && delta.y === 0) return; if (delta.x === 0 && delta.y === 0) return
const next = { const next = {
...item, ...item,
positionX: Math.max(12, Math.round(item.positionX + delta.x)), positionX: Math.max(12, Math.round(item.positionX + delta.x)),
positionY: Math.max(12, Math.round(item.positionY + delta.y)), positionY: Math.max(12, Math.round(item.positionY + delta.y)),
}; }
runAction(async () => { runAction(async () => {
await workflowService.saveCriteria(next); await workflowService.saveCriteria({ ...normalizeCriteria(next), listFormCode })
setSelectedId(next.id); setSelectedId(next.id)
}); })
}; }
const connectNodes = (sourceId: string, outcome: string, targetId: string) => { const connectNodes = (sourceId: string, outcome: string, targetId: string) => {
const source = currentCriteria.find((item) => item.id === sourceId); const source = currentCriteria.find((item) => item.id === sourceId)
if (!source || source.id === targetId) return; if (!source || source.id === targetId) return
const next: WorkflowCriteriaForm = toCriteriaForm(source); const next: WorkflowCriteriaForm = toCriteriaForm(source)
if (outcome.startsWith("compareOutcomes:")) { if (outcome.startsWith('compareOutcomes:')) {
const outcomeIndex = Number(outcome.split(":")[1]); const outcomeIndex = Number(outcome.split(':')[1])
next.compareOutcomes = [...(source.compareOutcomes || [])]; next.compareOutcomes = [...(source.compareOutcomes || [])]
next.compareOutcomes[outcomeIndex] = { next.compareOutcomes[outcomeIndex] = {
...next.compareOutcomes[outcomeIndex], ...next.compareOutcomes[outcomeIndex],
targetId, targetId,
}; }
if (outcomeIndex === 0) next.nextOnTrue = targetId; if (outcomeIndex === 0) next.nextOnTrue = targetId
if (outcomeIndex === 1) next.nextOnFalse = targetId; if (outcomeIndex === 1) next.nextOnFalse = targetId
} else { } else {
(next as Record<string, unknown>)[outcome] = targetId; ;(next as Record<string, unknown>)[outcome] = targetId
} }
setPendingLink(null); setPendingLink(null)
runAction(async () => { runAction(async () => {
await workflowService.saveCriteria(normalizeCriteria(next)); await workflowService.saveCriteria({ ...normalizeCriteria(next), listFormCode })
setSelectedId(""); setSelectedId('')
}); })
}; }
const fitFlowLayout = () => { const fitFlowLayout = () => {
if (!currentCriteria.length || busy) return; if (!currentCriteria.length || busy) return
const nextPositions = buildFitLayout(currentCriteria); const nextPositions = buildFitLayout(currentCriteria)
setDesignerTab("flow"); setDesignerTab('flow')
setCanvasZoom(1); setCanvasZoom(1)
runAction(async () => { runAction(async () => {
for (const item of currentCriteria) { for (const item of currentCriteria) {
const position = nextPositions.get(item.id); const position = nextPositions.get(item.id)
if (!position) continue; if (!position) continue
await workflowService.saveCriteria({ await workflowService.saveCriteria({
...normalizeCriteria(item), ...normalizeCriteria(item),
listFormCode,
positionX: position.x, positionX: position.x,
positionY: position.y, positionY: position.y,
}); })
} }
requestAnimationFrame(() => { requestAnimationFrame(() => {
canvasRef.current?.scrollTo({ left: 0, top: 0, behavior: "smooth" }); canvasRef.current?.scrollTo({ left: 0, top: 0, behavior: 'smooth' })
}); })
}); })
}; }
const selectWorkflow = (item: WorkflowItemDto) => {
setSelectedWorkflowId(item.id);
setPendingLink(null);
setSelectedId("");
};
const openCriteriaDetails = (id: string) => { const openCriteriaDetails = (id: string) => {
setSelectedId(id); setSelectedId(id)
setPendingLink(null); setPendingLink(null)
setDesignerTab("criteria"); setDesignerTab('criteria')
}; }
const clearCanvasSelection = () => { const clearCanvasSelection = () => {
setPendingLink(null); setPendingLink(null)
setSelectedId(""); setSelectedId('')
}; }
const beginLink = (sourceId: string, outcome: string) => { const beginLink = (sourceId: string, outcome: string) => {
setPendingLink({ sourceId, outcome }); setPendingLink({ sourceId, outcome })
setSelectedId(sourceId); setSelectedId(sourceId)
}; }
return ( return (
<DashboardShell <DashboardShell
busy={busy} busy={busy}
canvasRef={canvasRef} canvasRef={canvasRef}
canvasZoom={canvasZoom} canvasZoom={canvasZoom}
criteria={criteria}
criteriaForm={criteriaForm} criteriaForm={criteriaForm}
currentCriteria={currentCriteria} currentCriteria={currentCriteria}
designerTab={designerTab} designerTab={designerTab}
dialogPendingItems={dialogPendingItems}
dragPreview={dragPreview} dragPreview={dragPreview}
editingWorkflowId={editingWorkflowId}
pendingLink={pendingLink} pendingLink={pendingLink}
selectedId={selectedId} selectedId={selectedId}
selectedWorkflow={selectedWorkflow}
selectedWorkflowId={selectedWorkflowId}
showApprovalDialog={Boolean(approvalDialogWorkflowId)}
workflowEditForm={workflowEditForm}
workflowForm={workflowForm}
workflowItems={workflowItems}
onAddCriteria={addCriteria} onAddCriteria={addCriteria}
onBeginLink={beginLink} onBeginLink={beginLink}
onBeginWorkflowEdit={beginWorkflowEdit}
onCancelWorkflowEdit={cancelWorkflowEdit}
onChangeCriteriaForm={setCriteriaForm} onChangeCriteriaForm={setCriteriaForm}
onClearCanvasSelection={clearCanvasSelection} onClearCanvasSelection={clearCanvasSelection}
onCloseApprovalDialog={() => setApprovalDialogWorkflowId(null)}
onConnectNodes={connectNodes} onConnectNodes={connectNodes}
onCreateWorkflow={createWorkflow}
onDecision={(id: string, approved: boolean, note: string) =>
runAction(() => workflowService.decideWorkflow(id, { approved, note }))
}
onDeleteSelectedCriteria={deleteSelectedCriteria} onDeleteSelectedCriteria={deleteSelectedCriteria}
onDisconnectLink={disconnectLink} onDisconnectLink={disconnectLink}
onDragMove={(event: DragEndEvent | null) => onDragMove={(event: DragEndEvent | null) =>
setDragPreview( setDragPreview(event ? { id: event.active.id, delta: event.delta } : null)
event ? { id: event.active.id, delta: event.delta } : null,
)
} }
onFitFlowLayout={fitFlowLayout} onFitFlowLayout={fitFlowLayout}
onOpenCriteriaDetails={openCriteriaDetails} onOpenCriteriaDetails={openCriteriaDetails}
onResetDemo={() => runAction(workflowService.resetDemo)} onResetDemo={() => runAction(() => workflowService.resetDemo(listFormCode))}
onSaveCriteria={saveCriteria} onSaveCriteria={saveCriteria}
onSaveWorkflowEdit={saveWorkflowEdit}
onSelectCriteria={setSelectedId} onSelectCriteria={setSelectedId}
onSelectWorkflow={selectWorkflow}
onSetDesignerTab={setDesignerTab} onSetDesignerTab={setDesignerTab}
onStartWorkflow={startWorkflow}
onUpdateNodePosition={updateNodePosition} onUpdateNodePosition={updateNodePosition}
onWorkflowEditFormChange={setWorkflowEditForm} onZoomIn={() => setCanvasZoom((current) => Math.min(1.5, Number((current + 0.1).toFixed(2))))}
onWorkflowFormChange={setWorkflowForm}
onZoomIn={() =>
setCanvasZoom((current) =>
Math.min(1.5, Number((current + 0.1).toFixed(2))),
)
}
onZoomOut={() => onZoomOut={() =>
setCanvasZoom((current) => setCanvasZoom((current) => Math.max(0.6, Number((current - 0.1).toFixed(2))))
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 React from 'react'
import { FiSave, FiTrash2 } from "react-icons/fi"; import { FiSave, FiTrash2 } from 'react-icons/fi'
import classNames from "classnames"; import classNames from 'classnames'
import { import {
columnOptions, columnOptions,
kindIcon, kindIcon,
kindOptions, kindOptions,
operatorOptions, operatorOptions,
} from "@/utils/workflow/workflowConstants"; } from '@/utils/workflow/workflowConstants'
import { import {
compareOutcomeRuleText, compareOutcomeRuleText,
criteriaSummary, criteriaSummary,
emptyCompareOutcome, emptyCompareOutcome,
targetTitle, targetTitle,
} from "@/utils/workflow/workflowHelpers"; } from '@/utils/workflow/workflowHelpers'
import type { import type { CompareOutcomeDto, WorkflowCriteriaDto } from '@/services/workflow.service'
CompareOutcomeDto,
WorkflowCriteriaDto,
WorkflowItemDto,
} from "@/services/workflow.service";
const SaveIcon = FiSave as any; const SaveIcon = FiSave as any
const TrashIcon = FiTrash2 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 = { type CriteriaTableProps = {
criteria: WorkflowCriteriaDto[]; criteria: WorkflowCriteriaDto[]
selectedWorkflow?: WorkflowItemDto | null; selectedId: string
selectedId: string; form: any
activeNodeId?: string; busy: boolean
form: any; onSelect: (id: string) => void
busy: boolean; onChange: (form: any) => void
onSelect: (id: string) => void; onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
onChange: (form: any) => void; onDelete: (id: string) => void
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void; }
onDelete: (id: string) => void;
onAddCriteria?: (kind: string) => void;
};
export function CriteriaTable({ export function CriteriaTable({
criteria, criteria,
selectedWorkflow,
selectedId, selectedId,
activeNodeId,
form, form,
busy, busy,
onSelect, onSelect,
onChange, onChange,
onSubmit, onSubmit,
onDelete, onDelete,
onAddCriteria,
}: CriteriaTableProps) { }: CriteriaTableProps) {
const setField = (name: string, value: unknown) => const setField = (name: string, value: unknown) => onChange({ ...form, [name]: value })
onChange({ ...form, [name]: value });
const targetOptions = [ const targetOptions = [
{ value: "", label: "Bağlantı yok" }, { value: '', label: 'Bağlantı yok' },
...criteria ...criteria
.filter((item) => item.id !== form.id) .filter((item) => item.id !== form.id)
.map((item) => ({ value: item.id, label: `${item.id} - ${item.title}` })), .map((item) => ({ value: item.id, label: `${item.id} - ${item.title}` })),
]; ]
const updateCompareOutcome = (index: number, patch: Partial<CompareOutcomeDto>) => { const updateCompareOutcome = (index: number, patch: Partial<CompareOutcomeDto>) => {
const next = [...(form.compareOutcomes || [])]; const next = [...(form.compareOutcomes || [])]
next[index] = { ...next[index], ...patch }; next[index] = { ...next[index], ...patch }
setField("compareOutcomes", next); setField('compareOutcomes', next)
}; }
const updateCompareCondition = ( const updateCompareCondition = (
outcomeIndex: number, outcomeIndex: number,
conditionIndex: number, conditionIndex: number,
patch: Record<string, unknown>, patch: Record<string, unknown>,
) => { ) => {
const next = [...(form.compareOutcomes || [])]; const next = [...(form.compareOutcomes || [])]
const conditions = [...(next[outcomeIndex]?.conditions || [])]; const conditions = [...(next[outcomeIndex]?.conditions || [])]
conditions[conditionIndex] = { ...conditions[conditionIndex], ...patch }; conditions[conditionIndex] = { ...conditions[conditionIndex], ...patch }
next[outcomeIndex] = { ...next[outcomeIndex], conditions }; next[outcomeIndex] = { ...next[outcomeIndex], conditions }
setField("compareOutcomes", next); setField('compareOutcomes', next)
}; }
const addCompareCondition = (outcomeIndex: number) => { const addCompareCondition = (outcomeIndex: number) => {
const next = [...(form.compareOutcomes || [])]; const next = [...(form.compareOutcomes || [])]
next[outcomeIndex] = { next[outcomeIndex] = {
...next[outcomeIndex], ...next[outcomeIndex],
conditions: [ conditions: [
...(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 removeCompareCondition = (outcomeIndex: number, conditionIndex: number) => {
const next = [...(form.compareOutcomes || [])]; const next = [...(form.compareOutcomes || [])]
const conditions = (next[outcomeIndex]?.conditions || []).filter( const conditions = (next[outcomeIndex]?.conditions || []).filter(
(_: unknown, index: number) => index !== conditionIndex, (_: unknown, index: number) => index !== conditionIndex,
); )
next[outcomeIndex] = { ...next[outcomeIndex], conditions }; next[outcomeIndex] = { ...next[outcomeIndex], conditions }
setField("compareOutcomes", next); setField('compareOutcomes', next)
}; }
const removeCompareOutcome = (index: number) => { const removeCompareOutcome = (index: number) => {
setField( setField(
"compareOutcomes", 'compareOutcomes',
(form.compareOutcomes || []).filter( (form.compareOutcomes || []).filter((_: unknown, itemIndex: number) => itemIndex !== index),
(_: unknown, itemIndex: number) => itemIndex !== index, )
), }
);
};
const targetSelect = ( const targetSelect = (
value: string | null | undefined, value: string | null | undefined,
onSelectTarget: (value: string) => void, onSelectTarget: (value: string) => void,
@ -107,7 +97,7 @@ export function CriteriaTable({
) => ( ) => (
<select <select
required={required} required={required}
value={value || ""} value={value || ''}
onChange={(event) => onSelectTarget(event.target.value)} onChange={(event) => onSelectTarget(event.target.value)}
> >
{targetOptions.map((option) => ( {targetOptions.map((option) => (
@ -116,360 +106,304 @@ export function CriteriaTable({
</option> </option>
))} ))}
</select> </select>
); )
const toggleRow = (id: string) => onSelect(id === selectedId ? "" : id); const toggleRow = (id: string) => onSelect(id === selectedId ? '' : id)
return ( return (
<section className="min-w-0 rounded-lg"> <section className="min-w-0 rounded-lg">
<form className="block" onSubmit={onSubmit}> <form className="block" onSubmit={onSubmit}>
<div className="overflow-auto rounded-md border border-app-line"> <div className="overflow-auto rounded-md border border-gray-200 text-sm">
<table className="[&_button]:text-[13px] [&_input]:text-[13px] [&_select]:text-[13px] [&_td]:text-[13px] [&_th]:text-[13px]"> <div className="min-w-[920px]">
<thead> <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">
<tr> <div className="px-4 py-3">Id</div>
<th>Id</th> <div className="px-4 py-3">Tip</div>
<th>Tip</th> <div className="px-4 py-3">Başlık / Kural</div>
<th>Başlık / Kural</th> <div className="px-4 py-3">Bağlantılar</div>
<th>Bağlantılar</th> <div className="px-4 py-3">İşlem</div>
<th>İşlem</th> </div>
</tr>
</thead>
<tbody>
{criteria.map((item) => {
const isSelected = item.id === selectedId;
const isActive = item.id === activeNodeId;
const connectionSummary = criteriaConnectionSummary(
item,
criteria,
);
return ( {criteria.length === 0 && (
<React.Fragment key={item.id}> <div className="border-t border-gray-100 px-4 py-6 text-center text-slate-500">
<tr Seçili akışı için ıklama kaydı yok.
className={classNames({ </div>
"[&>td]:bg-[#eef5ff]": isSelected, )}
"[&>td]:bg-[#f0fdf4] [&>td]:shadow-[inset_0_1px_0_#bbf7d0,inset_0_-1px_0_#bbf7d0]":
isActive, {criteria.map((item) => {
"[&>td]:bg-[#e8f7ff]": isSelected && isActive, const isSelected = item.id === selectedId
})} const connectionSummary = criteriaConnectionSummary(item, criteria)
onClick={() => toggleRow(item.id)}
> return (
<td> <React.Fragment key={item.id}>
<strong>{item.id}</strong> <div
{isActive && ( className={classNames(
<span className="ml-2 inline-flex rounded-full bg-[#dcfce7] px-2 py-0.5 text-[11px] font-bold text-[#166534]"> 'grid cursor-pointer grid-cols-[minmax(110px,0.8fr)_120px_minmax(220px,1.4fr)_minmax(220px,1.2fr)_110px] border-t border-gray-100',
Aktif {
</span> '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> onClick={(event) => {
<td> event.stopPropagation()
{ toggleRow(item.id)
kindOptions.find( }}
(option) => option.value === item.kind, >
)?.label {isSelected ? 'Kapat' : 'Düzenle'}
} </button>
</td> </div>
<td>{criteriaSummaryContent(item)}</td> </div>
<td>{connectionSummary}</td> {isSelected && (
<td> <div className="grid gap-3.5 border-t border-gray-100 bg-slate-50 p-3.5">
<button <div className="grid grid-cols-3 gap-2.5 max-[720px]:grid-cols-1">
type="button" <Field label="Tip" required>
className="ml-1.5 min-h-8 border-[#cfd6e2] bg-white px-2.5 text-[#344054]" <select
onClick={(event) => { value={form.kind}
event.stopPropagation(); onChange={(event) => setField('kind', event.target.value)}
toggleRow(item.id); >
}} {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"} <input
</button> required={form.kind === 'Approval' || form.kind === 'Inform'}
</td> value={form.approver}
</tr> onChange={(event) => setField('approver', event.target.value)}
{isSelected && ( />
<tr className="[&>td]:bg-slate-50 [&>td]:p-3.5"> </Field>
<td colSpan={5}>
<div className="grid grid-cols-3 gap-2.5 max-[720px]:grid-cols-1"> {(form.kind === 'Start' || form.kind === 'Inform') && (
<Field label="Tip" required> <Field label="Sonraki adım" required>
<select {targetSelect(
value={form.kind} form.nextOnStart,
onChange={(event) => (value) => setField('nextOnStart', value),
setField("kind", event.target.value) true,
} )}
> </Field>
{kindOptions.map((option) => ( )}
<option
key={option.value} {form.kind === 'Approval' && (
value={option.value} <>
> <Field label="Onay adımı" required>
{option.label} {targetSelect(
</option> form.nextOnApprove,
))} (value) => setField('nextOnApprove', value),
</select> true,
)}
</Field> </Field>
<Field label="Başlık" required> <Field label="Red adımı" required>
<input {targetSelect(
required form.nextOnReject,
value={form.title} (value) => setField('nextOnReject', value),
onChange={(event) => true,
setField("title", event.target.value) )}
}
/>
</Field> </Field>
<Field </>
label="Onaylayacak Kişi" )}
required={ </div>
form.kind === "Approval" ||
form.kind === "Inform" {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 Ekle
required={ </button>
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>
</>
)}
</div> </div>
{(form.compareOutcomes || []).map(
{form.kind === "Compare" && ( (outcome: CompareOutcomeDto, index: number) => (
<div className="grid gap-2.5 rounded-lg border border-app-line bg-slate-50 p-2.5"> <div
<div className="flex items-center justify-between gap-2 text-[13px] font-bold text-[#344054]"> key={index}
<span>Karşılaştırma durumları</span> className="grid gap-2 border-t border-gray-200 pt-2 first:border-t-0 first:pt-0"
<button >
type="button" <div className="grid grid-cols-[minmax(130px,0.8fr)_minmax(200px,1.4fr)_auto] items-center gap-2 max-[720px]:grid-cols-1">
className="ml-1.5 min-h-8 border-[#cfd6e2] bg-white px-2.5 text-[#344054]" <input
disabled={ required
(form.compareOutcomes || []).length >= 4 value={outcome.label}
} aria-label="Durum adı zorunlu"
onClick={() => onChange={(event) =>
setField("compareOutcomes", [ updateCompareOutcome(index, {
...(form.compareOutcomes || []), label: event.target.value,
emptyCompareOutcome( })
`Durum ${(form.compareOutcomes || []).length + 1}`, }
), />
]) {targetSelect(
} outcome.targetId,
> (targetId) =>
Ekle updateCompareOutcome(index, {
</button> targetId,
</div> }),
{(form.compareOutcomes || []).map( true,
(outcome: CompareOutcomeDto, index: number) => ( )}
<div <button
key={index} type="button"
className="grid gap-2 border-t border-[#e4e7ec] pt-2 first:border-t-0 first:pt-0" 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 <input
required required
value={outcome.label} type="number"
aria-label="Durum adı zorunlu" step="0.01"
value={condition.compareValue}
onChange={(event) => onChange={(event) =>
updateCompareOutcome(index, { updateCompareCondition(index, conditionIndex, {
label: event.target.value, compareValue: event.target.value,
}) })
} }
/> />
{targetSelect(
outcome.targetId,
(targetId) =>
updateCompareOutcome(index, {
targetId,
}),
true,
)}
<button <button
type="button" type="button"
className="ml-1.5 min-h-8 border-[#cfd6e2] bg-white px-2.5 text-[#344054]" className={classNames(
disabled={ tableButtonClass,
(form.compareOutcomes || []).length <= 'ml-1.5 border-gray-300 bg-white text-slate-700',
2 )}
} disabled={(outcome.conditions || []).length <= 1}
onClick={() => onClick={() =>
removeCompareOutcome(index) removeCompareCondition(index, conditionIndex)
} }
> >
Sil Koşulu sil
</button> </button>
</div> </div>
<div className="grid gap-2.5"> ))}
{(outcome.conditions || []).map( <button
(condition, conditionIndex) => ( type="button"
<div className={classNames(
key={conditionIndex} tableButtonClass,
className="grid grid-cols-[minmax(100px,0.7fr)_82px_minmax(110px,0.8fr)_auto] items-center gap-1.5 max-[720px]:grid-cols-1" 'ml-1.5 border-gray-300 bg-white text-slate-700',
> )}
<select onClick={() => addCompareCondition(index)}
value={condition.column} >
onChange={(event) => Koşul ekle
updateCompareCondition( </button>
index, </div>
conditionIndex, </div>
{ ),
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>
)} )}
</div>
)}
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<button type="submit" disabled={busy}> <button
<SaveIcon /> type="submit"
Kaydet className={classNames(
</button> tableButtonClass,
<button 'border-blue-600 bg-blue-600 text-white',
type="button" )}
className="border-app-red bg-app-red text-white" disabled={busy}
disabled={busy || !form.id} >
onClick={() => onDelete(form.id)} <SaveIcon />
> Kaydet
<TrashIcon /> </button>
Sil <button
</button> type="button"
</div> className={classNames(
</td> tableButtonClass,
</tr> 'border-red-600 bg-red-600 text-white',
)} )}
</React.Fragment> disabled={busy || !form.id}
); onClick={() => onDelete(form.id)}
})} >
</tbody> <TrashIcon />
</table> Sil
</button>
</div>
</div>
)}
</React.Fragment>
)
})}
</div>
</div> </div>
</form> </form>
</section> </section>
); )
} }
function Field({ function Field({
@ -477,64 +411,61 @@ function Field({
children, children,
required = false, required = false,
}: { }: {
label: string; label: string
children: React.ReactNode; children: React.ReactNode
required?: boolean; required?: boolean
}) { }) {
return ( return (
<label className="grid gap-1.5 text-[12px] text-[#344054]"> <label className="grid gap-1.5 text-[12px] text-slate-700">
<span> <span>
{label} {label}
{required && <span className="font-bold text-app-red"> *</span>} {required && <span className="font-bold text-red-600"> *</span>}
</span> </span>
{children} {children}
</label> </label>
); )
} }
function criteriaSummaryContent(item: WorkflowCriteriaDto) { function criteriaSummaryContent(item: WorkflowCriteriaDto) {
if (item.kind === "Compare") { if (item.kind === 'Compare') {
const outcomes = item.compareOutcomes || []; const outcomes = item.compareOutcomes || []
if (!outcomes.length) return "-"; if (!outcomes.length) return '-'
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) => ( {outcomes.map((outcome, index: number) => (
<li key={`${outcome.label || "outcome"}-${index}`}> <li key={`${outcome.label || 'outcome'}-${index}`}>
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{" "} <strong>{outcome.label || `Durum ${index + 1}`}:</strong>{' '}
{compareOutcomeRuleText(outcome)} {compareOutcomeRuleText(outcome)}
</li> </li>
))} ))}
</ul> </ul>
); )
} }
return criteriaSummary(item); return criteriaSummary(item)
} }
function criteriaConnectionSummary( function criteriaConnectionSummary(item: WorkflowCriteriaDto, criteria: WorkflowCriteriaDto[]) {
item: WorkflowCriteriaDto, if (item.kind === 'Compare') {
criteria: WorkflowCriteriaDto[], const outcomes = item.compareOutcomes || []
) { if (!outcomes.length) return '-'
if (item.kind === "Compare") {
const outcomes = item.compareOutcomes || [];
if (!outcomes.length) return "-";
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) => ( {outcomes.map((outcome, index: number) => (
<li key={`${outcome.label || "target"}-${index}`}> <li key={`${outcome.label || 'target'}-${index}`}>
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{" "} <strong>{outcome.label || `Durum ${index + 1}`}:</strong>{' '}
{targetTitle(criteria, outcome.targetId)} {targetTitle(criteria, outcome.targetId)}
</li> </li>
))} ))}
</ul> </ul>
); )
} }
if (item.kind === "Approval") { if (item.kind === 'Approval') {
return ( return (
<ul className="m-0 grid gap-1 pl-[18px] [&_li]:pl-0.5"> <ul className="m-0 grid gap-1">
<li> <li>
<strong>Onay:</strong> {targetTitle(criteria, item.nextOnApprove)} <strong>Onay:</strong> {targetTitle(criteria, item.nextOnApprove)}
</li> </li>
@ -542,12 +473,12 @@ function criteriaConnectionSummary(
<strong>Red:</strong> {targetTitle(criteria, item.nextOnReject)} <strong>Red:</strong> {targetTitle(criteria, item.nextOnReject)}
</li> </li>
</ul> </ul>
); )
} }
if (item.kind === "Start" || item.kind === "Inform") { if (item.kind === 'Start' || item.kind === 'Inform') {
return targetTitle(criteria, item.nextOnStart); return targetTitle(criteria, item.nextOnStart)
} }
return "-"; return '-'
} }

View file

@ -1,89 +1,53 @@
import { ApprovalDialog } from "./ApprovalDialog"; import { WorkflowDesigner } from './WorkflowDesigner'
import { WorkflowDesigner } from "./WorkflowDesigner"; import { useEffect, useState, type FormEvent, type RefObject } from 'react'
import { WorkflowTable } from "./WorkflowTable"; import type { WorkflowCriteriaDto } from '@/services/workflow.service'
import type { FormEvent, RefObject } from "react"; import { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import type { import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
WorkflowCriteriaDto,
WorkflowItemDto,
} from "@/services/workflow.service";
type DashboardShellProps = { type DashboardShellProps = {
busy: boolean; busy: boolean
canvasRef: RefObject<HTMLDivElement>; canvasRef: RefObject<HTMLDivElement>
canvasZoom: number; canvasZoom: number
criteria: WorkflowCriteriaDto[]; criteriaForm: any
criteriaForm: any; currentCriteria: WorkflowCriteriaDto[]
currentCriteria: WorkflowCriteriaDto[]; designerTab: string
designerTab: string; dragPreview: any
dialogPendingItems: WorkflowItemDto[]; pendingLink: any
dragPreview: any; selectedId: string
editingWorkflowId: string | null; onAddCriteria: (kind: string) => void
pendingLink: any; onBeginLink: (sourceId: string, outcome: string) => void
selectedId: string; onChangeCriteriaForm: (form: any) => void
selectedWorkflow?: WorkflowItemDto | null; onClearCanvasSelection: () => void
selectedWorkflowId: string | null; onConnectNodes: (sourceId: string, outcome: string, targetId: string) => void
showApprovalDialog: boolean; onDeleteSelectedCriteria: (criteriaId?: string) => void
workflowEditForm: any; onDisconnectLink: (sourceId: string, outcome: string) => void
workflowForm: any; onDragMove: (event: any) => void
workflowItems: WorkflowItemDto[]; onFitFlowLayout: () => void
onAddCriteria: (kind: string) => void; onOpenCriteriaDetails: (id: string) => void
onBeginLink: (sourceId: string, outcome: string) => void; onResetDemo: () => void
onBeginWorkflowEdit: (item: WorkflowItemDto) => void; onSaveCriteria: (event: FormEvent<HTMLFormElement>) => void
onCancelWorkflowEdit: () => void; onSelectCriteria: (id: string) => void
onChangeCriteriaForm: (form: any) => void; onSetDesignerTab: (tab: string) => void
onClearCanvasSelection: () => void; onUpdateNodePosition: (event: any) => void
onCloseApprovalDialog: () => void; onZoomIn: () => void
onConnectNodes: (sourceId: string, outcome: string, targetId: string) => void; onZoomOut: () => 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;
};
export function DashboardShell({ export function DashboardShell({
busy, busy,
canvasRef, canvasRef,
canvasZoom, canvasZoom,
criteria,
criteriaForm, criteriaForm,
currentCriteria, currentCriteria,
designerTab, designerTab,
dialogPendingItems,
dragPreview, dragPreview,
editingWorkflowId,
pendingLink, pendingLink,
selectedId, selectedId,
selectedWorkflow,
selectedWorkflowId,
showApprovalDialog,
workflowEditForm,
workflowForm,
workflowItems,
onAddCriteria, onAddCriteria,
onBeginLink, onBeginLink,
onBeginWorkflowEdit,
onCancelWorkflowEdit,
onChangeCriteriaForm, onChangeCriteriaForm,
onClearCanvasSelection, onClearCanvasSelection,
onCloseApprovalDialog,
onConnectNodes, onConnectNodes,
onCreateWorkflow,
onDecision,
onDeleteSelectedCriteria, onDeleteSelectedCriteria,
onDisconnectLink, onDisconnectLink,
onDragMove, onDragMove,
@ -91,41 +55,34 @@ export function DashboardShell({
onOpenCriteriaDetails, onOpenCriteriaDetails,
onResetDemo, onResetDemo,
onSaveCriteria, onSaveCriteria,
onSaveWorkflowEdit,
onSelectCriteria, onSelectCriteria,
onSelectWorkflow,
onSetDesignerTab, onSetDesignerTab,
onStartWorkflow,
onUpdateNodePosition, onUpdateNodePosition,
onWorkflowEditFormChange,
onWorkflowFormChange,
onZoomIn, onZoomIn,
onZoomOut, onZoomOut,
}: DashboardShellProps) { }: 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 ( return (
<div className="min-h-screen"> <div className="min-h-screen">
<main className="grid gap-[18px] p-[18px]"> <main className="grid">
<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>
<WorkflowDesigner <WorkflowDesigner
busy={busy} busy={busy}
canvasRef={canvasRef} canvasRef={canvasRef}
@ -136,7 +93,6 @@ export function DashboardShell({
dragPreview={dragPreview} dragPreview={dragPreview}
pendingLink={pendingLink} pendingLink={pendingLink}
selectedCriteriaId={selectedId} selectedCriteriaId={selectedId}
selectedWorkflow={selectedWorkflow}
onAddCriteria={onAddCriteria} onAddCriteria={onAddCriteria}
onBeginLink={onBeginLink} onBeginLink={onBeginLink}
onChangeCriteriaForm={onChangeCriteriaForm} onChangeCriteriaForm={onChangeCriteriaForm}
@ -147,6 +103,7 @@ export function DashboardShell({
onDragMove={onDragMove} onDragMove={onDragMove}
onFitLayout={onFitFlowLayout} onFitLayout={onFitFlowLayout}
onOpenDetails={onOpenCriteriaDetails} onOpenDetails={onOpenCriteriaDetails}
onResetDemo={onResetDemo}
onSaveCriteria={onSaveCriteria} onSaveCriteria={onSaveCriteria}
onSelectCriteria={onSelectCriteria} onSelectCriteria={onSelectCriteria}
onSetDesignerTab={onSetDesignerTab} onSetDesignerTab={onSetDesignerTab}
@ -155,16 +112,6 @@ export function DashboardShell({
onZoomOut={onZoomOut} onZoomOut={onZoomOut}
/> />
</main> </main>
{showApprovalDialog && (
<ApprovalDialog
busy={busy}
criteria={criteria}
items={dialogPendingItems}
onClose={onCloseApprovalDialog}
onDecision={onDecision}
/>
)}
</div> </div>
); )
} }

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,11 @@
import { DndContext } from '@dnd-kit/core' import { DndContext } from '@dnd-kit/core'
import classNames from 'classnames' import classNames from 'classnames'
import dayjs from 'dayjs' import { FiMaximize2, FiRefreshCw, FiZoomIn, FiZoomOut } from 'react-icons/fi'
import { FiMaximize2, FiZoomIn, FiZoomOut } from 'react-icons/fi'
import { kindIcon, kindOptions } from '@/utils/workflow/workflowConstants' import { kindIcon, kindOptions } from '@/utils/workflow/workflowConstants'
import { CriteriaTable } from './CriteriaTable' import { CriteriaTable } from './CriteriaTable'
import { FlowCanvas } from './FlowCanvas' import { FlowCanvas } from './FlowCanvas'
import type { FormEvent, RefObject } from 'react' import type { FormEvent, RefObject } from 'react'
import type { import type { WorkflowCriteriaDto } from '@/services/workflow.service'
WorkflowCriteriaDto,
WorkflowItemDto,
} from '@/services/workflow.service'
const MaximizeIcon = FiMaximize2 as any
const ZoomInIcon = FiZoomIn as any
const ZoomOutIcon = FiZoomOut as any
type WorkflowDesignerProps = { type WorkflowDesignerProps = {
busy: boolean busy: boolean
@ -25,7 +17,6 @@ type WorkflowDesignerProps = {
dragPreview: any dragPreview: any
pendingLink: any pendingLink: any
selectedCriteriaId: string selectedCriteriaId: string
selectedWorkflow?: WorkflowItemDto | null
onAddCriteria: (kind: string) => void onAddCriteria: (kind: string) => void
onBeginLink: (sourceId: string, outcome: string) => void onBeginLink: (sourceId: string, outcome: string) => void
onChangeCriteriaForm: (form: any) => void onChangeCriteriaForm: (form: any) => void
@ -36,6 +27,7 @@ type WorkflowDesignerProps = {
onDragMove: (event: any) => void onDragMove: (event: any) => void
onFitLayout: () => void onFitLayout: () => void
onOpenDetails: (id: string) => void onOpenDetails: (id: string) => void
onResetDemo: () => void
onSaveCriteria: (event: FormEvent<HTMLFormElement>) => void onSaveCriteria: (event: FormEvent<HTMLFormElement>) => void
onSelectCriteria: (id: string) => void onSelectCriteria: (id: string) => void
onSetDesignerTab: (tab: string) => void onSetDesignerTab: (tab: string) => void
@ -44,6 +36,14 @@ type WorkflowDesignerProps = {
onZoomOut: () => void 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({ export function WorkflowDesigner({
busy, busy,
canvasRef, canvasRef,
@ -54,7 +54,6 @@ export function WorkflowDesigner({
dragPreview, dragPreview,
pendingLink, pendingLink,
selectedCriteriaId, selectedCriteriaId,
selectedWorkflow,
onAddCriteria, onAddCriteria,
onBeginLink, onBeginLink,
onChangeCriteriaForm, onChangeCriteriaForm,
@ -65,6 +64,7 @@ export function WorkflowDesigner({
onDragMove, onDragMove,
onFitLayout, onFitLayout,
onOpenDetails, onOpenDetails,
onResetDemo,
onSaveCriteria, onSaveCriteria,
onSelectCriteria, onSelectCriteria,
onSetDesignerTab, onSetDesignerTab,
@ -73,7 +73,7 @@ export function WorkflowDesigner({
onZoomOut, onZoomOut,
}: WorkflowDesignerProps) { }: WorkflowDesignerProps) {
return ( 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"> <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} /> <DesignerTabs activeTab={designerTab} onChange={onSetDesignerTab} />
@ -84,6 +84,7 @@ export function WorkflowDesigner({
zoom={canvasZoom} zoom={canvasZoom}
onAddCriteria={onAddCriteria} onAddCriteria={onAddCriteria}
onFitLayout={onFitLayout} onFitLayout={onFitLayout}
onResetDemo={onResetDemo}
onZoomIn={onZoomIn} onZoomIn={onZoomIn}
onZoomOut={onZoomOut} onZoomOut={onZoomOut}
/> />
@ -101,7 +102,6 @@ export function WorkflowDesigner({
currentCriteria={currentCriteria} currentCriteria={currentCriteria}
dragPreview={dragPreview} dragPreview={dragPreview}
zoom={canvasZoom} zoom={canvasZoom}
activeNodeId={selectedWorkflow?.currentNodeId}
selectedId={selectedCriteriaId} selectedId={selectedCriteriaId}
pendingLink={pendingLink} pendingLink={pendingLink}
canvasRef={canvasRef} canvasRef={canvasRef}
@ -120,20 +120,15 @@ export function WorkflowDesigner({
{designerTab === 'criteria' && ( {designerTab === 'criteria' && (
<CriteriaTable <CriteriaTable
criteria={currentCriteria} criteria={currentCriteria}
selectedWorkflow={selectedWorkflow}
selectedId={selectedCriteriaId} selectedId={selectedCriteriaId}
activeNodeId={selectedWorkflow?.currentNodeId}
form={criteriaForm} form={criteriaForm}
busy={busy} busy={busy}
onSelect={onSelectCriteria} onSelect={onSelectCriteria}
onChange={onChangeCriteriaForm} onChange={onChangeCriteriaForm}
onSubmit={onSaveCriteria} onSubmit={onSaveCriteria}
onDelete={onDeleteCriteria} onDelete={onDeleteCriteria}
onAddCriteria={onAddCriteria}
/> />
)} )}
{designerTab === 'history' && <ApprovalHistoryTable selectedWorkflow={selectedWorkflow} />}
</section> </section>
) )
} }
@ -144,6 +139,7 @@ function DesignerToolbar({
zoom, zoom,
onAddCriteria, onAddCriteria,
onFitLayout, onFitLayout,
onResetDemo,
onZoomIn, onZoomIn,
onZoomOut, onZoomOut,
}: { }: {
@ -152,6 +148,7 @@ function DesignerToolbar({
zoom: number zoom: number
onAddCriteria: (kind: string) => void onAddCriteria: (kind: string) => void
onFitLayout: () => void onFitLayout: () => void
onResetDemo: () => void
onZoomIn: () => void onZoomIn: () => void
onZoomOut: () => void onZoomOut: () => void
}) { }) {
@ -159,31 +156,41 @@ function DesignerToolbar({
<div className="flex flex-wrap justify-end gap-2"> <div className="flex flex-wrap justify-end gap-2">
<button <button
type="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} disabled={busy || currentCriteria.length === 0}
title="Düğümleri okunabilir şekilde yerleştir" title="Düğümleri okunabilir şekilde yerleştir"
onClick={onFitLayout} onClick={onFitLayout}
> >
<MaximizeIcon /> <FiMaximize2 />
Fit Fit
</button> </button>
<button <button
type="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" title="Yakınlaştır"
onClick={onZoomIn} onClick={onZoomIn}
> >
<ZoomInIcon /> <FiZoomIn />
</button> </button>
<button <button
type="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" title="Uzaklaştır"
onClick={onZoomOut} onClick={onZoomOut}
> >
<ZoomOutIcon /> <FiZoomOut />
</button> </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)}% {Math.round(zoom * 100)}%
</span> </span>
{kindOptions.map((option) => { {kindOptions.map((option) => {
@ -192,7 +199,7 @@ function DesignerToolbar({
<button <button
key={option.value} key={option.value}
type="button" type="button"
className="border-app-primary bg-white text-app-primary" className={classNames(designerButtonClass, 'border-blue-600 bg-white text-blue-600')}
disabled={busy} disabled={busy}
onClick={() => onAddCriteria(option.value)} onClick={() => onAddCriteria(option.value)}
> >
@ -218,10 +225,10 @@ function DesignerTabs({
type="button" type="button"
role="tab" role="tab"
className={classNames( className={classNames(
'min-h-8 rounded-md border px-3 py-1.5 bg-transparent text-[#475467] transition-colors', designerTabClass,
{ activeTab === 'flow'
'border-[#1d4ed8] bg-[#1d4ed8] text-white shadow-sm': activeTab === 'flow', ? 'border-blue-700 bg-blue-700 text-white shadow-sm'
}, : 'border-gray-200 bg-white text-slate-600',
)} )}
onClick={() => onChange('flow')} onClick={() => onChange('flow')}
> >
@ -231,66 +238,15 @@ function DesignerTabs({
type="button" type="button"
role="tab" role="tab"
className={classNames( className={classNames(
'min-h-8 rounded-md border px-3 py-1.5 bg-transparent text-[#475467] transition-colors', designerTabClass,
{ activeTab === 'criteria'
'border-[#1d4ed8] bg-[#1d4ed8] text-white shadow-sm': activeTab === 'criteria', ? 'border-blue-700 bg-blue-700 text-white shadow-sm'
}, : 'border-gray-200 bg-white text-slate-600',
)} )}
onClick={() => onChange('criteria')} onClick={() => onChange('criteria')}
> >
Adımlar Adımlar
</button> </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> </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>
)
}