fix: table user level, navbar show detail profile login
This commit is contained in:
parent
741fe97641
commit
24c0adfa8d
|
|
@ -0,0 +1,283 @@
|
||||||
|
"use client";
|
||||||
|
import * as React from "react";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
|
||||||
|
import { Eye, MoreVertical, SquarePen, Trash2 } from "lucide-react";
|
||||||
|
import { cn, getCookiesDecrypt } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuItem,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { deleteMedia } from "@/service/content/content";
|
||||||
|
import { error } from "@/lib/swal";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const useTableColumns = () => {
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
const userLevelId = getCookiesDecrypt("ulie");
|
||||||
|
|
||||||
|
const columns: ColumnDef<any>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "no",
|
||||||
|
header: "No",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<div className="flex-1 text-start">
|
||||||
|
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
|
||||||
|
{row.getValue("no")}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "aliasName",
|
||||||
|
header: "Name",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<div className="flex-1 text-start">
|
||||||
|
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
|
||||||
|
{row.getValue("aliasName")}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "group",
|
||||||
|
header: "Group",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<div className="flex-1 text-start">
|
||||||
|
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
|
||||||
|
{row.getValue("group")}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "parentLevelName",
|
||||||
|
header: "Parent Level",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<div className="flex-1 text-start">
|
||||||
|
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
|
||||||
|
{row.getValue("parentLevelName")}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// accessorKey: "createdAt",
|
||||||
|
// header: "Upload Date",
|
||||||
|
// cell: ({ row }) => {
|
||||||
|
// const createdAt = row.getValue("createdAt") as string | number | undefined;
|
||||||
|
// const formattedDate =
|
||||||
|
// createdAt && !isNaN(new Date(createdAt).getTime())
|
||||||
|
// ? format(new Date(createdAt), "dd-MM-yyyy HH:mm:ss")
|
||||||
|
// : "-";
|
||||||
|
// return <span className="whitespace-nowrap">{formattedDate}</span>;
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// accessorKey: "creatorName",
|
||||||
|
// header: "Creator Group",
|
||||||
|
// cell: ({ row }) => (
|
||||||
|
// <span className="whitespace-nowrap">
|
||||||
|
// {row.original.creatorName || row.original.createdByName || "-"}
|
||||||
|
// </span>
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// accessorKey: "creatorGroupLevelName",
|
||||||
|
// header: "Source",
|
||||||
|
// cell: ({ row }) => (
|
||||||
|
// <span className="whitespace-nowrap">
|
||||||
|
// {row.getValue("creatorGroupLevelName") || "-"}
|
||||||
|
// </span>
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// accessorKey: "publishedOn",
|
||||||
|
// header: "Published",
|
||||||
|
// cell: ({ row }) => {
|
||||||
|
// const isPublish = row.original.isPublish;
|
||||||
|
// const isPublishOnPolda = row.original.isPublishOnPolda;
|
||||||
|
// const creatorGroupParentLevelId = row.original.creatorGroupParentLevelId;
|
||||||
|
|
||||||
|
// let displayText = "-";
|
||||||
|
// if (isPublish && !isPublishOnPolda) {
|
||||||
|
// displayText = "Mabes";
|
||||||
|
// } else if (isPublish && isPublishOnPolda) {
|
||||||
|
// if (Number(creatorGroupParentLevelId) === 761) {
|
||||||
|
// displayText = "Mabes & Satker";
|
||||||
|
// } else {
|
||||||
|
// displayText = "Mabes & Polda";
|
||||||
|
// }
|
||||||
|
// } else if (!isPublish && isPublishOnPolda) {
|
||||||
|
// if (Number(creatorGroupParentLevelId) === 761) {
|
||||||
|
// displayText = "Satker";
|
||||||
|
// } else {
|
||||||
|
// displayText = "Polda";
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="text-center whitespace-nowrap" title={displayText}>
|
||||||
|
// {displayText}
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// accessorKey: "statusName",
|
||||||
|
// header: "Status",
|
||||||
|
// cell: ({ row }) => {
|
||||||
|
// const statusId = Number(row.original?.statusId);
|
||||||
|
// const reviewedAtLevel = row.original?.reviewedAtLevel || "";
|
||||||
|
// const creatorGroupLevelId = Number(row.original?.creatorGroupLevelId);
|
||||||
|
// const needApprovalFromLevel = Number(row.original?.needApprovalFromLevel);
|
||||||
|
|
||||||
|
// const userHasReviewed = reviewedAtLevel.includes(`:${userLevelId}:`);
|
||||||
|
// const isCreator = creatorGroupLevelId === Number(userLevelId);
|
||||||
|
|
||||||
|
// const isWaitingForReview = statusId === 2 && !userHasReviewed && !isCreator;
|
||||||
|
// const isApprovalNeeded = statusId === 1 && needApprovalFromLevel === Number(userLevelId);
|
||||||
|
|
||||||
|
// const label =
|
||||||
|
// isWaitingForReview || isApprovalNeeded
|
||||||
|
// ? "Menunggu Review"
|
||||||
|
// : statusId === 2
|
||||||
|
// ? "Diterima"
|
||||||
|
// : row.original?.statusName;
|
||||||
|
|
||||||
|
// const colors: Record<string, string> = {
|
||||||
|
// "Menunggu Review": "bg-orange-100 text-orange-600",
|
||||||
|
// Diterima: "bg-green-100 text-green-600",
|
||||||
|
// default: "bg-red-200 text-red-600",
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const statusStyles = colors[label] || colors.default;
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <Badge className={cn("rounded-full px-5 w-full whitespace-nowrap", statusStyles)}>
|
||||||
|
// {label}
|
||||||
|
// </Badge>
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
accessorKey: "action",
|
||||||
|
header: "Action",
|
||||||
|
enableHiding: false,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
|
async function doDelete(id: any) {
|
||||||
|
const data = { id };
|
||||||
|
const response = await deleteMedia(data);
|
||||||
|
if (response?.error) {
|
||||||
|
error(response.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
success();
|
||||||
|
}
|
||||||
|
|
||||||
|
function success() {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Sukses",
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "OK",
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteMedia = (id: any) => {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Hapus Data",
|
||||||
|
icon: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonColor: "#3085d6",
|
||||||
|
confirmButtonColor: "#d33",
|
||||||
|
confirmButtonText: "Hapus",
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
doDelete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const [isMabesApprover, setIsMabesApprover] = React.useState(false);
|
||||||
|
const userId = getCookiesDecrypt("uie");
|
||||||
|
const userLevelId = getCookiesDecrypt("ulie");
|
||||||
|
const roleId = getCookiesDecrypt("urie");
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (userLevelId !== undefined && roleId !== undefined) {
|
||||||
|
setIsMabesApprover(Number(userLevelId) === 216 && Number(roleId) === 3);
|
||||||
|
}
|
||||||
|
}, [userLevelId, roleId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
<MoreVertical className="h-4 w-4 text-default-800" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="p-0 hover:text-black" align="end">
|
||||||
|
<Link
|
||||||
|
href={`/admin/content/image/detail/${row.original.id}`}
|
||||||
|
className="hover:text-black"
|
||||||
|
>
|
||||||
|
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none">
|
||||||
|
<Eye className="w-4 h-4 me-1.5" />
|
||||||
|
View
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
|
{/* {(Number(row.original.uploadedById) === Number(userId) || isMabesApprover) && ( */}
|
||||||
|
<Link href={`/admin/content/image/update/${row.original.id}`}>
|
||||||
|
<DropdownMenuItem className="p-2 border-b text-default-700 rounded-none">
|
||||||
|
<SquarePen className="w-4 h-4 me-1.5" />
|
||||||
|
Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
|
{/* )} */}
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDeleteMedia(row.original.id)}
|
||||||
|
className="p-2 border-b text-destructive bg-destructive/30 focus:bg-destructive focus:text-white rounded-none"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 me-1.5" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTableColumns;
|
||||||
|
|
@ -0,0 +1,923 @@
|
||||||
|
"use client";
|
||||||
|
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 { ApprovalWorkflowForm } from "@/components/form/ApprovalWorkflowForm";
|
||||||
|
import { UserLevelsForm } from "@/components/form/UserLevelsForm";
|
||||||
|
import { useWorkflowModal } from "@/components/modals/WorkflowModalProvider";
|
||||||
|
import { useWorkflowStatusCheck } from "@/hooks/useWorkflowStatusCheck";
|
||||||
|
import {
|
||||||
|
CreateApprovalWorkflowWithClientSettingsRequest,
|
||||||
|
UserLevelsCreateRequest,
|
||||||
|
UserLevel,
|
||||||
|
getUserLevels,
|
||||||
|
getApprovalWorkflowComprehensiveDetails,
|
||||||
|
ComprehensiveWorkflowResponse,
|
||||||
|
createUserLevel,
|
||||||
|
} from "@/service/approval-workflows";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
PaginationState,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import TablePagination from "@/components/table/table-pagination";
|
||||||
|
import useTableColumns from "./columns";
|
||||||
|
|
||||||
|
function TenantSettingsContentTable() {
|
||||||
|
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 { checkWorkflowStatus } = useWorkflowStatusCheck();
|
||||||
|
const { showWorkflowModal } = useWorkflowModal();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const [comprehensiveWorkflowRes, userLevelsRes] = await Promise.all([
|
||||||
|
getApprovalWorkflowComprehensiveDetails(4),
|
||||||
|
getUserLevels(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!comprehensiveWorkflowRes?.error) {
|
||||||
|
setWorkflow(comprehensiveWorkflowRes?.data?.data || null);
|
||||||
|
} else {
|
||||||
|
setWorkflow(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userLevelsRes?.error) {
|
||||||
|
const data = userLevelsRes?.data?.data;
|
||||||
|
data.forEach((item: any, index: number) => {
|
||||||
|
item.no = (page - 1) * Number(showData) + index + 1;
|
||||||
|
item.parentLevelName =
|
||||||
|
data.length > 0
|
||||||
|
? data.find((ul: any) => ul.id === item.parentLevelId)?.name ||
|
||||||
|
`Level ${item.parentLevelId}`
|
||||||
|
: `Level ${item.parentLevelId}`;
|
||||||
|
});
|
||||||
|
setUserLevels(data);
|
||||||
|
console.log("LLL", data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading data:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWorkflowSave = async (
|
||||||
|
data: CreateApprovalWorkflowWithClientSettingsRequest
|
||||||
|
) => {
|
||||||
|
setIsEditingWorkflow(false);
|
||||||
|
await loadData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserLevelSave = async (data: UserLevelsCreateRequest) => {
|
||||||
|
try {
|
||||||
|
const response = await createUserLevel(data);
|
||||||
|
|
||||||
|
if (response?.error) {
|
||||||
|
console.error("Error creating user level:", response?.message);
|
||||||
|
} else {
|
||||||
|
console.log("User level created successfully:", response);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating user level:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUserLevelDialogOpen(false);
|
||||||
|
await loadData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBulkUserLevelSave = async (data: UserLevelsCreateRequest[]) => {
|
||||||
|
setIsUserLevelDialogOpen(false);
|
||||||
|
await loadData();
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = useTableColumns();
|
||||||
|
const [showData, setShowData] = React.useState("10");
|
||||||
|
const [page, setPage] = React.useState(1);
|
||||||
|
const [totalPage, setTotalPage] = React.useState(1);
|
||||||
|
const [totalData, setTotalData] = React.useState<number>(1);
|
||||||
|
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: Number(showData),
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: userLevels,
|
||||||
|
columns,
|
||||||
|
// onSortingChange: setSorting,
|
||||||
|
// onColumnFiltersChange: setColumnFilters,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
// onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
// onRowSelectionChange: setRowSelection,
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
state: {
|
||||||
|
// sorting,
|
||||||
|
// columnFilters,
|
||||||
|
// columnVisibility,
|
||||||
|
// rowSelection,
|
||||||
|
pagination,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6 space-y-6 border rounded-lg">
|
||||||
|
<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 border rounded-lg"
|
||||||
|
>
|
||||||
|
<WorkflowIcon className="h-4 w-4" />
|
||||||
|
Approval Workflows
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="user-levels"
|
||||||
|
className="flex items-center gap-2 border rounded-lg"
|
||||||
|
>
|
||||||
|
<UsersIcon className="h-4 w-4" />
|
||||||
|
User Levels
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* Approval Workflows Tab */}
|
||||||
|
<TabsContent value="workflows" className="space-y-6 border rounded-lg">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-2xl font-semibold ml-2 mt-4">
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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>Create New User Level</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<UserLevelsForm
|
||||||
|
mode="single"
|
||||||
|
onSave={handleUserLevelSave}
|
||||||
|
onCancel={() => setIsUserLevelDialogOpen(false)}
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Table className="overflow-hidden mt-3 mx-3">
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id} className="bg-default-200">
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
className="h-[75px]"
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center"
|
||||||
|
>
|
||||||
|
No results.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<TablePagination
|
||||||
|
table={table}
|
||||||
|
totalData={totalData}
|
||||||
|
totalPage={totalPage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <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 TenantSettingsPageTable() {
|
||||||
|
return <TenantSettingsContentTable />;
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,19 @@ import React, { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import {
|
||||||
import { PlusIcon, SettingsIcon, UsersIcon, WorkflowIcon } from "@/components/icons";
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
PlusIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
UsersIcon,
|
||||||
|
WorkflowIcon,
|
||||||
|
} from "@/components/icons";
|
||||||
import { ApprovalWorkflowForm } from "@/components/form/ApprovalWorkflowForm";
|
import { ApprovalWorkflowForm } from "@/components/form/ApprovalWorkflowForm";
|
||||||
import { UserLevelsForm } from "@/components/form/UserLevelsForm";
|
import { UserLevelsForm } from "@/components/form/UserLevelsForm";
|
||||||
import { useWorkflowModal } from "@/components/modals/WorkflowModalProvider";
|
import { useWorkflowModal } from "@/components/modals/WorkflowModalProvider";
|
||||||
|
|
@ -18,637 +29,29 @@ import {
|
||||||
ComprehensiveWorkflowResponse,
|
ComprehensiveWorkflowResponse,
|
||||||
createUserLevel,
|
createUserLevel,
|
||||||
} from "@/service/approval-workflows";
|
} from "@/service/approval-workflows";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
PaginationState,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
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 { checkWorkflowStatus } = useWorkflowStatusCheck();
|
|
||||||
const { showWorkflowModal } = useWorkflowModal();
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
loadData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadData = async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const [comprehensiveWorkflowRes, userLevelsRes] = await Promise.all([
|
|
||||||
getApprovalWorkflowComprehensiveDetails(4),
|
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUserLevelSave = async (data: UserLevelsCreateRequest) => {
|
|
||||||
try {
|
|
||||||
const response = await createUserLevel(data);
|
|
||||||
|
|
||||||
if (response?.error) {
|
|
||||||
console.error("Error creating user level:", response?.message);
|
|
||||||
} else {
|
|
||||||
console.log("User level created successfully:", response);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating user level:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsUserLevelDialogOpen(false);
|
|
||||||
await loadData();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBulkUserLevelSave = async (data: UserLevelsCreateRequest[]) => {
|
|
||||||
setIsUserLevelDialogOpen(false);
|
|
||||||
await loadData();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto p-6 space-y-6 border rounded-lg">
|
|
||||||
<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 border rounded-lg">
|
|
||||||
<WorkflowIcon className="h-4 w-4" />
|
|
||||||
Approval Workflows
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="user-levels" className="flex items-center gap-2 border rounded-lg">
|
|
||||||
<UsersIcon className="h-4 w-4" />
|
|
||||||
User Levels
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
{/* Approval Workflows Tab */}
|
|
||||||
<TabsContent value="workflows" className="space-y-6 border rounded-lg">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-2xl font-semibold ml-2 mt-4">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}
|
|
||||||
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>Create New User Level</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<UserLevelsForm
|
|
||||||
mode="single"
|
|
||||||
onSave={handleUserLevelSave}
|
|
||||||
onCancel={() => setIsUserLevelDialogOpen(false)}
|
|
||||||
/>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
{/* 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>
|
|
||||||
))}
|
|
||||||
</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() {
|
export default function TenantSettingsPage() {
|
||||||
return <TenantSettingsContent />;
|
return <TenantSettingsPageTable />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,9 +96,15 @@ export default function Login() {
|
||||||
Cookies.set("username", profile?.data?.data?.username, {
|
Cookies.set("username", profile?.data?.data?.username, {
|
||||||
expires: 1,
|
expires: 1,
|
||||||
});
|
});
|
||||||
Cookies.set("urie", profile?.data?.data?.roleId, {
|
Cookies.set("urie", profile?.data?.data?.userRoleId, {
|
||||||
expires: 1,
|
expires: 1,
|
||||||
});
|
});
|
||||||
|
console.log(
|
||||||
|
"RRR",
|
||||||
|
profile?.data?.data?.role.id,
|
||||||
|
"TTTT",
|
||||||
|
profile?.data?.data?.roleId
|
||||||
|
);
|
||||||
Cookies.set("roleName", profile?.data?.data?.roleName, {
|
Cookies.set("roleName", profile?.data?.data?.roleName, {
|
||||||
expires: 1,
|
expires: 1,
|
||||||
});
|
});
|
||||||
|
|
@ -114,7 +120,9 @@ export default function Login() {
|
||||||
Cookies.set("email", profile?.data?.data?.email, {
|
Cookies.set("email", profile?.data?.data?.email, {
|
||||||
expires: 1,
|
expires: 1,
|
||||||
});
|
});
|
||||||
router.push("/admin/dashboard");
|
// router.push("/admin/dashboard");
|
||||||
|
console.log("OOO", profile?.data?.data);
|
||||||
|
|
||||||
Cookies.set("status", "login", {
|
Cookies.set("status", "login", {
|
||||||
expires: 1,
|
expires: 1,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,51 +28,61 @@ const PUBLIKASI_SUBMENU = [
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
|
||||||
const [user, setUser] = useState<{
|
// const [user, setUser] = useState<{
|
||||||
id: number;
|
// id: number;
|
||||||
name: string;
|
// name: string;
|
||||||
avatar: string;
|
// avatar: string;
|
||||||
} | null>(null);
|
// } | null>(null);
|
||||||
|
|
||||||
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
||||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const roleId = getCookiesDecrypt("urie");
|
|
||||||
console.log("roleId", roleId);
|
|
||||||
|
|
||||||
switch (roleId) {
|
|
||||||
case "3":
|
|
||||||
setUser({
|
|
||||||
id: 3,
|
|
||||||
name: "Mabes Polri - Approver",
|
|
||||||
avatar: "/contributor.png",
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "7":
|
// useEffect(() => {
|
||||||
setUser({
|
// const roleId = getCookiesDecrypt("urie");
|
||||||
id: 7,
|
// console.log("roleId", roleId);
|
||||||
name: "DivHumas - RoMulmed - BagDise",
|
|
||||||
avatar: "/contributor.png",
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "6":
|
// switch (roleId) {
|
||||||
setUser({
|
// case "1":
|
||||||
id: 11,
|
// setUser({
|
||||||
name: "jurnalis-kompas1",
|
// id: 1,
|
||||||
avatar: "/contributor.png",
|
// name: "User Test",
|
||||||
});
|
// avatar: "/contributor.png",
|
||||||
break;
|
// });
|
||||||
|
// break;
|
||||||
|
// case "3":
|
||||||
|
// setUser({
|
||||||
|
// id: 3,
|
||||||
|
// name: "Mabes Polri - Approver",
|
||||||
|
// avatar: "/contributor.png",
|
||||||
|
// });
|
||||||
|
// break;
|
||||||
|
|
||||||
default:
|
// case "7":
|
||||||
setUser(null);
|
// setUser({
|
||||||
}
|
// id: 7,
|
||||||
}, []);
|
// name: "DivHumas - RoMulmed - BagDise",
|
||||||
|
// avatar: "/contributor.png",
|
||||||
|
// });
|
||||||
|
// break;
|
||||||
|
|
||||||
const isLoggedIn = user !== null;
|
// case "6":
|
||||||
|
// setUser({
|
||||||
|
// id: 11,
|
||||||
|
// name: "jurnalis-kompas1",
|
||||||
|
// avatar: "/contributor.png",
|
||||||
|
// });
|
||||||
|
// break;
|
||||||
|
|
||||||
|
// default:
|
||||||
|
// setUser(null);
|
||||||
|
// }
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
const roleId = getCookiesDecrypt("urie");
|
||||||
|
const isLoggedIn = roleId !== null;
|
||||||
|
|
||||||
const filteredNavItems = isLoggedIn
|
const filteredNavItems = isLoggedIn
|
||||||
? NAV_ITEMS.filter((item) => item.label !== "Mengikuti")
|
? NAV_ITEMS.filter((item) => item.label !== "Mengikuti")
|
||||||
|
|
@ -85,10 +95,27 @@ export default function Navbar() {
|
||||||
|
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
|
|
||||||
setUser(null);
|
// setUser(null);
|
||||||
setShowProfileMenu(false);
|
setShowProfileMenu(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const username = Cookies.get("username");
|
||||||
|
const fullname = Cookies.get("ufne");
|
||||||
|
// const router = useRouter();
|
||||||
|
// const [detail, setDetail] = useState<Detail>();
|
||||||
|
|
||||||
|
// const onLogout = () => {
|
||||||
|
// Object.keys(Cookies.get()).forEach((cookieName) => {
|
||||||
|
// Cookies.remove(cookieName);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// router.push("/");
|
||||||
|
// };
|
||||||
|
const [isLogin, setIsLogin] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLogin(fullname ? true : false);
|
||||||
|
}, [fullname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="relative max-w-[1400px] mx-auto flex items-center justify-between px-4 py-3 border-b bg-white z-50">
|
<header className="relative max-w-[1400px] mx-auto flex items-center justify-between px-4 py-3 border-b bg-white z-50">
|
||||||
<div className="flex flex-row items-center justify-between space-x-4 z-10">
|
<div className="flex flex-row items-center justify-between space-x-4 z-10">
|
||||||
|
|
@ -188,15 +215,15 @@ export default function Navbar() {
|
||||||
>
|
>
|
||||||
<div className="w-9 h-9 rounded-full overflow-hidden border">
|
<div className="w-9 h-9 rounded-full overflow-hidden border">
|
||||||
<Image
|
<Image
|
||||||
src={user.avatar}
|
src="/contributor.png"
|
||||||
alt={user.name}
|
alt={username as string}
|
||||||
width={36}
|
width={36}
|
||||||
height={36}
|
height={36}
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium text-gray-800">
|
<span className="text-sm font-medium text-gray-800">
|
||||||
{user.name}
|
{fullname}
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown className="w-4 h-4 text-gray-600" />
|
<ChevronDown className="w-4 h-4 text-gray-600" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -113,12 +113,12 @@ export function WorkflowModalProvider({ children }: WorkflowModalProviderProps)
|
||||||
return (
|
return (
|
||||||
<WorkflowModalContext.Provider value={{ showWorkflowModal, hideWorkflowModal, refreshWorkflowStatus }}>
|
<WorkflowModalContext.Provider value={{ showWorkflowModal, hideWorkflowModal, refreshWorkflowStatus }}>
|
||||||
{children}
|
{children}
|
||||||
<WorkflowSetupModal
|
{/* <WorkflowSetupModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={hideWorkflowModal}
|
onClose={hideWorkflowModal}
|
||||||
workflowInfo={workflowInfo}
|
workflowInfo={workflowInfo}
|
||||||
onRefresh={refreshWorkflowStatus}
|
onRefresh={refreshWorkflowStatus}
|
||||||
/>
|
/> */}
|
||||||
</WorkflowModalContext.Provider>
|
</WorkflowModalContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,8 @@ const LoginForm = () => {
|
||||||
setCookiesEncrypt("uie", profile?.data?.data?.id, {
|
setCookiesEncrypt("uie", profile?.data?.data?.id, {
|
||||||
expires: 1,
|
expires: 1,
|
||||||
});
|
});
|
||||||
setCookiesEncrypt("urie", profile?.data?.data?.roleId, {
|
console.log("QQQ", profile?.data?.data?.role.id, "LLL", profile?.data?.data?.roleId,)
|
||||||
|
setCookiesEncrypt("urie", profile?.data?.data?.userRoleId, {
|
||||||
expires: 1,
|
expires: 1,
|
||||||
});
|
});
|
||||||
setCookiesEncrypt("urne", profile?.data?.data?.role?.name, {
|
setCookiesEncrypt("urne", profile?.data?.data?.role?.name, {
|
||||||
|
|
@ -203,50 +204,53 @@ const LoginForm = () => {
|
||||||
setCookiesEncrypt("uinse", profile?.data?.data?.instituteId, {
|
setCookiesEncrypt("uinse", profile?.data?.data?.instituteId, {
|
||||||
expires: 1,
|
expires: 1,
|
||||||
});
|
});
|
||||||
console.log("ssaddd", profile?.data?.data?.roleId);
|
setCookiesEncrypt("username", profile?.data?.data?.username, {
|
||||||
if (
|
expires: 1,
|
||||||
Number(profile?.data?.data?.roleId) == 2 ||
|
});
|
||||||
Number(profile?.data?.data?.roleId) == 3 ||
|
console.log("KKKK", profile?.data?.data)
|
||||||
Number(profile?.data?.data?.roleId) == 4 ||
|
// if (
|
||||||
Number(profile?.data?.data?.roleId) == 9 ||
|
// Number(profile?.data?.data?.roleId) == 2 ||
|
||||||
Number(profile?.data?.data?.roleId) == 10 ||
|
// Number(profile?.data?.data?.roleId) == 3 ||
|
||||||
Number(profile?.data?.data?.roleId) == 11 ||
|
// Number(profile?.data?.data?.roleId) == 4 ||
|
||||||
Number(profile?.data?.data?.roleId) == 12 ||
|
// Number(profile?.data?.data?.roleId) == 9 ||
|
||||||
Number(profile?.data?.data?.roleId) == 18 ||
|
// Number(profile?.data?.data?.roleId) == 10 ||
|
||||||
Number(profile?.data?.data?.roleId) == 19
|
// Number(profile?.data?.data?.roleId) == 11 ||
|
||||||
) {
|
// Number(profile?.data?.data?.roleId) == 12 ||
|
||||||
if (profile?.data?.data?.roleId === 18) {
|
// Number(profile?.data?.data?.roleId) == 18 ||
|
||||||
window.location.href = "/in/dashboard/executive-data";
|
// Number(profile?.data?.data?.roleId) == 19
|
||||||
// router.push('/admin/dashboard');
|
// ) {
|
||||||
Cookies.set("status", "login", {
|
// if (profile?.data?.data?.roleId === 18) {
|
||||||
expires: 1,
|
// window.location.href = "/in/dashboard/executive-data";
|
||||||
});
|
// // router.push('/admin/dashboard');
|
||||||
} else if (profile?.data?.data?.roleId === 2) {
|
// Cookies.set("status", "login", {
|
||||||
window.location.href = "/in/dashboard/executive";
|
// expires: 1,
|
||||||
Cookies.set("status", "login", {
|
// });
|
||||||
expires: 1,
|
// } else if (profile?.data?.data?.roleId === 2) {
|
||||||
});
|
// window.location.href = "/in/dashboard/executive";
|
||||||
} else if (
|
// Cookies.set("status", "login", {
|
||||||
profile?.data?.data?.userLevel?.id == 794 ||
|
// expires: 1,
|
||||||
profile?.data?.data?.userLevel?.parentLevelId == 761
|
// });
|
||||||
) {
|
// } else if (
|
||||||
window.location.href = "/in/dashboard";
|
// profile?.data?.data?.userLevel?.id == 794 ||
|
||||||
Cookies.set("status", "login", {
|
// profile?.data?.data?.userLevel?.parentLevelId == 761
|
||||||
expires: 1,
|
// ) {
|
||||||
});
|
// window.location.href = "/in/dashboard";
|
||||||
} else {
|
// Cookies.set("status", "login", {
|
||||||
window.location.href = "/in/dashboard";
|
// expires: 1,
|
||||||
// router.push('/admin/dashboard');
|
// });
|
||||||
Cookies.set("status", "login", {
|
// } else {
|
||||||
expires: 1,
|
// window.location.href = "/in/dashboard";
|
||||||
});
|
// // router.push('/admin/dashboard');
|
||||||
}
|
// Cookies.set("status", "login", {
|
||||||
} else {
|
// expires: 1,
|
||||||
window.location.href = "/";
|
// });
|
||||||
Cookies.set("status", "login", {
|
// }
|
||||||
expires: 1,
|
// } else {
|
||||||
});
|
// window.location.href = "/";
|
||||||
}
|
// Cookies.set("status", "login", {
|
||||||
|
// expires: 1,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast.error(err.message || "An unexpected error occurred.");
|
toast.error(err.message || "An unexpected error occurred.");
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import Cookies from "js-cookie";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "@/components/navigation";
|
import { useRouter } from "@/components/navigation";
|
||||||
import { getInfoProfile } from "@/service/auth";
|
import { getInfoProfile } from "@/service/auth";
|
||||||
|
import { getCookiesDecrypt } from "@/lib/utils";
|
||||||
|
|
||||||
type Detail = {
|
type Detail = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -35,10 +36,11 @@ type Detail = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProfileInfo = () => {
|
const ProfileInfo = () => {
|
||||||
const username = Cookies.get("access_token");
|
const username = Cookies.get("username");
|
||||||
|
const fullname = Cookies.get("ufne");
|
||||||
const picture = Cookies.get("profile_picture");
|
const picture = Cookies.get("profile_picture");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [detail, setDetail] = useState<Detail>();
|
// const [detail, setDetail] = useState<Detail>();
|
||||||
|
|
||||||
const onLogout = () => {
|
const onLogout = () => {
|
||||||
Object.keys(Cookies.get()).forEach((cookieName) => {
|
Object.keys(Cookies.get()).forEach((cookieName) => {
|
||||||
|
|
@ -49,28 +51,28 @@ const ProfileInfo = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!username) {
|
if (!fullname) {
|
||||||
router.push("/auth");
|
router.push("/auth");
|
||||||
}
|
}
|
||||||
}, [username]);
|
}, [fullname]);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
async function initState() {
|
// async function initState() {
|
||||||
const response = await getInfoProfile();
|
// const response = await getInfoProfile();
|
||||||
const details = response?.data?.data;
|
// const details = response?.data?.data;
|
||||||
|
|
||||||
setDetail(details);
|
// setDetail(details);
|
||||||
console.log("data", details);
|
// console.log("data", details);
|
||||||
}
|
// }
|
||||||
|
|
||||||
initState();
|
// initState();
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="md:block hidden">
|
<div className="md:block hidden">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className=" cursor-pointer">
|
<DropdownMenuTrigger asChild className=" cursor-pointer">
|
||||||
{detail !== undefined ? (
|
{/* {detail !== undefined ? ( */}
|
||||||
<div className="flex items-center gap-3 text-default-800">
|
<div className="flex items-center gap-3 text-default-800">
|
||||||
<Image
|
<Image
|
||||||
src={"/avatar-profile.png"}
|
src={"/avatar-profile.png"}
|
||||||
|
|
@ -81,17 +83,17 @@ const ProfileInfo = () => {
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium capitalize lg:block hidden">
|
<div className="text-sm font-medium capitalize lg:block hidden">
|
||||||
{detail?.fullname}
|
{fullname}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs">({detail?.username})</p>
|
<p className="text-xs">({username})</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-base me-2.5 lg:inline-block hidden">
|
<span className="text-base me-2.5 lg:inline-block hidden">
|
||||||
<Icon icon="heroicons-outline:chevron-down"></Icon>
|
<Icon icon="heroicons-outline:chevron-down"></Icon>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
{/* ) : (
|
||||||
""
|
""
|
||||||
)}
|
)} */}
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56 p-0" align="end">
|
<DropdownMenuContent className="w-56 p-0" align="end">
|
||||||
{/* <DropdownMenuLabel className="flex gap-2 items-center mb-1 p-3">
|
{/* <DropdownMenuLabel className="flex gap-2 items-center mb-1 p-3">
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import {
|
||||||
AUTH_CONSTANTS,
|
AUTH_CONSTANTS,
|
||||||
} from "@/lib/auth-utils";
|
} from "@/lib/auth-utils";
|
||||||
import { warning } from "@/lib/swal";
|
import { warning } from "@/lib/swal";
|
||||||
|
import { setCookiesEncrypt } from "@/lib/utils";
|
||||||
|
|
||||||
export const useAuth = (): AuthContextType => {
|
export const useAuth = (): AuthContextType => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
@ -154,7 +155,7 @@ export const useAuth = (): AuthContextType => {
|
||||||
Cookies.set("username", profile?.username, {
|
Cookies.set("username", profile?.username, {
|
||||||
expires: 1,
|
expires: 1,
|
||||||
});
|
});
|
||||||
Cookies.set("urie", profile?.roleId, {
|
setCookiesEncrypt("urie", profile?.userRoleId, {
|
||||||
expires: 1,
|
expires: 1,
|
||||||
});
|
});
|
||||||
Cookies.set("roleName", profile?.roleName, {
|
Cookies.set("roleName", profile?.roleName, {
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export const setProfileCookies = (profile: ProfileData): void => {
|
||||||
Cookies.set("state-prov", profile.userLevel?.province?.provName || "", { expires: 1 });
|
Cookies.set("state-prov", profile.userLevel?.province?.provName || "", { expires: 1 });
|
||||||
|
|
||||||
setCookiesEncrypt("uie", profile.id, { expires: 1 });
|
setCookiesEncrypt("uie", profile.id, { expires: 1 });
|
||||||
setCookiesEncrypt("urie", profile.roleId.toString(), { expires: 1 });
|
setCookiesEncrypt("urie", profile.userRoleId.toString(), { expires: 1 });
|
||||||
setCookiesEncrypt("urne", profile.role?.name || "", { expires: 1 });
|
setCookiesEncrypt("urne", profile.role?.name || "", { expires: 1 });
|
||||||
setCookiesEncrypt("ulie", profile.userLevel?.id.toString() || "", { expires: 1 });
|
setCookiesEncrypt("ulie", profile.userLevel?.id.toString() || "", { expires: 1 });
|
||||||
setCookiesEncrypt("uplie", profile.userLevel?.parentLevelId?.toString() || "", { expires: 1 });
|
setCookiesEncrypt("uplie", profile.userLevel?.parentLevelId?.toString() || "", { expires: 1 });
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ export interface ProfileData {
|
||||||
fullname: string;
|
fullname: string;
|
||||||
email: string;
|
email: string;
|
||||||
roleId: number;
|
roleId: number;
|
||||||
|
userRoleId:number;
|
||||||
role: {
|
role: {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
@ -169,4 +170,5 @@ export interface AuthCookies {
|
||||||
ulnae: string; // user level name encrypted
|
ulnae: string; // user level name encrypted
|
||||||
uinse: string; // user institute id encrypted
|
uinse: string; // user institute id encrypted
|
||||||
status: string;
|
status: string;
|
||||||
|
username: string;
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue