2026-07-08 07:29:45 +00:00
|
|
|
|
import { MULTIVALUE_DELIMITER } from '@/constants/app.constant'
|
2026-05-22 20:41:36 +00:00
|
|
|
|
import { getNodeHeight, nodeSize } from './workflowConstants'
|
2026-05-22 11:50:09 +00:00
|
|
|
|
import type {
|
|
|
|
|
|
CompareOutcomeDto,
|
|
|
|
|
|
SaveCriteriaInput,
|
|
|
|
|
|
WorkflowConditionDto,
|
|
|
|
|
|
WorkflowCriteriaDto,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
} from '@/services/workflow.service'
|
2026-05-22 11:50:09 +00:00
|
|
|
|
|
|
|
|
|
|
export type WorkflowCriteriaForm = Partial<WorkflowCriteriaDto> & {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
id?: string | null
|
|
|
|
|
|
listFormCode: string
|
|
|
|
|
|
compareOutcomes: CompareOutcomeDto[]
|
|
|
|
|
|
}
|
2026-05-22 11:50:09 +00:00
|
|
|
|
|
|
|
|
|
|
export type WorkflowOutcome = {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
field: string
|
|
|
|
|
|
label: string
|
|
|
|
|
|
targetId?: string | null
|
|
|
|
|
|
}
|
2026-05-22 11:50:09 +00:00
|
|
|
|
|
|
|
|
|
|
export type WorkflowLinkPort = {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
field?: string
|
|
|
|
|
|
index?: number
|
|
|
|
|
|
count?: number
|
|
|
|
|
|
sourceSlotIndex?: number
|
|
|
|
|
|
sourceSlotCount?: number
|
|
|
|
|
|
targetSlotIndex?: number
|
|
|
|
|
|
targetSlotCount?: number
|
|
|
|
|
|
routeIndex?: number
|
|
|
|
|
|
routeCount?: number
|
|
|
|
|
|
}
|
2026-05-22 11:50:09 +00:00
|
|
|
|
|
|
|
|
|
|
export type WorkflowLink = {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
key: string
|
|
|
|
|
|
source: WorkflowCriteriaDto
|
|
|
|
|
|
target: WorkflowCriteriaDto
|
|
|
|
|
|
label: string
|
|
|
|
|
|
sourcePort: WorkflowLinkPort
|
|
|
|
|
|
}
|
2026-05-22 11:50:09 +00:00
|
|
|
|
|
|
|
|
|
|
type Endpoint = {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
link: WorkflowLink
|
|
|
|
|
|
role: 'source' | 'target'
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
export function buildFitLayout(criteria: WorkflowCriteriaDto[]) {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
const links = collectLinks(criteria)
|
|
|
|
|
|
const rankById = buildTraversalRanks(criteria, links)
|
|
|
|
|
|
const groups = new Map<number, WorkflowCriteriaDto[]>()
|
2026-05-22 09:40:35 +00:00
|
|
|
|
criteria.forEach((item) => {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
const column = fitColumn(item)
|
|
|
|
|
|
if (!groups.has(column)) groups.set(column, [])
|
|
|
|
|
|
groups.get(column)?.push(item)
|
|
|
|
|
|
})
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
const sortedColumns = [...groups.keys()].sort((a, b) => a - b)
|
|
|
|
|
|
const yGap = 74
|
2026-05-22 09:40:35 +00:00
|
|
|
|
const maxGroupHeight = Math.max(
|
|
|
|
|
|
1,
|
|
|
|
|
|
...[...groups.values()].map(
|
|
|
|
|
|
(items) =>
|
|
|
|
|
|
items.reduce((sum, item) => sum + getNodeHeight(item), 0) +
|
|
|
|
|
|
Math.max(0, items.length - 1) * yGap,
|
|
|
|
|
|
),
|
2026-05-22 20:41:36 +00:00
|
|
|
|
)
|
|
|
|
|
|
const top = 72
|
|
|
|
|
|
const left = 72
|
|
|
|
|
|
const xGap = 128
|
|
|
|
|
|
const positions = new Map<string, { x: number; y: number }>()
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
|
|
|
|
|
sortedColumns.forEach((column, columnIndex) => {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
const items = (groups.get(column) || []).sort((a, b) => compareLayoutNodes(a, b, rankById))
|
2026-05-22 09:40:35 +00:00
|
|
|
|
const groupHeight =
|
|
|
|
|
|
items.reduce((sum, item) => sum + getNodeHeight(item), 0) +
|
2026-05-22 20:41:36 +00:00
|
|
|
|
Math.max(0, items.length - 1) * yGap
|
|
|
|
|
|
let y = top + Math.max(0, (maxGroupHeight - groupHeight) / 2)
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
|
|
|
|
|
items.forEach((item) => {
|
|
|
|
|
|
positions.set(item.id, {
|
|
|
|
|
|
x: left + columnIndex * (nodeSize.width + xGap),
|
|
|
|
|
|
y: Math.round(y),
|
2026-05-22 20:41:36 +00:00
|
|
|
|
})
|
|
|
|
|
|
y += getNodeHeight(item) + yGap
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
return positions
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
function fitColumn(item: WorkflowCriteriaDto) {
|
|
|
|
|
|
const priority: Record<string, number> = {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
Start: 0,
|
|
|
|
|
|
Compare: 1,
|
|
|
|
|
|
Approval: 2,
|
|
|
|
|
|
Inform: 3,
|
|
|
|
|
|
End: 4,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
}
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
return priority[item.kind] ?? 2
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
function compareLayoutNodes(
|
|
|
|
|
|
a: WorkflowCriteriaDto,
|
|
|
|
|
|
b: WorkflowCriteriaDto,
|
|
|
|
|
|
rankById = new Map<string, number>(),
|
|
|
|
|
|
) {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
return (
|
|
|
|
|
|
(rankById.get(a.id) ?? 999) - (rankById.get(b.id) ?? 999) ||
|
2026-05-22 20:41:36 +00:00
|
|
|
|
a.title.localeCompare(b.title, 'tr')
|
|
|
|
|
|
)
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
function buildTraversalRanks(criteria: WorkflowCriteriaDto[], links: WorkflowLink[]) {
|
|
|
|
|
|
const rankById = new Map<string, number>()
|
|
|
|
|
|
const outgoing = new Map<string, string[]>(criteria.map((item) => [item.id, []]))
|
2026-05-22 09:40:35 +00:00
|
|
|
|
links.forEach((link) => {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
outgoing.get(link.source.id)?.push(link.target.id)
|
|
|
|
|
|
})
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
const roots = criteria.filter((item) => item.kind === 'Start')
|
|
|
|
|
|
const queue = roots.length ? roots.map((item) => item.id) : criteria.map((item) => item.id)
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
|
|
|
|
|
while (queue.length) {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
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)
|
|
|
|
|
|
})
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
criteria.forEach((item) => {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
if (!rankById.has(item.id)) rankById.set(item.id, rankById.size)
|
|
|
|
|
|
})
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
return rankById
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
export function collectLinks(criteria: WorkflowCriteriaDto[]) {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
const links: WorkflowLink[] = []
|
2026-05-22 09:40:35 +00:00
|
|
|
|
criteria.forEach((source) => {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
if (source.kind === 'Compare' && source.compareOutcomes?.length) {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
source.compareOutcomes.forEach((outcome, index) => {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
addLink(links, criteria, source, outcome.targetId, outcome.label, `compare-${index}`, {
|
|
|
|
|
|
index,
|
|
|
|
|
|
count: source.compareOutcomes.length,
|
|
|
|
|
|
field: `compareOutcomes:${index}`,
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
addLink(links, criteria, source, source.nextOnStart, 'Sonraki', 'next', {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
index: 0,
|
|
|
|
|
|
count: 1,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
field: 'nextOnStart',
|
|
|
|
|
|
})
|
|
|
|
|
|
addLink(links, criteria, source, source.nextOnTrue, 'Doğru', 'true', {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
index: 0,
|
|
|
|
|
|
count: 2,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
field: 'nextOnTrue',
|
|
|
|
|
|
})
|
|
|
|
|
|
addLink(links, criteria, source, source.nextOnFalse, 'Yanlış', 'false', {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
index: 1,
|
|
|
|
|
|
count: 2,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
field: 'nextOnFalse',
|
|
|
|
|
|
})
|
|
|
|
|
|
addLink(links, criteria, source, source.nextOnApprove, 'Onay', 'approve', {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
index: 0,
|
|
|
|
|
|
count: 2,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
field: 'nextOnApprove',
|
|
|
|
|
|
})
|
|
|
|
|
|
addLink(links, criteria, source, source.nextOnReject, 'Red', 'reject', {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
index: 1,
|
|
|
|
|
|
count: 2,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
field: 'nextOnReject',
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
2026-07-08 07:29:45 +00:00
|
|
|
|
return assignLinkSlots(links)
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-08 07:29:45 +00:00
|
|
|
|
export function assignLinkSlots(links: WorkflowLink[]) {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
const endpointGroups = new Map<string, Endpoint[]>()
|
2026-05-22 11:50:09 +00:00
|
|
|
|
const addEndpoint = (nodeId: string, side: string, endpoint: Endpoint) => {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
const key = `${nodeId}:${side}`
|
|
|
|
|
|
if (!endpointGroups.has(key)) endpointGroups.set(key, [])
|
|
|
|
|
|
endpointGroups.get(key)?.push(endpoint)
|
|
|
|
|
|
}
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
|
|
|
|
|
links.forEach((link) => {
|
|
|
|
|
|
addEndpoint(link.source.id, sideToward(link.source, link.target), {
|
|
|
|
|
|
link,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
role: 'source',
|
|
|
|
|
|
})
|
2026-05-22 09:40:35 +00:00
|
|
|
|
addEndpoint(link.target.id, sideToward(link.target, link.source), {
|
|
|
|
|
|
link,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
role: 'target',
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
|
|
|
|
|
endpointGroups.forEach((endpoints) => {
|
|
|
|
|
|
endpoints.forEach((endpoint, index) => {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
if (endpoint.role === 'source') {
|
|
|
|
|
|
endpoint.link.sourcePort.sourceSlotIndex = index
|
|
|
|
|
|
endpoint.link.sourcePort.sourceSlotCount = endpoints.length
|
2026-05-22 09:40:35 +00:00
|
|
|
|
} else {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
endpoint.link.sourcePort.targetSlotIndex = index
|
|
|
|
|
|
endpoint.link.sourcePort.targetSlotCount = endpoints.length
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
2026-05-22 20:41:36 +00:00
|
|
|
|
})
|
|
|
|
|
|
})
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
|
|
|
|
|
links.forEach((link) => {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
link.sourcePort.routeIndex = link.sourcePort.targetSlotIndex ?? 0
|
|
|
|
|
|
link.sourcePort.routeCount = link.sourcePort.targetSlotCount ?? 1
|
|
|
|
|
|
})
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
return links
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
function sideToward(from: WorkflowCriteriaDto, to: WorkflowCriteriaDto) {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
const fromLeft = Number(from.positionX || 0)
|
|
|
|
|
|
const fromTop = Number(from.positionY || 0)
|
2026-05-22 09:40:35 +00:00
|
|
|
|
const fromCenter = {
|
|
|
|
|
|
x: fromLeft + nodeSize.width / 2,
|
|
|
|
|
|
y: fromTop + getNodeHeight(from) / 2,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
}
|
2026-05-22 09:40:35 +00:00
|
|
|
|
const toCenter = {
|
|
|
|
|
|
x: Number(to.positionX || 0) + nodeSize.width / 2,
|
|
|
|
|
|
y: Number(to.positionY || 0) + getNodeHeight(to) / 2,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
}
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
const dx = toCenter.x - fromCenter.x
|
|
|
|
|
|
const dy = toCenter.y - fromCenter.y
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
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'
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
export function getNodeOutcomes(item: WorkflowCriteriaDto): WorkflowOutcome[] {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
if (item.kind === 'Compare') {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
const outcomes = item.compareOutcomes?.length
|
|
|
|
|
|
? item.compareOutcomes
|
|
|
|
|
|
: [
|
2026-05-22 20:41:36 +00:00
|
|
|
|
{ label: 'Doğru', targetId: item.nextOnTrue },
|
|
|
|
|
|
{ label: 'Yanlış', targetId: item.nextOnFalse },
|
|
|
|
|
|
]
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
|
|
|
|
|
return outcomes.slice(0, 4).map((outcome, index) => ({
|
|
|
|
|
|
field: `compareOutcomes:${index}`,
|
|
|
|
|
|
label: outcome.label || `Durum ${index + 1}`,
|
|
|
|
|
|
targetId: outcome.targetId,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
}))
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
if (item.kind === 'Approval') {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
return [
|
2026-05-22 20:41:36 +00:00
|
|
|
|
{ field: 'nextOnApprove', label: 'Onay', targetId: item.nextOnApprove },
|
|
|
|
|
|
{ field: 'nextOnReject', label: 'Red', targetId: item.nextOnReject },
|
|
|
|
|
|
]
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
if (item.kind === 'End') return []
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
return [{ field: 'nextOnStart', label: 'Sonraki', targetId: item.nextOnStart }]
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
export function outcomeLabel(field?: string) {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
if (field?.startsWith('compareOutcomes:')) return 'Karşılaştırma durumu'
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
const labels: Record<string, string> = {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
nextOnStart: 'Sonraki',
|
|
|
|
|
|
nextOnTrue: 'Doğru',
|
|
|
|
|
|
nextOnFalse: 'Yanlış',
|
|
|
|
|
|
nextOnApprove: 'Onay',
|
|
|
|
|
|
nextOnReject: 'Red',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return field ? labels[field] : undefined
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function addLink(
|
2026-05-22 11:50:09 +00:00
|
|
|
|
links: WorkflowLink[],
|
|
|
|
|
|
criteria: WorkflowCriteriaDto[],
|
|
|
|
|
|
source: WorkflowCriteriaDto,
|
|
|
|
|
|
targetId: string | null | undefined,
|
|
|
|
|
|
label: string,
|
|
|
|
|
|
type: string,
|
|
|
|
|
|
sourcePort: WorkflowLinkPort = {},
|
2026-05-22 09:40:35 +00:00
|
|
|
|
) {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
if (!targetId) return
|
|
|
|
|
|
const target = criteria.find((item) => item.id === targetId)
|
2026-05-22 09:40:35 +00:00
|
|
|
|
if (target) {
|
|
|
|
|
|
links.push({
|
|
|
|
|
|
key: `${source.id}-${target.id}-${type}`,
|
|
|
|
|
|
source,
|
|
|
|
|
|
target,
|
|
|
|
|
|
label,
|
|
|
|
|
|
sourcePort,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
})
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
export function emptyCriteria(kind = 'Compare', listFormCode = ''): WorkflowCriteriaForm {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
return {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
id: '',
|
|
|
|
|
|
listFormCode,
|
2026-05-22 09:40:35 +00:00
|
|
|
|
kind,
|
|
|
|
|
|
title: defaultTitle(kind),
|
2026-05-23 21:12:01 +00:00
|
|
|
|
compareColumn: 'Price',
|
2026-05-22 20:41:36 +00:00
|
|
|
|
compareOperator: '>',
|
2026-05-22 09:40:35 +00:00
|
|
|
|
compareValue: 5000,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
approver: '',
|
|
|
|
|
|
nextOnStart: '',
|
|
|
|
|
|
nextOnTrue: '',
|
|
|
|
|
|
nextOnFalse: '',
|
|
|
|
|
|
nextOnApprove: '',
|
|
|
|
|
|
nextOnReject: '',
|
2026-05-22 09:40:35 +00:00
|
|
|
|
compareOutcomes:
|
2026-05-23 13:41:52 +00:00
|
|
|
|
kind === 'Compare' ? [emptyCompareOutcome1('>5000'), emptyCompareOutcome2('<=5000')] : [],
|
2026-05-22 09:40:35 +00:00
|
|
|
|
positionX: 32,
|
|
|
|
|
|
positionY: 150,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
}
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-06 22:22:35 +00:00
|
|
|
|
export function uniqueCriteriaTitle(
|
|
|
|
|
|
kind: string,
|
|
|
|
|
|
criteria: Array<Pick<WorkflowCriteriaDto, 'id' | 'kind' | 'title'>>,
|
|
|
|
|
|
currentId?: string | null,
|
|
|
|
|
|
preferredTitle?: string | null,
|
|
|
|
|
|
) {
|
|
|
|
|
|
const hasPreferredTitle = Boolean(preferredTitle?.trim())
|
|
|
|
|
|
const baseTitle = (preferredTitle || defaultTitle(kind)).trim()
|
|
|
|
|
|
const usedTitles = new Set(
|
|
|
|
|
|
criteria
|
|
|
|
|
|
.filter((item) => !currentId || item.id !== currentId)
|
|
|
|
|
|
.map((item) => (item.title || '').trim().toLocaleLowerCase('tr-TR'))
|
|
|
|
|
|
.filter(Boolean),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasPreferredTitle) {
|
|
|
|
|
|
const sameKindCount = criteria.filter(
|
|
|
|
|
|
(item) =>
|
|
|
|
|
|
(!currentId || item.id !== currentId) &&
|
|
|
|
|
|
item.kind === kind &&
|
|
|
|
|
|
isDefaultTitleVariant(item.title, baseTitle),
|
|
|
|
|
|
).length
|
|
|
|
|
|
let index = sameKindCount + 1
|
|
|
|
|
|
let candidate = `${baseTitle}${index}`
|
|
|
|
|
|
while (usedTitles.has(candidate.toLocaleLowerCase('tr-TR'))) {
|
|
|
|
|
|
index += 1
|
|
|
|
|
|
candidate = `${baseTitle}${index}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return candidate
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!usedTitles.has(baseTitle.toLocaleLowerCase('tr-TR'))) {
|
|
|
|
|
|
return baseTitle
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let index = 1
|
|
|
|
|
|
let candidate = `${baseTitle}${index}`
|
|
|
|
|
|
while (usedTitles.has(candidate.toLocaleLowerCase('tr-TR'))) {
|
|
|
|
|
|
index += 1
|
|
|
|
|
|
candidate = `${baseTitle}${index}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return candidate
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function uniqueCriteriaId(
|
|
|
|
|
|
criteria: Array<Pick<WorkflowCriteriaDto, 'id'>>,
|
|
|
|
|
|
reservedIds: string[] = [],
|
|
|
|
|
|
) {
|
|
|
|
|
|
const usedIds = new Set(
|
|
|
|
|
|
[...criteria.map((item) => item.id), ...reservedIds]
|
|
|
|
|
|
.map((id) => (id || '').trim().toLocaleLowerCase('tr-TR'))
|
|
|
|
|
|
.filter(Boolean),
|
|
|
|
|
|
)
|
|
|
|
|
|
const maxNumber = [...usedIds].reduce((max, id) => Math.max(max, parseCriteriaIdNumber(id)), 0)
|
|
|
|
|
|
let nextNumber = maxNumber + 1
|
|
|
|
|
|
let candidate = formatCriteriaId(nextNumber)
|
|
|
|
|
|
|
|
|
|
|
|
while (usedIds.has(candidate.toLocaleLowerCase('tr-TR'))) {
|
|
|
|
|
|
nextNumber += 1
|
|
|
|
|
|
candidate = formatCriteriaId(nextNumber)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return candidate
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseCriteriaIdNumber(id: string) {
|
|
|
|
|
|
const match = id.match(/^(?:n)?(\d+)$/iu)
|
|
|
|
|
|
return match ? Number(match[1]) : 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatCriteriaId(number: number) {
|
|
|
|
|
|
return `N${String(number).padStart(3, '0')}`.slice(-4)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isDefaultTitleVariant(title: string | null | undefined, baseTitle: string) {
|
|
|
|
|
|
const normalized = (title || '').trim()
|
|
|
|
|
|
return normalized === baseTitle || new RegExp(`^${escapeRegExp(baseTitle)}\\d+$`, 'u').test(normalized)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function escapeRegExp(value: string) {
|
|
|
|
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
const sharedPerson = item.approver || ''
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
...emptyCriteria(item.kind),
|
|
|
|
|
|
...item,
|
|
|
|
|
|
approver: sharedPerson,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
nextOnStart: item.nextOnStart || '',
|
|
|
|
|
|
nextOnTrue: item.nextOnTrue || '',
|
|
|
|
|
|
nextOnFalse: item.nextOnFalse || '',
|
|
|
|
|
|
nextOnApprove: item.nextOnApprove || '',
|
|
|
|
|
|
nextOnReject: item.nextOnReject || '',
|
2026-05-22 09:40:35 +00:00
|
|
|
|
compareOutcomes: item.compareOutcomes?.length
|
|
|
|
|
|
? item.compareOutcomes.map(toCompareOutcomeForm)
|
|
|
|
|
|
: emptyCriteria(item.kind).compareOutcomes,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
}
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput {
|
2026-06-06 18:31:03 +00:00
|
|
|
|
const sharedPerson =
|
|
|
|
|
|
item.kind === 'Approval' || item.kind === 'Inform' ? item.approver || '' : ''
|
2026-05-23 21:12:01 +00:00
|
|
|
|
const compareOutcomes = (item.compareOutcomes || [])
|
|
|
|
|
|
.slice(0, 4)
|
|
|
|
|
|
.filter((outcome) => outcome.label?.trim())
|
|
|
|
|
|
.map(normalizeCompareOutcome)
|
|
|
|
|
|
const firstCompareColumn = compareOutcomes
|
|
|
|
|
|
.flatMap((outcome) => outcome.conditions || [])
|
|
|
|
|
|
.find((condition) => condition.compareColumn)?.compareColumn
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: item.id || null,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
listFormCode: item.listFormCode || '',
|
2026-05-23 21:12:01 +00:00
|
|
|
|
kind: item.kind || 'Compare',
|
|
|
|
|
|
title: item.title || defaultTitle(item.kind || 'Compare'),
|
|
|
|
|
|
compareColumn: firstCompareColumn || item.compareColumn || 'Price',
|
|
|
|
|
|
compareOperator: item.compareOperator || '>',
|
2026-05-22 09:40:35 +00:00
|
|
|
|
compareValue: Number(item.compareValue || 0),
|
|
|
|
|
|
approver: sharedPerson,
|
2026-05-23 21:12:01 +00:00
|
|
|
|
nextOnStart: item.nextOnStart || '',
|
|
|
|
|
|
nextOnTrue: compareOutcomes[0]?.targetId || item.nextOnTrue || '',
|
|
|
|
|
|
nextOnFalse: compareOutcomes[1]?.targetId || item.nextOnFalse || '',
|
|
|
|
|
|
nextOnApprove: item.nextOnApprove || '',
|
|
|
|
|
|
nextOnReject: item.nextOnReject || '',
|
2026-05-22 09:40:35 +00:00
|
|
|
|
positionX: Number(item.positionX || 32),
|
|
|
|
|
|
positionY: Number(item.positionY || 150),
|
2026-05-23 21:12:01 +00:00
|
|
|
|
compareOutcomes,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
}
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
export function defaultTitle(kind: string) {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
return (
|
|
|
|
|
|
{
|
|
|
|
|
|
Start: 'İş Akışı Başlat',
|
2026-05-23 21:12:01 +00:00
|
|
|
|
Compare: 'Karşılaştırma',
|
|
|
|
|
|
Approval: 'Onay',
|
|
|
|
|
|
Inform: 'Bilgilendirme',
|
|
|
|
|
|
End: 'İş Akışı Bitir',
|
2026-05-22 20:41:36 +00:00
|
|
|
|
}[kind] ?? 'İş Akışı Adımı'
|
|
|
|
|
|
)
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 21:12:01 +00:00
|
|
|
|
export function emptyCompareOutcome1(label = 'Durum', compareColumn = 'Price'): CompareOutcomeDto {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
return {
|
|
|
|
|
|
label,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
targetId: '',
|
2026-05-23 21:12:01 +00:00
|
|
|
|
conditions: [{ compareColumn, compareOperator: '>', compareValue: 5000 }],
|
2026-05-22 20:41:36 +00:00
|
|
|
|
}
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-23 21:12:01 +00:00
|
|
|
|
export function emptyCompareOutcome2(label = 'Durum', compareColumn = 'Price'): CompareOutcomeDto {
|
2026-05-23 13:41:52 +00:00
|
|
|
|
return {
|
|
|
|
|
|
label,
|
|
|
|
|
|
targetId: '',
|
2026-05-23 21:12:01 +00:00
|
|
|
|
conditions: [{ compareColumn, compareOperator: '<=', compareValue: 5000 }],
|
2026-05-23 13:41:52 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
export function toCompareOutcomeForm(
|
|
|
|
|
|
outcome: Partial<CompareOutcomeDto> &
|
|
|
|
|
|
Partial<WorkflowConditionDto> & {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
conditions?: Partial<WorkflowConditionDto>[]
|
2026-05-22 11:50:09 +00:00
|
|
|
|
},
|
|
|
|
|
|
): CompareOutcomeDto {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
const conditions = outcome.conditions?.length
|
|
|
|
|
|
? outcome.conditions
|
|
|
|
|
|
: [
|
|
|
|
|
|
{
|
2026-05-23 21:12:01 +00:00
|
|
|
|
compareColumn: outcome.compareColumn || 'Price',
|
2026-05-22 20:41:36 +00:00
|
|
|
|
compareOperator: outcome.compareOperator || '>',
|
2026-05-22 09:40:35 +00:00
|
|
|
|
compareValue: outcome.compareValue || 0,
|
|
|
|
|
|
},
|
2026-05-22 20:41:36 +00:00
|
|
|
|
]
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
|
|
|
|
|
return {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
label: outcome.label || '',
|
|
|
|
|
|
targetId: outcome.targetId || '',
|
2026-05-22 09:40:35 +00:00
|
|
|
|
conditions: conditions.map((condition) => ({
|
2026-05-23 21:12:01 +00:00
|
|
|
|
compareColumn: condition.compareColumn || 'Price',
|
2026-05-22 20:41:36 +00:00
|
|
|
|
compareOperator: condition.compareOperator || '>',
|
2026-05-22 09:40:35 +00:00
|
|
|
|
compareValue: condition.compareValue ?? 0,
|
|
|
|
|
|
})),
|
2026-05-22 20:41:36 +00:00
|
|
|
|
}
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
export function normalizeCompareOutcome(
|
|
|
|
|
|
outcome: Partial<CompareOutcomeDto> &
|
|
|
|
|
|
Partial<WorkflowConditionDto> & {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
conditions?: Partial<WorkflowConditionDto>[]
|
2026-05-22 11:50:09 +00:00
|
|
|
|
},
|
|
|
|
|
|
): CompareOutcomeDto {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
const conditions = (
|
|
|
|
|
|
outcome.conditions?.length
|
|
|
|
|
|
? outcome.conditions
|
|
|
|
|
|
: [
|
|
|
|
|
|
{
|
2026-05-23 21:12:01 +00:00
|
|
|
|
compareColumn: outcome.compareColumn || 'Price',
|
2026-05-22 20:41:36 +00:00
|
|
|
|
compareOperator: outcome.compareOperator || '>',
|
2026-05-22 09:40:35 +00:00
|
|
|
|
compareValue: outcome.compareValue || 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
2026-05-22 20:41:36 +00:00
|
|
|
|
.filter((condition) => condition.compareOperator && String(condition.compareValue ?? '') !== '')
|
2026-05-22 09:40:35 +00:00
|
|
|
|
.map((condition) => ({
|
2026-05-23 21:12:01 +00:00
|
|
|
|
compareColumn: condition.compareColumn || 'Price',
|
2026-05-22 20:41:36 +00:00
|
|
|
|
compareOperator: condition.compareOperator || '>',
|
2026-05-22 09:40:35 +00:00
|
|
|
|
compareValue: Number(condition.compareValue || 0),
|
2026-05-22 20:41:36 +00:00
|
|
|
|
}))
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
|
|
|
|
|
return {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
label: (outcome.label || '').trim(),
|
2026-05-22 09:40:35 +00:00
|
|
|
|
targetId: outcome.targetId || null,
|
|
|
|
|
|
conditions,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
}
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
export function compareOutcomeRuleText(
|
|
|
|
|
|
outcome: Partial<CompareOutcomeDto> & Partial<WorkflowConditionDto>,
|
|
|
|
|
|
) {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
const conditions = outcome.conditions?.length
|
|
|
|
|
|
? outcome.conditions
|
2026-05-22 20:41:36 +00:00
|
|
|
|
: outcome.compareOperator
|
2026-05-22 09:40:35 +00:00
|
|
|
|
? [
|
|
|
|
|
|
{
|
2026-05-22 20:41:36 +00:00
|
|
|
|
compareColumn: outcome.compareColumn,
|
|
|
|
|
|
compareOperator: outcome.compareOperator,
|
2026-05-22 09:40:35 +00:00
|
|
|
|
compareValue: outcome.compareValue,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
2026-05-22 20:41:36 +00:00
|
|
|
|
: []
|
2026-05-22 09:40:35 +00:00
|
|
|
|
|
|
|
|
|
|
return conditions.length
|
|
|
|
|
|
? conditions
|
|
|
|
|
|
.map(
|
|
|
|
|
|
(condition) =>
|
2026-05-22 20:41:36 +00:00
|
|
|
|
`${condition.compareColumn} ${condition.compareOperator} ${formatCompactValue(condition.compareValue)}`,
|
2026-05-22 09:40:35 +00:00
|
|
|
|
)
|
2026-05-22 20:41:36 +00:00
|
|
|
|
.join(' ve ')
|
|
|
|
|
|
: 'Kural yok'
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
export function formatCompactValue(value: number | string | null | undefined) {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
return new Intl.NumberFormat('tr-TR', {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
maximumFractionDigits: 2,
|
2026-05-22 20:41:36 +00:00
|
|
|
|
}).format(Number(value || 0))
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
export function criteriaSummary(item: WorkflowCriteriaDto) {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
if (item.kind === 'Compare') {
|
2026-05-22 09:40:35 +00:00
|
|
|
|
return (
|
|
|
|
|
|
(item.compareOutcomes || [])
|
2026-05-22 20:41:36 +00:00
|
|
|
|
.map((outcome) => `${outcome.label}: ${compareOutcomeRuleText(outcome)}`)
|
|
|
|
|
|
.join(' / ') || '-'
|
|
|
|
|
|
)
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
2026-05-23 21:12:01 +00:00
|
|
|
|
if (item.kind === 'Approval' || item.kind === 'Inform') {
|
2026-07-08 07:29:45 +00:00
|
|
|
|
return `${item.title} ${item.approver ? `- ${formatWorkflowApprovers(item.approver)}` : ''}`
|
2026-05-23 21:12:01 +00:00
|
|
|
|
}
|
2026-06-06 18:31:03 +00:00
|
|
|
|
return item.title
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-08 07:29:45 +00:00
|
|
|
|
export function splitWorkflowApprovers(value?: string | null) {
|
|
|
|
|
|
return String(value ?? '')
|
|
|
|
|
|
.split(MULTIVALUE_DELIMITER)
|
|
|
|
|
|
.map((item) => item.trim())
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function formatWorkflowApprovers(value?: string | null) {
|
|
|
|
|
|
return splitWorkflowApprovers(value).join(', ')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 20:41:36 +00:00
|
|
|
|
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
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
export function statusClass(status?: string) {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
if (status === 'Onay Bekliyor') return 'pending'
|
|
|
|
|
|
if (status === 'Bitti') return 'done'
|
|
|
|
|
|
if (status === 'Bilgilendirildi') return 'info'
|
|
|
|
|
|
return ''
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 11:50:09 +00:00
|
|
|
|
export function formatMoney(value?: number | string | null) {
|
2026-05-22 20:41:36 +00:00
|
|
|
|
return new Intl.NumberFormat('tr-TR', {
|
|
|
|
|
|
style: 'currency',
|
|
|
|
|
|
currency: 'TRY',
|
|
|
|
|
|
}).format(Number(value || 0))
|
2026-05-22 09:40:35 +00:00
|
|
|
|
}
|