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",
|
||||
"tr": "Widget'lar"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.TabWorkflow",
|
||||
"en": "Workflow",
|
||||
"tr": "İş Akışı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListFormEdit.TabFields",
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ export const tabVisibilityConfig: Record<string, string[]> = {
|
|||
'fields',
|
||||
'editForm',
|
||||
'widget',
|
||||
'workflow',
|
||||
//Chart tabları
|
||||
'commonSettings',
|
||||
'series',
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ export type CreateUpdateWorkflowInput = Partial<WorkflowItemDto> & {
|
|||
tarih?: string
|
||||
}
|
||||
|
||||
export type SaveCriteriaInput = Partial<WorkflowCriteriaDto> & {
|
||||
export type SaveCriteriaInput = Omit<Partial<WorkflowCriteriaDto>, 'id'> & {
|
||||
id?: string | null
|
||||
workflowItemId: string
|
||||
}
|
||||
|
||||
|
|
@ -149,4 +150,3 @@ export const workflowService = {
|
|||
return response.data
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export const columnOptions = ["Tutar", "Id"].map((value) => ({
|
|||
label: value,
|
||||
}));
|
||||
|
||||
export const kindIcon = {
|
||||
export const kindIcon: Record<string, any> = {
|
||||
Start: FiPlay as any,
|
||||
Compare: FiGitBranch as any,
|
||||
Approval: FiCheck as any,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,53 @@
|
|||
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;
|
||||
|
||||
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 rankById = buildTraversalRanks(criteria, links);
|
||||
const groups = new Map<any, any[]>();
|
||||
const groups = new Map<number, WorkflowCriteriaDto[]>();
|
||||
criteria.forEach((item) => {
|
||||
const column = fitColumn(item);
|
||||
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);
|
||||
|
|
@ -34,12 +81,12 @@ export function buildFitLayout(criteria) {
|
|||
const top = 72;
|
||||
const left = 72;
|
||||
const xGap = 128;
|
||||
const positions = new Map();
|
||||
const positions = new Map<string, { x: number; y: number }>();
|
||||
|
||||
sortedColumns.forEach((column, columnIndex) => {
|
||||
const items = groups
|
||||
.get(column)
|
||||
.sort((a, b) => compareLayoutNodes(a, b, rankById));
|
||||
const items = (groups.get(column) || []).sort((a, b) =>
|
||||
compareLayoutNodes(a, b, rankById),
|
||||
);
|
||||
const groupHeight =
|
||||
items.reduce((sum, item) => sum + getNodeHeight(item), 0) +
|
||||
Math.max(0, items.length - 1) * yGap;
|
||||
|
|
@ -57,8 +104,8 @@ export function buildFitLayout(criteria) {
|
|||
return positions;
|
||||
}
|
||||
|
||||
function fitColumn(item) {
|
||||
const priority = {
|
||||
function fitColumn(item: WorkflowCriteriaDto) {
|
||||
const priority: Record<string, number> = {
|
||||
Start: 0,
|
||||
Compare: 1,
|
||||
Approval: 2,
|
||||
|
|
@ -69,16 +116,25 @@ function fitColumn(item) {
|
|||
return priority[item.kind] ?? 2;
|
||||
}
|
||||
|
||||
function compareLayoutNodes(a, b, rankById = new Map()) {
|
||||
function compareLayoutNodes(
|
||||
a: WorkflowCriteriaDto,
|
||||
b: WorkflowCriteriaDto,
|
||||
rankById = new Map<string, number>(),
|
||||
) {
|
||||
return (
|
||||
(rankById.get(a.id) ?? 999) - (rankById.get(b.id) ?? 999) ||
|
||||
a.title.localeCompare(b.title, "tr")
|
||||
);
|
||||
}
|
||||
|
||||
function buildTraversalRanks(criteria, links) {
|
||||
const rankById = new Map();
|
||||
const outgoing = new Map<any, any[]>(criteria.map((item) => [item.id, []]));
|
||||
function buildTraversalRanks(
|
||||
criteria: WorkflowCriteriaDto[],
|
||||
links: WorkflowLink[],
|
||||
) {
|
||||
const rankById = new Map<string, number>();
|
||||
const outgoing = new Map<string, string[]>(
|
||||
criteria.map((item) => [item.id, []]),
|
||||
);
|
||||
links.forEach((link) => {
|
||||
outgoing.get(link.source.id)?.push(link.target.id);
|
||||
});
|
||||
|
|
@ -90,6 +146,7 @@ function buildTraversalRanks(criteria, links) {
|
|||
|
||||
while (queue.length) {
|
||||
const id = queue.shift();
|
||||
if (!id) continue;
|
||||
if (rankById.has(id)) continue;
|
||||
|
||||
rankById.set(id, rankById.size);
|
||||
|
|
@ -105,8 +162,8 @@ function buildTraversalRanks(criteria, links) {
|
|||
return rankById;
|
||||
}
|
||||
|
||||
export function collectLinks(criteria) {
|
||||
const links = [];
|
||||
export function collectLinks(criteria: WorkflowCriteriaDto[]) {
|
||||
const links: WorkflowLink[] = [];
|
||||
criteria.forEach((source) => {
|
||||
if (source.kind === "Compare" && source.compareOutcomes?.length) {
|
||||
source.compareOutcomes.forEach((outcome, index) => {
|
||||
|
|
@ -156,12 +213,15 @@ export function collectLinks(criteria) {
|
|||
return assignLinkSlots(links, criteria);
|
||||
}
|
||||
|
||||
export function assignLinkSlots(links, criteria) {
|
||||
const endpointGroups = new Map();
|
||||
const addEndpoint = (nodeId, side, endpoint) => {
|
||||
export function assignLinkSlots(
|
||||
links: WorkflowLink[],
|
||||
criteria: WorkflowCriteriaDto[],
|
||||
) {
|
||||
const endpointGroups = new Map<string, Endpoint[]>();
|
||||
const addEndpoint = (nodeId: string, side: string, endpoint: Endpoint) => {
|
||||
const key = `${nodeId}:${side}`;
|
||||
if (!endpointGroups.has(key)) endpointGroups.set(key, []);
|
||||
endpointGroups.get(key).push(endpoint);
|
||||
endpointGroups.get(key)?.push(endpoint);
|
||||
};
|
||||
|
||||
links.forEach((link) => {
|
||||
|
|
@ -195,7 +255,7 @@ export function assignLinkSlots(links, criteria) {
|
|||
return links;
|
||||
}
|
||||
|
||||
function sideToward(from, to) {
|
||||
function sideToward(from: WorkflowCriteriaDto, to: WorkflowCriteriaDto) {
|
||||
const fromLeft = Number(from.positionX || 0);
|
||||
const fromTop = Number(from.positionY || 0);
|
||||
const fromCenter = {
|
||||
|
|
@ -216,7 +276,7 @@ function sideToward(from, to) {
|
|||
return dy >= 0 ? "bottom" : "top";
|
||||
}
|
||||
|
||||
export function getNodeOutcomes(item) {
|
||||
export function getNodeOutcomes(item: WorkflowCriteriaDto): WorkflowOutcome[] {
|
||||
if (item.kind === "Compare") {
|
||||
const outcomes = item.compareOutcomes?.length
|
||||
? 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";
|
||||
|
||||
return {
|
||||
const labels: Record<string, string> = {
|
||||
nextOnStart: "Sonraki",
|
||||
nextOnTrue: "Doğru",
|
||||
nextOnFalse: "Yanlış",
|
||||
nextOnApprove: "Onay",
|
||||
nextOnReject: "Red",
|
||||
}[field];
|
||||
};
|
||||
|
||||
return field ? labels[field] : undefined;
|
||||
}
|
||||
|
||||
export function addLink(
|
||||
links,
|
||||
criteria,
|
||||
source,
|
||||
targetId,
|
||||
label,
|
||||
type,
|
||||
sourcePort = null,
|
||||
links: WorkflowLink[],
|
||||
criteria: WorkflowCriteriaDto[],
|
||||
source: WorkflowCriteriaDto,
|
||||
targetId: string | null | undefined,
|
||||
label: string,
|
||||
type: string,
|
||||
sourcePort: WorkflowLinkPort = {},
|
||||
) {
|
||||
if (!targetId) return;
|
||||
const target = criteria.find((item) => item.id === targetId);
|
||||
|
|
@ -280,7 +342,10 @@ export function addLink(
|
|||
}
|
||||
}
|
||||
|
||||
export function emptyCriteria(kind = "Compare", workflowItemId = null) {
|
||||
export function emptyCriteria(
|
||||
kind = "Compare",
|
||||
workflowItemId = "",
|
||||
): WorkflowCriteriaForm {
|
||||
return {
|
||||
id: "",
|
||||
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 || "";
|
||||
|
||||
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 || "";
|
||||
|
||||
return {
|
||||
...item,
|
||||
id: item.id || null,
|
||||
workflowItemId: item.workflowItemId || null,
|
||||
workflowItemId: item.workflowItemId || "",
|
||||
compareValue: Number(item.compareValue || 0),
|
||||
approver: sharedPerson,
|
||||
informPerson: sharedPerson,
|
||||
|
|
@ -343,17 +408,17 @@ export function normalizeCriteria(item) {
|
|||
};
|
||||
}
|
||||
|
||||
export function defaultTitle(kind) {
|
||||
export function defaultTitle(kind: string) {
|
||||
return {
|
||||
Start: "İş Akışı Başlat",
|
||||
Compare: "Tutar > 5000 TL",
|
||||
Approval: "Onaylanacak Kişi",
|
||||
Inform: "Bilgilendirme Yapılacak Personel",
|
||||
End: "Akışı Bitir",
|
||||
}[kind];
|
||||
}[kind] ?? "İş Akışı Adımı";
|
||||
}
|
||||
|
||||
export function emptyCompareOutcome(label = "Durum") {
|
||||
export function emptyCompareOutcome(label = "Durum"): CompareOutcomeDto {
|
||||
return {
|
||||
label,
|
||||
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
|
||||
? 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 = (
|
||||
outcome.conditions?.length
|
||||
? 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) => ({
|
||||
column: condition.column || "Tutar",
|
||||
operator: condition.operator || ">",
|
||||
|
|
@ -403,13 +481,15 @@ export function normalizeCompareOutcome(outcome) {
|
|||
}));
|
||||
|
||||
return {
|
||||
label: outcome.label.trim(),
|
||||
label: (outcome.label || "").trim(),
|
||||
targetId: outcome.targetId || null,
|
||||
conditions,
|
||||
};
|
||||
}
|
||||
|
||||
export function compareOutcomeRuleText(outcome) {
|
||||
export function compareOutcomeRuleText(
|
||||
outcome: Partial<CompareOutcomeDto> & Partial<WorkflowConditionDto>,
|
||||
) {
|
||||
const conditions = outcome.conditions?.length
|
||||
? outcome.conditions
|
||||
: outcome.operator
|
||||
|
|
@ -432,13 +512,13 @@ export function compareOutcomeRuleText(outcome) {
|
|||
: "Kural yok";
|
||||
}
|
||||
|
||||
export function formatCompactValue(value) {
|
||||
export function formatCompactValue(value: number | string | null | undefined) {
|
||||
return new Intl.NumberFormat("tr-TR", {
|
||||
maximumFractionDigits: 2,
|
||||
}).format(Number(value || 0));
|
||||
}
|
||||
|
||||
export function criteriaSummary(item) {
|
||||
export function criteriaSummary(item: WorkflowCriteriaDto) {
|
||||
if (item.kind === "Compare") {
|
||||
return (
|
||||
(item.compareOutcomes || [])
|
||||
|
|
@ -454,22 +534,25 @@ export function criteriaSummary(item) {
|
|||
return item.title;
|
||||
}
|
||||
|
||||
export function targetTitle(criteria, id) {
|
||||
export function targetTitle(
|
||||
criteria: WorkflowCriteriaDto[],
|
||||
id?: string | null,
|
||||
) {
|
||||
if (!id) return "-";
|
||||
const item = criteria.find((candidate) => candidate.id === id);
|
||||
return item ? `${item.id} - ${item.title}` : id;
|
||||
}
|
||||
|
||||
export function statusClass(status) {
|
||||
export function statusClass(status?: string) {
|
||||
if (status === "Onay Bekliyor") return "pending";
|
||||
if (status === "Bitti") return "done";
|
||||
if (status === "Bilgilendirildi") return "info";
|
||||
return "";
|
||||
}
|
||||
|
||||
export function formatMoney(value) {
|
||||
export function formatMoney(value?: number | string | null) {
|
||||
return new Intl.NumberFormat("tr-TR", {
|
||||
style: "currency",
|
||||
currency: "TRY",
|
||||
}).format(value || 0);
|
||||
}).format(Number(value || 0));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import FormTabRow from './FormTabRow'
|
|||
import FormTabGantt from './FormTabGantt'
|
||||
import FormTabScheduler from './FormTabScheduler'
|
||||
import { APP_NAME } from '@/constants/app.constant'
|
||||
import { FormTabWorkflow } from './FormTabWorkflow'
|
||||
|
||||
export interface FormEditProps {
|
||||
onSubmit: (
|
||||
|
|
@ -271,6 +272,9 @@ const FormEdit = () => {
|
|||
{visibleTabs.includes('widget') && (
|
||||
<TabNav value="widget">{translate('::ListForms.ListFormEdit.TabWidgets')}</TabNav>
|
||||
)}
|
||||
{visibleTabs.includes('workflow') && (
|
||||
<TabNav value="workflow">{translate('::ListForms.ListFormEdit.TabWorkflow')}</TabNav>
|
||||
)}
|
||||
|
||||
{/* Chart Tabs */}
|
||||
{visibleTabs.includes('commonSettings') && (
|
||||
|
|
@ -387,6 +391,9 @@ const FormEdit = () => {
|
|||
<TabContent value="widget" className="px-2">
|
||||
<FormTabWidgets listFormCode={listFormCode} />
|
||||
</TabContent>
|
||||
<TabContent value="workflow" className="px-2">
|
||||
<FormTabWorkflow listFormCode={listFormCode} />
|
||||
</TabContent>
|
||||
<TabContent value="fields" className="px-2">
|
||||
<FormFields
|
||||
listFormCode={listFormCode}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { FormEvent } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/tr";
|
||||
import {
|
||||
|
|
@ -7,36 +8,71 @@ import {
|
|||
isPendingApproval,
|
||||
normalizeCriteria,
|
||||
toCriteriaForm,
|
||||
type WorkflowCriteriaForm,
|
||||
} from "@/utils/workflow/workflowHelpers";
|
||||
import { DashboardShell } from "./DashboardShell";
|
||||
import { workflowService, workflowService } from "@/services/workflow.service";
|
||||
import {
|
||||
workflowService,
|
||||
type WorkflowCriteriaDto,
|
||||
type WorkflowItemDto,
|
||||
} from "@/services/workflow.service";
|
||||
import { DashboardShell } from "../workflow/DashboardShell";
|
||||
|
||||
dayjs.locale("tr");
|
||||
|
||||
export function Dashboard() {
|
||||
const [workflowItems, setWorkflowItems] = useState([]);
|
||||
const [criteria, setCriteria] = useState([]);
|
||||
const [selectedWorkflowId, setSelectedWorkflowId] = useState(null);
|
||||
type WorkflowForm = {
|
||||
sorumlu: string;
|
||||
amount: number | string;
|
||||
};
|
||||
|
||||
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 [pendingLink, setPendingLink] = useState(null);
|
||||
const [workflowForm, setWorkflowForm] = useState({
|
||||
const [pendingLink, setPendingLink] = useState<PendingLink>(null);
|
||||
const [workflowForm, setWorkflowForm] = useState<WorkflowForm>({
|
||||
sorumlu: "",
|
||||
amount: 7200,
|
||||
});
|
||||
const [editingWorkflowId, setEditingWorkflowId] = useState(null);
|
||||
const [workflowEditForm, setWorkflowEditForm] = useState({
|
||||
const [editingWorkflowId, setEditingWorkflowId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [workflowEditForm, setWorkflowEditForm] = useState<WorkflowEditForm>({
|
||||
sorumlu: "",
|
||||
tarih: "",
|
||||
amount: 0,
|
||||
});
|
||||
const [criteriaForm, setCriteriaForm] = useState(emptyCriteria());
|
||||
const [dragPreview, setDragPreview] = useState(null);
|
||||
const [criteriaForm, setCriteriaForm] = useState<WorkflowCriteriaForm>(
|
||||
emptyCriteria(),
|
||||
);
|
||||
const [dragPreview, setDragPreview] = useState<DragPreview>(null);
|
||||
const [canvasZoom, setCanvasZoom] = useState(1);
|
||||
const [designerTab, setDesignerTab] = useState("flow");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [approvalDialogWorkflowId, setApprovalDialogWorkflowId] =
|
||||
useState(null);
|
||||
const canvasRef = useRef(null);
|
||||
useState<string | null>(null);
|
||||
const canvasRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const currentCriteria = useMemo(
|
||||
() => criteria.filter((item) => item.workflowItemId === selectedWorkflowId),
|
||||
|
|
@ -74,7 +110,7 @@ export function Dashboard() {
|
|||
}, []);
|
||||
|
||||
const runAction = useCallback(
|
||||
async (action) => {
|
||||
async (action: () => Promise<unknown>) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await action();
|
||||
|
|
@ -120,7 +156,7 @@ export function Dashboard() {
|
|||
}
|
||||
}, [approvalDialogWorkflowId, pendingItems]);
|
||||
|
||||
const createWorkflow = (event) => {
|
||||
const createWorkflow = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
runAction(async () => {
|
||||
const created = await workflowService.createWorkflow({
|
||||
|
|
@ -134,7 +170,7 @@ export function Dashboard() {
|
|||
});
|
||||
};
|
||||
|
||||
const beginWorkflowEdit = (item) => {
|
||||
const beginWorkflowEdit = (item: WorkflowItemDto) => {
|
||||
setSelectedWorkflowId(item.id);
|
||||
setPendingLink(null);
|
||||
setSelectedId("");
|
||||
|
|
@ -151,7 +187,7 @@ export function Dashboard() {
|
|||
setWorkflowEditForm({ sorumlu: "", tarih: "", amount: 0 });
|
||||
};
|
||||
|
||||
const saveWorkflowEdit = (id) => {
|
||||
const saveWorkflowEdit = (id: string) => {
|
||||
runAction(async () => {
|
||||
await workflowService.updateWorkflow(id, {
|
||||
sorumlu: workflowEditForm.sorumlu,
|
||||
|
|
@ -163,7 +199,7 @@ export function Dashboard() {
|
|||
};
|
||||
|
||||
const startWorkflow = useCallback(
|
||||
(id) => {
|
||||
(id: string) => {
|
||||
runAction(async () => {
|
||||
await workflowService.startWorkflow(id);
|
||||
const data = await loadState();
|
||||
|
|
@ -179,7 +215,7 @@ export function Dashboard() {
|
|||
[loadState, runAction],
|
||||
);
|
||||
|
||||
const saveCriteria = (event) => {
|
||||
const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
runAction(async () => {
|
||||
await workflowService.saveCriteria(normalizeCriteria(criteriaForm));
|
||||
|
|
@ -187,7 +223,7 @@ export function Dashboard() {
|
|||
});
|
||||
};
|
||||
|
||||
const addCriteria = (kind) => {
|
||||
const addCriteria = (kind: string) => {
|
||||
if (!selectedWorkflowId) return;
|
||||
|
||||
setDesignerTab("flow");
|
||||
|
|
@ -202,7 +238,7 @@ export function Dashboard() {
|
|||
};
|
||||
|
||||
const deleteSelectedCriteria = useCallback(
|
||||
(criteriaId = selectedId) => {
|
||||
(criteriaId: string = selectedId) => {
|
||||
if (!criteriaId || busy) return;
|
||||
|
||||
runAction(async () => {
|
||||
|
|
@ -214,17 +250,17 @@ export function Dashboard() {
|
|||
);
|
||||
|
||||
const disconnectLink = useCallback(
|
||||
(sourceId, outcome) => {
|
||||
(sourceId: string, outcome: string) => {
|
||||
if (!sourceId || !outcome || busy) return;
|
||||
|
||||
const source = currentCriteria.find((item) => item.id === sourceId);
|
||||
if (!source) return;
|
||||
|
||||
const next = { ...source };
|
||||
const next: WorkflowCriteriaForm = toCriteriaForm(source);
|
||||
if (outcome.startsWith("compareOutcomes:")) {
|
||||
const outcomeIndex = Number(outcome.split(":")[1]);
|
||||
next.compareOutcomes = [...(source.compareOutcomes || [])];
|
||||
if (next.compareOutcomes[outcomeIndex]) {
|
||||
if (next.compareOutcomes?.[outcomeIndex]) {
|
||||
next.compareOutcomes[outcomeIndex] = {
|
||||
...next.compareOutcomes[outcomeIndex],
|
||||
targetId: null,
|
||||
|
|
@ -233,7 +269,7 @@ export function Dashboard() {
|
|||
if (outcomeIndex === 0) next.nextOnTrue = null;
|
||||
if (outcomeIndex === 1) next.nextOnFalse = null;
|
||||
} else {
|
||||
next[outcome] = null;
|
||||
(next as Record<string, unknown>)[outcome] = null;
|
||||
}
|
||||
|
||||
runAction(async () => {
|
||||
|
|
@ -246,10 +282,10 @@ export function Dashboard() {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
const deleteWithKeyboard = (event) => {
|
||||
const deleteWithKeyboard = (event: globalThis.KeyboardEvent) => {
|
||||
const activeTag = document.activeElement?.tagName?.toLowerCase();
|
||||
const isEditing =
|
||||
["input", "textarea", "select"].includes(activeTag) ||
|
||||
Boolean(activeTag && ["input", "textarea", "select"].includes(activeTag)) ||
|
||||
(document.activeElement instanceof HTMLElement &&
|
||||
document.activeElement.isContentEditable);
|
||||
|
||||
|
|
@ -267,7 +303,7 @@ export function Dashboard() {
|
|||
return () => window.removeEventListener("keydown", deleteWithKeyboard);
|
||||
}, [deleteSelectedCriteria, disconnectLink, pendingLink]);
|
||||
|
||||
const updateNodePosition = ({ active, delta }) => {
|
||||
const updateNodePosition = ({ active, delta }: DragEndEvent) => {
|
||||
setDragPreview(null);
|
||||
|
||||
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);
|
||||
if (!source || source.id === targetId) return;
|
||||
|
||||
const next = { ...source };
|
||||
const next: WorkflowCriteriaForm = toCriteriaForm(source);
|
||||
if (outcome.startsWith("compareOutcomes:")) {
|
||||
const outcomeIndex = Number(outcome.split(":")[1]);
|
||||
next.compareOutcomes = [...(source.compareOutcomes || [])];
|
||||
|
|
@ -305,7 +341,7 @@ export function Dashboard() {
|
|||
if (outcomeIndex === 0) next.nextOnTrue = targetId;
|
||||
if (outcomeIndex === 1) next.nextOnFalse = targetId;
|
||||
} else {
|
||||
next[outcome] = targetId;
|
||||
(next as Record<string, unknown>)[outcome] = targetId;
|
||||
}
|
||||
|
||||
setPendingLink(null);
|
||||
|
|
@ -340,13 +376,13 @@ export function Dashboard() {
|
|||
});
|
||||
};
|
||||
|
||||
const selectWorkflow = (item) => {
|
||||
const selectWorkflow = (item: WorkflowItemDto) => {
|
||||
setSelectedWorkflowId(item.id);
|
||||
setPendingLink(null);
|
||||
setSelectedId("");
|
||||
};
|
||||
|
||||
const openCriteriaDetails = (id) => {
|
||||
const openCriteriaDetails = (id: string) => {
|
||||
setSelectedId(id);
|
||||
setPendingLink(null);
|
||||
setDesignerTab("criteria");
|
||||
|
|
@ -357,7 +393,7 @@ export function Dashboard() {
|
|||
setSelectedId("");
|
||||
};
|
||||
|
||||
const beginLink = (sourceId, outcome) => {
|
||||
const beginLink = (sourceId: string, outcome: string) => {
|
||||
setPendingLink({ sourceId, outcome });
|
||||
setSelectedId(sourceId);
|
||||
};
|
||||
|
|
@ -391,12 +427,12 @@ export function Dashboard() {
|
|||
onCloseApprovalDialog={() => setApprovalDialogWorkflowId(null)}
|
||||
onConnectNodes={connectNodes}
|
||||
onCreateWorkflow={createWorkflow}
|
||||
onDecision={(id, approved, note) =>
|
||||
onDecision={(id: string, approved: boolean, note: string) =>
|
||||
runAction(() => workflowService.decideWorkflow(id, { approved, note }))
|
||||
}
|
||||
onDeleteSelectedCriteria={deleteSelectedCriteria}
|
||||
onDisconnectLink={disconnectLink}
|
||||
onDragMove={(event) =>
|
||||
onDragMove={(event: DragEndEvent | null) =>
|
||||
setDragPreview(
|
||||
event ? { id: event.active.id, delta: event.delta } : null,
|
||||
)
|
||||
|
|
@ -1,12 +1,30 @@
|
|||
import { formatMoney } from "@/utils/workflow/workflowHelpers";
|
||||
import { useState } from "react";
|
||||
import { FiCheck, FiSlash, FiX } from "react-icons/fi";
|
||||
import type {
|
||||
WorkflowCriteriaDto,
|
||||
WorkflowItemDto,
|
||||
} from "@/services/workflow.service";
|
||||
|
||||
const CloseIcon = FiX as any;
|
||||
const CheckIcon = FiCheck 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;
|
||||
|
||||
return (
|
||||
|
|
@ -59,8 +77,8 @@ function PendingApprovals({
|
|||
busy,
|
||||
onDecision,
|
||||
showChrome = true,
|
||||
}) {
|
||||
const [notes, setNotes] = useState({});
|
||||
}: Omit<ApprovalDialogProps, "onClose"> & { showChrome?: boolean }) {
|
||||
const [notes, setNotes] = useState<Record<string, string>>({});
|
||||
|
||||
const content = (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -13,10 +13,29 @@ import {
|
|||
emptyCompareOutcome,
|
||||
targetTitle,
|
||||
} from "@/utils/workflow/workflowHelpers";
|
||||
import type {
|
||||
CompareOutcomeDto,
|
||||
WorkflowCriteriaDto,
|
||||
WorkflowItemDto,
|
||||
} from "@/services/workflow.service";
|
||||
|
||||
const SaveIcon = FiSave 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({
|
||||
criteria,
|
||||
selectedWorkflow,
|
||||
|
|
@ -29,27 +48,32 @@ export function CriteriaTable({
|
|||
onSubmit,
|
||||
onDelete,
|
||||
onAddCriteria,
|
||||
}) {
|
||||
const setField = (name, value) => onChange({ ...form, [name]: value });
|
||||
}: CriteriaTableProps) {
|
||||
const setField = (name: string, value: unknown) =>
|
||||
onChange({ ...form, [name]: value });
|
||||
const targetOptions = [
|
||||
{ value: "", label: "Bağlantı yok" },
|
||||
...criteria
|
||||
.filter((item) => item.id !== form.id)
|
||||
.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 || [])];
|
||||
next[index] = { ...next[index], ...patch };
|
||||
setField("compareOutcomes", next);
|
||||
};
|
||||
const updateCompareCondition = (outcomeIndex, conditionIndex, patch) => {
|
||||
const updateCompareCondition = (
|
||||
outcomeIndex: number,
|
||||
conditionIndex: number,
|
||||
patch: Record<string, unknown>,
|
||||
) => {
|
||||
const next = [...(form.compareOutcomes || [])];
|
||||
const conditions = [...(next[outcomeIndex]?.conditions || [])];
|
||||
conditions[conditionIndex] = { ...conditions[conditionIndex], ...patch };
|
||||
next[outcomeIndex] = { ...next[outcomeIndex], conditions };
|
||||
setField("compareOutcomes", next);
|
||||
};
|
||||
const addCompareCondition = (outcomeIndex) => {
|
||||
const addCompareCondition = (outcomeIndex: number) => {
|
||||
const next = [...(form.compareOutcomes || [])];
|
||||
next[outcomeIndex] = {
|
||||
...next[outcomeIndex],
|
||||
|
|
@ -60,23 +84,27 @@ export function CriteriaTable({
|
|||
};
|
||||
setField("compareOutcomes", next);
|
||||
};
|
||||
const removeCompareCondition = (outcomeIndex, conditionIndex) => {
|
||||
const removeCompareCondition = (outcomeIndex: number, conditionIndex: number) => {
|
||||
const next = [...(form.compareOutcomes || [])];
|
||||
const conditions = (next[outcomeIndex]?.conditions || []).filter(
|
||||
(_, index) => index !== conditionIndex,
|
||||
(_: unknown, index: number) => index !== conditionIndex,
|
||||
);
|
||||
next[outcomeIndex] = { ...next[outcomeIndex], conditions };
|
||||
setField("compareOutcomes", next);
|
||||
};
|
||||
const removeCompareOutcome = (index) => {
|
||||
const removeCompareOutcome = (index: number) => {
|
||||
setField(
|
||||
"compareOutcomes",
|
||||
(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
|
||||
required={required}
|
||||
value={value || ""}
|
||||
|
|
@ -89,7 +117,7 @@ export function CriteriaTable({
|
|||
))}
|
||||
</select>
|
||||
);
|
||||
const toggleRow = (id) => onSelect(id === selectedId ? "" : id);
|
||||
const toggleRow = (id: string) => onSelect(id === selectedId ? "" : id);
|
||||
|
||||
return (
|
||||
<section className="min-w-0 rounded-lg">
|
||||
|
|
@ -276,7 +304,7 @@ export function CriteriaTable({
|
|||
</button>
|
||||
</div>
|
||||
{(form.compareOutcomes || []).map(
|
||||
(outcome, index) => (
|
||||
(outcome: CompareOutcomeDto, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
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 (
|
||||
<label className="grid gap-1.5 text-[12px] text-[#344054]">
|
||||
<span>
|
||||
|
|
@ -456,14 +492,14 @@ function Field({ label, children, required = false }) {
|
|||
);
|
||||
}
|
||||
|
||||
function criteriaSummaryContent(item) {
|
||||
function criteriaSummaryContent(item: WorkflowCriteriaDto) {
|
||||
if (item.kind === "Compare") {
|
||||
const outcomes = item.compareOutcomes || [];
|
||||
if (!outcomes.length) return "-";
|
||||
|
||||
return (
|
||||
<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}`}>
|
||||
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{" "}
|
||||
{compareOutcomeRuleText(outcome)}
|
||||
|
|
@ -476,14 +512,17 @@ function criteriaSummaryContent(item) {
|
|||
return criteriaSummary(item);
|
||||
}
|
||||
|
||||
function criteriaConnectionSummary(item, criteria) {
|
||||
function criteriaConnectionSummary(
|
||||
item: WorkflowCriteriaDto,
|
||||
criteria: WorkflowCriteriaDto[],
|
||||
) {
|
||||
if (item.kind === "Compare") {
|
||||
const outcomes = item.compareOutcomes || [];
|
||||
if (!outcomes.length) return "-";
|
||||
|
||||
return (
|
||||
<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}`}>
|
||||
<strong>{outcome.label || `Durum ${index + 1}`}:</strong>{" "}
|
||||
{targetTitle(criteria, outcome.targetId)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,59 @@
|
|||
import { ApprovalDialog } from "./ApprovalDialog";
|
||||
import { WorkflowDesigner } from "./WorkflowDesigner";
|
||||
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({
|
||||
busy,
|
||||
|
|
@ -48,7 +101,7 @@ export function DashboardShell({
|
|||
onWorkflowFormChange,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
}) {
|
||||
}: DashboardShellProps) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<main className="grid gap-[18px] p-[18px]">
|
||||
|
|
|
|||
|
|
@ -12,7 +12,34 @@ import {
|
|||
collectLinks,
|
||||
getNodeOutcomes,
|
||||
outcomeLabel,
|
||||
type WorkflowLink,
|
||||
type WorkflowOutcome,
|
||||
} 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({
|
||||
currentCriteria,
|
||||
|
|
@ -29,6 +56,21 @@ export function FlowCanvas({
|
|||
onDeleteLink,
|
||||
onBeginLink,
|
||||
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(
|
||||
1240,
|
||||
|
|
@ -55,9 +97,12 @@ export function FlowCanvas({
|
|||
),
|
||||
[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;
|
||||
event.preventDefault();
|
||||
if (pendingLink) {
|
||||
|
|
@ -68,8 +113,11 @@ export function FlowCanvas({
|
|||
onDelete(selectedId);
|
||||
};
|
||||
|
||||
const handleCanvasClick = (event) => {
|
||||
if (event.target.closest("[data-flow-node], [data-flow-link]")) return;
|
||||
const handleCanvasClick = (event: MouseEvent<HTMLDivElement>) => {
|
||||
if (
|
||||
(event.target as HTMLElement).closest("[data-flow-node], [data-flow-link]")
|
||||
)
|
||||
return;
|
||||
onClearSelection();
|
||||
};
|
||||
|
||||
|
|
@ -244,7 +292,7 @@ function FlowNode({
|
|||
onOpenDetails,
|
||||
onDelete,
|
||||
onBeginLink,
|
||||
}) {
|
||||
}: FlowNodeProps) {
|
||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
id: item.id,
|
||||
disabled: Boolean(pendingLink),
|
||||
|
|
@ -313,7 +361,7 @@ function FlowNode({
|
|||
{item.id}
|
||||
</small>
|
||||
<div className="mt-0.5 flex max-w-full flex-wrap gap-[3px]">
|
||||
{getNodeOutcomes(item).map((outcome) => (
|
||||
{(getNodeOutcomes(item) as WorkflowOutcome[]).map((outcome) => (
|
||||
<span
|
||||
key={outcome.field}
|
||||
role="button"
|
||||
|
|
@ -343,7 +391,7 @@ function FlowNode({
|
|||
</span>
|
||||
))}
|
||||
</div>
|
||||
{getNodeOutcomes(item).map((outcome, index, outcomes) => {
|
||||
{(getNodeOutcomes(item) as WorkflowOutcome[]).map((outcome, index, outcomes) => {
|
||||
const link = links.find(
|
||||
(candidate) =>
|
||||
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(
|
||||
link.source,
|
||||
link.target,
|
||||
|
|
@ -412,7 +470,9 @@ function Arrow({ link, criteria, pendingLink, onBeginLink }) {
|
|||
pendingLink?.sourceId === link.source.id &&
|
||||
pendingLink?.outcome === link.sourcePort?.field;
|
||||
|
||||
const selectLink = (event) => {
|
||||
const selectLink = (
|
||||
event: MouseEvent<SVGGElement> | KeyboardEvent<SVGGElement>,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
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;
|
||||
|
||||
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 label = link.label || "";
|
||||
if (field === "nextOnReject" || label === "Red") return "reject";
|
||||
|
|
@ -515,7 +583,7 @@ function linkTone(link) {
|
|||
return "neutral";
|
||||
}
|
||||
|
||||
function linkToneClass(tone) {
|
||||
function linkToneClass(tone: string) {
|
||||
if (tone === "next") {
|
||||
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)]";
|
||||
}
|
||||
|
||||
function portSideClass(side) {
|
||||
function portSideClass(side: string) {
|
||||
if (side === "left" || side === "right") return "-translate-y-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 sourceTop = Number(source.positionY || 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 fromTop = Number(from.positionY || 0);
|
||||
const fromCenter = {
|
||||
|
|
@ -657,7 +730,13 @@ function sideToward(from, to) {
|
|||
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 top = Number(item.positionY || 0);
|
||||
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 };
|
||||
}
|
||||
|
||||
function getPortStyle(item, side, index, count) {
|
||||
function getPortStyle(item: any, side: string, index: number, count: number) {
|
||||
const point = getPortPoint(
|
||||
{ ...item, positionX: 0, positionY: 0 },
|
||||
side,
|
||||
|
|
@ -688,7 +767,7 @@ function getPortStyle(item, side, index, count) {
|
|||
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;
|
||||
const gap = 28;
|
||||
const center = length / 2;
|
||||
|
|
@ -696,7 +775,13 @@ function getPortOffsetAlong(length, index, count) {
|
|||
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 exit = extendFromSide(start, startSide, 26);
|
||||
const entry = extendFromSide(end, endSide, 38);
|
||||
|
|
@ -765,8 +850,8 @@ function buildSideAwareRoute(start, startSide, end, endSide, slots: any = {}) {
|
|||
};
|
||||
}
|
||||
|
||||
function routeLabelPoint(points, slots: any = {}) {
|
||||
const segments = [];
|
||||
function routeLabelPoint(points: any[], slots: any = {}) {
|
||||
const segments: any[] = [];
|
||||
for (let index = 1; index < points.length - 1; index += 1) {
|
||||
const a = points[index - 1];
|
||||
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) => {
|
||||
if (index === 0) return true;
|
||||
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;
|
||||
}
|
||||
|
||||
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 === "right") return { x: point.x + distance, y: point.y };
|
||||
if (side === "top") 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";
|
||||
}
|
||||
|
||||
function roundedPolylinePath(points) {
|
||||
function roundedPolylinePath(points: any[]) {
|
||||
const routePoints = points.filter((point, index) => {
|
||||
if (index === 0) return true;
|
||||
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 { CriteriaTable } from './CriteriaTable'
|
||||
import { FlowCanvas } from './FlowCanvas'
|
||||
import type { FormEvent, RefObject } from 'react'
|
||||
import type {
|
||||
WorkflowCriteriaDto,
|
||||
WorkflowItemDto,
|
||||
} from '@/services/workflow.service'
|
||||
|
||||
const MaximizeIcon = FiMaximize2 as any
|
||||
const ZoomInIcon = FiZoomIn 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({
|
||||
busy,
|
||||
canvasRef,
|
||||
|
|
@ -37,7 +71,7 @@ export function WorkflowDesigner({
|
|||
onUpdateNodePosition,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
}) {
|
||||
}: WorkflowDesignerProps) {
|
||||
return (
|
||||
<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">
|
||||
|
|
@ -112,6 +146,14 @@ function DesignerToolbar({
|
|||
onFitLayout,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
}: {
|
||||
busy: boolean
|
||||
currentCriteria: WorkflowCriteriaDto[]
|
||||
zoom: number
|
||||
onAddCriteria: (kind: string) => void
|
||||
onFitLayout: () => void
|
||||
onZoomIn: () => void
|
||||
onZoomOut: () => void
|
||||
}) {
|
||||
return (
|
||||
<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 (
|
||||
<div className="inline-flex gap-1 rounded-lg" role="tablist" aria-label="Akış tasarımı">
|
||||
<button
|
||||
|
|
@ -209,7 +257,11 @@ function DesignerTabs({ activeTab, onChange }) {
|
|||
)
|
||||
}
|
||||
|
||||
function ApprovalHistoryTable({ selectedWorkflow }: any) {
|
||||
function ApprovalHistoryTable({
|
||||
selectedWorkflow,
|
||||
}: {
|
||||
selectedWorkflow?: WorkflowItemDto | null
|
||||
}) {
|
||||
const history = selectedWorkflow?.history || []
|
||||
|
||||
return (
|
||||
|
|
@ -229,7 +281,7 @@ function ApprovalHistoryTable({ selectedWorkflow }: any) {
|
|||
<td colSpan={3}>Seçili iş akışı için açıklama kaydı yok.</td>
|
||||
</tr>
|
||||
)}
|
||||
{history.map((item: any, index: number) => (
|
||||
{history.map((item: WorkflowItemDto['history'][number], index: number) => (
|
||||
<tr key={`${item.time}-${index}`}>
|
||||
<td>{dayjs(item.time).format('DD MMM YYYY HH:mm')}</td>
|
||||
<td>{item.action}</td>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ import { FiCheck, FiEdit2, FiPlay, FiPlus, FiRefreshCw, FiX } from 'react-icons/
|
|||
import classNames from 'classnames'
|
||||
import dayjs from 'dayjs'
|
||||
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 EditIcon = FiEdit2 as any
|
||||
|
|
@ -10,6 +15,25 @@ const PlusIcon = FiPlus as any
|
|||
const RefreshIcon = FiRefreshCw 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({
|
||||
items,
|
||||
criteria,
|
||||
|
|
@ -27,7 +51,7 @@ export function WorkflowTable({
|
|||
onSelect,
|
||||
onStart,
|
||||
onResetDemo,
|
||||
}: any) {
|
||||
}: WorkflowTableProps) {
|
||||
return (
|
||||
<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">
|
||||
|
|
@ -93,9 +117,9 @@ export function WorkflowTable({
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item: any) => {
|
||||
{items.map((item) => {
|
||||
const currentStep = criteria.find(
|
||||
(candidate: any) =>
|
||||
(candidate) =>
|
||||
candidate.workflowItemId === item.id && candidate.id === item.currentNodeId,
|
||||
)
|
||||
const statusTitle = currentStep?.title || item.durum
|
||||
|
|
@ -240,7 +264,7 @@ export function WorkflowTable({
|
|||
)
|
||||
}
|
||||
|
||||
function StatusPill({ status }: any) {
|
||||
function StatusPill({ status }: { status: string }) {
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
|
|
|
|||
Loading…
Reference in a new issue