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