875 lines
28 KiB
TypeScript
875 lines
28 KiB
TypeScript
"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,
|
||
UpdateApprovalWorkflowWithClientSettingsRequest,
|
||
ApprovalWorkflowStepRequest,
|
||
ClientApprovalSettingsRequest,
|
||
UserLevel,
|
||
User,
|
||
UserRole,
|
||
ArticleCategory,
|
||
createApprovalWorkflowWithClientSettings,
|
||
updateApprovalWorkflowWithClientSettings,
|
||
getUserLevels,
|
||
getUsers,
|
||
getUserRoles,
|
||
getArticleCategories,
|
||
} from "@/service/approval-workflows";
|
||
import Swal from "sweetalert2";
|
||
|
||
interface ApprovalWorkflowFormProps {
|
||
initialData?: CreateApprovalWorkflowWithClientSettingsRequest;
|
||
workflowId?: number; // For update mode
|
||
onSave?: (data: CreateApprovalWorkflowWithClientSettingsRequest) => void;
|
||
onCancel?: () => void;
|
||
isLoading?: boolean;
|
||
}
|
||
|
||
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,
|
||
});
|
||
|
||
export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
||
initialData,
|
||
workflowId,
|
||
onSave,
|
||
onCancel,
|
||
isLoading = false,
|
||
}) => {
|
||
// Form state
|
||
const [formData, setFormData] =
|
||
useState<CreateApprovalWorkflowWithClientSettingsRequest>({
|
||
name: "",
|
||
description: "",
|
||
isActive: true,
|
||
isDefault: true,
|
||
requiresApproval: true,
|
||
autoPublish: false,
|
||
steps: [],
|
||
clientApprovalSettings: {
|
||
requiresApproval: true,
|
||
autoPublishArticles: false,
|
||
approvalExemptUsers: [],
|
||
approvalExemptRoles: [],
|
||
approvalExemptCategories: [],
|
||
requireApprovalFor: [],
|
||
skipApprovalFor: [],
|
||
isActive: true,
|
||
},
|
||
});
|
||
|
||
// API data
|
||
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
|
||
const [users, setUsers] = useState<User[]>([]);
|
||
const [userRoles, setUserRoles] = useState<UserRole[]>([]);
|
||
const [articleCategories, setArticleCategories] = useState<ArticleCategory[]>(
|
||
[],
|
||
);
|
||
|
||
// 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>();
|
||
|
||
// 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);
|
||
if (
|
||
conditionData.applies_to_levels &&
|
||
Array.isArray(conditionData.applies_to_levels)
|
||
) {
|
||
conditionData.applies_to_levels.forEach((levelId: number) => {
|
||
usedLevelIds.add(levelId);
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error("Error parsing conditionValue:", error);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Filter out used levels and return available ones
|
||
return userLevels.filter((level) => !usedLevelIds.has(level.id));
|
||
};
|
||
|
||
// Load initial data
|
||
useEffect(() => {
|
||
if (initialData) {
|
||
setFormData(initialData);
|
||
}
|
||
}, [initialData]);
|
||
|
||
// Load API data
|
||
useEffect(() => {
|
||
const loadData = async () => {
|
||
try {
|
||
const [userLevelsRes, usersRes, userRolesRes, categoriesRes] =
|
||
await Promise.all([
|
||
getUserLevels(),
|
||
getUsers(),
|
||
getUserRoles(),
|
||
getArticleCategories(),
|
||
]);
|
||
|
||
if (!userLevelsRes?.error)
|
||
setUserLevels(userLevelsRes?.data?.data || []);
|
||
if (!usersRes?.error) setUsers(usersRes?.data || []);
|
||
if (!userRolesRes?.error) setUserRoles(userRolesRes?.data || []);
|
||
if (!categoriesRes?.error)
|
||
setArticleCategories(categoriesRes?.data || []);
|
||
} 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";
|
||
} else if (formData.description.trim().length < 3) {
|
||
newErrors.description = "Description must be at least 3 characters";
|
||
}
|
||
|
||
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) {
|
||
newErrors[`steps.${index}.requiredUserLevelId`] =
|
||
"Required user level is required";
|
||
}
|
||
if (step.stepOrder <= 0) {
|
||
newErrors[`steps.${index}.stepOrder`] =
|
||
"Step order must be greater than 0";
|
||
}
|
||
});
|
||
|
||
setErrors(newErrors);
|
||
return Object.keys(newErrors).length === 0;
|
||
};
|
||
|
||
// Form handlers
|
||
const handleBasicInfoChange = (
|
||
field: keyof CreateApprovalWorkflowWithClientSettingsRequest,
|
||
value: any,
|
||
) => {
|
||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||
if (errors[field]) {
|
||
setErrors((prev) => ({ ...prev, [field]: "" }));
|
||
}
|
||
};
|
||
|
||
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;
|
||
});
|
||
|
||
setFormData((prev) => ({ ...prev, steps: updatedSteps }));
|
||
};
|
||
|
||
const renderStepForm = (
|
||
step: ApprovalWorkflowStepRequest,
|
||
index: number,
|
||
onUpdate: (step: ApprovalWorkflowStepRequest) => void,
|
||
onDelete: () => void,
|
||
) => {
|
||
const stepErrors = Object.keys(errors).filter((key) =>
|
||
key.startsWith(`steps.${index}`),
|
||
);
|
||
|
||
// Check if this step has parallel steps (same stepOrder)
|
||
const parallelSteps = formData.steps.filter(
|
||
(s, i) => s.stepOrder === step.stepOrder && i !== index,
|
||
);
|
||
const isParallelStep = parallelSteps.length > 0;
|
||
|
||
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}
|
||
onChange={(value) =>
|
||
onUpdate({
|
||
...step,
|
||
stepOrder: value ? Number(value) : index + 1,
|
||
})
|
||
}
|
||
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"
|
||
placeholder={
|
||
userLevels.length > 0
|
||
? "Select user level"
|
||
: "No user levels available"
|
||
}
|
||
value={step.requiredUserLevelId}
|
||
onChange={(value) =>
|
||
onUpdate({ ...step, requiredUserLevelId: Number(value) })
|
||
}
|
||
error={errors[`steps.${index}.requiredUserLevelId`]}
|
||
options={
|
||
userLevels.length > 0
|
||
? userLevels.map((level) => ({
|
||
value: level.id,
|
||
label: `${level.name} (Level ${level.levelNumber})`,
|
||
}))
|
||
: []
|
||
}
|
||
required
|
||
disabled={userLevels.length === 0}
|
||
helpText={
|
||
userLevels.length === 0
|
||
? "No user levels found. Please create user levels first."
|
||
: undefined
|
||
}
|
||
/>
|
||
</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}
|
||
onChange={(value) =>
|
||
onUpdate({
|
||
...step,
|
||
autoApproveAfterHours: value ? Number(value) : undefined,
|
||
})
|
||
}
|
||
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 })}
|
||
/>
|
||
|
||
<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);
|
||
return availableLevels.length > 0
|
||
? "Select user levels..."
|
||
: "No available user levels";
|
||
})()}
|
||
options={(() => {
|
||
const availableLevels = getAvailableUserLevels(index);
|
||
return availableLevels.map((level) => ({
|
||
value: level.id,
|
||
label: `${level.name} (Level ${level.levelNumber})`,
|
||
}));
|
||
})()}
|
||
value={(() => {
|
||
try {
|
||
return step.conditionValue
|
||
? JSON.parse(step.conditionValue).applies_to_levels || []
|
||
: [];
|
||
} catch {
|
||
return [];
|
||
}
|
||
})()}
|
||
onChange={(value) => {
|
||
const conditionValue = JSON.stringify({
|
||
applies_to_levels: value,
|
||
});
|
||
onUpdate({
|
||
...step,
|
||
conditionType: "user_level_hierarchy",
|
||
conditionValue,
|
||
});
|
||
}}
|
||
searchable={true}
|
||
disabled={getAvailableUserLevels(index).length === 0}
|
||
helpText={(() => {
|
||
const availableLevels = getAvailableUserLevels(index);
|
||
const usedLevels = userLevels.length - availableLevels.length;
|
||
|
||
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>
|
||
);
|
||
};
|
||
|
||
function normalizeBoolean(value?: boolean): boolean {
|
||
return value ?? false;
|
||
}
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
|
||
if (!validateForm()) {
|
||
Swal.fire({
|
||
title: "Validation Error",
|
||
text: "Please fix the errors before submitting",
|
||
icon: "error",
|
||
confirmButtonText: "OK",
|
||
customClass: {
|
||
popup: "swal-z-index-9999",
|
||
},
|
||
});
|
||
return;
|
||
}
|
||
|
||
setIsSubmitting(true);
|
||
|
||
try {
|
||
// Hardcoded client approval settings
|
||
const hardcodedClientSettings = {
|
||
approvalExemptCategories: [],
|
||
approvalExemptRoles: [],
|
||
approvalExemptUsers: [],
|
||
autoPublishArticles: true,
|
||
isActive: true,
|
||
requireApprovalFor: [],
|
||
requiresApproval: true,
|
||
skipApprovalFor: [],
|
||
};
|
||
|
||
const clientSettingsPayload = {
|
||
approvalExemptCategories: [],
|
||
approvalExemptRoles: [],
|
||
approvalExemptUsers: [],
|
||
requireApprovalFor: [],
|
||
skipApprovalFor: [],
|
||
|
||
autoPublishArticles:
|
||
formData.clientApprovalSettings.autoPublishArticles ?? false,
|
||
|
||
requiresApproval:
|
||
formData.clientApprovalSettings.requiresApproval ?? false,
|
||
|
||
isActive: formData.clientApprovalSettings.isActive ?? false,
|
||
};
|
||
|
||
if (workflowId) {
|
||
// Update mode
|
||
const updateData: UpdateApprovalWorkflowWithClientSettingsRequest = {
|
||
workflowId,
|
||
name: formData.name,
|
||
description: formData.description,
|
||
|
||
isActive: normalizeBoolean(formData.isActive),
|
||
isDefault: normalizeBoolean(formData.isDefault),
|
||
|
||
steps: formData.steps.map((step) => ({
|
||
...step,
|
||
branchName: step.stepName,
|
||
})),
|
||
|
||
clientSettings: {
|
||
approvalExemptCategories: [],
|
||
approvalExemptRoles: [],
|
||
approvalExemptUsers: [],
|
||
requireApprovalFor: [],
|
||
skipApprovalFor: [],
|
||
|
||
// 🔥 AMBIL LANGSUNG DARI CHECKBOX UI
|
||
requiresApproval: normalizeBoolean(formData.requiresApproval),
|
||
autoPublishArticles: normalizeBoolean(formData.autoPublish),
|
||
isActive: normalizeBoolean(formData.isActive),
|
||
},
|
||
};
|
||
|
||
// 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
|
||
// };
|
||
|
||
console.log("Update Data: ", updateData);
|
||
|
||
const response =
|
||
await updateApprovalWorkflowWithClientSettings(updateData);
|
||
|
||
console.log("Update Response: ", response);
|
||
|
||
if (response?.error) {
|
||
Swal.fire({
|
||
title: "Error",
|
||
text:
|
||
response?.message?.messages?.[0] ||
|
||
"Failed to update approval workflow",
|
||
icon: "error",
|
||
confirmButtonText: "OK",
|
||
customClass: {
|
||
popup: "swal-z-index-9999",
|
||
},
|
||
});
|
||
} else {
|
||
Swal.fire({
|
||
title: "Success",
|
||
text: "Approval workflow updated successfully",
|
||
icon: "success",
|
||
confirmButtonText: "OK",
|
||
customClass: {
|
||
popup: "swal-z-index-9999",
|
||
},
|
||
}).then(() => {
|
||
// Call onSave to trigger parent refresh
|
||
if (onSave) {
|
||
onSave(formData);
|
||
}
|
||
});
|
||
}
|
||
} else {
|
||
// Create mode
|
||
const submitData = {
|
||
...formData,
|
||
clientApprovalSettings: hardcodedClientSettings,
|
||
};
|
||
|
||
console.log("Create Data: ", submitData);
|
||
|
||
const response =
|
||
await createApprovalWorkflowWithClientSettings(submitData);
|
||
|
||
console.log("Create Response: ", response);
|
||
|
||
if (response?.error) {
|
||
Swal.fire({
|
||
title: "Error",
|
||
text:
|
||
response?.message?.messages?.[0] ||
|
||
"Failed to create approval workflow",
|
||
icon: "error",
|
||
confirmButtonText: "OK",
|
||
customClass: {
|
||
popup: "swal-z-index-9999",
|
||
},
|
||
});
|
||
} else {
|
||
Swal.fire({
|
||
title: "Success",
|
||
text: "Approval workflow created successfully",
|
||
icon: "success",
|
||
confirmButtonText: "OK",
|
||
customClass: {
|
||
popup: "swal-z-index-9999",
|
||
},
|
||
}).then(() => {
|
||
// Call onSave to trigger parent refresh
|
||
if (onSave) {
|
||
onSave(submitData);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("Error submitting form:", error);
|
||
Swal.fire({
|
||
title: "Error",
|
||
text: "An unexpected error occurred",
|
||
icon: "error",
|
||
confirmButtonText: "OK",
|
||
customClass: {
|
||
popup: "swal-z-index-9999",
|
||
},
|
||
});
|
||
} 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",
|
||
cancelButtonText: "Cancel",
|
||
customClass: {
|
||
popup: "swal-z-index-9999",
|
||
},
|
||
}).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>
|
||
)}
|
||
|
||
<Tabs defaultValue="basic" className="w-full">
|
||
<TabsList className="grid w-full grid-cols-3">
|
||
<TabsTrigger value="basic" disabled={isLoadingData}>
|
||
Basic Information
|
||
</TabsTrigger>
|
||
<TabsTrigger value="steps" disabled={isLoadingData}>
|
||
Workflow Steps
|
||
</TabsTrigger>
|
||
<TabsTrigger value="settings" disabled={isLoadingData}>
|
||
Client Settings
|
||
</TabsTrigger>
|
||
</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}
|
||
onChange={(value) =>
|
||
handleBasicInfoChange("description", value)
|
||
}
|
||
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}
|
||
onChange={(value) =>
|
||
handleBasicInfoChange("isDefault", value)
|
||
}
|
||
/>
|
||
|
||
<FormField
|
||
label="Requires Approval"
|
||
name="requiresApproval"
|
||
type="checkbox"
|
||
value={formData.requiresApproval}
|
||
onChange={(value) =>
|
||
handleBasicInfoChange("requiresApproval", value)
|
||
}
|
||
/>
|
||
|
||
<FormField
|
||
label="Auto Publish After Approval"
|
||
name="autoPublish"
|
||
type="checkbox"
|
||
value={formData.autoPublish}
|
||
onChange={(value) =>
|
||
handleBasicInfoChange("autoPublish", value)
|
||
}
|
||
/>
|
||
</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">
|
||
{formData.steps.length} step
|
||
{formData.steps.length !== 1 ? "s" : ""}
|
||
{(() => {
|
||
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" : ""}`
|
||
: "";
|
||
})()}
|
||
</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>
|
||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||
Settings Pre-configured
|
||
</h3>
|
||
<p className="text-gray-600">
|
||
Client approval settings are automatically configured with
|
||
optimal defaults.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="bg-gray-50 rounded-lg p-4 text-left max-w-md mx-auto">
|
||
<h4 className="font-medium text-gray-900 mb-2">
|
||
Default Settings:
|
||
</h4>
|
||
<ul className="text-sm text-gray-600 space-y-1">
|
||
<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>
|
||
</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>
|
||
)}
|
||
|
||
<Button
|
||
type="submit"
|
||
variant="outline"
|
||
disabled={isSubmitting || isLoading || isLoadingData}
|
||
className="flex items-center gap-2"
|
||
>
|
||
<SaveIcon className="h-4 w-4" />
|
||
{isSubmitting
|
||
? "Saving..."
|
||
: isLoadingData
|
||
? "Loading..."
|
||
: workflowId
|
||
? "Update Workflow"
|
||
: "Save Workflow"}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
);
|
||
};
|