kontenhumas-fe/components/form/ApprovalWorkflowForm.tsx

875 lines
28 KiB
TypeScript
Raw Permalink Normal View History

2025-10-02 05:04:42 +00:00
"use client";
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { SaveIcon, EyeIcon, RotateCcwIcon } from "@/components/icons";
import { FormField } from "./common/FormField";
import { DynamicArray } from "./common/DynamicArray";
import { MultiSelect } from "./common/MultiSelect";
import {
CreateApprovalWorkflowWithClientSettingsRequest,
2025-10-03 00:53:19 +00:00
UpdateApprovalWorkflowWithClientSettingsRequest,
2025-10-02 05:04:42 +00:00
ApprovalWorkflowStepRequest,
ClientApprovalSettingsRequest,
UserLevel,
User,
UserRole,
ArticleCategory,
createApprovalWorkflowWithClientSettings,
2025-10-03 00:53:19 +00:00
updateApprovalWorkflowWithClientSettings,
2025-10-02 05:04:42 +00:00
getUserLevels,
getUsers,
getUserRoles,
getArticleCategories,
} from "@/service/approval-workflows";
import Swal from "sweetalert2";
interface ApprovalWorkflowFormProps {
initialData?: CreateApprovalWorkflowWithClientSettingsRequest;
2025-10-03 00:53:19 +00:00
workflowId?: number; // For update mode
2025-10-02 05:04:42 +00:00
onSave?: (data: CreateApprovalWorkflowWithClientSettingsRequest) => void;
onCancel?: () => void;
isLoading?: boolean;
}
2026-01-29 02:11:43 +00:00
const normalizeClientSettings = (settings: ClientApprovalSettingsRequest) => ({
approvalExemptCategories: settings.approvalExemptCategories ?? [],
approvalExemptRoles: settings.approvalExemptRoles ?? [],
approvalExemptUsers: settings.approvalExemptUsers ?? [],
requireApprovalFor: settings.requireApprovalFor ?? [],
skipApprovalFor: settings.skipApprovalFor ?? [],
autoPublishArticles: settings.autoPublishArticles ?? false,
requiresApproval: settings.requiresApproval ?? false,
isActive: settings.isActive ?? false,
});
2025-10-02 05:04:42 +00:00
export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
initialData,
2025-10-03 00:53:19 +00:00
workflowId,
2025-10-02 05:04:42 +00:00
onSave,
onCancel,
isLoading = false,
}) => {
// Form state
2026-01-29 02:11:43 +00:00
const [formData, setFormData] =
useState<CreateApprovalWorkflowWithClientSettingsRequest>({
name: "",
description: "",
2025-10-02 05:04:42 +00:00
isActive: true,
2026-01-29 02:11:43 +00:00
isDefault: true,
requiresApproval: true,
autoPublish: false,
steps: [],
clientApprovalSettings: {
requiresApproval: true,
autoPublishArticles: false,
approvalExemptUsers: [],
approvalExemptRoles: [],
approvalExemptCategories: [],
requireApprovalFor: [],
skipApprovalFor: [],
isActive: true,
},
});
2025-10-02 05:04:42 +00:00
// API data
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
const [users, setUsers] = useState<User[]>([]);
const [userRoles, setUserRoles] = useState<UserRole[]>([]);
2026-01-29 02:11:43 +00:00
const [articleCategories, setArticleCategories] = useState<ArticleCategory[]>(
[],
);
2025-10-02 05:04:42 +00:00
// UI state
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [expandedSteps, setExpandedSteps] = useState<Set<number>>(new Set());
const [isLoadingData, setIsLoadingData] = useState(true);
// Get available user levels for a specific step (excluding already selected ones)
const getAvailableUserLevels = (currentStepIndex: number) => {
const usedLevelIds = new Set<number>();
2026-01-29 02:11:43 +00:00
2025-10-02 05:04:42 +00:00
// Collect all user level IDs that are already used by other steps
formData.steps.forEach((step, stepIndex) => {
if (stepIndex !== currentStepIndex && step.conditionValue) {
try {
const conditionData = JSON.parse(step.conditionValue);
2026-01-29 02:11:43 +00:00
if (
conditionData.applies_to_levels &&
Array.isArray(conditionData.applies_to_levels)
) {
2025-10-02 05:04:42 +00:00
conditionData.applies_to_levels.forEach((levelId: number) => {
usedLevelIds.add(levelId);
});
}
} catch (error) {
2026-01-29 02:11:43 +00:00
console.error("Error parsing conditionValue:", error);
2025-10-02 05:04:42 +00:00
}
}
});
2026-01-29 02:11:43 +00:00
2025-10-02 05:04:42 +00:00
// Filter out used levels and return available ones
2026-01-29 02:11:43 +00:00
return userLevels.filter((level) => !usedLevelIds.has(level.id));
2025-10-02 05:04:42 +00:00
};
// Load initial data
useEffect(() => {
if (initialData) {
setFormData(initialData);
}
}, [initialData]);
// Load API data
useEffect(() => {
const loadData = async () => {
try {
2026-01-29 02:11:43 +00:00
const [userLevelsRes, usersRes, userRolesRes, categoriesRes] =
await Promise.all([
getUserLevels(),
getUsers(),
getUserRoles(),
getArticleCategories(),
]);
if (!userLevelsRes?.error)
setUserLevels(userLevelsRes?.data?.data || []);
2025-10-02 05:04:42 +00:00
if (!usersRes?.error) setUsers(usersRes?.data || []);
if (!userRolesRes?.error) setUserRoles(userRolesRes?.data || []);
2026-01-29 02:11:43 +00:00
if (!categoriesRes?.error)
setArticleCategories(categoriesRes?.data || []);
2025-10-02 05:04:42 +00:00
} catch (error) {
console.error("Error loading form data:", error);
} finally {
setIsLoadingData(false);
}
};
loadData();
}, []);
// Validation
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) {
newErrors.name = "Workflow name is required";
} else if (formData.name.trim().length < 3) {
newErrors.name = "Workflow name must be at least 3 characters";
}
if (!formData.description.trim()) {
newErrors.description = "Description is required";
2025-10-03 00:53:19 +00:00
} else if (formData.description.trim().length < 3) {
newErrors.description = "Description must be at least 3 characters";
2025-10-02 05:04:42 +00:00
}
if (formData.steps.length === 0) {
newErrors.steps = "At least one workflow step is required";
}
// Validate steps
formData.steps.forEach((step, index) => {
if (!step.stepName.trim()) {
newErrors[`steps.${index}.stepName`] = "Step name is required";
}
if (!step.requiredUserLevelId) {
2026-01-29 02:11:43 +00:00
newErrors[`steps.${index}.requiredUserLevelId`] =
"Required user level is required";
2025-10-02 05:04:42 +00:00
}
if (step.stepOrder <= 0) {
2026-01-29 02:11:43 +00:00
newErrors[`steps.${index}.stepOrder`] =
"Step order must be greater than 0";
2025-10-02 05:04:42 +00:00
}
});
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// Form handlers
2026-01-29 02:11:43 +00:00
const handleBasicInfoChange = (
field: keyof CreateApprovalWorkflowWithClientSettingsRequest,
value: any,
) => {
setFormData((prev) => ({ ...prev, [field]: value }));
2025-10-02 05:04:42 +00:00
if (errors[field]) {
2026-01-29 02:11:43 +00:00
setErrors((prev) => ({ ...prev, [field]: "" }));
2025-10-02 05:04:42 +00:00
}
};
const handleStepsChange = (steps: ApprovalWorkflowStepRequest[]) => {
// Allow manual step order assignment for parallel steps
// Don't auto-assign if user has manually set stepOrder
const updatedSteps = steps.map((step, index) => {
// If stepOrder is not set or is 0, auto-assign based on index
if (!step.stepOrder || step.stepOrder === 0) {
return {
...step,
stepOrder: index + 1,
};
}
// Keep existing stepOrder if manually set
return step;
});
2026-01-29 02:11:43 +00:00
setFormData((prev) => ({ ...prev, steps: updatedSteps }));
2025-10-02 05:04:42 +00:00
};
2026-01-29 02:11:43 +00:00
const renderStepForm = (
step: ApprovalWorkflowStepRequest,
index: number,
onUpdate: (step: ApprovalWorkflowStepRequest) => void,
onDelete: () => void,
) => {
const stepErrors = Object.keys(errors).filter((key) =>
key.startsWith(`steps.${index}`),
);
2025-10-02 05:04:42 +00:00
// Check if this step has parallel steps (same stepOrder)
2026-01-29 02:11:43 +00:00
const parallelSteps = formData.steps.filter(
(s, i) => s.stepOrder === step.stepOrder && i !== index,
);
2025-10-02 05:04:42 +00:00
const isParallelStep = parallelSteps.length > 0;
2026-01-29 02:11:43 +00:00
2025-10-02 05:04:42 +00:00
return (
<div className="space-y-4">
{/* Parallel Step Indicator */}
{isParallelStep && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 mb-4">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<span className="text-sm font-medium text-blue-800">
Parallel Step - Order {step.stepOrder}
</span>
<span className="text-xs text-blue-600">
({parallelSteps.length + 1} steps running simultaneously)
</span>
</div>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField
label="Step Order"
name={`stepOrder-${index}`}
type="number"
placeholder="1"
value={step.stepOrder}
2026-01-29 02:11:43 +00:00
onChange={(value) =>
onUpdate({
...step,
stepOrder: value ? Number(value) : index + 1,
})
}
2025-10-02 05:04:42 +00:00
error={errors[`steps.${index}.stepOrder`]}
min={1}
helpText="Same order = parallel steps"
/>
<FormField
label="Step Name"
name={`stepName-${index}`}
type="text"
placeholder="e.g., Editor Review, Manager Approval"
value={step.stepName}
onChange={(value) => onUpdate({ ...step, stepName: value })}
error={errors[`steps.${index}.stepName`]}
required
/>
<FormField
label="Required User Level"
name={`requiredUserLevelId-${index}`}
type="select"
2026-01-29 02:11:43 +00:00
placeholder={
userLevels.length > 0
? "Select user level"
: "No user levels available"
}
2025-10-02 05:04:42 +00:00
value={step.requiredUserLevelId}
2026-01-29 02:11:43 +00:00
onChange={(value) =>
onUpdate({ ...step, requiredUserLevelId: Number(value) })
}
2025-10-02 05:04:42 +00:00
error={errors[`steps.${index}.requiredUserLevelId`]}
2026-01-29 02:11:43 +00:00
options={
userLevels.length > 0
? userLevels.map((level) => ({
value: level.id,
label: `${level.name} (Level ${level.levelNumber})`,
}))
: []
}
2025-10-02 05:04:42 +00:00
required
disabled={userLevels.length === 0}
2026-01-29 02:11:43 +00:00
helpText={
userLevels.length === 0
? "No user levels found. Please create user levels first."
: undefined
}
2025-10-02 05:04:42 +00:00
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
label="Auto Approve After Hours"
name={`autoApproveAfterHours-${index}`}
type="number"
placeholder="Leave empty for manual approval"
value={step.autoApproveAfterHours}
2026-01-29 02:11:43 +00:00
onChange={(value) =>
onUpdate({
...step,
autoApproveAfterHours: value ? Number(value) : undefined,
})
}
2025-10-02 05:04:42 +00:00
helpText="Automatically approve after specified hours"
min={1}
/>
<div className="space-y-2">
<FormField
label="Can Skip This Step"
name={`canSkip-${index}`}
type="checkbox"
value={step.canSkip || false}
onChange={(value) => onUpdate({ ...step, canSkip: value })}
/>
2026-01-29 02:11:43 +00:00
2025-10-02 05:04:42 +00:00
<FormField
label="Is Active"
name={`isActive-${index}`}
type="checkbox"
value={step.isActive !== false}
onChange={(value) => onUpdate({ ...step, isActive: value })}
/>
</div>
</div>
{/* Condition Settings */}
<div className="space-y-4">
<MultiSelect
label="Applies to User Levels"
placeholder={(() => {
const availableLevels = getAvailableUserLevels(index);
2026-01-29 02:11:43 +00:00
return availableLevels.length > 0
? "Select user levels..."
: "No available user levels";
2025-10-02 05:04:42 +00:00
})()}
options={(() => {
const availableLevels = getAvailableUserLevels(index);
2026-01-29 02:11:43 +00:00
return availableLevels.map((level) => ({
2025-10-02 05:04:42 +00:00
value: level.id,
label: `${level.name} (Level ${level.levelNumber})`,
}));
})()}
value={(() => {
try {
2026-01-29 02:11:43 +00:00
return step.conditionValue
? JSON.parse(step.conditionValue).applies_to_levels || []
: [];
2025-10-02 05:04:42 +00:00
} catch {
return [];
}
})()}
onChange={(value) => {
2026-01-29 02:11:43 +00:00
const conditionValue = JSON.stringify({
applies_to_levels: value,
});
onUpdate({
...step,
conditionType: "user_level_hierarchy",
conditionValue,
});
2025-10-02 05:04:42 +00:00
}}
searchable={true}
disabled={getAvailableUserLevels(index).length === 0}
helpText={(() => {
const availableLevels = getAvailableUserLevels(index);
const usedLevels = userLevels.length - availableLevels.length;
2026-01-29 02:11:43 +00:00
2025-10-02 05:04:42 +00:00
if (availableLevels.length === 0) {
return "All user levels are already assigned to other steps";
} else if (usedLevels > 0) {
return `${usedLevels} user level(s) already assigned to other steps. ${availableLevels.length} available.`;
} else {
return "Select which user levels this step applies to";
}
})()}
/>
</div>
</div>
);
};
2026-01-29 02:11:43 +00:00
function normalizeBoolean(value?: boolean): boolean {
return value ?? false;
}
2025-10-02 05:04:42 +00:00
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
2026-01-29 02:11:43 +00:00
2025-10-02 05:04:42 +00:00
if (!validateForm()) {
Swal.fire({
title: "Validation Error",
text: "Please fix the errors before submitting",
icon: "error",
2025-10-03 00:53:19 +00:00
confirmButtonText: "OK",
customClass: {
2026-01-29 02:11:43 +00:00
popup: "swal-z-index-9999",
},
2025-10-02 05:04:42 +00:00
});
return;
}
setIsSubmitting(true);
2026-01-29 02:11:43 +00:00
2025-10-02 05:04:42 +00:00
try {
// Hardcoded client approval settings
const hardcodedClientSettings = {
approvalExemptCategories: [],
approvalExemptRoles: [],
approvalExemptUsers: [],
autoPublishArticles: true,
isActive: true,
requireApprovalFor: [],
requiresApproval: true,
2026-01-29 02:11:43 +00:00
skipApprovalFor: [],
};
const clientSettingsPayload = {
approvalExemptCategories: [],
approvalExemptRoles: [],
approvalExemptUsers: [],
requireApprovalFor: [],
skipApprovalFor: [],
autoPublishArticles:
formData.clientApprovalSettings.autoPublishArticles ?? false,
requiresApproval:
formData.clientApprovalSettings.requiresApproval ?? false,
isActive: formData.clientApprovalSettings.isActive ?? false,
2025-10-02 05:04:42 +00:00
};
2025-10-03 00:53:19 +00:00
if (workflowId) {
// Update mode
const updateData: UpdateApprovalWorkflowWithClientSettingsRequest = {
workflowId,
name: formData.name,
description: formData.description,
2026-01-29 02:11:43 +00:00
isActive: normalizeBoolean(formData.isActive),
isDefault: normalizeBoolean(formData.isDefault),
steps: formData.steps.map((step) => ({
2025-10-03 00:53:19 +00:00
...step,
2026-01-29 02:11:43 +00:00
branchName: step.stepName,
2025-10-03 00:53:19 +00:00
})),
2026-01-29 02:11:43 +00:00
clientSettings: {
approvalExemptCategories: [],
approvalExemptRoles: [],
approvalExemptUsers: [],
requireApprovalFor: [],
skipApprovalFor: [],
// 🔥 AMBIL LANGSUNG DARI CHECKBOX UI
requiresApproval: normalizeBoolean(formData.requiresApproval),
autoPublishArticles: normalizeBoolean(formData.autoPublish),
isActive: normalizeBoolean(formData.isActive),
},
2025-10-03 00:53:19 +00:00
};
2025-10-02 05:04:42 +00:00
2026-01-29 02:11:43 +00:00
// const updateData: UpdateApprovalWorkflowWithClientSettingsRequest = {
// workflowId,
// name: formData.name,
// description: formData.description,
// isActive: formData.isActive,
// isDefault: formData.isDefault,
// steps: formData.steps.map(step => ({
// ...step,
// branchName: step.stepName, // branchName should be same as stepName
// })),
// clientSettings: hardcodedClientSettings
// };
2025-10-03 00:53:19 +00:00
console.log("Update Data: ", updateData);
2026-01-29 02:11:43 +00:00
const response =
await updateApprovalWorkflowWithClientSettings(updateData);
2025-10-03 00:53:19 +00:00
console.log("Update Response: ", response);
if (response?.error) {
Swal.fire({
title: "Error",
2026-01-29 02:11:43 +00:00
text:
response?.message?.messages?.[0] ||
"Failed to update approval workflow",
2025-10-03 00:53:19 +00:00
icon: "error",
confirmButtonText: "OK",
customClass: {
2026-01-29 02:11:43 +00:00
popup: "swal-z-index-9999",
},
2025-10-03 00:53:19 +00:00
});
} else {
Swal.fire({
title: "Success",
text: "Approval workflow updated successfully",
icon: "success",
confirmButtonText: "OK",
customClass: {
2026-01-29 02:11:43 +00:00
popup: "swal-z-index-9999",
},
2025-10-03 00:53:19 +00:00
}).then(() => {
// Call onSave to trigger parent refresh
if (onSave) {
onSave(formData);
}
});
}
2025-10-02 05:04:42 +00:00
} else {
2025-10-03 00:53:19 +00:00
// Create mode
const submitData = {
...formData,
2026-01-29 02:11:43 +00:00
clientApprovalSettings: hardcodedClientSettings,
2025-10-03 00:53:19 +00:00
};
console.log("Create Data: ", submitData);
2026-01-29 02:11:43 +00:00
const response =
await createApprovalWorkflowWithClientSettings(submitData);
2025-10-03 00:53:19 +00:00
console.log("Create Response: ", response);
if (response?.error) {
Swal.fire({
title: "Error",
2026-01-29 02:11:43 +00:00
text:
response?.message?.messages?.[0] ||
"Failed to create approval workflow",
2025-10-03 00:53:19 +00:00
icon: "error",
confirmButtonText: "OK",
customClass: {
2026-01-29 02:11:43 +00:00
popup: "swal-z-index-9999",
},
2025-10-03 00:53:19 +00:00
});
} else {
Swal.fire({
title: "Success",
text: "Approval workflow created successfully",
icon: "success",
confirmButtonText: "OK",
customClass: {
2026-01-29 02:11:43 +00:00
popup: "swal-z-index-9999",
},
2025-10-03 00:53:19 +00:00
}).then(() => {
// Call onSave to trigger parent refresh
if (onSave) {
onSave(submitData);
}
});
}
2025-10-02 05:04:42 +00:00
}
} catch (error) {
console.error("Error submitting form:", error);
Swal.fire({
title: "Error",
text: "An unexpected error occurred",
icon: "error",
2025-10-03 00:53:19 +00:00
confirmButtonText: "OK",
customClass: {
2026-01-29 02:11:43 +00:00
popup: "swal-z-index-9999",
},
2025-10-02 05:04:42 +00:00
});
} finally {
setIsSubmitting(false);
}
};
const handleReset = () => {
Swal.fire({
title: "Reset Form",
text: "Are you sure you want to reset all form data?",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Yes, reset",
2025-10-03 00:53:19 +00:00
cancelButtonText: "Cancel",
customClass: {
2026-01-29 02:11:43 +00:00
popup: "swal-z-index-9999",
},
2025-10-02 05:04:42 +00:00
}).then((result) => {
if (result.isConfirmed) {
setFormData({
name: "",
description: "",
isActive: true,
isDefault: false,
requiresApproval: true,
autoPublish: false,
steps: [],
clientApprovalSettings: {
requiresApproval: true,
autoPublishArticles: false,
approvalExemptUsers: [],
approvalExemptRoles: [],
approvalExemptCategories: [],
requireApprovalFor: [],
skipApprovalFor: [],
isActive: true,
},
});
setErrors({});
}
});
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
{isLoadingData && (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2 text-gray-600">Loading form data...</span>
</div>
)}
2026-01-29 02:11:43 +00:00
2025-10-02 05:04:42 +00:00
<Tabs defaultValue="basic" className="w-full">
<TabsList className="grid w-full grid-cols-3">
2026-01-29 02:11:43 +00:00
<TabsTrigger value="basic" disabled={isLoadingData}>
Basic Information
</TabsTrigger>
<TabsTrigger value="steps" disabled={isLoadingData}>
Workflow Steps
</TabsTrigger>
<TabsTrigger value="settings" disabled={isLoadingData}>
Client Settings
</TabsTrigger>
2025-10-02 05:04:42 +00:00
</TabsList>
{/* Basic Information Tab */}
<TabsContent value="basic" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Workflow Basic Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<FormField
label="Workflow Name"
name="name"
type="text"
placeholder="e.g., Standard Article Approval"
value={formData.name}
onChange={(value) => handleBasicInfoChange("name", value)}
error={errors.name}
required
/>
<FormField
label="Workflow Description"
name="description"
type="textarea"
placeholder="Describe the purpose and process of this workflow"
value={formData.description}
2026-01-29 02:11:43 +00:00
onChange={(value) =>
handleBasicInfoChange("description", value)
}
2025-10-02 05:04:42 +00:00
error={errors.description}
required
rows={4}
/>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<FormField
label="Is Active"
name="isActive"
type="checkbox"
value={formData.isActive}
onChange={(value) => handleBasicInfoChange("isActive", value)}
/>
<FormField
label="Set as Default Workflow"
name="isDefault"
type="checkbox"
value={formData.isDefault}
2026-01-29 02:11:43 +00:00
onChange={(value) =>
handleBasicInfoChange("isDefault", value)
}
2025-10-02 05:04:42 +00:00
/>
<FormField
label="Requires Approval"
name="requiresApproval"
type="checkbox"
value={formData.requiresApproval}
2026-01-29 02:11:43 +00:00
onChange={(value) =>
handleBasicInfoChange("requiresApproval", value)
}
2025-10-02 05:04:42 +00:00
/>
<FormField
label="Auto Publish After Approval"
name="autoPublish"
type="checkbox"
value={formData.autoPublish}
2026-01-29 02:11:43 +00:00
onChange={(value) =>
handleBasicInfoChange("autoPublish", value)
}
2025-10-02 05:04:42 +00:00
/>
</div>
</CardContent>
</Card>
</TabsContent>
{/* Workflow Steps Tab */}
<TabsContent value="steps" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>Workflow Steps Configuration</span>
<div className="text-sm text-gray-500">
2026-01-29 02:11:43 +00:00
{formData.steps.length} step
{formData.steps.length !== 1 ? "s" : ""}
2025-10-02 05:04:42 +00:00
{(() => {
2026-01-29 02:11:43 +00:00
const parallelGroups = formData.steps.reduce(
(acc, step) => {
acc[step.stepOrder] = (acc[step.stepOrder] || 0) + 1;
return acc;
},
{} as Record<number, number>,
);
const parallelCount = Object.values(parallelGroups).filter(
(count) => count > 1,
).length;
return parallelCount > 0
? `${parallelCount} parallel group${parallelCount !== 1 ? "s" : ""}`
: "";
2025-10-02 05:04:42 +00:00
})()}
</div>
</CardTitle>
</CardHeader>
<CardContent>
<DynamicArray
items={formData.steps}
onItemsChange={handleStepsChange}
renderItem={renderStepForm}
addItemLabel="Add Workflow Step"
emptyStateMessage="No workflow steps configured yet. Add at least one step to define the approval process."
allowReorder={true}
allowDuplicate={true}
/>
{errors.steps && (
<p className="text-red-500 text-sm mt-2">{errors.steps}</p>
)}
</CardContent>
</Card>
</TabsContent>
{/* Client Settings Tab */}
<TabsContent value="settings" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Client Approval Settings</CardTitle>
</CardHeader>
<CardContent>
<div className="text-center py-8">
<div className="text-gray-500 mb-4">
<div className="h-12 w-12 mx-auto mb-2 bg-gray-100 rounded-full flex items-center justify-center">
<span className="text-gray-400 text-xl"></span>
</div>
2026-01-29 02:11:43 +00:00
<h3 className="text-lg font-medium text-gray-900 mb-2">
Settings Pre-configured
</h3>
2025-10-02 05:04:42 +00:00
<p className="text-gray-600">
2026-01-29 02:11:43 +00:00
Client approval settings are automatically configured with
optimal defaults.
2025-10-02 05:04:42 +00:00
</p>
</div>
2026-01-29 02:11:43 +00:00
2025-10-02 05:04:42 +00:00
<div className="bg-gray-50 rounded-lg p-4 text-left max-w-md mx-auto">
2026-01-29 02:11:43 +00:00
<h4 className="font-medium text-gray-900 mb-2">
Default Settings:
</h4>
2025-10-02 05:04:42 +00:00
<ul className="text-sm text-gray-600 space-y-1">
2026-01-29 02:11:43 +00:00
<li>
Requires Approval:{" "}
<span className="font-medium text-green-600">Yes</span>
</li>
<li>
Auto Publish Articles:{" "}
<span className="font-medium text-green-600">Yes</span>
</li>
<li>
Is Active:{" "}
<span className="font-medium text-green-600">Yes</span>
</li>
<li>
Exempt Users:{" "}
<span className="font-medium text-gray-500">None</span>
</li>
<li>
Exempt Roles:{" "}
<span className="font-medium text-gray-500">None</span>
</li>
<li>
Exempt Categories:{" "}
<span className="font-medium text-gray-500">None</span>
</li>
2025-10-02 05:04:42 +00:00
</ul>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* Form Actions */}
<div className="flex items-center justify-between pt-6 border-t">
<div className="flex items-center gap-2">
<Button
type="button"
variant="outline"
onClick={handleReset}
disabled={isSubmitting}
>
<RotateCcwIcon className="h-4 w-4 mr-2" />
Reset
</Button>
</div>
<div className="flex items-center gap-2">
{onCancel && (
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={isSubmitting}
>
Cancel
</Button>
)}
2026-01-29 02:11:43 +00:00
2025-10-02 05:04:42 +00:00
<Button
type="submit"
variant="outline"
2025-10-02 05:04:42 +00:00
disabled={isSubmitting || isLoading || isLoadingData}
className="flex items-center gap-2"
>
<SaveIcon className="h-4 w-4" />
2026-01-29 02:11:43 +00:00
{isSubmitting
? "Saving..."
: isLoadingData
? "Loading..."
: workflowId
? "Update Workflow"
: "Save Workflow"}
2025-10-02 05:04:42 +00:00
</Button>
</div>
</div>
</form>
);
};