sozsoft-platform/ui/src/utils/workflow/workflowHelpers.ts

476 lines
12 KiB
TypeScript
Raw Normal View History

2026-05-22 09:40:35 +00:00
import { getNodeHeight, nodeSize } from "./workflowConstants";
export function isPendingApproval(item, criteria) {
if (!item) return false;
return criteria.some(
(candidate) =>
candidate.workflowItemId === item.id &&
candidate.id === item.currentNodeId &&
candidate.kind === "Approval",
);
}
export function buildFitLayout(criteria) {
const links = collectLinks(criteria);
const rankById = buildTraversalRanks(criteria, links);
const groups = new Map<any, any[]>();
criteria.forEach((item) => {
const column = fitColumn(item);
if (!groups.has(column)) groups.set(column, []);
groups.get(column).push(item);
});
const sortedColumns = [...groups.keys()].sort((a, b) => a - b);
const yGap = 74;
const maxGroupHeight = Math.max(
1,
...[...groups.values()].map(
(items) =>
items.reduce((sum, item) => sum + getNodeHeight(item), 0) +
Math.max(0, items.length - 1) * yGap,
),
);
const top = 72;
const left = 72;
const xGap = 128;
const positions = new Map();
sortedColumns.forEach((column, columnIndex) => {
const items = groups
.get(column)
.sort((a, b) => compareLayoutNodes(a, b, rankById));
const groupHeight =
items.reduce((sum, item) => sum + getNodeHeight(item), 0) +
Math.max(0, items.length - 1) * yGap;
let y = top + Math.max(0, (maxGroupHeight - groupHeight) / 2);
items.forEach((item) => {
positions.set(item.id, {
x: left + columnIndex * (nodeSize.width + xGap),
y: Math.round(y),
});
y += getNodeHeight(item) + yGap;
});
});
return positions;
}
function fitColumn(item) {
const priority = {
Start: 0,
Compare: 1,
Approval: 2,
Inform: 3,
End: 4,
};
return priority[item.kind] ?? 2;
}
function compareLayoutNodes(a, b, rankById = new Map()) {
return (
(rankById.get(a.id) ?? 999) - (rankById.get(b.id) ?? 999) ||
a.title.localeCompare(b.title, "tr")
);
}
function buildTraversalRanks(criteria, links) {
const rankById = new Map();
const outgoing = new Map<any, any[]>(criteria.map((item) => [item.id, []]));
links.forEach((link) => {
outgoing.get(link.source.id)?.push(link.target.id);
});
const roots = criteria.filter((item) => item.kind === "Start");
const queue = roots.length
? roots.map((item) => item.id)
: criteria.map((item) => item.id);
while (queue.length) {
const id = queue.shift();
if (rankById.has(id)) continue;
rankById.set(id, rankById.size);
(outgoing.get(id) || []).forEach((targetId) => {
if (targetId && !rankById.has(targetId)) queue.push(targetId);
});
}
criteria.forEach((item) => {
if (!rankById.has(item.id)) rankById.set(item.id, rankById.size);
});
return rankById;
}
export function collectLinks(criteria) {
const links = [];
criteria.forEach((source) => {
if (source.kind === "Compare" && source.compareOutcomes?.length) {
source.compareOutcomes.forEach((outcome, index) => {
addLink(
links,
criteria,
source,
outcome.targetId,
outcome.label,
`compare-${index}`,
{
index,
count: source.compareOutcomes.length,
field: `compareOutcomes:${index}`,
},
);
});
return;
}
addLink(links, criteria, source, source.nextOnStart, "Sonraki", "next", {
index: 0,
count: 1,
field: "nextOnStart",
});
addLink(links, criteria, source, source.nextOnTrue, "Doğru", "true", {
index: 0,
count: 2,
field: "nextOnTrue",
});
addLink(links, criteria, source, source.nextOnFalse, "Yanlış", "false", {
index: 1,
count: 2,
field: "nextOnFalse",
});
addLink(links, criteria, source, source.nextOnApprove, "Onay", "approve", {
index: 0,
count: 2,
field: "nextOnApprove",
});
addLink(links, criteria, source, source.nextOnReject, "Red", "reject", {
index: 1,
count: 2,
field: "nextOnReject",
});
});
return assignLinkSlots(links, criteria);
}
export function assignLinkSlots(links, criteria) {
const endpointGroups = new Map();
const addEndpoint = (nodeId, side, endpoint) => {
const key = `${nodeId}:${side}`;
if (!endpointGroups.has(key)) endpointGroups.set(key, []);
endpointGroups.get(key).push(endpoint);
};
links.forEach((link) => {
addEndpoint(link.source.id, sideToward(link.source, link.target), {
link,
role: "source",
});
addEndpoint(link.target.id, sideToward(link.target, link.source), {
link,
role: "target",
});
});
endpointGroups.forEach((endpoints) => {
endpoints.forEach((endpoint, index) => {
if (endpoint.role === "source") {
endpoint.link.sourcePort.sourceSlotIndex = index;
endpoint.link.sourcePort.sourceSlotCount = endpoints.length;
} else {
endpoint.link.sourcePort.targetSlotIndex = index;
endpoint.link.sourcePort.targetSlotCount = endpoints.length;
}
});
});
links.forEach((link) => {
link.sourcePort.routeIndex = link.sourcePort.targetSlotIndex ?? 0;
link.sourcePort.routeCount = link.sourcePort.targetSlotCount ?? 1;
});
return links;
}
function sideToward(from, to) {
const fromLeft = Number(from.positionX || 0);
const fromTop = Number(from.positionY || 0);
const fromCenter = {
x: fromLeft + nodeSize.width / 2,
y: fromTop + getNodeHeight(from) / 2,
};
const toCenter = {
x: Number(to.positionX || 0) + nodeSize.width / 2,
y: Number(to.positionY || 0) + getNodeHeight(to) / 2,
};
const dx = toCenter.x - fromCenter.x;
const dy = toCenter.y - fromCenter.y;
const horizontalDistance = Math.abs(dx) / (nodeSize.width / 2);
const verticalDistance = Math.abs(dy) / (getNodeHeight(from) / 2);
if (horizontalDistance >= verticalDistance) return dx >= 0 ? "right" : "left";
return dy >= 0 ? "bottom" : "top";
}
export function getNodeOutcomes(item) {
if (item.kind === "Compare") {
const outcomes = item.compareOutcomes?.length
? item.compareOutcomes
: [
{ label: "Doğru", targetId: item.nextOnTrue },
{ label: "Yanlış", targetId: item.nextOnFalse },
];
return outcomes.slice(0, 4).map((outcome, index) => ({
field: `compareOutcomes:${index}`,
label: outcome.label || `Durum ${index + 1}`,
targetId: outcome.targetId,
}));
}
if (item.kind === "Approval") {
return [
{ field: "nextOnApprove", label: "Onay", targetId: item.nextOnApprove },
{ field: "nextOnReject", label: "Red", targetId: item.nextOnReject },
];
}
if (item.kind === "End") return [];
return [
{ field: "nextOnStart", label: "Sonraki", targetId: item.nextOnStart },
];
}
export function outcomeLabel(field) {
if (field?.startsWith("compareOutcomes:")) return "Karşılaştırma durumu";
return {
nextOnStart: "Sonraki",
nextOnTrue: "Doğru",
nextOnFalse: "Yanlış",
nextOnApprove: "Onay",
nextOnReject: "Red",
}[field];
}
export function addLink(
links,
criteria,
source,
targetId,
label,
type,
sourcePort = null,
) {
if (!targetId) return;
const target = criteria.find((item) => item.id === targetId);
if (target) {
links.push({
key: `${source.id}-${target.id}-${type}`,
source,
target,
label,
sourcePort,
});
}
}
export function emptyCriteria(kind = "Compare", workflowItemId = null) {
return {
id: "",
workflowItemId,
kind,
title: defaultTitle(kind),
column: "Tutar",
operator: ">",
compareValue: 5000,
approver: "",
informPerson: "",
nextOnStart: "",
nextOnTrue: "",
nextOnFalse: "",
nextOnApprove: "",
nextOnReject: "",
compareOutcomes:
kind === "Compare"
? [emptyCompareOutcome("Durum 1"), emptyCompareOutcome("Durum 2")]
: [],
positionX: 32,
positionY: 150,
};
}
export function toCriteriaForm(item) {
const sharedPerson = item.approver || item.informPerson || "";
return {
...emptyCriteria(item.kind),
...item,
approver: sharedPerson,
informPerson: sharedPerson,
nextOnStart: item.nextOnStart || "",
nextOnTrue: item.nextOnTrue || "",
nextOnFalse: item.nextOnFalse || "",
nextOnApprove: item.nextOnApprove || "",
nextOnReject: item.nextOnReject || "",
compareOutcomes: item.compareOutcomes?.length
? item.compareOutcomes.map(toCompareOutcomeForm)
: emptyCriteria(item.kind).compareOutcomes,
};
}
export function normalizeCriteria(item) {
const sharedPerson = item.approver || item.informPerson || "";
return {
...item,
id: item.id || null,
workflowItemId: item.workflowItemId || null,
compareValue: Number(item.compareValue || 0),
approver: sharedPerson,
informPerson: sharedPerson,
positionX: Number(item.positionX || 32),
positionY: Number(item.positionY || 150),
compareOutcomes: (item.compareOutcomes || [])
.slice(0, 4)
.filter((outcome) => outcome.label?.trim())
.map(normalizeCompareOutcome),
};
}
export function defaultTitle(kind) {
return {
Start: "İş Akışı Başlat",
Compare: "Tutar > 5000 TL",
Approval: "Onaylanacak Kişi",
Inform: "Bilgilendirme Yapılacak Personel",
End: "Akışı Bitir",
}[kind];
}
export function emptyCompareOutcome(label = "Durum") {
return {
label,
targetId: "",
conditions: [{ column: "Tutar", operator: ">", compareValue: 5000 }],
};
}
export function toCompareOutcomeForm(outcome) {
const conditions = outcome.conditions?.length
? outcome.conditions
: [
{
column: outcome.column || "Tutar",
operator: outcome.operator || ">",
compareValue: outcome.compareValue || 0,
},
];
return {
label: outcome.label || "",
targetId: outcome.targetId || "",
conditions: conditions.map((condition) => ({
column: condition.column || "Tutar",
operator: condition.operator || ">",
compareValue: condition.compareValue ?? 0,
})),
};
}
export function normalizeCompareOutcome(outcome) {
const conditions = (
outcome.conditions?.length
? outcome.conditions
: [
{
column: outcome.column || "Tutar",
operator: outcome.operator || ">",
compareValue: outcome.compareValue || 0,
},
]
)
.filter((condition) => condition.operator && condition.compareValue !== "")
.map((condition) => ({
column: condition.column || "Tutar",
operator: condition.operator || ">",
compareValue: Number(condition.compareValue || 0),
}));
return {
label: outcome.label.trim(),
targetId: outcome.targetId || null,
conditions,
};
}
export function compareOutcomeRuleText(outcome) {
const conditions = outcome.conditions?.length
? outcome.conditions
: outcome.operator
? [
{
column: outcome.column || "Tutar",
operator: outcome.operator,
compareValue: outcome.compareValue,
},
]
: [];
return conditions.length
? conditions
.map(
(condition) =>
`${condition.column} ${condition.operator} ${formatCompactValue(condition.compareValue)}`,
)
.join(" ve ")
: "Kural yok";
}
export function formatCompactValue(value) {
return new Intl.NumberFormat("tr-TR", {
maximumFractionDigits: 2,
}).format(Number(value || 0));
}
export function criteriaSummary(item) {
if (item.kind === "Compare") {
return (
(item.compareOutcomes || [])
.map(
(outcome) => `${outcome.label}: ${compareOutcomeRuleText(outcome)}`,
)
.join(" / ") || "-"
);
}
if (item.kind === "Approval")
return item.approver || item.informPerson || "-";
if (item.kind === "Inform") return item.approver || item.informPerson || "-";
return item.title;
}
export function targetTitle(criteria, id) {
if (!id) return "-";
const item = criteria.find((candidate) => candidate.id === id);
return item ? `${item.id} - ${item.title}` : id;
}
export function statusClass(status) {
if (status === "Onay Bekliyor") return "pending";
if (status === "Bitti") return "done";
if (status === "Bilgilendirildi") return "info";
return "";
}
export function formatMoney(value) {
return new Intl.NumberFormat("tr-TR", {
style: "currency",
currency: "TRY",
}).format(value || 0);
}