"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 = ({ initialData, workflowId, onSave, onCancel, isLoading = false, }) => { // Form state const [formData, setFormData] = useState({ 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([]); const [users, setUsers] = useState([]); const [userRoles, setUserRoles] = useState([]); const [articleCategories, setArticleCategories] = useState( [], ); // UI state const [errors, setErrors] = useState>({}); const [isSubmitting, setIsSubmitting] = useState(false); const [expandedSteps, setExpandedSteps] = useState>(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(); // 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 = {}; 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 (
{/* Parallel Step Indicator */} {isParallelStep && (
Parallel Step - Order {step.stepOrder} ({parallelSteps.length + 1} steps running simultaneously)
)}
onUpdate({ ...step, stepOrder: value ? Number(value) : index + 1, }) } error={errors[`steps.${index}.stepOrder`]} min={1} helpText="Same order = parallel steps" /> onUpdate({ ...step, stepName: value })} error={errors[`steps.${index}.stepName`]} required /> 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 } />
onUpdate({ ...step, autoApproveAfterHours: value ? Number(value) : undefined, }) } helpText="Automatically approve after specified hours" min={1} />
onUpdate({ ...step, canSkip: value })} /> onUpdate({ ...step, isActive: value })} />
{/* Condition Settings */}
{ 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"; } })()} />
); }; 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 (
{isLoadingData && (
Loading form data...
)} Basic Information Workflow Steps Client Settings {/* Basic Information Tab */} Workflow Basic Information handleBasicInfoChange("name", value)} error={errors.name} required /> handleBasicInfoChange("description", value) } error={errors.description} required rows={4} />
handleBasicInfoChange("isActive", value)} /> handleBasicInfoChange("isDefault", value) } /> handleBasicInfoChange("requiresApproval", value) } /> handleBasicInfoChange("autoPublish", value) } />
{/* Workflow Steps Tab */} Workflow Steps Configuration
{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, ); const parallelCount = Object.values(parallelGroups).filter( (count) => count > 1, ).length; return parallelCount > 0 ? ` • ${parallelCount} parallel group${parallelCount !== 1 ? "s" : ""}` : ""; })()}
{errors.steps && (

{errors.steps}

)}
{/* Client Settings Tab */} Client Approval Settings
⚙️

Settings Pre-configured

Client approval settings are automatically configured with optimal defaults.

Default Settings:

  • • Requires Approval:{" "} Yes
  • • Auto Publish Articles:{" "} Yes
  • • Is Active:{" "} Yes
  • • Exempt Users:{" "} None
  • • Exempt Roles:{" "} None
  • • Exempt Categories:{" "} None
{/* Form Actions */}
{onCancel && ( )}
); };