ListFormWorkflow Entegrasyon
This commit is contained in:
parent
85fee9c067
commit
49d82d6123
13 changed files with 548 additions and 144 deletions
|
|
@ -4320,6 +4320,12 @@
|
||||||
"en": "Widgets",
|
"en": "Widgets",
|
||||||
"tr": "Widget'lar"
|
"tr": "Widget'lar"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "ListForms.ListFormEdit.TabWorkflow",
|
||||||
|
"en": "Workflow",
|
||||||
|
"tr": "İş Akışı"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "ListForms.ListFormEdit.TabFields",
|
"key": "ListForms.ListFormEdit.TabFields",
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ export const tabVisibilityConfig: Record<string, string[]> = {
|
||||||
'fields',
|
'fields',
|
||||||
'editForm',
|
'editForm',
|
||||||
'widget',
|
'widget',
|
||||||
|
'workflow',
|
||||||
//Chart tabları
|
//Chart tabları
|
||||||
'commonSettings',
|
'commonSettings',
|
||||||
'series',
|
'series',
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,8 @@ export type CreateUpdateWorkflowInput = Partial<WorkflowItemDto> & {
|
||||||
tarih?: string
|
tarih?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SaveCriteriaInput = Partial<WorkflowCriteriaDto> & {
|
export type SaveCriteriaInput = Omit<Partial<WorkflowCriteriaDto>, 'id'> & {
|
||||||
|
id?: string | null
|
||||||
workflowItemId: string
|
workflowItemId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,4 +150,3 @@ export const workflowService = {
|
||||||
return response.data
|
return response.data
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export const columnOptions = ["Tutar", "Id"].map((value) => ({
|
||||||
label: value,
|
label: value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const kindIcon = {
|
export const kindIcon: Record<string, any> = {
|
||||||
Start: FiPlay as any,
|
Start: FiPlay as any,
|
||||||
Compare: FiGitBranch as any,
|
Compare: FiGitBranch as any,
|
||||||
Approval: FiCheck as any,
|
Approval: FiCheck as any,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,53 @@
|
||||||
import { getNodeHeight, nodeSize } from "./workflowConstants";
|
import { getNodeHeight, nodeSize } from "./workflowConstants";
|
||||||
|
import type {
|
||||||
|
CompareOutcomeDto,
|
||||||
|
SaveCriteriaInput,
|
||||||
|
WorkflowConditionDto,
|
||||||
|
WorkflowCriteriaDto,
|
||||||
|
WorkflowItemDto,
|
||||||
|
} from "@/services/workflow.service";
|
||||||
|
|
||||||
export function isPendingApproval(item, criteria) {
|
export type WorkflowCriteriaForm = Partial<WorkflowCriteriaDto> & {
|
||||||
|
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;
|
if (!item) return false;
|
||||||
|
|
||||||
return criteria.some(
|
return criteria.some(
|
||||||
|
|
@ -11,14 +58,14 @@ export function isPendingApproval(item, criteria) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildFitLayout(criteria) {
|
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<any, any[]>();
|
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);
|
||||||
|
|
@ -34,12 +81,12 @@ export function buildFitLayout(criteria) {
|
||||||
const top = 72;
|
const top = 72;
|
||||||
const left = 72;
|
const left = 72;
|
||||||
const xGap = 128;
|
const xGap = 128;
|
||||||
const positions = new Map();
|
const positions = new Map<string, { x: number; y: number }>();
|
||||||
|
|
||||||
sortedColumns.forEach((column, columnIndex) => {
|
sortedColumns.forEach((column, columnIndex) => {
|
||||||
const items = groups
|
const items = (groups.get(column) || []).sort((a, b) =>
|
||||||
.get(column)
|
compareLayoutNodes(a, b, rankById),
|
||||||
.sort((a, b) => 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;
|
||||||
|
|
@ -57,8 +104,8 @@ export function buildFitLayout(criteria) {
|
||||||
return positions;
|
return positions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fitColumn(item) {
|
function fitColumn(item: WorkflowCriteriaDto) {
|
||||||
const priority = {
|
const priority: Record<string, number> = {
|
||||||
Start: 0,
|
Start: 0,
|
||||||
Compare: 1,
|
Compare: 1,
|
||||||
Approval: 2,
|
Approval: 2,
|
||||||
|
|
@ -69,16 +116,25 @@ function fitColumn(item) {
|
||||||
return priority[item.kind] ?? 2;
|
return priority[item.kind] ?? 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareLayoutNodes(a, b, rankById = new Map()) {
|
function compareLayoutNodes(
|
||||||
|
a: WorkflowCriteriaDto,
|
||||||
|
b: WorkflowCriteriaDto,
|
||||||
|
rankById = new Map<string, number>(),
|
||||||
|
) {
|
||||||
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(criteria, links) {
|
function buildTraversalRanks(
|
||||||
const rankById = new Map();
|
criteria: WorkflowCriteriaDto[],
|
||||||
const outgoing = new Map<any, any[]>(criteria.map((item) => [item.id, []]));
|
links: WorkflowLink[],
|
||||||
|
) {
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
|
|
@ -90,6 +146,7 @@ function buildTraversalRanks(criteria, links) {
|
||||||
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const id = queue.shift();
|
const id = queue.shift();
|
||||||
|
if (!id) continue;
|
||||||
if (rankById.has(id)) continue;
|
if (rankById.has(id)) continue;
|
||||||
|
|
||||||
rankById.set(id, rankById.size);
|
rankById.set(id, rankById.size);
|
||||||
|
|
@ -105,8 +162,8 @@ function buildTraversalRanks(criteria, links) {
|
||||||
return rankById;
|
return rankById;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collectLinks(criteria) {
|
export function collectLinks(criteria: WorkflowCriteriaDto[]) {
|
||||||
const links = [];
|
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) => {
|
||||||
|
|
@ -156,12 +213,15 @@ export function collectLinks(criteria) {
|
||||||
return assignLinkSlots(links, criteria);
|
return assignLinkSlots(links, criteria);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assignLinkSlots(links, criteria) {
|
export function assignLinkSlots(
|
||||||
const endpointGroups = new Map();
|
links: WorkflowLink[],
|
||||||
const addEndpoint = (nodeId, side, endpoint) => {
|
criteria: WorkflowCriteriaDto[],
|
||||||
|
) {
|
||||||
|
const endpointGroups = new Map<string, 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) => {
|
||||||
|
|
@ -195,7 +255,7 @@ export function assignLinkSlots(links, criteria) {
|
||||||
return links;
|
return links;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sideToward(from, to) {
|
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 = {
|
||||||
|
|
@ -216,7 +276,7 @@ function sideToward(from, to) {
|
||||||
return dy >= 0 ? "bottom" : "top";
|
return dy >= 0 ? "bottom" : "top";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNodeOutcomes(item) {
|
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
|
||||||
|
|
@ -246,26 +306,28 @@ export function getNodeOutcomes(item) {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function outcomeLabel(field) {
|
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";
|
||||||
|
|
||||||
return {
|
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",
|
||||||
}[field];
|
};
|
||||||
|
|
||||||
|
return field ? labels[field] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addLink(
|
export function addLink(
|
||||||
links,
|
links: WorkflowLink[],
|
||||||
criteria,
|
criteria: WorkflowCriteriaDto[],
|
||||||
source,
|
source: WorkflowCriteriaDto,
|
||||||
targetId,
|
targetId: string | null | undefined,
|
||||||
label,
|
label: string,
|
||||||
type,
|
type: string,
|
||||||
sourcePort = null,
|
sourcePort: WorkflowLinkPort = {},
|
||||||
) {
|
) {
|
||||||
if (!targetId) return;
|
if (!targetId) return;
|
||||||
const target = criteria.find((item) => item.id === targetId);
|
const target = criteria.find((item) => item.id === targetId);
|
||||||
|
|
@ -280,7 +342,10 @@ export function addLink(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emptyCriteria(kind = "Compare", workflowItemId = null) {
|
export function emptyCriteria(
|
||||||
|
kind = "Compare",
|
||||||
|
workflowItemId = "",
|
||||||
|
): WorkflowCriteriaForm {
|
||||||
return {
|
return {
|
||||||
id: "",
|
id: "",
|
||||||
workflowItemId,
|
workflowItemId,
|
||||||
|
|
@ -305,7 +370,7 @@ export function emptyCriteria(kind = "Compare", workflowItemId = null) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toCriteriaForm(item) {
|
export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm {
|
||||||
const sharedPerson = item.approver || item.informPerson || "";
|
const sharedPerson = item.approver || item.informPerson || "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -324,13 +389,13 @@ export function toCriteriaForm(item) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeCriteria(item) {
|
export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput {
|
||||||
const sharedPerson = item.approver || item.informPerson || "";
|
const sharedPerson = item.approver || item.informPerson || "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
id: item.id || null,
|
id: item.id || null,
|
||||||
workflowItemId: item.workflowItemId || null,
|
workflowItemId: item.workflowItemId || "",
|
||||||
compareValue: Number(item.compareValue || 0),
|
compareValue: Number(item.compareValue || 0),
|
||||||
approver: sharedPerson,
|
approver: sharedPerson,
|
||||||
informPerson: sharedPerson,
|
informPerson: sharedPerson,
|
||||||
|
|
@ -343,17 +408,17 @@ export function normalizeCriteria(item) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defaultTitle(kind) {
|
export function defaultTitle(kind: string) {
|
||||||
return {
|
return {
|
||||||
Start: "İş Akışı Başlat",
|
Start: "İş Akışı Başlat",
|
||||||
Compare: "Tutar > 5000 TL",
|
Compare: "Tutar > 5000 TL",
|
||||||
Approval: "Onaylanacak Kişi",
|
Approval: "Onaylanacak Kişi",
|
||||||
Inform: "Bilgilendirme Yapılacak Personel",
|
Inform: "Bilgilendirme Yapılacak Personel",
|
||||||
End: "Akışı Bitir",
|
End: "Akışı Bitir",
|
||||||
}[kind];
|
}[kind] ?? "İş Akışı Adımı";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emptyCompareOutcome(label = "Durum") {
|
export function emptyCompareOutcome(label = "Durum"): CompareOutcomeDto {
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
targetId: "",
|
targetId: "",
|
||||||
|
|
@ -361,7 +426,12 @@ export function emptyCompareOutcome(label = "Durum") {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toCompareOutcomeForm(outcome) {
|
export function toCompareOutcomeForm(
|
||||||
|
outcome: Partial<CompareOutcomeDto> &
|
||||||
|
Partial<WorkflowConditionDto> & {
|
||||||
|
conditions?: Partial<WorkflowConditionDto>[];
|
||||||
|
},
|
||||||
|
): CompareOutcomeDto {
|
||||||
const conditions = outcome.conditions?.length
|
const conditions = outcome.conditions?.length
|
||||||
? outcome.conditions
|
? outcome.conditions
|
||||||
: [
|
: [
|
||||||
|
|
@ -383,7 +453,12 @@ export function toCompareOutcomeForm(outcome) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeCompareOutcome(outcome) {
|
export function normalizeCompareOutcome(
|
||||||
|
outcome: Partial<CompareOutcomeDto> &
|
||||||
|
Partial<WorkflowConditionDto> & {
|
||||||
|
conditions?: Partial<WorkflowConditionDto>[];
|
||||||
|
},
|
||||||
|
): CompareOutcomeDto {
|
||||||
const conditions = (
|
const conditions = (
|
||||||
outcome.conditions?.length
|
outcome.conditions?.length
|
||||||
? outcome.conditions
|
? outcome.conditions
|
||||||
|
|
@ -395,7 +470,10 @@ export function normalizeCompareOutcome(outcome) {
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
.filter((condition) => condition.operator && condition.compareValue !== "")
|
.filter(
|
||||||
|
(condition) =>
|
||||||
|
condition.operator && String(condition.compareValue ?? "") !== "",
|
||||||
|
)
|
||||||
.map((condition) => ({
|
.map((condition) => ({
|
||||||
column: condition.column || "Tutar",
|
column: condition.column || "Tutar",
|
||||||
operator: condition.operator || ">",
|
operator: condition.operator || ">",
|
||||||
|
|
@ -403,13 +481,15 @@ export function normalizeCompareOutcome(outcome) {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: outcome.label.trim(),
|
label: (outcome.label || "").trim(),
|
||||||
targetId: outcome.targetId || null,
|
targetId: outcome.targetId || null,
|
||||||
conditions,
|
conditions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compareOutcomeRuleText(outcome) {
|
export function compareOutcomeRuleText(
|
||||||
|
outcome: Partial<CompareOutcomeDto> & Partial<WorkflowConditionDto>,
|
||||||
|
) {
|
||||||
const conditions = outcome.conditions?.length
|
const conditions = outcome.conditions?.length
|
||||||
? outcome.conditions
|
? outcome.conditions
|
||||||
: outcome.operator
|
: outcome.operator
|
||||||
|
|
@ -432,13 +512,13 @@ export function compareOutcomeRuleText(outcome) {
|
||||||
: "Kural yok";
|
: "Kural yok";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatCompactValue(value) {
|
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) {
|
export function criteriaSummary(item: WorkflowCriteriaDto) {
|
||||||
if (item.kind === "Compare") {
|
if (item.kind === "Compare") {
|
||||||
return (
|
return (
|
||||||
(item.compareOutcomes || [])
|
(item.compareOutcomes || [])
|
||||||
|
|
@ -454,22 +534,25 @@ export function criteriaSummary(item) {
|
||||||
return item.title;
|
return item.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function targetTitle(criteria, id) {
|
export function targetTitle(
|
||||||
|
criteria: WorkflowCriteriaDto[],
|
||||||
|
id?: string | null,
|
||||||
|
) {
|
||||||
if (!id) return "-";
|
if (!id) return "-";
|
||||||
const item = criteria.find((candidate) => candidate.id === id);
|
const item = criteria.find((candidate) => candidate.id === id);
|
||||||
return item ? `${item.id} - ${item.title}` : id;
|
return item ? `${item.id} - ${item.title}` : id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function statusClass(status) {
|
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) {
|
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(value || 0);
|
}).format(Number(value || 0));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import FormTabRow from './FormTabRow'
|
||||||
import FormTabGantt from './FormTabGantt'
|
import FormTabGantt from './FormTabGantt'
|
||||||
import FormTabScheduler from './FormTabScheduler'
|
import FormTabScheduler from './FormTabScheduler'
|
||||||
import { APP_NAME } from '@/constants/app.constant'
|
import { APP_NAME } from '@/constants/app.constant'
|
||||||
|
import { FormTabWorkflow } from './FormTabWorkflow'
|
||||||
|
|
||||||
export interface FormEditProps {
|
export interface FormEditProps {
|
||||||
onSubmit: (
|
onSubmit: (
|
||||||
|
|
@ -271,6 +272,9 @@ const FormEdit = () => {
|
||||||
{visibleTabs.includes('widget') && (
|
{visibleTabs.includes('widget') && (
|
||||||
<TabNav value="widget">{translate('::ListForms.ListFormEdit.TabWidgets')}</TabNav>
|
<TabNav value="widget">{translate('::ListForms.ListFormEdit.TabWidgets')}</TabNav>
|
||||||
)}
|
)}
|
||||||
|
{visibleTabs.includes('workflow') && (
|
||||||
|
<TabNav value="workflow">{translate('::ListForms.ListFormEdit.TabWorkflow')}</TabNav>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Chart Tabs */}
|
{/* Chart Tabs */}
|
||||||
{visibleTabs.includes('commonSettings') && (
|
{visibleTabs.includes('commonSettings') && (
|
||||||
|
|
@ -387,6 +391,9 @@ const FormEdit = () => {
|
||||||
<TabContent value="widget" className="px-2">
|
<TabContent value="widget" className="px-2">
|
||||||
<FormTabWidgets listFormCode={listFormCode} />
|
<FormTabWidgets listFormCode={listFormCode} />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
<TabContent value="workflow" className="px-2">
|
||||||
|
<FormTabWorkflow listFormCode={listFormCode} />
|
||||||
|
</TabContent>
|
||||||
<TabContent value="fields" className="px-2">
|
<TabContent value="fields" className="px-2">
|
||||||
<FormFields
|
<FormFields
|
||||||
listFormCode={listFormCode}
|
listFormCode={listFormCode}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import type { FormEvent } from "react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import "dayjs/locale/tr";
|
import "dayjs/locale/tr";
|
||||||
import {
|
import {
|
||||||
|
|
@ -7,36 +8,71 @@ import {
|
||||||
isPendingApproval,
|
isPendingApproval,
|
||||||
normalizeCriteria,
|
normalizeCriteria,
|
||||||
toCriteriaForm,
|
toCriteriaForm,
|
||||||
|
type WorkflowCriteriaForm,
|
||||||
} from "@/utils/workflow/workflowHelpers";
|
} from "@/utils/workflow/workflowHelpers";
|
||||||
import { DashboardShell } from "./DashboardShell";
|
import {
|
||||||
import { workflowService, workflowService } from "@/services/workflow.service";
|
workflowService,
|
||||||
|
type WorkflowCriteriaDto,
|
||||||
|
type WorkflowItemDto,
|
||||||
|
} from "@/services/workflow.service";
|
||||||
|
import { DashboardShell } from "../workflow/DashboardShell";
|
||||||
|
|
||||||
dayjs.locale("tr");
|
dayjs.locale("tr");
|
||||||
|
|
||||||
export function Dashboard() {
|
type WorkflowForm = {
|
||||||
const [workflowItems, setWorkflowItems] = useState([]);
|
sorumlu: string;
|
||||||
const [criteria, setCriteria] = useState([]);
|
amount: number | string;
|
||||||
const [selectedWorkflowId, setSelectedWorkflowId] = useState(null);
|
};
|
||||||
|
|
||||||
|
type WorkflowEditForm = WorkflowForm & {
|
||||||
|
tarih: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PendingLink = {
|
||||||
|
sourceId: string;
|
||||||
|
outcome: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
type DragPreview = {
|
||||||
|
id: string;
|
||||||
|
delta: { x: number; y: number };
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
type DragEndEvent = {
|
||||||
|
active: { id: string };
|
||||||
|
delta: { x: number; y: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FormTabWorkflow(props: { listFormCode: string }) {
|
||||||
|
const [workflowItems, setWorkflowItems] = useState<WorkflowItemDto[]>([]);
|
||||||
|
const [criteria, setCriteria] = useState<WorkflowCriteriaDto[]>([]);
|
||||||
|
const [selectedWorkflowId, setSelectedWorkflowId] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
const [selectedId, setSelectedId] = useState("");
|
const [selectedId, setSelectedId] = useState("");
|
||||||
const [pendingLink, setPendingLink] = useState(null);
|
const [pendingLink, setPendingLink] = useState<PendingLink>(null);
|
||||||
const [workflowForm, setWorkflowForm] = useState({
|
const [workflowForm, setWorkflowForm] = useState<WorkflowForm>({
|
||||||
sorumlu: "",
|
sorumlu: "",
|
||||||
amount: 7200,
|
amount: 7200,
|
||||||
});
|
});
|
||||||
const [editingWorkflowId, setEditingWorkflowId] = useState(null);
|
const [editingWorkflowId, setEditingWorkflowId] = useState<string | null>(
|
||||||
const [workflowEditForm, setWorkflowEditForm] = useState({
|
null,
|
||||||
|
);
|
||||||
|
const [workflowEditForm, setWorkflowEditForm] = useState<WorkflowEditForm>({
|
||||||
sorumlu: "",
|
sorumlu: "",
|
||||||
tarih: "",
|
tarih: "",
|
||||||
amount: 0,
|
amount: 0,
|
||||||
});
|
});
|
||||||
const [criteriaForm, setCriteriaForm] = useState(emptyCriteria());
|
const [criteriaForm, setCriteriaForm] = useState<WorkflowCriteriaForm>(
|
||||||
const [dragPreview, setDragPreview] = useState(null);
|
emptyCriteria(),
|
||||||
|
);
|
||||||
|
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 [approvalDialogWorkflowId, setApprovalDialogWorkflowId] =
|
||||||
useState(null);
|
useState<string | null>(null);
|
||||||
const canvasRef = useRef(null);
|
const canvasRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const currentCriteria = useMemo(
|
const currentCriteria = useMemo(
|
||||||
() => criteria.filter((item) => item.workflowItemId === selectedWorkflowId),
|
() => criteria.filter((item) => item.workflowItemId === selectedWorkflowId),
|
||||||
|
|
@ -74,7 +110,7 @@ export function Dashboard() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const runAction = useCallback(
|
const runAction = useCallback(
|
||||||
async (action) => {
|
async (action: () => Promise<unknown>) => {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await action();
|
await action();
|
||||||
|
|
@ -120,7 +156,7 @@ export function Dashboard() {
|
||||||
}
|
}
|
||||||
}, [approvalDialogWorkflowId, pendingItems]);
|
}, [approvalDialogWorkflowId, pendingItems]);
|
||||||
|
|
||||||
const createWorkflow = (event) => {
|
const createWorkflow = (event: FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
runAction(async () => {
|
runAction(async () => {
|
||||||
const created = await workflowService.createWorkflow({
|
const created = await workflowService.createWorkflow({
|
||||||
|
|
@ -134,7 +170,7 @@ export function Dashboard() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const beginWorkflowEdit = (item) => {
|
const beginWorkflowEdit = (item: WorkflowItemDto) => {
|
||||||
setSelectedWorkflowId(item.id);
|
setSelectedWorkflowId(item.id);
|
||||||
setPendingLink(null);
|
setPendingLink(null);
|
||||||
setSelectedId("");
|
setSelectedId("");
|
||||||
|
|
@ -151,7 +187,7 @@ export function Dashboard() {
|
||||||
setWorkflowEditForm({ sorumlu: "", tarih: "", amount: 0 });
|
setWorkflowEditForm({ sorumlu: "", tarih: "", amount: 0 });
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveWorkflowEdit = (id) => {
|
const saveWorkflowEdit = (id: string) => {
|
||||||
runAction(async () => {
|
runAction(async () => {
|
||||||
await workflowService.updateWorkflow(id, {
|
await workflowService.updateWorkflow(id, {
|
||||||
sorumlu: workflowEditForm.sorumlu,
|
sorumlu: workflowEditForm.sorumlu,
|
||||||
|
|
@ -163,7 +199,7 @@ export function Dashboard() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const startWorkflow = useCallback(
|
const startWorkflow = useCallback(
|
||||||
(id) => {
|
(id: string) => {
|
||||||
runAction(async () => {
|
runAction(async () => {
|
||||||
await workflowService.startWorkflow(id);
|
await workflowService.startWorkflow(id);
|
||||||
const data = await loadState();
|
const data = await loadState();
|
||||||
|
|
@ -179,7 +215,7 @@ export function Dashboard() {
|
||||||
[loadState, runAction],
|
[loadState, runAction],
|
||||||
);
|
);
|
||||||
|
|
||||||
const saveCriteria = (event) => {
|
const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
runAction(async () => {
|
runAction(async () => {
|
||||||
await workflowService.saveCriteria(normalizeCriteria(criteriaForm));
|
await workflowService.saveCriteria(normalizeCriteria(criteriaForm));
|
||||||
|
|
@ -187,7 +223,7 @@ export function Dashboard() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addCriteria = (kind) => {
|
const addCriteria = (kind: string) => {
|
||||||
if (!selectedWorkflowId) return;
|
if (!selectedWorkflowId) return;
|
||||||
|
|
||||||
setDesignerTab("flow");
|
setDesignerTab("flow");
|
||||||
|
|
@ -202,7 +238,7 @@ export function Dashboard() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSelectedCriteria = useCallback(
|
const deleteSelectedCriteria = useCallback(
|
||||||
(criteriaId = selectedId) => {
|
(criteriaId: string = selectedId) => {
|
||||||
if (!criteriaId || busy) return;
|
if (!criteriaId || busy) return;
|
||||||
|
|
||||||
runAction(async () => {
|
runAction(async () => {
|
||||||
|
|
@ -214,17 +250,17 @@ export function Dashboard() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const disconnectLink = useCallback(
|
const disconnectLink = useCallback(
|
||||||
(sourceId, outcome) => {
|
(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 = { ...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,
|
||||||
|
|
@ -233,7 +269,7 @@ export function Dashboard() {
|
||||||
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[outcome] = null;
|
(next as Record<string, unknown>)[outcome] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
runAction(async () => {
|
runAction(async () => {
|
||||||
|
|
@ -246,10 +282,10 @@ export function Dashboard() {
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const deleteWithKeyboard = (event) => {
|
const deleteWithKeyboard = (event: globalThis.KeyboardEvent) => {
|
||||||
const activeTag = document.activeElement?.tagName?.toLowerCase();
|
const activeTag = document.activeElement?.tagName?.toLowerCase();
|
||||||
const isEditing =
|
const isEditing =
|
||||||
["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);
|
||||||
|
|
||||||
|
|
@ -267,7 +303,7 @@ export function Dashboard() {
|
||||||
return () => window.removeEventListener("keydown", deleteWithKeyboard);
|
return () => window.removeEventListener("keydown", deleteWithKeyboard);
|
||||||
}, [deleteSelectedCriteria, disconnectLink, pendingLink]);
|
}, [deleteSelectedCriteria, disconnectLink, pendingLink]);
|
||||||
|
|
||||||
const updateNodePosition = ({ active, delta }) => {
|
const updateNodePosition = ({ active, delta }: DragEndEvent) => {
|
||||||
setDragPreview(null);
|
setDragPreview(null);
|
||||||
|
|
||||||
const item = currentCriteria.find(
|
const item = currentCriteria.find(
|
||||||
|
|
@ -290,11 +326,11 @@ export function Dashboard() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const connectNodes = (sourceId, outcome, targetId) => {
|
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 = { ...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 || [])];
|
||||||
|
|
@ -305,7 +341,7 @@ export function Dashboard() {
|
||||||
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[outcome] = targetId;
|
(next as Record<string, unknown>)[outcome] = targetId;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPendingLink(null);
|
setPendingLink(null);
|
||||||
|
|
@ -340,13 +376,13 @@ export function Dashboard() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectWorkflow = (item) => {
|
const selectWorkflow = (item: WorkflowItemDto) => {
|
||||||
setSelectedWorkflowId(item.id);
|
setSelectedWorkflowId(item.id);
|
||||||
setPendingLink(null);
|
setPendingLink(null);
|
||||||
setSelectedId("");
|
setSelectedId("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const openCriteriaDetails = (id) => {
|
const openCriteriaDetails = (id: string) => {
|
||||||
setSelectedId(id);
|
setSelectedId(id);
|
||||||
setPendingLink(null);
|
setPendingLink(null);
|
||||||
setDesignerTab("criteria");
|
setDesignerTab("criteria");
|
||||||
|
|
@ -357,7 +393,7 @@ export function Dashboard() {
|
||||||
setSelectedId("");
|
setSelectedId("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const beginLink = (sourceId, outcome) => {
|
const beginLink = (sourceId: string, outcome: string) => {
|
||||||
setPendingLink({ sourceId, outcome });
|
setPendingLink({ sourceId, outcome });
|
||||||
setSelectedId(sourceId);
|
setSelectedId(sourceId);
|
||||||
};
|
};
|
||||||
|
|
@ -391,12 +427,12 @@ export function Dashboard() {
|
||||||
onCloseApprovalDialog={() => setApprovalDialogWorkflowId(null)}
|
onCloseApprovalDialog={() => setApprovalDialogWorkflowId(null)}
|
||||||
onConnectNodes={connectNodes}
|
onConnectNodes={connectNodes}
|
||||||
onCreateWorkflow={createWorkflow}
|
onCreateWorkflow={createWorkflow}
|
||||||
onDecision={(id, approved, note) =>
|
onDecision={(id: string, approved: boolean, note: string) =>
|
||||||
runAction(() => workflowService.decideWorkflow(id, { approved, note }))
|
runAction(() => workflowService.decideWorkflow(id, { approved, note }))
|
||||||
}
|
}
|
||||||
onDeleteSelectedCriteria={deleteSelectedCriteria}
|
onDeleteSelectedCriteria={deleteSelectedCriteria}
|
||||||
onDisconnectLink={disconnectLink}
|
onDisconnectLink={disconnectLink}
|
||||||
onDragMove={(event) =>
|
onDragMove={(event: DragEndEvent | null) =>
|
||||||
setDragPreview(
|
setDragPreview(
|
||||||
event ? { id: event.active.id, delta: event.delta } : null,
|
event ? { id: event.active.id, delta: event.delta } : null,
|
||||||
)
|
)
|
||||||
|
|
@ -1,12 +1,30 @@
|
||||||
import { formatMoney } from "@/utils/workflow/workflowHelpers";
|
import { formatMoney } from "@/utils/workflow/workflowHelpers";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FiCheck, FiSlash, FiX } from "react-icons/fi";
|
import { FiCheck, FiSlash, FiX } from "react-icons/fi";
|
||||||
|
import type {
|
||||||
|
WorkflowCriteriaDto,
|
||||||
|
WorkflowItemDto,
|
||||||
|
} from "@/services/workflow.service";
|
||||||
|
|
||||||
const CloseIcon = FiX as any;
|
const CloseIcon = FiX as any;
|
||||||
const CheckIcon = FiCheck as any;
|
const CheckIcon = FiCheck as any;
|
||||||
const SlashIcon = FiSlash as any;
|
const SlashIcon = FiSlash as any;
|
||||||
|
|
||||||
export function ApprovalDialog({ busy, criteria, items, onClose, onDecision }) {
|
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;
|
if (!items.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -59,8 +77,8 @@ function PendingApprovals({
|
||||||
busy,
|
busy,
|
||||||
onDecision,
|
onDecision,
|
||||||
showChrome = true,
|
showChrome = true,
|
||||||
}) {
|
}: Omit<ApprovalDialogProps, "onClose"> & { showChrome?: boolean }) {
|
||||||
const [notes, setNotes] = useState({});
|
const [notes, setNotes] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,29 @@ import {
|
||||||
emptyCompareOutcome,
|
emptyCompareOutcome,
|
||||||
targetTitle,
|
targetTitle,
|
||||||
} from "@/utils/workflow/workflowHelpers";
|
} from "@/utils/workflow/workflowHelpers";
|
||||||
|
import type {
|
||||||
|
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;
|
||||||
|
|
||||||
|
type CriteriaTableProps = {
|
||||||
|
criteria: WorkflowCriteriaDto[];
|
||||||
|
selectedWorkflow?: WorkflowItemDto | null;
|
||||||
|
selectedId: string;
|
||||||
|
activeNodeId?: string;
|
||||||
|
form: any;
|
||||||
|
busy: boolean;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
onChange: (form: any) => void;
|
||||||
|
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
onAddCriteria?: (kind: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export function CriteriaTable({
|
export function CriteriaTable({
|
||||||
criteria,
|
criteria,
|
||||||
selectedWorkflow,
|
selectedWorkflow,
|
||||||
|
|
@ -29,27 +48,32 @@ export function CriteriaTable({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onAddCriteria,
|
onAddCriteria,
|
||||||
}) {
|
}: CriteriaTableProps) {
|
||||||
const setField = (name, value) => onChange({ ...form, [name]: value });
|
const setField = (name: string, value: unknown) =>
|
||||||
|
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, patch) => {
|
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 = (outcomeIndex, conditionIndex, patch) => {
|
const updateCompareCondition = (
|
||||||
|
outcomeIndex: number,
|
||||||
|
conditionIndex: number,
|
||||||
|
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) => {
|
const addCompareCondition = (outcomeIndex: number) => {
|
||||||
const next = [...(form.compareOutcomes || [])];
|
const next = [...(form.compareOutcomes || [])];
|
||||||
next[outcomeIndex] = {
|
next[outcomeIndex] = {
|
||||||
...next[outcomeIndex],
|
...next[outcomeIndex],
|
||||||
|
|
@ -60,23 +84,27 @@ export function CriteriaTable({
|
||||||
};
|
};
|
||||||
setField("compareOutcomes", next);
|
setField("compareOutcomes", next);
|
||||||
};
|
};
|
||||||
const removeCompareCondition = (outcomeIndex, conditionIndex) => {
|
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(
|
||||||
(_, index) => 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) => {
|
const removeCompareOutcome = (index: number) => {
|
||||||
setField(
|
setField(
|
||||||
"compareOutcomes",
|
"compareOutcomes",
|
||||||
(form.compareOutcomes || []).filter(
|
(form.compareOutcomes || []).filter(
|
||||||
(_, itemIndex) => itemIndex !== index,
|
(_: unknown, itemIndex: number) => itemIndex !== index,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const targetSelect = (value, onSelectTarget, required = false) => (
|
const targetSelect = (
|
||||||
|
value: string | null | undefined,
|
||||||
|
onSelectTarget: (value: string) => void,
|
||||||
|
required = false,
|
||||||
|
) => (
|
||||||
<select
|
<select
|
||||||
required={required}
|
required={required}
|
||||||
value={value || ""}
|
value={value || ""}
|
||||||
|
|
@ -89,7 +117,7 @@ export function CriteriaTable({
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
);
|
);
|
||||||
const toggleRow = (id) => 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">
|
||||||
|
|
@ -276,7 +304,7 @@ export function CriteriaTable({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{(form.compareOutcomes || []).map(
|
{(form.compareOutcomes || []).map(
|
||||||
(outcome, index) => (
|
(outcome: CompareOutcomeDto, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="grid gap-2 border-t border-[#e4e7ec] pt-2 first:border-t-0 first:pt-0"
|
className="grid gap-2 border-t border-[#e4e7ec] pt-2 first:border-t-0 first:pt-0"
|
||||||
|
|
@ -444,7 +472,15 @@ export function CriteriaTable({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Field({ label, children, required = false }) {
|
function Field({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
required = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
required?: boolean;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<label className="grid gap-1.5 text-[12px] text-[#344054]">
|
<label className="grid gap-1.5 text-[12px] text-[#344054]">
|
||||||
<span>
|
<span>
|
||||||
|
|
@ -456,14 +492,14 @@ function Field({ label, children, required = false }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function criteriaSummaryContent(item) {
|
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 pl-[18px] [&_li]:pl-0.5">
|
||||||
{outcomes.map((outcome, index) => (
|
{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)}
|
||||||
|
|
@ -476,14 +512,17 @@ function criteriaSummaryContent(item) {
|
||||||
return criteriaSummary(item);
|
return criteriaSummary(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
function criteriaConnectionSummary(item, criteria) {
|
function criteriaConnectionSummary(
|
||||||
|
item: WorkflowCriteriaDto,
|
||||||
|
criteria: 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 pl-[18px] [&_li]:pl-0.5">
|
||||||
{outcomes.map((outcome, index) => (
|
{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)}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,59 @@
|
||||||
import { ApprovalDialog } from "./ApprovalDialog";
|
import { ApprovalDialog } from "./ApprovalDialog";
|
||||||
import { WorkflowDesigner } from "./WorkflowDesigner";
|
import { WorkflowDesigner } from "./WorkflowDesigner";
|
||||||
import { WorkflowTable } from "./WorkflowTable";
|
import { WorkflowTable } from "./WorkflowTable";
|
||||||
|
import type { FormEvent, RefObject } from "react";
|
||||||
|
import type {
|
||||||
|
WorkflowCriteriaDto,
|
||||||
|
WorkflowItemDto,
|
||||||
|
} from "@/services/workflow.service";
|
||||||
|
|
||||||
|
type DashboardShellProps = {
|
||||||
|
busy: boolean;
|
||||||
|
canvasRef: RefObject<HTMLDivElement>;
|
||||||
|
canvasZoom: number;
|
||||||
|
criteria: WorkflowCriteriaDto[];
|
||||||
|
criteriaForm: any;
|
||||||
|
currentCriteria: WorkflowCriteriaDto[];
|
||||||
|
designerTab: string;
|
||||||
|
dialogPendingItems: WorkflowItemDto[];
|
||||||
|
dragPreview: any;
|
||||||
|
editingWorkflowId: string | null;
|
||||||
|
pendingLink: any;
|
||||||
|
selectedId: string;
|
||||||
|
selectedWorkflow?: WorkflowItemDto | null;
|
||||||
|
selectedWorkflowId: string | null;
|
||||||
|
showApprovalDialog: boolean;
|
||||||
|
workflowEditForm: any;
|
||||||
|
workflowForm: any;
|
||||||
|
workflowItems: WorkflowItemDto[];
|
||||||
|
onAddCriteria: (kind: string) => void;
|
||||||
|
onBeginLink: (sourceId: string, outcome: string) => void;
|
||||||
|
onBeginWorkflowEdit: (item: WorkflowItemDto) => void;
|
||||||
|
onCancelWorkflowEdit: () => void;
|
||||||
|
onChangeCriteriaForm: (form: any) => void;
|
||||||
|
onClearCanvasSelection: () => void;
|
||||||
|
onCloseApprovalDialog: () => void;
|
||||||
|
onConnectNodes: (sourceId: string, outcome: string, targetId: string) => 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,
|
||||||
|
|
@ -48,7 +101,7 @@ export function DashboardShell({
|
||||||
onWorkflowFormChange,
|
onWorkflowFormChange,
|
||||||
onZoomIn,
|
onZoomIn,
|
||||||
onZoomOut,
|
onZoomOut,
|
||||||
}) {
|
}: DashboardShellProps) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<main className="grid gap-[18px] p-[18px]">
|
<main className="grid gap-[18px] p-[18px]">
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,34 @@ import {
|
||||||
collectLinks,
|
collectLinks,
|
||||||
getNodeOutcomes,
|
getNodeOutcomes,
|
||||||
outcomeLabel,
|
outcomeLabel,
|
||||||
|
type WorkflowLink,
|
||||||
|
type WorkflowOutcome,
|
||||||
} from "@/utils/workflow/workflowHelpers";
|
} from "@/utils/workflow/workflowHelpers";
|
||||||
|
import type { KeyboardEvent, MouseEvent, RefObject } from "react";
|
||||||
|
import type { WorkflowCriteriaDto } from "@/services/workflow.service";
|
||||||
|
|
||||||
|
type PendingLink = {
|
||||||
|
sourceId: string;
|
||||||
|
outcome: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
type DragPreview = {
|
||||||
|
id: string;
|
||||||
|
delta: { x: number; y: number };
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
type FlowNodeProps = {
|
||||||
|
item: WorkflowCriteriaDto;
|
||||||
|
criteria: WorkflowCriteriaDto[];
|
||||||
|
links: WorkflowLink[];
|
||||||
|
selected: boolean;
|
||||||
|
active: boolean;
|
||||||
|
pendingLink: PendingLink;
|
||||||
|
onSelect: () => void;
|
||||||
|
onOpenDetails: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
onBeginLink: (sourceId: string, outcome: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export function FlowCanvas({
|
export function FlowCanvas({
|
||||||
currentCriteria,
|
currentCriteria,
|
||||||
|
|
@ -29,6 +56,21 @@ export function FlowCanvas({
|
||||||
onDeleteLink,
|
onDeleteLink,
|
||||||
onBeginLink,
|
onBeginLink,
|
||||||
onConnect,
|
onConnect,
|
||||||
|
}: {
|
||||||
|
currentCriteria: WorkflowCriteriaDto[];
|
||||||
|
dragPreview: DragPreview;
|
||||||
|
zoom: number;
|
||||||
|
activeNodeId?: string;
|
||||||
|
selectedId: string;
|
||||||
|
pendingLink: PendingLink;
|
||||||
|
canvasRef: RefObject<HTMLDivElement>;
|
||||||
|
onSelect: (id: string) => void;
|
||||||
|
onOpenDetails: (id: string) => void;
|
||||||
|
onClearSelection: () => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
onDeleteLink: (sourceId: string, outcome: string) => void;
|
||||||
|
onBeginLink: (sourceId: string, outcome: string) => void;
|
||||||
|
onConnect: (sourceId: string, outcome: string, targetId: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const canvasWidth = Math.max(
|
const canvasWidth = Math.max(
|
||||||
1240,
|
1240,
|
||||||
|
|
@ -55,9 +97,12 @@ export function FlowCanvas({
|
||||||
),
|
),
|
||||||
[currentCriteria, dragPreview],
|
[currentCriteria, dragPreview],
|
||||||
);
|
);
|
||||||
const links = useMemo(() => collectLinks(arrowCriteria), [arrowCriteria]);
|
const links = useMemo(
|
||||||
|
() => collectLinks(arrowCriteria) as WorkflowLink[],
|
||||||
|
[arrowCriteria],
|
||||||
|
);
|
||||||
|
|
||||||
const handleKeyDown = (event) => {
|
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (event.key !== "Delete") return;
|
if (event.key !== "Delete") return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (pendingLink) {
|
if (pendingLink) {
|
||||||
|
|
@ -68,8 +113,11 @@ export function FlowCanvas({
|
||||||
onDelete(selectedId);
|
onDelete(selectedId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCanvasClick = (event) => {
|
const handleCanvasClick = (event: MouseEvent<HTMLDivElement>) => {
|
||||||
if (event.target.closest("[data-flow-node], [data-flow-link]")) return;
|
if (
|
||||||
|
(event.target as HTMLElement).closest("[data-flow-node], [data-flow-link]")
|
||||||
|
)
|
||||||
|
return;
|
||||||
onClearSelection();
|
onClearSelection();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -244,7 +292,7 @@ function FlowNode({
|
||||||
onOpenDetails,
|
onOpenDetails,
|
||||||
onDelete,
|
onDelete,
|
||||||
onBeginLink,
|
onBeginLink,
|
||||||
}) {
|
}: FlowNodeProps) {
|
||||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
disabled: Boolean(pendingLink),
|
disabled: Boolean(pendingLink),
|
||||||
|
|
@ -313,7 +361,7 @@ function FlowNode({
|
||||||
{item.id}
|
{item.id}
|
||||||
</small>
|
</small>
|
||||||
<div className="mt-0.5 flex max-w-full flex-wrap gap-[3px]">
|
<div className="mt-0.5 flex max-w-full flex-wrap gap-[3px]">
|
||||||
{getNodeOutcomes(item).map((outcome) => (
|
{(getNodeOutcomes(item) as WorkflowOutcome[]).map((outcome) => (
|
||||||
<span
|
<span
|
||||||
key={outcome.field}
|
key={outcome.field}
|
||||||
role="button"
|
role="button"
|
||||||
|
|
@ -343,7 +391,7 @@ function FlowNode({
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{getNodeOutcomes(item).map((outcome, index, outcomes) => {
|
{(getNodeOutcomes(item) as WorkflowOutcome[]).map((outcome, index, outcomes) => {
|
||||||
const link = links.find(
|
const link = links.find(
|
||||||
(candidate) =>
|
(candidate) =>
|
||||||
candidate.source.id === item.id &&
|
candidate.source.id === item.id &&
|
||||||
|
|
@ -399,7 +447,17 @@ function FlowNode({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Arrow({ link, criteria, pendingLink, onBeginLink }) {
|
function Arrow({
|
||||||
|
link,
|
||||||
|
criteria,
|
||||||
|
pendingLink,
|
||||||
|
onBeginLink,
|
||||||
|
}: {
|
||||||
|
link: WorkflowLink;
|
||||||
|
criteria: WorkflowCriteriaDto[];
|
||||||
|
pendingLink: PendingLink;
|
||||||
|
onBeginLink: (sourceId: string, outcome: string) => void;
|
||||||
|
}) {
|
||||||
const route = buildArrowRoute(
|
const route = buildArrowRoute(
|
||||||
link.source,
|
link.source,
|
||||||
link.target,
|
link.target,
|
||||||
|
|
@ -412,7 +470,9 @@ function Arrow({ link, criteria, pendingLink, onBeginLink }) {
|
||||||
pendingLink?.sourceId === link.source.id &&
|
pendingLink?.sourceId === link.source.id &&
|
||||||
pendingLink?.outcome === link.sourcePort?.field;
|
pendingLink?.outcome === link.sourcePort?.field;
|
||||||
|
|
||||||
const selectLink = (event) => {
|
const selectLink = (
|
||||||
|
event: MouseEvent<SVGGElement> | KeyboardEvent<SVGGElement>,
|
||||||
|
) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (!link.sourcePort?.field) return;
|
if (!link.sourcePort?.field) return;
|
||||||
|
|
@ -460,7 +520,15 @@ function Arrow({ link, criteria, pendingLink, onBeginLink }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ArrowLabel({ link, criteria, pendingLink }) {
|
function ArrowLabel({
|
||||||
|
link,
|
||||||
|
criteria,
|
||||||
|
pendingLink,
|
||||||
|
}: {
|
||||||
|
link: WorkflowLink;
|
||||||
|
criteria: WorkflowCriteriaDto[];
|
||||||
|
pendingLink: PendingLink;
|
||||||
|
}) {
|
||||||
if (!link.label) return null;
|
if (!link.label) return null;
|
||||||
|
|
||||||
const route = buildArrowRoute(
|
const route = buildArrowRoute(
|
||||||
|
|
@ -504,7 +572,7 @@ function ArrowLabel({ link, criteria, pendingLink }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkTone(link) {
|
function linkTone(link: WorkflowLink) {
|
||||||
const field = link.sourcePort?.field || "";
|
const field = link.sourcePort?.field || "";
|
||||||
const label = link.label || "";
|
const label = link.label || "";
|
||||||
if (field === "nextOnReject" || label === "Red") return "reject";
|
if (field === "nextOnReject" || label === "Red") return "reject";
|
||||||
|
|
@ -515,7 +583,7 @@ function linkTone(link) {
|
||||||
return "neutral";
|
return "neutral";
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkToneClass(tone) {
|
function linkToneClass(tone: string) {
|
||||||
if (tone === "next") {
|
if (tone === "next") {
|
||||||
return "[--link-color:#2563eb] [--link-soft:rgba(37,99,235,0.14)]";
|
return "[--link-color:#2563eb] [--link-soft:rgba(37,99,235,0.14)]";
|
||||||
}
|
}
|
||||||
|
|
@ -531,12 +599,17 @@ function linkToneClass(tone) {
|
||||||
return "[--link-color:#475467] [--link-soft:rgba(71,84,103,0.14)]";
|
return "[--link-color:#475467] [--link-soft:rgba(71,84,103,0.14)]";
|
||||||
}
|
}
|
||||||
|
|
||||||
function portSideClass(side) {
|
function portSideClass(side: string) {
|
||||||
if (side === "left" || side === "right") return "-translate-y-1/2";
|
if (side === "left" || side === "right") return "-translate-y-1/2";
|
||||||
return "-translate-x-1/2";
|
return "-translate-x-1/2";
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildArrowRoute(source, target, sourcePort = null, criteria = []) {
|
function buildArrowRoute(
|
||||||
|
source: any,
|
||||||
|
target: any,
|
||||||
|
sourcePort: any = null,
|
||||||
|
criteria: any[] = [],
|
||||||
|
) {
|
||||||
const sourceLeft = Number(source.positionX || 0);
|
const sourceLeft = Number(source.positionX || 0);
|
||||||
const sourceTop = Number(source.positionY || 0);
|
const sourceTop = Number(source.positionY || 0);
|
||||||
const targetLeft = Number(target.positionX || 0);
|
const targetLeft = Number(target.positionX || 0);
|
||||||
|
|
@ -636,7 +709,7 @@ function buildArrowRoute(source, target, sourcePort = null, criteria = []) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function sideToward(from, to) {
|
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 = {
|
||||||
|
|
@ -657,7 +730,13 @@ function sideToward(from, to) {
|
||||||
return dy >= 0 ? "bottom" : "top";
|
return dy >= 0 ? "bottom" : "top";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPortPoint(item, side, index = 0, count = 1, outward = 0) {
|
function getPortPoint(
|
||||||
|
item: any,
|
||||||
|
side: string,
|
||||||
|
index = 0,
|
||||||
|
count = 1,
|
||||||
|
outward = 0,
|
||||||
|
) {
|
||||||
const left = Number(item.positionX || 0);
|
const left = Number(item.positionX || 0);
|
||||||
const top = Number(item.positionY || 0);
|
const top = Number(item.positionY || 0);
|
||||||
const height = getNodeHeight(item);
|
const height = getNodeHeight(item);
|
||||||
|
|
@ -671,7 +750,7 @@ function getPortPoint(item, side, index = 0, count = 1, outward = 0) {
|
||||||
return { x: left + horizontalOffset, y: top + height + outward };
|
return { x: left + horizontalOffset, y: top + height + outward };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPortStyle(item, side, index, count) {
|
function getPortStyle(item: any, side: string, index: number, count: number) {
|
||||||
const point = getPortPoint(
|
const point = getPortPoint(
|
||||||
{ ...item, positionX: 0, positionY: 0 },
|
{ ...item, positionX: 0, positionY: 0 },
|
||||||
side,
|
side,
|
||||||
|
|
@ -688,7 +767,7 @@ function getPortStyle(item, side, index, count) {
|
||||||
return { left: point.x - borderOffset, bottom: edgeOffset };
|
return { left: point.x - borderOffset, bottom: edgeOffset };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPortOffsetAlong(length, index, count) {
|
function getPortOffsetAlong(length: number, index: number, count: number) {
|
||||||
if (count <= 1) return length / 2;
|
if (count <= 1) return length / 2;
|
||||||
const gap = 28;
|
const gap = 28;
|
||||||
const center = length / 2;
|
const center = length / 2;
|
||||||
|
|
@ -696,7 +775,13 @@ function getPortOffsetAlong(length, index, count) {
|
||||||
return Math.min(length - 18, Math.max(18, center + offset));
|
return Math.min(length - 18, Math.max(18, center + offset));
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSideAwareRoute(start, startSide, end, endSide, slots: any = {}) {
|
function buildSideAwareRoute(
|
||||||
|
start: any,
|
||||||
|
startSide: string,
|
||||||
|
end: any,
|
||||||
|
endSide: string,
|
||||||
|
slots: any = {},
|
||||||
|
) {
|
||||||
const routeOffset = slotOffset(slots.routeIndex, slots.routeCount, 46);
|
const routeOffset = slotOffset(slots.routeIndex, slots.routeCount, 46);
|
||||||
const exit = extendFromSide(start, startSide, 26);
|
const exit = extendFromSide(start, startSide, 26);
|
||||||
const entry = extendFromSide(end, endSide, 38);
|
const entry = extendFromSide(end, endSide, 38);
|
||||||
|
|
@ -765,8 +850,8 @@ function buildSideAwareRoute(start, startSide, end, endSide, slots: any = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function routeLabelPoint(points, slots: any = {}) {
|
function routeLabelPoint(points: any[], slots: any = {}) {
|
||||||
const segments = [];
|
const segments: any[] = [];
|
||||||
for (let index = 1; index < points.length - 1; index += 1) {
|
for (let index = 1; index < points.length - 1; index += 1) {
|
||||||
const a = points[index - 1];
|
const a = points[index - 1];
|
||||||
const b = points[index];
|
const b = points[index];
|
||||||
|
|
@ -806,7 +891,7 @@ function routeLabelPoint(points, slots: any = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function compactRoutePoints(points) {
|
function compactRoutePoints(points: any[]) {
|
||||||
return points.filter((point, index) => {
|
return points.filter((point, index) => {
|
||||||
if (index === 0) return true;
|
if (index === 0) return true;
|
||||||
const previous = points[index - 1];
|
const previous = points[index - 1];
|
||||||
|
|
@ -826,18 +911,18 @@ function targetLaneDistance(index = 0, count = 1) {
|
||||||
return 40 + Math.max(0, Math.min(index, count - 1)) * 32;
|
return 40 + Math.max(0, Math.min(index, count - 1)) * 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extendFromSide(point, side, distance) {
|
function extendFromSide(point: any, side: string, distance: number) {
|
||||||
if (side === "left") return { x: point.x - distance, y: point.y };
|
if (side === "left") return { x: point.x - distance, y: point.y };
|
||||||
if (side === "right") return { x: point.x + distance, y: point.y };
|
if (side === "right") return { x: point.x + distance, y: point.y };
|
||||||
if (side === "top") return { x: point.x, y: point.y - distance };
|
if (side === "top") return { x: point.x, y: point.y - distance };
|
||||||
return { x: point.x, y: point.y + distance };
|
return { x: point.x, y: point.y + distance };
|
||||||
}
|
}
|
||||||
|
|
||||||
function isHorizontalSide(side) {
|
function isHorizontalSide(side: string) {
|
||||||
return side === "left" || side === "right";
|
return side === "left" || side === "right";
|
||||||
}
|
}
|
||||||
|
|
||||||
function roundedPolylinePath(points) {
|
function roundedPolylinePath(points: any[]) {
|
||||||
const routePoints = points.filter((point, index) => {
|
const routePoints = points.filter((point, index) => {
|
||||||
if (index === 0) return true;
|
if (index === 0) return true;
|
||||||
const previous = points[index - 1];
|
const previous = points[index - 1];
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,45 @@ 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 {
|
||||||
|
WorkflowCriteriaDto,
|
||||||
|
WorkflowItemDto,
|
||||||
|
} from '@/services/workflow.service'
|
||||||
|
|
||||||
const MaximizeIcon = FiMaximize2 as any
|
const MaximizeIcon = FiMaximize2 as any
|
||||||
const ZoomInIcon = FiZoomIn as any
|
const ZoomInIcon = FiZoomIn as any
|
||||||
const ZoomOutIcon = FiZoomOut as any
|
const ZoomOutIcon = FiZoomOut as any
|
||||||
|
|
||||||
|
type WorkflowDesignerProps = {
|
||||||
|
busy: boolean
|
||||||
|
canvasRef: RefObject<HTMLDivElement>
|
||||||
|
canvasZoom: number
|
||||||
|
criteriaForm: any
|
||||||
|
currentCriteria: WorkflowCriteriaDto[]
|
||||||
|
designerTab: string
|
||||||
|
dragPreview: any
|
||||||
|
pendingLink: any
|
||||||
|
selectedCriteriaId: string
|
||||||
|
selectedWorkflow?: WorkflowItemDto | null
|
||||||
|
onAddCriteria: (kind: string) => void
|
||||||
|
onBeginLink: (sourceId: string, outcome: string) => void
|
||||||
|
onChangeCriteriaForm: (form: any) => void
|
||||||
|
onClearSelection: () => void
|
||||||
|
onConnect: (sourceId: string, outcome: string, targetId: string) => void
|
||||||
|
onDeleteCriteria: (criteriaId?: string) => void
|
||||||
|
onDeleteLink: (sourceId: string, outcome: string) => void
|
||||||
|
onDragMove: (event: any) => void
|
||||||
|
onFitLayout: () => void
|
||||||
|
onOpenDetails: (id: string) => void
|
||||||
|
onSaveCriteria: (event: FormEvent<HTMLFormElement>) => void
|
||||||
|
onSelectCriteria: (id: string) => void
|
||||||
|
onSetDesignerTab: (tab: string) => void
|
||||||
|
onUpdateNodePosition: (event: any) => void
|
||||||
|
onZoomIn: () => void
|
||||||
|
onZoomOut: () => void
|
||||||
|
}
|
||||||
|
|
||||||
export function WorkflowDesigner({
|
export function WorkflowDesigner({
|
||||||
busy,
|
busy,
|
||||||
canvasRef,
|
canvasRef,
|
||||||
|
|
@ -37,7 +71,7 @@ export function WorkflowDesigner({
|
||||||
onUpdateNodePosition,
|
onUpdateNodePosition,
|
||||||
onZoomIn,
|
onZoomIn,
|
||||||
onZoomOut,
|
onZoomOut,
|
||||||
}) {
|
}: 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-app-line bg-app-surface 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">
|
||||||
|
|
@ -112,6 +146,14 @@ function DesignerToolbar({
|
||||||
onFitLayout,
|
onFitLayout,
|
||||||
onZoomIn,
|
onZoomIn,
|
||||||
onZoomOut,
|
onZoomOut,
|
||||||
|
}: {
|
||||||
|
busy: boolean
|
||||||
|
currentCriteria: WorkflowCriteriaDto[]
|
||||||
|
zoom: number
|
||||||
|
onAddCriteria: (kind: string) => void
|
||||||
|
onFitLayout: () => void
|
||||||
|
onZoomIn: () => void
|
||||||
|
onZoomOut: () => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap justify-end gap-2">
|
<div className="flex flex-wrap justify-end gap-2">
|
||||||
|
|
@ -163,7 +205,13 @@ function DesignerToolbar({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DesignerTabs({ activeTab, onChange }) {
|
function DesignerTabs({
|
||||||
|
activeTab,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
activeTab: string
|
||||||
|
onChange: (tab: string) => void
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex gap-1 rounded-lg" role="tablist" aria-label="Akış tasarımı">
|
<div className="inline-flex gap-1 rounded-lg" role="tablist" aria-label="Akış tasarımı">
|
||||||
<button
|
<button
|
||||||
|
|
@ -209,7 +257,11 @@ function DesignerTabs({ activeTab, onChange }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ApprovalHistoryTable({ selectedWorkflow }: any) {
|
function ApprovalHistoryTable({
|
||||||
|
selectedWorkflow,
|
||||||
|
}: {
|
||||||
|
selectedWorkflow?: WorkflowItemDto | null
|
||||||
|
}) {
|
||||||
const history = selectedWorkflow?.history || []
|
const history = selectedWorkflow?.history || []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -229,7 +281,7 @@ function ApprovalHistoryTable({ selectedWorkflow }: any) {
|
||||||
<td colSpan={3}>Seçili iş akışı için açıklama kaydı yok.</td>
|
<td colSpan={3}>Seçili iş akışı için açıklama kaydı yok.</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{history.map((item: any, index: number) => (
|
{history.map((item: WorkflowItemDto['history'][number], index: number) => (
|
||||||
<tr key={`${item.time}-${index}`}>
|
<tr key={`${item.time}-${index}`}>
|
||||||
<td>{dayjs(item.time).format('DD MMM YYYY HH:mm')}</td>
|
<td>{dayjs(item.time).format('DD MMM YYYY HH:mm')}</td>
|
||||||
<td>{item.action}</td>
|
<td>{item.action}</td>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ import { FiCheck, FiEdit2, FiPlay, FiPlus, FiRefreshCw, FiX } from 'react-icons/
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { formatMoney, statusClass } from '@/utils/workflow/workflowHelpers'
|
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 CheckIcon = FiCheck as any
|
||||||
const EditIcon = FiEdit2 as any
|
const EditIcon = FiEdit2 as any
|
||||||
|
|
@ -10,6 +15,25 @@ const PlusIcon = FiPlus as any
|
||||||
const RefreshIcon = FiRefreshCw as any
|
const RefreshIcon = FiRefreshCw as any
|
||||||
const CloseIcon = FiX 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({
|
export function WorkflowTable({
|
||||||
items,
|
items,
|
||||||
criteria,
|
criteria,
|
||||||
|
|
@ -27,7 +51,7 @@ export function WorkflowTable({
|
||||||
onSelect,
|
onSelect,
|
||||||
onStart,
|
onStart,
|
||||||
onResetDemo,
|
onResetDemo,
|
||||||
}: any) {
|
}: WorkflowTableProps) {
|
||||||
return (
|
return (
|
||||||
<section className="min-w-0 rounded-lg border border-app-line bg-app-surface p-4">
|
<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="mb-3 flex items-start justify-between gap-4 max-[720px]:flex-col max-[720px]:items-stretch">
|
||||||
|
|
@ -93,9 +117,9 @@ export function WorkflowTable({
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{items.map((item: any) => {
|
{items.map((item) => {
|
||||||
const currentStep = criteria.find(
|
const currentStep = criteria.find(
|
||||||
(candidate: any) =>
|
(candidate) =>
|
||||||
candidate.workflowItemId === item.id && candidate.id === item.currentNodeId,
|
candidate.workflowItemId === item.id && candidate.id === item.currentNodeId,
|
||||||
)
|
)
|
||||||
const statusTitle = currentStep?.title || item.durum
|
const statusTitle = currentStep?.title || item.durum
|
||||||
|
|
@ -240,7 +264,7 @@ export function WorkflowTable({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusPill({ status }: any) {
|
function StatusPill({ status }: { status: string }) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue