feat: update fixing tenant

This commit is contained in:
hanif salafi 2025-10-02 13:41:49 +07:00
parent 580900ac14
commit f7475d16fd
11 changed files with 585 additions and 187 deletions

View File

@ -7,24 +7,27 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
import { PlusIcon, SettingsIcon, UsersIcon, WorkflowIcon } from "@/components/icons";
import { ApprovalWorkflowForm } from "@/components/form/ApprovalWorkflowForm";
import { UserLevelsForm } from "@/components/form/UserLevelsForm";
import { WorkflowModalProvider } from "@/components/modals/WorkflowModalProvider";
import { useWorkflowModal } from "@/components/modals/WorkflowModalProvider";
import { useWorkflowStatusCheck } from "@/hooks/useWorkflowStatusCheck";
import {
CreateApprovalWorkflowWithClientSettingsRequest,
UserLevelsCreateRequest,
UserLevel,
getUserLevels,
getApprovalWorkflows,
getApprovalWorkflowComprehensiveDetails,
ComprehensiveWorkflowResponse,
createUserLevel,
} from "@/service/approval-workflows";
function TenantSettingsContent() {
const [activeTab, setActiveTab] = useState("workflows");
const [isUserLevelDialogOpen, setIsUserLevelDialogOpen] = useState(false);
const [workflow, setWorkflow] = useState<any>(null);
const [workflow, setWorkflow] = useState<ComprehensiveWorkflowResponse | null>(null);
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isEditingWorkflow, setIsEditingWorkflow] = useState(false);
const { checkWorkflowStatus } = useWorkflowStatusCheck();
const { showWorkflowModal } = useWorkflowModal();
// Load data on component mount
React.useEffect(() => {
@ -34,16 +37,17 @@ function TenantSettingsContent() {
const loadData = async () => {
setIsLoading(true);
try {
const [workflowsRes, userLevelsRes] = await Promise.all([
getApprovalWorkflows(),
const [comprehensiveWorkflowRes, userLevelsRes] = await Promise.all([
getApprovalWorkflowComprehensiveDetails(4), // Using workflow ID 4 as per API example
getUserLevels(),
]);
if (!workflowsRes?.error) {
const workflows = workflowsRes?.data || [];
// Set the first workflow as the single workflow for this tenant
setWorkflow(workflows.length > 0 ? workflows[0] : null);
if (!comprehensiveWorkflowRes?.error) {
setWorkflow(comprehensiveWorkflowRes?.data?.data || null);
} else {
setWorkflow(null);
}
if (!userLevelsRes?.error) {
setUserLevels(userLevelsRes?.data?.data || []);
}
@ -60,6 +64,20 @@ function TenantSettingsContent() {
};
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);
await loadData(); // Reload data after saving
};
@ -87,6 +105,14 @@ function TenantSettingsContent() {
>
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>
@ -132,7 +158,34 @@ function TenantSettingsContent() {
</CardHeader>
<CardContent>
<ApprovalWorkflowForm
initialData={workflow}
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}
onSave={handleWorkflowSave}
onCancel={() => setIsEditingWorkflow(false)}
/>
@ -142,14 +195,14 @@ function TenantSettingsContent() {
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>{workflow.name}</span>
<span>{workflow.workflow.name}</span>
<div className="flex items-center gap-2">
{workflow.isDefault && (
{workflow.workflow.isDefault && (
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
Default
</span>
)}
{workflow.isActive ? (
{workflow.workflow.isActive ? (
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
Active
</span>
@ -163,34 +216,32 @@ function TenantSettingsContent() {
</CardHeader>
<CardContent>
<p className="text-gray-600 text-sm mb-4">
{workflow.description}
{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.steps?.length || 0}</div>
<div className="text-sm text-gray-600">Workflow Steps</div>
<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 ${workflow.requiresApproval ? 'text-green-600' : 'text-red-600'}`}>
{workflow.requiresApproval ? 'Yes' : 'No'}
<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.autoPublish ? 'text-green-600' : 'text-red-600'}`}>
{workflow.autoPublish ? 'Yes' : 'No'}
</div>
<div className="text-sm text-gray-600">Auto Publish</div>
</div>
<div className="text-center p-4 bg-gray-50 rounded-lg">
<div className="text-2xl font-bold text-purple-600">
{workflow.clientApprovalSettings?.approvalExemptUsers?.length || 0}
<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">Exempt Users</div>
<div className="text-sm text-gray-600">Auto Publish</div>
</div>
</div>
@ -210,6 +261,7 @@ function TenantSettingsContent() {
<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>
@ -224,6 +276,21 @@ function TenantSettingsContent() {
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>
))}
@ -231,38 +298,102 @@ function TenantSettingsContent() {
</div>
)}
{/* Client Settings Overview */}
{workflow.clientApprovalSettings && (
<div className="mb-6">
<h4 className="text-lg font-medium mb-3">Client Approval 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">Exempt Users</div>
<div className="text-sm text-gray-600">
{workflow.clientApprovalSettings.approvalExemptUsers?.length || 0} users
</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 className="p-3 bg-gray-50 rounded-lg">
<div className="text-sm font-medium text-gray-700 mb-1">Exempt Roles</div>
<div className="text-sm text-gray-600">
{workflow.clientApprovalSettings.approvalExemptRoles?.length || 0} roles
</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 className="p-3 bg-gray-50 rounded-lg">
<div className="text-sm font-medium text-gray-700 mb-1">Exempt Categories</div>
<div className="text-sm text-gray-600">
{workflow.clientApprovalSettings.approvalExemptCategories?.length || 0} categories
</div>
</div>
<div className="p-3 bg-gray-50 rounded-lg">
<div className="text-sm font-medium text-gray-700 mb-1">Content Types</div>
<div className="text-sm text-gray-600">
{workflow.clientApprovalSettings.requireApprovalFor?.length || 0} requiring approval
</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>
) : (
@ -295,7 +426,7 @@ function TenantSettingsContent() {
Create User Level
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogContent className="md:max-w-6xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create New User Level</DialogTitle>
</DialogHeader>
@ -465,7 +596,7 @@ function TenantSettingsContent() {
<div className="flex items-center justify-between text-sm">
<span className="text-gray-500">Parent Level:</span>
<span className="font-medium">
{userLevels.find(ul => ul.id === userLevel.parentLevelId)?.name || `Level ${userLevel.parentLevelId}`}
{ userLevels.length > 0 ? userLevels.find(ul => ul.id === userLevel.parentLevelId)?.name || `Level ${userLevel.parentLevelId}` : `Level ${userLevel.parentLevelId}`}
</span>
</div>
)}
@ -522,9 +653,5 @@ function TenantSettingsContent() {
}
export default function TenantSettingsPage() {
return (
<WorkflowModalProvider>
<TenantSettingsContent />
</WorkflowModalProvider>
);
return <TenantSettingsContent />;
}

View File

@ -5,16 +5,19 @@ import DashCodeFooter from "@/components/partials/footer";
import ThemeCustomize from "@/components/partials/customizer";
import DashCodeHeader from "@/components/partials/header";
import MountedProvider from "@/providers/mounted.provider";
import { WorkflowModalProvider } from "@/components/modals/WorkflowModalProvider";
const layout = async ({ children }: { children: React.ReactNode }) => {
return (
<MountedProvider isProtected={true}>
<LayoutProvider>
<ThemeCustomize />
<DashCodeHeader />
<DashCodeSidebar />
<LayoutContentProvider>{children}</LayoutContentProvider>
<DashCodeFooter />
<WorkflowModalProvider>
<ThemeCustomize />
<DashCodeHeader />
<DashCodeSidebar />
<LayoutContentProvider>{children}</LayoutContentProvider>
<DashCodeFooter />
</WorkflowModalProvider>
</LayoutProvider>
</MountedProvider>
);

View File

@ -73,8 +73,8 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
getProvinces(),
]);
if (!userLevelsRes?.error) setUserLevels(userLevelsRes?.data || []);
if (!provincesRes?.error) setProvinces(provincesRes?.data || []);
if (!userLevelsRes?.error) setUserLevels(userLevelsRes?.data?.data || []);
if (!provincesRes?.error) setProvinces(provincesRes?.data?.data || []);
} catch (error) {
console.error("Error loading form data:", error);
} finally {
@ -99,8 +99,6 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
newErrors.aliasName = "Alias name is required";
} else if (data.aliasName.trim().length < 3) {
newErrors.aliasName = "Alias name must be at least 3 characters";
} else if (!/^[A-Z_]+$/.test(data.aliasName.trim())) {
newErrors.aliasName = "Alias name must contain only uppercase letters and underscores";
}
if (!data.levelNumber || data.levelNumber <= 0) {
@ -108,10 +106,10 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
}
// Check for duplicate level numbers
const existingLevel = userLevels.find(level => level.levelNumber === data.levelNumber);
if (existingLevel && (!initialData || existingLevel.id !== (initialData as any).id)) {
newErrors.levelNumber = "Level number already exists";
}
// const existingLevel = userLevels.length > 0 ? userLevels.find(level => level.levelNumber === data.levelNumber) : null;
// if (existingLevel && (!initialData || existingLevel.id !== (initialData as any).id)) {
// newErrors.levelNumber = "Level number already exists";
// }
return newErrors;
};
@ -202,11 +200,14 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
type="select"
placeholder={userLevels.length > 0 ? "Select parent level" : "No parent levels available"}
value={item.parentLevelId}
onChange={(value) => onUpdate({ ...item, parentLevelId: value ? Number(value) : undefined })}
options={userLevels.map(level => ({
value: level.id,
label: `${level.name} (Level ${level.levelNumber})`,
}))}
onChange={(value) => onUpdate({ ...item, parentLevelId: value !== undefined ? Number(value) : undefined })}
options={[
{ value: 0, label: "No Parent (Root Level)" },
...(userLevels.length > 0 ? userLevels.map(level => ({
value: level.id,
label: `${level.name} (Level ${level.levelNumber})`,
})) : [])
]}
helpText={userLevels.length === 0 ? "No parent levels found. This will be a root level." : "Select parent level for hierarchy"}
disabled={userLevels.length === 0}
/>
@ -218,10 +219,10 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
placeholder={provinces.length > 0 ? "Select province" : "No provinces available"}
value={item.provinceId}
onChange={(value) => onUpdate({ ...item, provinceId: value ? Number(value) : undefined })}
options={provinces.map(province => ({
options={provinces.length > 0 ? provinces.map(province => ({
value: province.id,
label: province.provinceName,
}))}
label: province.prov_name,
})) : []}
helpText={provinces.length === 0 ? "No provinces found. Please ensure provinces are available in the system." : "Geographic scope for this level"}
disabled={provinces.length === 0}
/>
@ -265,6 +266,8 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
if (mode === "single") {
const validationErrors = validateForm(formData);
console.log(validationErrors);
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
Swal.fire({
@ -429,7 +432,7 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
<Tabs defaultValue={mode === "single" ? "basic" : "bulk"} 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>
{/* <TabsTrigger value="hierarchy" disabled={isLoadingData}>Hierarchy</TabsTrigger> */}
<TabsTrigger value="bulk" disabled={isLoadingData}>Bulk Operations</TabsTrigger>
</TabsList>
@ -485,11 +488,14 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
type="select"
placeholder={userLevels.length > 0 ? "Select parent level" : "No parent levels available"}
value={formData.parentLevelId}
onChange={(value) => handleFieldChange("parentLevelId", value ? Number(value) : undefined)}
options={userLevels.map(level => ({
value: level.id,
label: `${level.name} (Level ${level.levelNumber})`,
}))}
onChange={(value) => handleFieldChange("parentLevelId", value !== undefined ? Number(value) : undefined)}
options={[
{ value: 0, label: "No Parent (Root Level)" },
...(userLevels.length > 0 ? userLevels.map(level => ({
value: level.id,
label: `${level.name} (Level ${level.levelNumber})`,
})) : [])
]}
helpText={userLevels.length === 0 ? "No parent levels found. This will be a root level." : "Select parent level for hierarchy"}
disabled={userLevels.length === 0}
/>
@ -501,10 +507,10 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
placeholder={provinces.length > 0 ? "Select province" : "No provinces available"}
value={formData.provinceId}
onChange={(value) => handleFieldChange("provinceId", value ? Number(value) : undefined)}
options={provinces.map(province => ({
options={provinces.length > 0 ? provinces.map(province => ({
value: province.id,
label: province.provinceName,
}))}
label: province.prov_name,
})) : []}
helpText={provinces.length === 0 ? "No provinces found. Please ensure provinces are available in the system." : "Geographic scope for this level"}
disabled={provinces.length === 0}
/>
@ -544,7 +550,7 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
</TabsContent>
{/* Hierarchy Tab */}
<TabsContent value="hierarchy" className="space-y-6">
{/* <TabsContent value="hierarchy" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
@ -572,7 +578,7 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
</Collapsible>
</CardContent>
</Card>
</TabsContent>
</TabsContent> */}
{/* Bulk Operations Tab */}
<TabsContent value="bulk" className="space-y-6">

View File

@ -2868,19 +2868,53 @@ export const ArrowDownIcon = ({ size = 24, width, height, ...props }: IconSvgPro
export const SettingsIcon = ({ size = 24, width, height, ...props }: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
width={width || size}
height={height || size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1 1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg>
);
export const AlertTriangleIcon = ({ size = 24, width, height, ...props }: IconSvgProps) => (
<svg
width={width || size}
height={height || size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
<path d="M12 9v4" />
<path d="M12 17h.01" />
</svg>
);
export const CheckCircleIcon = ({ size = 24, width, height, ...props }: IconSvgProps) => (
<svg
width={width || size}
height={height || size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<path d="m9 11 3 3L22 4" />
</svg>
);

View File

@ -12,7 +12,9 @@ export default function Layout({ children }: LayoutProps) {
<WorkflowModalProvider>
<div className="min-h-screen bg-gray-50">
<Header>
{/* Header content will be provided by parent components */}
<div className="container mx-auto px-4 py-4">
{/* Header content */}
</div>
</Header>
<main className="flex-1">
{children}

View File

@ -1,6 +1,8 @@
"use client";
import React, { createContext, useContext, useState, ReactNode } from "react";
import React, { createContext, useContext, useState, ReactNode, useEffect, useCallback } from "react";
import WorkflowSetupModal from "./WorkflowSetupModal";
import { getInfoProfile } from "@/service/auth";
import { usePathname } from "next/navigation";
interface WorkflowInfo {
hasWorkflowSetup: boolean;
@ -14,6 +16,7 @@ interface WorkflowInfo {
interface WorkflowModalContextType {
showWorkflowModal: (workflowInfo: WorkflowInfo) => void;
hideWorkflowModal: () => void;
refreshWorkflowStatus: () => Promise<void>;
}
const WorkflowModalContext = createContext<WorkflowModalContextType | undefined>(undefined);
@ -33,24 +36,88 @@ interface WorkflowModalProviderProps {
export function WorkflowModalProvider({ children }: WorkflowModalProviderProps) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [workflowInfo, setWorkflowInfo] = useState<WorkflowInfo | undefined>(undefined);
const pathname = usePathname();
// Force hide modal when on tenant settings page
const forceHideModal = useCallback(() => {
console.log("Force hiding modal");
setIsModalOpen(false);
setWorkflowInfo(undefined);
}, []);
const refreshWorkflowStatus = async () => {
try {
const response = await getInfoProfile();
if (response?.data?.data && response?.data?.data?.approvalWorkflowInfo) {
const workflowInfo = response.data.data.approvalWorkflowInfo;
// Update workflow info
setWorkflowInfo(workflowInfo);
// If workflow is now setup, allow closing modal
if (workflowInfo.hasWorkflowSetup) {
console.log("Workflow is now setup, modal can be closed");
}
}
} catch (error) {
console.error("Error refreshing workflow status:", error);
}
};
// Auto-hide modal when user is on tenant settings page
useEffect(() => {
console.log("Current pathname:", pathname);
// Add small delay to ensure pathname is fully updated
const timeoutId = setTimeout(() => {
if (pathname?.includes('/admin/settings/tenant') || pathname?.includes('/tenant')) {
console.log("User is on tenant settings page, hiding modal");
forceHideModal();
}
}, 100); // Small delay to ensure pathname is updated
return () => clearTimeout(timeoutId);
}, [pathname, forceHideModal]);
// Auto-refresh workflow status when modal is open and user is not on tenant settings page
useEffect(() => {
let interval: NodeJS.Timeout;
if (isModalOpen && !pathname?.includes('/admin/settings/tenant') && !pathname?.includes('/tenant')) {
console.log("Starting auto-refresh for workflow status");
interval = setInterval(async () => {
await refreshWorkflowStatus();
}, 5000); // Refresh every 5 seconds
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [isModalOpen, pathname, refreshWorkflowStatus]);
const showWorkflowModal = (info: WorkflowInfo) => {
console.log("Show workflow modal: ", info);
setWorkflowInfo(info);
setIsModalOpen(true);
};
const hideWorkflowModal = () => {
console.log("Hide workflow modal");
setIsModalOpen(false);
setWorkflowInfo(undefined);
};
return (
<WorkflowModalContext.Provider value={{ showWorkflowModal, hideWorkflowModal }}>
<WorkflowModalContext.Provider value={{ showWorkflowModal, hideWorkflowModal, refreshWorkflowStatus }}>
{children}
<WorkflowSetupModal
isOpen={isModalOpen}
onClose={hideWorkflowModal}
workflowInfo={workflowInfo}
onRefresh={refreshWorkflowStatus}
/>
</WorkflowModalContext.Provider>
);

View File

@ -3,8 +3,8 @@ import React, { useState, useEffect } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { AlertTriangleIcon, CheckCircleIcon, SettingsIcon } from "@/components/icons";
import { useRouter } from "next/navigation";
import { IconX, SettingsIcon } from "@/components/icons";
import { useRouter, usePathname } from "next/navigation";
interface WorkflowSetupModalProps {
isOpen: boolean;
@ -17,10 +17,12 @@ interface WorkflowSetupModalProps {
autoPublishArticles?: boolean;
isApprovalActive?: boolean;
};
onRefresh?: () => Promise<void>;
}
export default function WorkflowSetupModal({ isOpen, onClose, workflowInfo }: WorkflowSetupModalProps) {
export default function WorkflowSetupModal({ isOpen, onClose, workflowInfo, onRefresh }: WorkflowSetupModalProps) {
const router = useRouter();
const pathname = usePathname();
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
@ -30,28 +32,53 @@ export default function WorkflowSetupModal({ isOpen, onClose, workflowInfo }: Wo
}, [isOpen]);
const handleClose = () => {
setIsVisible(false);
setTimeout(() => {
onClose();
}, 200);
// Allow closing if workflow is setup OR if user is on tenant settings page
if (workflowInfo?.hasWorkflowSetup || pathname?.includes('/admin/settings/tenant') || pathname?.includes('/tenant')) {
setIsVisible(false);
setTimeout(() => {
onClose();
}, 200);
}
};
const handleSetupWorkflow = () => {
handleClose();
// Navigate to tenant settings
router.push("/admin/settings/tenant");
// Modal will be auto-hidden by WorkflowModalProvider when user reaches tenant settings page
};
if (!isOpen) return null;
return (
<Dialog open={isVisible} onOpenChange={handleClose}>
<DialogContent className="max-w-md">
<Dialog
open={isVisible}
onOpenChange={(workflowInfo?.hasWorkflowSetup || pathname?.includes('/admin/settings/tenant') || pathname?.includes('/tenant')) ? handleClose : undefined}
>
<DialogContent
className="max-w-md"
onPointerDownOutside={(e) => {
// Prevent closing by clicking outside unless workflow is setup or on tenant settings page
if (!workflowInfo?.hasWorkflowSetup && !pathname?.includes('/admin/settings/tenant') && !pathname?.includes('/tenant')) {
e.preventDefault();
}
}}
onEscapeKeyDown={(e) => {
// Prevent closing by pressing ESC unless workflow is setup or on tenant settings page
if (!workflowInfo?.hasWorkflowSetup && !pathname?.includes('/admin/settings/tenant') && !pathname?.includes('/tenant')) {
e.preventDefault();
}
}}
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{workflowInfo?.hasWorkflowSetup ? (
<CheckCircleIcon className="h-5 w-5 text-green-600" />
<div className="h-5 w-5 rounded-full bg-green-600 flex items-center justify-center">
<span className="text-white text-xs"></span>
</div>
) : (
<AlertTriangleIcon className="h-5 w-5 text-orange-600" />
<div className="h-5 w-5 rounded-full bg-orange-600 flex items-center justify-center">
<span className="text-white text-xs">!</span>
</div>
)}
Workflow Status
</DialogTitle>
@ -63,7 +90,9 @@ export default function WorkflowSetupModal({ isOpen, onClose, workflowInfo }: Wo
<Card className="border-orange-200 bg-orange-50">
<CardContent className="p-4">
<div className="flex items-start gap-3">
<AlertTriangleIcon className="h-6 w-6 text-orange-600 mt-1" />
<div className="h-6 w-6 rounded-full bg-orange-600 flex items-center justify-center mt-1">
<span className="text-white text-sm">!</span>
</div>
<div className="flex-1">
<h3 className="font-medium text-orange-900 mb-2">
Workflow Belum Dikonfigurasi
@ -80,74 +109,71 @@ export default function WorkflowSetupModal({ isOpen, onClose, workflowInfo }: Wo
<SettingsIcon className="h-4 w-4 mr-2" />
Setup Workflow
</Button>
<Button
variant="outline"
onClick={handleClose}
size="sm"
>
Nanti
</Button>
{(pathname?.includes('/admin/settings/tenant') || pathname?.includes('/tenant')) && (
<Button
variant="outline"
onClick={handleClose}
size="sm"
>
Cancel
</Button>
)}
</div>
</div>
</div>
</CardContent>
</Card>
) : (
) : ''
// Workflow Setup Complete
<Card className="border-green-200 bg-green-50">
<CardContent className="p-4">
<div className="flex items-start gap-3">
<CheckCircleIcon className="h-6 w-6 text-green-600 mt-1" />
<div className="flex-1">
<h3 className="font-medium text-green-900 mb-2">
Workflow Sudah Dikonfigurasi
</h3>
<div className="space-y-2 text-sm text-green-700">
<div className="flex items-center justify-between">
<span>Workflow:</span>
<span className="font-medium">{workflowInfo.defaultWorkflowName}</span>
</div>
<div className="flex items-center justify-between">
<span>Requires Approval:</span>
<span className={`font-medium ${workflowInfo.requiresApproval ? 'text-green-600' : 'text-gray-500'}`}>
{workflowInfo.requiresApproval ? 'Yes' : 'No'}
</span>
</div>
<div className="flex items-center justify-between">
<span>Auto Publish:</span>
<span className={`font-medium ${workflowInfo.autoPublishArticles ? 'text-green-600' : 'text-gray-500'}`}>
{workflowInfo.autoPublishArticles ? 'Yes' : 'No'}
</span>
</div>
<div className="flex items-center justify-between">
<span>Status:</span>
<span className={`font-medium ${workflowInfo.isApprovalActive ? 'text-green-600' : 'text-gray-500'}`}>
{workflowInfo.isApprovalActive ? 'Active' : 'Inactive'}
</span>
</div>
</div>
<div className="flex gap-2 mt-4">
<Button
onClick={handleSetupWorkflow}
variant="outline"
size="sm"
>
<SettingsIcon className="h-4 w-4 mr-2" />
Manage Workflow
</Button>
<Button
onClick={handleClose}
size="sm"
className="bg-green-600 hover:bg-green-700 text-white"
>
OK
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
)}
// <Card className="border-green-200 bg-green-50">
// <CardContent className="p-4">
// <div className="flex items-start gap-3">
// <div className="h-6 w-6 rounded-full bg-green-600 flex items-center justify-center mt-1">
// <span className="text-white text-sm">✓</span>
// </div>
// <div className="flex-1">
// <h3 className="font-medium text-green-900 mb-2">
// Workflow Sudah Dikonfigurasi
// </h3>
// <div className="space-y-2 text-sm text-green-700">
// <div className="flex items-center justify-between">
// <span>Workflow:</span>
// <span className="font-medium">{workflowInfo.defaultWorkflowName}</span>
// </div>
// <div className="flex items-center justify-between">
// <span>Requires Approval:</span>
// <span className={`font-medium ${workflowInfo.requiresApproval ? 'text-green-600' : 'text-gray-500'}`}>
// {workflowInfo.requiresApproval ? 'Yes' : 'No'}
// </span>
// </div>
// <div className="flex items-center justify-between">
// <span>Auto Publish:</span>
// <span className={`font-medium ${workflowInfo.autoPublishArticles ? 'text-green-600' : 'text-gray-500'}`}>
// {workflowInfo.autoPublishArticles ? 'Yes' : 'No'}
// </span>
// </div>
// <div className="flex items-center justify-between">
// <span>Status:</span>
// <span className={`font-medium ${workflowInfo.isApprovalActive ? 'text-green-600' : 'text-gray-500'}`}>
// {workflowInfo.isApprovalActive ? 'Active' : 'Inactive'}
// </span>
// </div>
// </div>
// <div className="flex gap-2 mt-4">
// <Button
// onClick={handleSetupWorkflow}
// variant="outline"
// size="sm"
// >
// <SettingsIcon className="h-4 w-4 mr-2" />
// Manage Workflow
// </Button>
// </div>
// </div>
// </div>
// </CardContent>
// </Card>
}
</div>
</DialogContent>
</Dialog>

View File

@ -13,8 +13,12 @@ import { SheetMenu } from "@/components/partials/sidebar/menu/sheet-menu";
import HorizontalMenu from "./horizontal-menu";
import LocalSwitcher from "./locale-switcher";
import HeaderLogo from "./header-logo";
import { useAutoWorkflowCheck } from "@/hooks/useWorkflowStatusCheck";
const DashCodeHeader = () => {
// Auto-check workflow status when header mounts
useAutoWorkflowCheck();
return (
<>
<HeaderContent>

View File

@ -121,6 +121,12 @@ export const useAuth = (): AuthContextType => {
Cookies.set("time_refresh", newTime, {
expires: 1,
});
if (response?.data?.data?.approvalWorkflowInfo?.hasWorkflowSetup) {
Cookies.set("default_workflow", response?.data?.data?.approvalWorkflowInfo?.defaultWorkflowId, {
expires: 1,
});
}
Cookies.set("is_first_login", "true", {
secure: true,
sameSite: "strict",

View File

@ -1,7 +1,8 @@
"use client";
import { useEffect } from "react";
import { useWorkflowModal } from "./WorkflowModalProvider";
import { httpGetInterceptor } from "@/service/http-config/http-interceptor-service";
import { useWorkflowModal } from "@/components/modals/WorkflowModalProvider";
import { getInfoProfile } from "@/service/auth";
interface UserInfo {
id: number;
@ -48,19 +49,17 @@ export function useWorkflowStatusCheck() {
const checkWorkflowStatus = async () => {
try {
const response = await httpGetInterceptor("users/info") as ApiResponse;
const response = await getInfoProfile();
console.log("Response approvalWorkflowInfo: ", response);
if (response?.success && response?.data?.approvalWorkflowInfo) {
const workflowInfo = response.data.approvalWorkflowInfo;
if (response?.data?.data && response?.data?.data?.approvalWorkflowInfo) {
const workflowInfo = response.data.data.approvalWorkflowInfo;
// Show modal if workflow is not setup
if (!workflowInfo.hasWorkflowSetup) {
showWorkflowModal(workflowInfo);
}
// Optional: Also show modal if workflow is setup (for confirmation)
// else {
// showWorkflowModal(workflowInfo);
// }
}
} catch (error) {
console.error("Error checking workflow status:", error);

View File

@ -60,6 +60,124 @@ export interface UserLevel {
updatedAt?: string;
}
export interface WorkflowStep {
id: number;
workflowId: number;
stepOrder: number;
stepName: string;
requiredUserLevelId: number;
canSkip: boolean;
autoApproveAfterHours?: number;
isActive: boolean;
parentStepId?: number;
conditionType: string;
conditionValue: string;
isParallel: boolean;
branchName?: string;
branchOrder?: number;
clientId: string;
createdAt: string;
updatedAt: string;
workflow?: any;
requiredUserLevel?: any;
parentStep?: any;
childSteps?: any;
}
export interface ApprovalWorkflow {
id: number;
name: string;
description: string;
isDefault: boolean;
isActive: boolean;
requiresApproval: boolean;
autoPublish: boolean;
clientId: string;
createdAt: string;
updatedAt: string;
steps: WorkflowStep[];
}
export interface ComprehensiveWorkflowStep {
id: number;
workflowId: number;
stepOrder: number;
stepName: string;
requiredUserLevelId: number;
requiredUserLevelName: string;
canSkip: boolean;
autoApproveAfterHours?: number;
isActive: boolean;
parentStepId?: number;
parentStepName?: string;
conditionType: string;
conditionValue: string;
isParallel: boolean;
branchName?: string;
branchOrder?: number;
hasChildren: boolean;
isFirstStep: boolean;
isLastStep: boolean;
}
export interface ComprehensiveWorkflow {
id: number;
name: string;
description: string;
isDefault: boolean;
isActive: boolean;
requiresApproval: boolean;
autoPublish: boolean;
clientId: string;
createdAt: string;
updatedAt: string;
totalSteps: number;
activeSteps: number;
hasBranches: boolean;
maxStepOrder: number;
}
export interface ClientSettings {
id: number;
clientId: string;
requiresApproval: boolean;
defaultWorkflowId: number;
defaultWorkflowName: string;
autoPublishArticles: boolean;
approvalExemptUsers?: any;
approvalExemptRoles?: any;
approvalExemptCategories?: any;
requireApprovalFor?: any;
skipApprovalFor?: any;
isActive: boolean;
createdAt: string;
updatedAt: string;
exemptUsersDetails: any[];
exemptRolesDetails: any[];
exemptCategoriesDetails: any[];
}
export interface WorkflowStatistics {
totalArticlesProcessed: number;
pendingArticles: number;
approvedArticles: number;
rejectedArticles: number;
averageProcessingTime: number;
mostActiveStep: string;
lastUsedAt?: string;
}
export interface ComprehensiveWorkflowResponse {
workflow: ComprehensiveWorkflow;
steps: ComprehensiveWorkflowStep[];
clientSettings: ClientSettings;
relatedData: {
userLevels: any[];
};
statistics: WorkflowStatistics;
lastUpdated: string;
}
export interface User {
id: number;
fullname: string;
@ -81,7 +199,7 @@ export interface ArticleCategory {
export interface Province {
id: number;
provinceName: string;
prov_name: string;
}
// API Functions
@ -126,7 +244,7 @@ export async function getArticleCategories() {
}
export async function getProvinces() {
const url = "provinces";
const url = "provinces?limit=100";
return httpGetInterceptor(url);
}
@ -144,3 +262,9 @@ export async function deleteApprovalWorkflow(id: number) {
const url = `approval-workflows/${id}`;
return httpDeleteInterceptor(url);
}
export async function getApprovalWorkflowComprehensiveDetails(workflowId: number) {
const url = `approval-workflows/comprehensive-details`;
const data = { workflowId };
return httpPostInterceptor(url, data);
}