ListFormWorkflow Entegrasyon

This commit is contained in:
Sedat ÖZTÜRK 2026-05-22 14:50:09 +03:00
parent 85fee9c067
commit 49d82d6123
13 changed files with 548 additions and 144 deletions

View file

@ -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",

View file

@ -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',

View file

@ -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
}, },
} }

View file

@ -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,

View file

@ -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));
} }

View file

@ -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}

View file

@ -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,
) )

View file

@ -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 = (
<> <>

View file

@ -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)}

View file

@ -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]">

View file

@ -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];

View file

@ -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 akışı için ıklama kaydı yok.</td> <td colSpan={3}>Seçili akışı için ı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>

View file

@ -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(