import { getNodeHeight, nodeSize } from "./workflowConstants"; import type { CompareOutcomeDto, SaveCriteriaInput, WorkflowConditionDto, WorkflowCriteriaDto, WorkflowItemDto, } from "@/services/workflow.service"; export type WorkflowCriteriaForm = Partial & { id?: string | null; workflowItemId: string; compareOutcomes: CompareOutcomeDto[]; }; export type WorkflowOutcome = { field: string; label: string; targetId?: string | null; }; export type WorkflowLinkPort = { field?: string; index?: number; count?: number; sourceSlotIndex?: number; sourceSlotCount?: number; targetSlotIndex?: number; targetSlotCount?: number; routeIndex?: number; routeCount?: number; }; export type WorkflowLink = { key: string; source: WorkflowCriteriaDto; target: WorkflowCriteriaDto; label: string; sourcePort: WorkflowLinkPort; }; type Endpoint = { link: WorkflowLink; role: "source" | "target"; }; export function isPendingApproval( item: WorkflowItemDto | undefined | null, criteria: WorkflowCriteriaDto[], ) { if (!item) return false; return criteria.some( (candidate) => candidate.workflowItemId === item.id && candidate.id === item.currentNodeId && candidate.kind === "Approval", ); } export function buildFitLayout(criteria: WorkflowCriteriaDto[]) { 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: WorkflowCriteriaDto) { const priority: Record = { Start: 0, Compare: 1, Approval: 2, Inform: 3, End: 4, }; return priority[item.kind] ?? 2; } function compareLayoutNodes( a: WorkflowCriteriaDto, b: WorkflowCriteriaDto, rankById = new Map(), ) { return ( (rankById.get(a.id) ?? 999) - (rankById.get(b.id) ?? 999) || a.title.localeCompare(b.title, "tr") ); } function buildTraversalRanks( criteria: WorkflowCriteriaDto[], links: WorkflowLink[], ) { 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 (!id) continue; if (rankById.has(id)) continue; rankById.set(id, rankById.size); (outgoing.get(id) || []).forEach((targetId) => { if (targetId && !rankById.has(targetId)) queue.push(targetId); }); } criteria.forEach((item) => { if (!rankById.has(item.id)) rankById.set(item.id, rankById.size); }); return rankById; } export function collectLinks(criteria: WorkflowCriteriaDto[]) { const links: WorkflowLink[] = []; 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: WorkflowLink[], criteria: WorkflowCriteriaDto[], ) { const endpointGroups = new Map(); const addEndpoint = (nodeId: string, side: string, endpoint: 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: WorkflowCriteriaDto, to: WorkflowCriteriaDto) { 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: WorkflowCriteriaDto): WorkflowOutcome[] { 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?: string) { if (field?.startsWith("compareOutcomes:")) return "Karşılaştırma durumu"; const labels: Record = { nextOnStart: "Sonraki", nextOnTrue: "Doğru", nextOnFalse: "Yanlış", nextOnApprove: "Onay", nextOnReject: "Red", }; return field ? labels[field] : undefined; } export function addLink( links: WorkflowLink[], criteria: WorkflowCriteriaDto[], source: WorkflowCriteriaDto, targetId: string | null | undefined, label: string, type: string, sourcePort: WorkflowLinkPort = {}, ) { 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 = "", ): WorkflowCriteriaForm { 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: WorkflowCriteriaDto): WorkflowCriteriaForm { 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: WorkflowCriteriaForm): SaveCriteriaInput { const sharedPerson = item.approver || item.informPerson || ""; return { ...item, id: item.id || null, workflowItemId: item.workflowItemId || "", compareValue: Number(item.compareValue || 0), approver: sharedPerson, informPerson: sharedPerson, positionX: Number(item.positionX || 32), positionY: Number(item.positionY || 150), compareOutcomes: (item.compareOutcomes || []) .slice(0, 4) .filter((outcome) => outcome.label?.trim()) .map(normalizeCompareOutcome), }; } export function defaultTitle(kind: string) { return { Start: "İş Akışı Başlat", Compare: "Tutar > 5000 TL", Approval: "Onaylanacak Kişi", Inform: "Bilgilendirme Yapılacak Personel", End: "Akışı Bitir", }[kind] ?? "İş Akışı Adımı"; } export function emptyCompareOutcome(label = "Durum"): CompareOutcomeDto { return { label, targetId: "", conditions: [{ column: "Tutar", operator: ">", compareValue: 5000 }], }; } export function toCompareOutcomeForm( outcome: Partial & Partial & { conditions?: Partial[]; }, ): CompareOutcomeDto { 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: Partial & Partial & { conditions?: Partial[]; }, ): CompareOutcomeDto { const conditions = ( outcome.conditions?.length ? outcome.conditions : [ { column: outcome.column || "Tutar", operator: outcome.operator || ">", compareValue: outcome.compareValue || 0, }, ] ) .filter( (condition) => condition.operator && String(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: Partial & Partial, ) { 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: number | string | null | undefined) { return new Intl.NumberFormat("tr-TR", { maximumFractionDigits: 2, }).format(Number(value || 0)); } export function criteriaSummary(item: WorkflowCriteriaDto) { 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: WorkflowCriteriaDto[], id?: string | null, ) { if (!id) return "-"; const item = criteria.find((candidate) => candidate.id === id); return item ? `${item.id} - ${item.title}` : id; } export function statusClass(status?: string) { if (status === "Onay Bekliyor") return "pending"; if (status === "Bitti") return "done"; if (status === "Bilgilendirildi") return "info"; return ""; } export function formatMoney(value?: number | string | null) { return new Intl.NumberFormat("tr-TR", { style: "currency", currency: "TRY", }).format(Number(value || 0)); }