fix: conflict when pull main
This commit is contained in:
commit
f44a7c4be1
|
|
@ -3,19 +3,8 @@ import React, { useState } 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 {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
PlusIcon,
|
||||
SettingsIcon,
|
||||
UsersIcon,
|
||||
WorkflowIcon,
|
||||
} from "@/components/icons";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { PlusIcon, SettingsIcon, UsersIcon, WorkflowIcon, DotsIcon, DeleteIcon } from "@/components/icons";
|
||||
import { ApprovalWorkflowForm } from "@/components/form/ApprovalWorkflowForm";
|
||||
import { UserLevelsForm } from "@/components/form/UserLevelsForm";
|
||||
import { useWorkflowModal } from "@/components/modals/WorkflowModalProvider";
|
||||
|
|
@ -50,7 +39,713 @@ import useTableColumns from "./component/columns";
|
|||
import TablePagination from "@/components/table/table-pagination";
|
||||
import TenantSettingsPageTable from "./component/table-user-level";
|
||||
|
||||
function TenantSettingsContent() {
|
||||
const [activeTab, setActiveTab] = useState("workflows");
|
||||
const [isUserLevelDialogOpen, setIsUserLevelDialogOpen] = useState(false);
|
||||
const [workflow, setWorkflow] = useState<ComprehensiveWorkflowResponse | null>(null);
|
||||
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isEditingWorkflow, setIsEditingWorkflow] = useState(false);
|
||||
const [editingUserLevel, setEditingUserLevel] = useState<UserLevel | null>(null);
|
||||
const { checkWorkflowStatus } = useWorkflowStatusCheck();
|
||||
const { showWorkflowModal } = useWorkflowModal();
|
||||
|
||||
// Load data on component mount
|
||||
React.useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [comprehensiveWorkflowRes, userLevelsRes] = await Promise.all([
|
||||
getApprovalWorkflowComprehensiveDetails(4), // Using workflow ID 4 as per API example
|
||||
getUserLevels(),
|
||||
]);
|
||||
|
||||
if (!comprehensiveWorkflowRes?.error) {
|
||||
setWorkflow(comprehensiveWorkflowRes?.data?.data || null);
|
||||
} else {
|
||||
setWorkflow(null);
|
||||
}
|
||||
|
||||
if (!userLevelsRes?.error) {
|
||||
setUserLevels(userLevelsRes?.data?.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWorkflowSave = async (data: CreateApprovalWorkflowWithClientSettingsRequest) => {
|
||||
setIsEditingWorkflow(false);
|
||||
await loadData(); // Reload data after saving
|
||||
};
|
||||
|
||||
const handleUserLevelSave = async (data: UserLevelsCreateRequest) => {
|
||||
try {
|
||||
const response = await createUserLevel(data);
|
||||
|
||||
if (response?.error) {
|
||||
console.error("Error creating user level:", response?.message);
|
||||
// You can add error handling here (e.g., show error message)
|
||||
} else {
|
||||
console.log("User level created successfully:", response);
|
||||
// You can add success handling here (e.g., show success message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating user level:", error);
|
||||
}
|
||||
|
||||
setIsUserLevelDialogOpen(false);
|
||||
setEditingUserLevel(null);
|
||||
await loadData(); // Reload data after saving
|
||||
};
|
||||
|
||||
const handleEditUserLevel = (userLevel: UserLevel) => {
|
||||
setEditingUserLevel(userLevel);
|
||||
setIsUserLevelDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteUserLevel = async (userLevel: UserLevel) => {
|
||||
if (window.confirm(`Are you sure you want to delete "${userLevel.name}"? This action cannot be undone.`)) {
|
||||
try {
|
||||
// TODO: Implement delete API call
|
||||
console.log("Delete user level:", userLevel.id);
|
||||
await loadData(); // Reload data after deletion
|
||||
} catch (error) {
|
||||
console.error("Error deleting user level:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkUserLevelSave = async (data: UserLevelsCreateRequest[]) => {
|
||||
setIsUserLevelDialogOpen(false);
|
||||
await loadData(); // Reload data after saving
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Tenant Settings</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage approval workflows and user levels for your tenant
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SettingsIcon className="h-6 w-6 text-gray-500" />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={checkWorkflowStatus}
|
||||
>
|
||||
Check Workflow Status
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => showWorkflowModal({ hasWorkflowSetup: false })}
|
||||
className="bg-red-50 text-red-600 border-red-200 hover:bg-red-100"
|
||||
>
|
||||
Test Modal
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="workflows" className="flex items-center gap-2">
|
||||
<WorkflowIcon className="h-4 w-4" />
|
||||
Approval Workflows
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="user-levels" className="flex items-center gap-2">
|
||||
<UsersIcon className="h-4 w-4" />
|
||||
User Levels
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Approval Workflows Tab */}
|
||||
<TabsContent value="workflows" className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold">Approval Workflow Setup</h2>
|
||||
{workflow && !isEditingWorkflow && (
|
||||
<Button
|
||||
onClick={() => setIsEditingWorkflow(true)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<SettingsIcon className="h-4 w-4" />
|
||||
Edit Workflow
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditingWorkflow ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Setup Approval Workflow</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsEditingWorkflow(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ApprovalWorkflowForm
|
||||
initialData={workflow ? {
|
||||
name: workflow.workflow.name,
|
||||
description: workflow.workflow.description,
|
||||
isDefault: workflow.workflow.isDefault,
|
||||
isActive: workflow.workflow.isActive,
|
||||
requiresApproval: workflow.workflow.requiresApproval,
|
||||
autoPublish: workflow.workflow.autoPublish,
|
||||
steps: workflow.steps?.map(step => ({
|
||||
stepOrder: step.stepOrder,
|
||||
stepName: step.stepName,
|
||||
requiredUserLevelId: step.requiredUserLevelId,
|
||||
canSkip: step.canSkip,
|
||||
autoApproveAfterHours: step.autoApproveAfterHours,
|
||||
isActive: step.isActive,
|
||||
conditionType: step.conditionType,
|
||||
conditionValue: step.conditionValue,
|
||||
})) || [],
|
||||
clientApprovalSettings: {
|
||||
approvalExemptCategories: workflow.clientSettings.exemptCategoriesDetails || [],
|
||||
approvalExemptRoles: workflow.clientSettings.exemptRolesDetails || [],
|
||||
approvalExemptUsers: workflow.clientSettings.exemptUsersDetails || [],
|
||||
autoPublishArticles: workflow.clientSettings.autoPublishArticles,
|
||||
isActive: workflow.clientSettings.isActive,
|
||||
requireApprovalFor: workflow.clientSettings.requireApprovalFor || [],
|
||||
requiresApproval: workflow.clientSettings.requiresApproval,
|
||||
skipApprovalFor: workflow.clientSettings.skipApprovalFor || []
|
||||
}
|
||||
} : undefined}
|
||||
workflowId={workflow?.workflow.id}
|
||||
onSave={handleWorkflowSave}
|
||||
onCancel={() => setIsEditingWorkflow(false)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : workflow ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>{workflow.workflow.name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{workflow.workflow.isDefault && (
|
||||
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
|
||||
Default
|
||||
</span>
|
||||
)}
|
||||
{workflow.workflow.isActive ? (
|
||||
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-800 rounded-full">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
{workflow.workflow.description}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">{workflow.workflow.totalSteps}</div>
|
||||
<div className="text-sm text-gray-600">Total Steps</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">{workflow.workflow.activeSteps}</div>
|
||||
<div className="text-sm text-gray-600">Active Steps</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className={`text-2xl font-bold ${workflow.workflow.requiresApproval ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{workflow.workflow.requiresApproval ? 'Yes' : 'No'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Requires Approval</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className={`text-2xl font-bold ${workflow.workflow.autoPublish ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{workflow.workflow.autoPublish ? 'Yes' : 'No'}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Auto Publish</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflow Steps Overview */}
|
||||
{workflow.steps && workflow.steps.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h4 className="text-lg font-medium mb-3">Workflow Steps</h4>
|
||||
<div className="space-y-2">
|
||||
{workflow.steps.map((step: any, index: number) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-sm font-medium">
|
||||
{step.stepOrder}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{step.stepName}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{step.conditionType && `Condition: ${step.conditionType}`}
|
||||
{step.autoApproveAfterHours && ` • Auto-approve after ${step.autoApproveAfterHours}h`}
|
||||
{step.requiredUserLevelName && ` • Required Level: ${step.requiredUserLevelName}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{step.canSkip && (
|
||||
<span className="px-2 py-1 text-xs bg-yellow-100 text-yellow-800 rounded-full">
|
||||
Can Skip
|
||||
</span>
|
||||
)}
|
||||
{step.isParallel && (
|
||||
<span className="px-2 py-1 text-xs bg-purple-100 text-purple-800 rounded-full">
|
||||
Parallel
|
||||
</span>
|
||||
)}
|
||||
{step.isActive && (
|
||||
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
{step.isFirstStep && (
|
||||
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
|
||||
First Step
|
||||
</span>
|
||||
)}
|
||||
{step.isLastStep && (
|
||||
<span className="px-2 py-1 text-xs bg-orange-100 text-orange-800 rounded-full">
|
||||
Last Step
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Client Settings */}
|
||||
<div className="mb-6">
|
||||
<h4 className="text-lg font-medium mb-3">Client Settings</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Default Workflow</div>
|
||||
<div className="text-sm text-gray-600">{workflow.clientSettings.defaultWorkflowName}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Auto Publish Articles</div>
|
||||
<div className={`text-sm font-medium ${workflow.clientSettings.autoPublishArticles ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{workflow.clientSettings.autoPublishArticles ? 'Yes' : 'No'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Requires Approval</div>
|
||||
<div className={`text-sm font-medium ${workflow.clientSettings.requiresApproval ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{workflow.clientSettings.requiresApproval ? 'Yes' : 'No'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Settings Active</div>
|
||||
<div className={`text-sm font-medium ${workflow.clientSettings.isActive ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{workflow.clientSettings.isActive ? 'Yes' : 'No'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
<div className="mb-6">
|
||||
<h4 className="text-lg font-medium mb-3">Workflow Statistics</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Total Articles Processed</div>
|
||||
<div className="text-2xl font-bold text-blue-600">{workflow.statistics.totalArticlesProcessed}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Pending Articles</div>
|
||||
<div className="text-2xl font-bold text-yellow-600">{workflow.statistics.pendingArticles}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Approved Articles</div>
|
||||
<div className="text-2xl font-bold text-green-600">{workflow.statistics.approvedArticles}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Rejected Articles</div>
|
||||
<div className="text-2xl font-bold text-red-600">{workflow.statistics.rejectedArticles}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Average Processing Time</div>
|
||||
<div className="text-2xl font-bold text-purple-600">{workflow.statistics.averageProcessingTime}h</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Most Active Step</div>
|
||||
<div className="text-sm text-gray-600">{workflow.statistics.mostActiveStep || 'N/A'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Workflow Metadata */}
|
||||
<div className="mb-6">
|
||||
<h4 className="text-lg font-medium mb-3">Workflow Information</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Client ID</div>
|
||||
<div className="text-sm text-gray-600 font-mono">{workflow.workflow.clientId}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Created At</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{new Date(workflow.workflow.createdAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Updated At</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{new Date(workflow.workflow.updatedAt).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Workflow ID</div>
|
||||
<div className="text-sm text-gray-600 font-mono">{workflow.workflow.id}</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Has Branches</div>
|
||||
<div className={`text-sm font-medium ${workflow.workflow.hasBranches ? 'text-green-600' : 'text-gray-600'}`}>
|
||||
{workflow.workflow.hasBranches ? 'Yes' : 'No'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 rounded-lg">
|
||||
<div className="text-sm font-medium text-gray-700 mb-1">Max Step Order</div>
|
||||
<div className="text-sm text-gray-600">{workflow.workflow.maxStepOrder}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<WorkflowIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No Workflow Configured</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Set up your approval workflow to manage content approval process
|
||||
</p>
|
||||
<Button onClick={() => setIsEditingWorkflow(true)}>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Setup Workflow
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* User Levels Tab */}
|
||||
<TabsContent value="user-levels" className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold">User Levels</h2>
|
||||
<Dialog open={isUserLevelDialogOpen} onOpenChange={setIsUserLevelDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex items-center gap-2">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create User Level
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="md:max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingUserLevel ? `Edit User Level: ${editingUserLevel.name}` : "Create New User Level"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<UserLevelsForm
|
||||
mode="single"
|
||||
initialData={editingUserLevel ? {
|
||||
name: editingUserLevel.name,
|
||||
aliasName: editingUserLevel.aliasName,
|
||||
levelNumber: editingUserLevel.levelNumber,
|
||||
parentLevelId: editingUserLevel.parentLevelId || 0,
|
||||
provinceId: editingUserLevel.provinceId,
|
||||
group: editingUserLevel.group || "",
|
||||
isApprovalActive: editingUserLevel.isApprovalActive,
|
||||
isActive: editingUserLevel.isActive,
|
||||
} : undefined}
|
||||
onSave={handleUserLevelSave}
|
||||
onCancel={() => {
|
||||
setIsUserLevelDialogOpen(false);
|
||||
setEditingUserLevel(null);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* User Levels Summary */}
|
||||
{userLevels.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">{userLevels.length}</div>
|
||||
<div className="text-sm text-gray-600">Total User Levels</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{userLevels.filter(ul => ul.isActive).length}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Active Levels</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{userLevels.filter(ul => ul.isApprovalActive).length}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Approval Active</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{userLevels.filter(ul => ul.parentLevelId).length}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Child Levels</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User Levels Hierarchy */}
|
||||
{userLevels.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<UsersIcon className="h-5 w-5" />
|
||||
User Levels Hierarchy
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{userLevels
|
||||
.filter(ul => !ul.parentLevelId) // Root levels
|
||||
.sort((a, b) => a.levelNumber - b.levelNumber)
|
||||
.map(rootLevel => (
|
||||
<div key={rootLevel.id} className="space-y-2">
|
||||
{/* Root Level */}
|
||||
<div className="flex items-center gap-3 p-3 bg-blue-50 rounded-lg border-l-4 border-blue-500">
|
||||
<div className="w-8 h-8 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-sm font-medium">
|
||||
{rootLevel.levelNumber}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{rootLevel.name}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{rootLevel.aliasName} • {rootLevel.group || 'No group'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{rootLevel.isActive && (
|
||||
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
{rootLevel.isApprovalActive && (
|
||||
<span className="px-2 py-1 text-xs bg-purple-100 text-purple-800 rounded-full">
|
||||
Approval Active
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEditUserLevel(rootLevel)}
|
||||
className="h-8 w-8 p-0 border-gray-300 hover:border-blue-500 hover:bg-blue-50"
|
||||
title="Edit User Level"
|
||||
>
|
||||
<DotsIcon className="h-4 w-4 text-gray-600 hover:text-blue-600" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteUserLevel(rootLevel)}
|
||||
className="h-8 w-8 p-0 border-gray-300 hover:border-red-500 hover:bg-red-50"
|
||||
title="Delete User Level"
|
||||
>
|
||||
<DeleteIcon className="h-4 w-4 text-gray-600 hover:text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Child Levels */}
|
||||
{userLevels
|
||||
.filter(ul => ul.parentLevelId === rootLevel.id)
|
||||
.sort((a, b) => a.levelNumber - b.levelNumber)
|
||||
.map(childLevel => (
|
||||
<div key={childLevel.id} className="ml-8 flex items-center gap-3 p-3 bg-gray-50 rounded-lg border-l-4 border-gray-300">
|
||||
<div className="w-6 h-6 bg-gray-100 text-gray-600 rounded-full flex items-center justify-center text-xs font-medium">
|
||||
{childLevel.levelNumber}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{childLevel.name}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{childLevel.aliasName} • {childLevel.group || 'No group'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{childLevel.isActive && (
|
||||
<span className="px-1 py-0.5 text-xs bg-green-100 text-green-800 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
{childLevel.isApprovalActive && (
|
||||
<span className="px-1 py-0.5 text-xs bg-purple-100 text-purple-800 rounded-full">
|
||||
Approval
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleEditUserLevel(childLevel)}
|
||||
className="h-7 w-7 p-0 border-gray-300 hover:border-blue-500 hover:bg-blue-50"
|
||||
title="Edit User Level"
|
||||
>
|
||||
<DotsIcon className="h-3.5 w-3.5 text-gray-600 hover:text-blue-600" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteUserLevel(childLevel)}
|
||||
className="h-7 w-7 p-0 border-gray-300 hover:border-red-500 hover:bg-red-50"
|
||||
title="Delete User Level"
|
||||
>
|
||||
<DeleteIcon className="h-3.5 w-3.5 text-gray-600 hover:text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{userLevels.length > 0 ? userLevels.map((userLevel) => (
|
||||
<Card key={userLevel.id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="truncate">{userLevel.name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
|
||||
Level {userLevel.levelNumber}
|
||||
</span>
|
||||
{userLevel.isActive ? (
|
||||
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-800 rounded-full">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-500">Alias:</span>
|
||||
<span className="font-mono text-xs bg-gray-100 px-2 py-1 rounded">
|
||||
{userLevel.aliasName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{userLevel.group && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-500">Group:</span>
|
||||
<span className="font-medium">{userLevel.group}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-500">Approval Active:</span>
|
||||
<span className={`font-medium ${userLevel.isApprovalActive ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{userLevel.isApprovalActive ? 'Yes' : 'No'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{userLevel.parentLevelId && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-500">Parent Level:</span>
|
||||
<span className="font-medium">
|
||||
{ userLevels.length > 0 ? userLevels.find(ul => ul.id === userLevel.parentLevelId)?.name || `Level ${userLevel.parentLevelId}` : `Level ${userLevel.parentLevelId}`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{userLevel.provinceId && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-500">Province:</span>
|
||||
<span className="font-medium">Province {userLevel.provinceId}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-500">Created:</span>
|
||||
<span className="font-medium text-xs">
|
||||
{userLevel.createdAt ? new Date(userLevel.createdAt).toLocaleDateString() : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="flex-1">
|
||||
Users
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)) : ''}
|
||||
</div> */}
|
||||
|
||||
{userLevels.length === 0 && !isLoading && (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<UsersIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No User Levels Found</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Create your first user level to define approval hierarchy
|
||||
</p>
|
||||
<Button onClick={() => setIsUserLevelDialogOpen(true)}>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create User Level
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TenantSettingsPage() {
|
||||
return <TenantSettingsPageTable />;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
/* SweetAlert2 z-index fix */
|
||||
.swal-z-index-9999 {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { DynamicArray } from "./common/DynamicArray";
|
|||
import { MultiSelect } from "./common/MultiSelect";
|
||||
import {
|
||||
CreateApprovalWorkflowWithClientSettingsRequest,
|
||||
UpdateApprovalWorkflowWithClientSettingsRequest,
|
||||
ApprovalWorkflowStepRequest,
|
||||
ClientApprovalSettingsRequest,
|
||||
UserLevel,
|
||||
|
|
@ -16,6 +17,7 @@ import {
|
|||
UserRole,
|
||||
ArticleCategory,
|
||||
createApprovalWorkflowWithClientSettings,
|
||||
updateApprovalWorkflowWithClientSettings,
|
||||
getUserLevels,
|
||||
getUsers,
|
||||
getUserRoles,
|
||||
|
|
@ -25,6 +27,7 @@ import Swal from "sweetalert2";
|
|||
|
||||
interface ApprovalWorkflowFormProps {
|
||||
initialData?: CreateApprovalWorkflowWithClientSettingsRequest;
|
||||
workflowId?: number; // For update mode
|
||||
onSave?: (data: CreateApprovalWorkflowWithClientSettingsRequest) => void;
|
||||
onCancel?: () => void;
|
||||
isLoading?: boolean;
|
||||
|
|
@ -32,6 +35,7 @@ interface ApprovalWorkflowFormProps {
|
|||
|
||||
export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
||||
initialData,
|
||||
workflowId,
|
||||
onSave,
|
||||
onCancel,
|
||||
isLoading = false,
|
||||
|
|
@ -137,8 +141,8 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
|
||||
if (!formData.description.trim()) {
|
||||
newErrors.description = "Description is required";
|
||||
} else if (formData.description.trim().length < 10) {
|
||||
newErrors.description = "Description must be at least 10 characters";
|
||||
} else if (formData.description.trim().length < 3) {
|
||||
newErrors.description = "Description must be at least 3 characters";
|
||||
}
|
||||
|
||||
if (formData.steps.length === 0) {
|
||||
|
|
@ -338,7 +342,10 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
title: "Validation Error",
|
||||
text: "Please fix the errors before submitting",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK"
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -358,31 +365,92 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
skipApprovalFor: []
|
||||
};
|
||||
|
||||
const submitData = {
|
||||
...formData,
|
||||
clientApprovalSettings: hardcodedClientSettings
|
||||
};
|
||||
if (workflowId) {
|
||||
// Update mode
|
||||
const updateData: UpdateApprovalWorkflowWithClientSettingsRequest = {
|
||||
workflowId,
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
isActive: formData.isActive || false,
|
||||
isDefault: formData.isDefault || false,
|
||||
steps: formData.steps.map(step => ({
|
||||
...step,
|
||||
branchName: step.stepName, // branchName should be same as stepName
|
||||
})),
|
||||
clientSettings: hardcodedClientSettings
|
||||
};
|
||||
|
||||
console.log("Submit Data: ", submitData);
|
||||
console.log("Update Data: ", updateData);
|
||||
|
||||
const response = await createApprovalWorkflowWithClientSettings(submitData);
|
||||
const response = await updateApprovalWorkflowWithClientSettings(updateData);
|
||||
|
||||
console.log("Response: ", response);
|
||||
console.log("Update Response: ", response);
|
||||
|
||||
if (response?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: response?.message?.messages[0] || "Failed to create approval workflow",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK"
|
||||
});
|
||||
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 {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Approval workflow created successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK"
|
||||
});
|
||||
// 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);
|
||||
|
|
@ -390,7 +458,10 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK"
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -404,7 +475,10 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, reset",
|
||||
cancelButtonText: "Cancel"
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
setFormData({
|
||||
|
|
@ -617,7 +691,7 @@ export const ApprovalWorkflowForm: React.FC<ApprovalWorkflowFormProps> = ({
|
|||
className="flex items-center gap-2"
|
||||
>
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
{isSubmitting ? "Saving..." : isLoadingData ? "Loading..." : "Save Workflow"}
|
||||
{isSubmitting ? "Saving..." : isLoadingData ? "Loading..." : workflowId ? "Update Workflow" : "Save Workflow"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ 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 { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { ChevronDownIcon, ChevronUpIcon, SaveIcon, EyeIcon, RotateCcwIcon, UsersIcon, HierarchyIcon } from "@/components/icons";
|
||||
import { ChevronDownIcon, ChevronUpIcon, SaveIcon, EyeIcon, RotateCcwIcon, UsersIcon, HierarchyIcon, PlusIcon, TrashIcon } from "@/components/icons";
|
||||
import { FormField } from "./common/FormField";
|
||||
import { DynamicArray } from "./common/DynamicArray";
|
||||
import {
|
||||
|
|
@ -56,6 +56,7 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [expandedHierarchy, setExpandedHierarchy] = useState(false);
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState(mode === "single" ? "basic" : "bulk");
|
||||
|
||||
// Load initial data
|
||||
useEffect(() => {
|
||||
|
|
@ -148,8 +149,28 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
});
|
||||
};
|
||||
|
||||
const handleBulkItemsChange = (items: UserLevelsCreateRequest[]) => {
|
||||
setBulkFormData(items);
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveTab(value);
|
||||
// Update mode based on active tab
|
||||
if (value === "bulk") {
|
||||
// Mode will be determined by activeTab in handleSubmit
|
||||
} else {
|
||||
// Mode will be determined by activeTab in handleSubmit
|
||||
}
|
||||
};
|
||||
|
||||
const addBulkItem = () => {
|
||||
const newItem: UserLevelsCreateRequest = {
|
||||
name: "",
|
||||
aliasName: "",
|
||||
levelNumber: 1,
|
||||
parentLevelId: undefined,
|
||||
provinceId: undefined,
|
||||
group: "",
|
||||
isApprovalActive: true,
|
||||
isActive: true,
|
||||
};
|
||||
setBulkFormData(prev => [...prev, newItem]);
|
||||
};
|
||||
|
||||
const renderBulkItemForm = (item: UserLevelsCreateRequest, index: number, onUpdate: (item: UserLevelsCreateRequest) => void, onDelete: () => void) => {
|
||||
|
|
@ -264,9 +285,13 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (mode === "single") {
|
||||
// Determine current mode based on active tab
|
||||
const currentMode = activeTab === "bulk" ? "bulk" : "single";
|
||||
|
||||
if (currentMode === "single") {
|
||||
const validationErrors = validateForm(formData);
|
||||
console.log(validationErrors);
|
||||
console.log("Form mode: ", currentMode);
|
||||
console.log("Error single ", validationErrors);
|
||||
|
||||
if (Object.keys(validationErrors).length > 0) {
|
||||
setErrors(validationErrors);
|
||||
|
|
@ -274,7 +299,10 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
title: "Validation Error",
|
||||
text: "Please fix the errors before submitting",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK"
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -284,7 +312,10 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
title: "Validation Error",
|
||||
text: "Please fix the errors before submitting",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK"
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -294,31 +325,75 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
|
||||
try {
|
||||
if (onSave) {
|
||||
if (mode === "single") {
|
||||
if (currentMode === "single") {
|
||||
onSave(formData);
|
||||
} else {
|
||||
// For bulk mode, save each item individually
|
||||
let hasErrors = false;
|
||||
let successCount = 0;
|
||||
|
||||
for (const item of bulkFormData) {
|
||||
await createUserLevel(item);
|
||||
const response = await createUserLevel(item);
|
||||
|
||||
if (response?.error) {
|
||||
hasErrors = true;
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: `Failed to create user level "${item.name}": ${response?.message || "Unknown error"}`,
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
successCount++;
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: `User level "${item.name}" created successfully`,
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh page if at least one item was created successfully
|
||||
if (successCount > 0) {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000); // Small delay to let user see the success message
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (mode === "single") {
|
||||
if (currentMode === "single") {
|
||||
const response = await createUserLevel(formData);
|
||||
console.log("Create Response: ", response);
|
||||
|
||||
if (response?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: response?.message || "Failed to create user level",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK"
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "User level created successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK"
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
}).then(() => {
|
||||
// Refresh page after successful creation
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
|
@ -326,6 +401,8 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
const promises = bulkFormData.map(item => createUserLevel(item));
|
||||
const responses = await Promise.all(promises);
|
||||
|
||||
console.log("Create Responses: ", responses);
|
||||
|
||||
const failedCount = responses.filter((r: any) => r.error).length;
|
||||
const successCount = responses.length - failedCount;
|
||||
|
||||
|
|
@ -334,14 +411,20 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
title: "Success",
|
||||
text: `All ${successCount} user levels created successfully`,
|
||||
icon: "success",
|
||||
confirmButtonText: "OK"
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Partial Success",
|
||||
text: `${successCount} user levels created successfully, ${failedCount} failed`,
|
||||
icon: "warning",
|
||||
confirmButtonText: "OK"
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -352,7 +435,10 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK"
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -366,7 +452,10 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, reset",
|
||||
cancelButtonText: "Cancel"
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
if (mode === "single") {
|
||||
|
|
@ -429,7 +518,7 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue={mode === "single" ? "basic" : "bulk"} className="w-full">
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="basic" disabled={isLoadingData}>Basic Information</TabsTrigger>
|
||||
{/* <TabsTrigger value="hierarchy" disabled={isLoadingData}>Hierarchy</TabsTrigger> */}
|
||||
|
|
@ -590,15 +679,68 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DynamicArray
|
||||
items={bulkFormData}
|
||||
onItemsChange={handleBulkItemsChange}
|
||||
renderItem={renderBulkItemForm}
|
||||
addItemLabel="Add User Level"
|
||||
emptyStateMessage="No user levels added yet. Add multiple levels to create them in bulk."
|
||||
allowReorder={true}
|
||||
allowDuplicate={true}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-medium">User Levels</h3>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={addBulkItem}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add User Level
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{bulkFormData.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<UsersIcon className="h-12 w-12 mx-auto mb-4 text-gray-400" />
|
||||
<p>No user levels added yet. Add multiple levels to create them in bulk.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{bulkFormData.map((item, index) => (
|
||||
<Card key={index} className="border-l-4 border-l-blue-500">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">
|
||||
User Level #{index + 1}
|
||||
</CardTitle>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
const newItems = bulkFormData.filter((_, i) => i !== index);
|
||||
setBulkFormData(newItems);
|
||||
}}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{renderBulkItemForm(
|
||||
item,
|
||||
index,
|
||||
(updatedItem) => {
|
||||
const newItems = [...bulkFormData];
|
||||
newItems[index] = updatedItem;
|
||||
setBulkFormData(newItems);
|
||||
},
|
||||
() => {
|
||||
const newItems = bulkFormData.filter((_, i) => i !== index);
|
||||
setBulkFormData(newItems);
|
||||
}
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
|
@ -636,7 +778,7 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
className="flex items-center gap-2"
|
||||
>
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
{isSubmitting ? "Saving..." : isLoadingData ? "Loading..." : `Save ${mode === "single" ? "User Level" : "User Levels"}`}
|
||||
{isSubmitting ? "Saving..." : isLoadingData ? "Loading..." : `Save ${activeTab === "bulk" ? "User Levels" : "User Level"}`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2996,3 +2996,34 @@ export const RotateCcwIcon = ({ size = 24, width, height, ...props }: IconSvgPro
|
|||
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const EditIcon = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: IconSvgProps) => (
|
||||
<svg
|
||||
width={size || width}
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
|
@ -24,6 +24,25 @@ export interface ClientApprovalSettingsRequest {
|
|||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateApprovalWorkflowWithClientSettingsRequest {
|
||||
workflowId: number;
|
||||
name: string;
|
||||
description: string;
|
||||
isActive: boolean;
|
||||
isDefault: boolean;
|
||||
steps: ApprovalWorkflowStepRequest[];
|
||||
clientSettings: {
|
||||
approvalExemptCategories: any[];
|
||||
approvalExemptRoles: any[];
|
||||
approvalExemptUsers: any[];
|
||||
autoPublishArticles: boolean;
|
||||
isActive: boolean;
|
||||
requireApprovalFor: any[];
|
||||
requiresApproval: boolean;
|
||||
skipApprovalFor: any[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateApprovalWorkflowWithClientSettingsRequest {
|
||||
name: string;
|
||||
description: string;
|
||||
|
|
@ -208,6 +227,11 @@ export async function createApprovalWorkflowWithClientSettings(data: CreateAppro
|
|||
return httpPostInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function updateApprovalWorkflowWithClientSettings(data: any) {
|
||||
const url = "approval-workflows/with-client-settings";
|
||||
return httpPutInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function getUserLevels() {
|
||||
const url = "user-levels";
|
||||
return httpGetInterceptor(url);
|
||||
|
|
|
|||
|
|
@ -64,11 +64,13 @@ export async function doLogin(data: any) {
|
|||
// }
|
||||
|
||||
export async function getCsrfToken() {
|
||||
const pathUrl = "csrf";
|
||||
const pathUrl = "csrf-token";
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
return httpGet(pathUrl, headers);
|
||||
|
||||
|
||||
// const url = 'https://kontenhumas.com/api/csrf';
|
||||
// try {
|
||||
// const response = await fetch(url, {
|
||||
|
|
|
|||
Loading…
Reference in New Issue