fix: pull main
This commit is contained in:
commit
3b4e621f4b
2
.env
2
.env
|
|
@ -1,2 +1,2 @@
|
|||
NETIDHUB_CLIENT_KEY=b1ce6602-07ad-46c2-85eb-0cd6decfefa3
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8080
|
||||
NEXT_PUBLIC_API_URL=https://kontenhumas.com/api
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
# API Configuration for Local Development
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8080/api/
|
||||
|
|
@ -36,3 +36,4 @@ yarn-error.log*
|
|||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
.env.local
|
||||
|
|
|
|||
20
Dockerfile
20
Dockerfile
|
|
@ -4,6 +4,16 @@ FROM node:23.5.0-alpine
|
|||
# Mengatur port
|
||||
ENV PORT 3000
|
||||
|
||||
# Build arguments untuk environment variables (build-time)
|
||||
# Bisa di-override saat docker build dengan --build-arg
|
||||
ARG NEXT_PUBLIC_API_URL
|
||||
ARG NEXT_PUBLIC_SITE_URL
|
||||
|
||||
# Set sebagai environment variables untuk build
|
||||
# Next.js membaca NEXT_PUBLIC_* variables saat BUILD TIME
|
||||
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||
ENV NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}
|
||||
|
||||
# Install pnpm secara global
|
||||
RUN npm install -g pnpm
|
||||
|
||||
|
|
@ -16,17 +26,19 @@ COPY package.json ./
|
|||
# Menyalin direktori ckeditor5 jika diperlukan
|
||||
COPY vendor/ckeditor5 ./vendor/ckeditor5
|
||||
|
||||
# Menyalin env
|
||||
COPY .env .env
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install
|
||||
# RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Menyalin source code aplikasi
|
||||
# Menyalin source code aplikasi (termasuk .env jika ada)
|
||||
# PENTING: Next.js akan membaca file .env otomatis jika ada
|
||||
# Tapi jika ARG di-set, ARG akan override nilai dari .env
|
||||
COPY . .
|
||||
|
||||
# Build aplikasi
|
||||
# Next.js membaca NEXT_PUBLIC_* dari:
|
||||
# 1. Environment variables (ENV) - prioritas tertinggi
|
||||
# 2. File .env - jika ENV tidak di-set
|
||||
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm next build
|
||||
|
||||
# Expose port untuk server
|
||||
|
|
|
|||
|
|
@ -0,0 +1,974 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { PlusIcon, MenuIcon, EditIcon, DeleteIcon } from "@/components/icons";
|
||||
import {
|
||||
MasterMenu,
|
||||
getMasterMenus,
|
||||
getMasterMenuById,
|
||||
createMasterMenu,
|
||||
updateMasterMenu,
|
||||
deleteMasterMenu,
|
||||
} from "@/service/menu-modules";
|
||||
import {
|
||||
MenuAction,
|
||||
getMenuActionsByMenuId,
|
||||
createMenuAction,
|
||||
updateMenuAction,
|
||||
deleteMenuAction,
|
||||
createMenuActionsBatch,
|
||||
} from "@/service/menu-actions";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import Swal from "sweetalert2";
|
||||
import { FormField } from "@/components/form/common/FormField";
|
||||
import { getCookiesDecrypt } from "@/lib/utils";
|
||||
|
||||
export default function MenuManagementPage() {
|
||||
const router = useRouter();
|
||||
const [menus, setMenus] = useState<MasterMenu[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [editingMenu, setEditingMenu] = useState<MasterMenu | null>(null);
|
||||
const [selectedMenuForActions, setSelectedMenuForActions] = useState<MasterMenu | null>(null);
|
||||
const [menuActions, setMenuActions] = useState<MenuAction[]>([]);
|
||||
const [isActionsDialogOpen, setIsActionsDialogOpen] = useState(false);
|
||||
const [isActionFormOpen, setIsActionFormOpen] = useState(false);
|
||||
const [editingAction, setEditingAction] = useState<MenuAction | null>(null);
|
||||
const [actionFormData, setActionFormData] = useState({
|
||||
actionCode: "",
|
||||
actionName: "",
|
||||
description: "",
|
||||
pathUrl: "",
|
||||
httpMethod: "none",
|
||||
position: 0,
|
||||
});
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
group: "",
|
||||
statusId: 1,
|
||||
parentMenuId: undefined as number | undefined,
|
||||
icon: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has roleId = 1
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
Swal.fire({
|
||||
title: "Access Denied",
|
||||
text: "You don't have permission to access this page",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
}).then(() => {
|
||||
router.push("/admin/dashboard");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
loadData();
|
||||
}, [router]);
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const menusRes = await getMasterMenus({ limit: 100 });
|
||||
|
||||
if (menusRes?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: menusRes?.message || "Failed to load menus",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
setMenus([]);
|
||||
} else {
|
||||
// Transform snake_case to camelCase for consistency
|
||||
const menusData = (menusRes?.data?.data || []).map((menu: any) => ({
|
||||
...menu,
|
||||
moduleId: menu.module_id || menu.moduleId,
|
||||
parentMenuId: menu.parent_menu_id !== undefined ? menu.parent_menu_id : menu.parentMenuId,
|
||||
statusId: menu.status_id || menu.statusId,
|
||||
isActive: menu.is_active !== undefined ? menu.is_active : menu.isActive,
|
||||
}));
|
||||
setMenus(menusData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading data:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred while loading menus",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
setMenus([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDialog = async (menu?: MasterMenu) => {
|
||||
if (menu) {
|
||||
// Fetch fresh data from API to ensure all fields are loaded correctly
|
||||
try {
|
||||
const res = await getMasterMenuById(menu.id);
|
||||
if (!res?.error && res?.data?.data) {
|
||||
const menuData = res.data.data as any;
|
||||
setEditingMenu(menu);
|
||||
setFormData({
|
||||
name: menuData.name || menu.name || "",
|
||||
description: menuData.description || menu.description || "",
|
||||
group: menuData.group || menu.group || "",
|
||||
statusId: menuData.status_id || menuData.statusId || menu.statusId || 1,
|
||||
parentMenuId: menuData.parent_menu_id !== undefined && menuData.parent_menu_id !== null
|
||||
? menuData.parent_menu_id
|
||||
: (menuData.parentMenuId !== undefined && menuData.parentMenuId !== null
|
||||
? menuData.parentMenuId
|
||||
: (menu.parentMenuId || undefined)),
|
||||
icon: menuData.icon || menu.icon || "",
|
||||
});
|
||||
} else {
|
||||
// Fallback to menu object if API call fails
|
||||
setEditingMenu(menu);
|
||||
setFormData({
|
||||
name: menu.name || "",
|
||||
description: menu.description || "",
|
||||
group: menu.group || "",
|
||||
statusId: (menu as any).status_id || menu.statusId || 1,
|
||||
parentMenuId: (menu as any).parent_menu_id !== undefined && (menu as any).parent_menu_id !== null
|
||||
? (menu as any).parent_menu_id
|
||||
: (menu.parentMenuId || undefined),
|
||||
icon: menu.icon || "",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading menu details:", error);
|
||||
// Fallback to menu object if API call fails
|
||||
setEditingMenu(menu);
|
||||
setFormData({
|
||||
name: menu.name || "",
|
||||
description: menu.description || "",
|
||||
group: menu.group || "",
|
||||
statusId: (menu as any).status_id || menu.statusId || 1,
|
||||
parentMenuId: (menu as any).parent_menu_id !== undefined && (menu as any).parent_menu_id !== null
|
||||
? (menu as any).parent_menu_id
|
||||
: (menu.parentMenuId || undefined),
|
||||
icon: menu.icon || "",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setEditingMenu(null);
|
||||
setFormData({
|
||||
name: "",
|
||||
description: "",
|
||||
group: "",
|
||||
statusId: 1,
|
||||
parentMenuId: undefined,
|
||||
icon: "",
|
||||
});
|
||||
}
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// Prepare payload without moduleId
|
||||
const payload = {
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
group: formData.group,
|
||||
statusId: formData.statusId,
|
||||
parentMenuId: formData.parentMenuId,
|
||||
icon: formData.icon,
|
||||
};
|
||||
|
||||
if (editingMenu) {
|
||||
const res = await updateMasterMenu(editingMenu.id, payload);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to update menu",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Menu updated successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
} else {
|
||||
const res = await createMasterMenu(payload);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to create menu",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Menu created successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving menu:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleManageActions = async (menu: MasterMenu) => {
|
||||
try {
|
||||
setSelectedMenuForActions(menu);
|
||||
setIsActionsDialogOpen(true);
|
||||
await loadMenuActions(menu.id);
|
||||
} catch (error) {
|
||||
console.error("Error opening actions dialog:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "Failed to open actions management",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const loadMenuActions = async (menuId: number) => {
|
||||
try {
|
||||
const res = await getMenuActionsByMenuId(menuId);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to load menu actions",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
setMenuActions([]);
|
||||
} else {
|
||||
setMenuActions(res?.data?.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading menu actions:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred while loading menu actions",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
setMenuActions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenActionForm = (action?: MenuAction) => {
|
||||
if (action) {
|
||||
setEditingAction(action);
|
||||
setActionFormData({
|
||||
actionCode: action.actionCode,
|
||||
actionName: action.actionName,
|
||||
description: action.description || "",
|
||||
pathUrl: action.pathUrl || "",
|
||||
httpMethod: action.httpMethod || "none",
|
||||
position: action.position || 0,
|
||||
});
|
||||
} else {
|
||||
setEditingAction(null);
|
||||
setActionFormData({
|
||||
actionCode: "",
|
||||
actionName: "",
|
||||
description: "",
|
||||
pathUrl: "",
|
||||
httpMethod: "none",
|
||||
position: menuActions.length + 1,
|
||||
});
|
||||
}
|
||||
setIsActionFormOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveAction = async () => {
|
||||
if (!selectedMenuForActions) return;
|
||||
|
||||
try {
|
||||
// Prepare payload, converting "none" back to undefined for httpMethod
|
||||
const payload = {
|
||||
menuId: selectedMenuForActions.id,
|
||||
actionCode: actionFormData.actionCode,
|
||||
actionName: actionFormData.actionName,
|
||||
description: actionFormData.description || undefined,
|
||||
pathUrl: actionFormData.pathUrl || undefined,
|
||||
httpMethod: actionFormData.httpMethod === "none" ? undefined : actionFormData.httpMethod,
|
||||
position: actionFormData.position,
|
||||
};
|
||||
|
||||
if (editingAction) {
|
||||
const res = await updateMenuAction(editingAction.id, payload);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to update action",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Action updated successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadMenuActions(selectedMenuForActions.id);
|
||||
setIsActionFormOpen(false);
|
||||
}
|
||||
} else {
|
||||
const res = await createMenuAction(payload);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to create action",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Action created successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadMenuActions(selectedMenuForActions.id);
|
||||
setIsActionFormOpen(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving action:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAction = async (action: MenuAction) => {
|
||||
const result = await Swal.fire({
|
||||
title: "Delete Action?",
|
||||
text: `Are you sure you want to delete "${action.actionName}"?`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, delete it",
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const res = await deleteMenuAction(action.id);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to delete action",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Deleted!",
|
||||
text: "Action has been deleted.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
if (selectedMenuForActions) {
|
||||
await loadMenuActions(selectedMenuForActions.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting action:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickAddActions = async () => {
|
||||
if (!selectedMenuForActions) return;
|
||||
|
||||
const standardActions = ["view", "create", "edit", "delete", "approve", "export"];
|
||||
const existingActionCodes = menuActions.map(a => a.actionCode);
|
||||
const newActions = standardActions.filter(code => !existingActionCodes.includes(code));
|
||||
|
||||
if (newActions.length === 0) {
|
||||
Swal.fire({
|
||||
title: "Info",
|
||||
text: "All standard actions already exist",
|
||||
icon: "info",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await createMenuActionsBatch({
|
||||
menuId: selectedMenuForActions.id,
|
||||
actionCodes: newActions,
|
||||
});
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to create actions",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: `${newActions.length} actions created successfully`,
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadMenuActions(selectedMenuForActions.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating actions:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (menu: MasterMenu) => {
|
||||
const result = await Swal.fire({
|
||||
title: "Delete Menu?",
|
||||
text: `Are you sure you want to delete "${menu.name}"? This action cannot be undone.`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, delete it",
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const res = await deleteMasterMenu(menu.id);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to delete menu",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Deleted!",
|
||||
text: "Menu has been deleted.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting menu:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
return null; // Will redirect in useEffect
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteBreadcrumb />
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Menu Management</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage system menus and their configurations
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex items-center gap-2" onClick={() => handleOpenDialog()}>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Menu
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
{/* @ts-ignore */}
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingMenu ? `Edit Menu: ${editingMenu.name}` : "Create New Menu"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
label="Menu Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="e.g., Dashboard, Content Management"
|
||||
value={formData.name}
|
||||
onChange={(value) => setFormData({ ...formData, name: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Description"
|
||||
name="description"
|
||||
type="text"
|
||||
placeholder="Brief description of the menu"
|
||||
value={formData.description}
|
||||
onChange={(value) => setFormData({ ...formData, description: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Group"
|
||||
name="group"
|
||||
type="text"
|
||||
placeholder="e.g., Main, Settings, Content"
|
||||
value={formData.group}
|
||||
onChange={(value) => setFormData({ ...formData, group: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Parent Menu"
|
||||
name="parentMenuId"
|
||||
type="select"
|
||||
placeholder="Select parent menu (optional)"
|
||||
value={formData.parentMenuId || 0}
|
||||
onChange={(value) => setFormData({ ...formData, parentMenuId: Number(value) === 0 ? undefined : Number(value) })}
|
||||
options={[
|
||||
{ value: 0, label: "No Parent (Root Menu)" },
|
||||
...menus
|
||||
.filter((m) => !m.parentMenuId || m.id !== editingMenu?.id)
|
||||
.map((menu) => ({
|
||||
value: menu.id,
|
||||
label: `${menu.name} - ${menu.group}`,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
<FormField
|
||||
label="Icon"
|
||||
name="icon"
|
||||
type="text"
|
||||
placeholder="e.g., material-symbols:dashboard, heroicons:bars-3"
|
||||
value={formData.icon}
|
||||
onChange={(value) => setFormData({ ...formData, icon: value })}
|
||||
helpText="Icon identifier (e.g., from Iconify)"
|
||||
/>
|
||||
<FormField
|
||||
label="Status ID"
|
||||
name="statusId"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
value={formData.statusId}
|
||||
onChange={(value) => setFormData({ ...formData, statusId: Number(value) || 1 })}
|
||||
required
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
{editingMenu ? "Update" : "Create"} Menu
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading menus...</p>
|
||||
</div>
|
||||
) : menus.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{menus.map((menu) => {
|
||||
const parentMenu = menus.find((m) => m.id === menu.parentMenuId);
|
||||
return (
|
||||
<Card key={menu.id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="truncate">{menu.name}</span>
|
||||
{menu.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>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="text-sm text-gray-600">{menu.description}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
<span className="font-medium">Group:</span> {menu.group}
|
||||
</div>
|
||||
{parentMenu && (
|
||||
<div className="text-xs text-gray-500">
|
||||
<span className="font-medium">Parent:</span> {parentMenu.name}
|
||||
</div>
|
||||
)}
|
||||
{menu.icon && (
|
||||
<div className="text-xs text-gray-500">
|
||||
<span className="font-medium">Icon:</span> {menu.icon}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => handleOpenDialog(menu)}
|
||||
>
|
||||
<EditIcon className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => handleManageActions(menu)}
|
||||
>
|
||||
<MenuIcon className="h-4 w-4 mr-2" />
|
||||
Actions
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleDelete(menu)}
|
||||
>
|
||||
<DeleteIcon className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<MenuIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No Menus Found</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Create your first menu to define system navigation
|
||||
</p>
|
||||
<Button onClick={() => handleOpenDialog()}>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create Menu
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Actions Management Dialog */}
|
||||
<Dialog open={isActionsDialogOpen} onOpenChange={setIsActionsDialogOpen}>
|
||||
{/* @ts-ignore */}
|
||||
<DialogContent size="lg" className="!max-w-[60vw] !w-[60vw] !min-w-[60vw] max-h-[95vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Manage Actions: {selectedMenuForActions?.name}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-600">
|
||||
Manage actions available for this menu
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleQuickAddActions}
|
||||
>
|
||||
Quick Add Standard Actions
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleOpenActionForm()}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Add Action
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{menuActions.length > 0 ? (
|
||||
<div className="border rounded-lg">
|
||||
<table className="w-full">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Code</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Path URL</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Position</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{menuActions.map((action) => (
|
||||
<tr key={action.id}>
|
||||
<td className="px-4 py-3 text-sm font-medium text-gray-900">{action.actionCode}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">{action.actionName}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">{action.pathUrl || "-"}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">{action.httpMethod || "-"}</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-600">{action.position || "-"}</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleOpenActionForm(action)}
|
||||
>
|
||||
<EditIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleDeleteAction(action)}
|
||||
>
|
||||
<DeleteIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-500 mb-4">No actions found for this menu</p>
|
||||
<Button onClick={() => handleOpenActionForm()}>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Add First Action
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Action Form Dialog */}
|
||||
<Dialog open={isActionFormOpen} onOpenChange={setIsActionFormOpen}>
|
||||
{/* @ts-ignore */}
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingAction ? `Edit Action: ${editingAction.actionName}` : "Create New Action"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
label="Action Code"
|
||||
name="actionCode"
|
||||
type="text"
|
||||
placeholder="e.g., view, create, edit, delete"
|
||||
value={actionFormData.actionCode}
|
||||
onChange={(value) => setActionFormData({ ...actionFormData, actionCode: value })}
|
||||
required
|
||||
helpText="Unique identifier for the action"
|
||||
/>
|
||||
<FormField
|
||||
label="Action Name"
|
||||
name="actionName"
|
||||
type="text"
|
||||
placeholder="e.g., View Content, Create Content"
|
||||
value={actionFormData.actionName}
|
||||
onChange={(value) => setActionFormData({ ...actionFormData, actionName: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Description"
|
||||
name="description"
|
||||
type="textarea"
|
||||
placeholder="Brief description of the action"
|
||||
value={actionFormData.description}
|
||||
onChange={(value) => setActionFormData({ ...actionFormData, description: value })}
|
||||
/>
|
||||
<FormField
|
||||
label="Path URL"
|
||||
name="pathUrl"
|
||||
type="text"
|
||||
placeholder="e.g., /admin/articles, /api/articles"
|
||||
value={actionFormData.pathUrl}
|
||||
onChange={(value) => setActionFormData({ ...actionFormData, pathUrl: value })}
|
||||
helpText="Optional: URL path for routing"
|
||||
/>
|
||||
<FormField
|
||||
label="HTTP Method"
|
||||
name="httpMethod"
|
||||
type="select"
|
||||
placeholder="Select HTTP method"
|
||||
value={actionFormData.httpMethod || "none"}
|
||||
onChange={(value) => setActionFormData({ ...actionFormData, httpMethod: value })}
|
||||
options={[
|
||||
{ value: "none", label: "Not specified" },
|
||||
{ value: "GET", label: "GET" },
|
||||
{ value: "POST", label: "POST" },
|
||||
{ value: "PUT", label: "PUT" },
|
||||
{ value: "PATCH", label: "PATCH" },
|
||||
{ value: "DELETE", label: "DELETE" },
|
||||
]}
|
||||
/>
|
||||
<FormField
|
||||
label="Position"
|
||||
name="position"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
value={actionFormData.position}
|
||||
onChange={(value) => setActionFormData({ ...actionFormData, position: Number(value) || 0 })}
|
||||
helpText="Order of action in the menu"
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsActionFormOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSaveAction}>
|
||||
{editingAction ? "Update" : "Create"} Action
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,501 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { PlusIcon, ModuleIcon, EditIcon, DeleteIcon } from "@/components/icons";
|
||||
import {
|
||||
MasterModule,
|
||||
MasterMenu,
|
||||
getMasterModules,
|
||||
getMasterMenus,
|
||||
getMenuModulesByModuleId,
|
||||
createMasterModule,
|
||||
updateMasterModule,
|
||||
deleteMasterModule,
|
||||
createMenuModulesBatch,
|
||||
} from "@/service/menu-modules";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import Swal from "sweetalert2";
|
||||
import { FormField } from "@/components/form/common/FormField";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { getCookiesDecrypt } from "@/lib/utils";
|
||||
|
||||
export default function ModuleManagementPage() {
|
||||
const router = useRouter();
|
||||
const [modules, setModules] = useState<MasterModule[]>([]);
|
||||
const [menus, setMenus] = useState<MasterMenu[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [editingModule, setEditingModule] = useState<MasterModule | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
description: "",
|
||||
pathUrl: "",
|
||||
actionType: "",
|
||||
statusId: 1,
|
||||
menuIds: [] as number[],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has roleId = 1
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
Swal.fire({
|
||||
title: "Access Denied",
|
||||
text: "You don't have permission to access this page",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
}).then(() => {
|
||||
router.push("/admin/dashboard");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
loadData();
|
||||
}, [router]);
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await getMasterModules({ limit: 100 });
|
||||
if (!res?.error) {
|
||||
setModules(res?.data?.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading modules:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadMenus = async () => {
|
||||
try {
|
||||
const res = await getMasterMenus({ limit: 100 });
|
||||
if (!res?.error) {
|
||||
setMenus(res?.data?.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading menus:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadModuleMenus = async (moduleId: number) => {
|
||||
try {
|
||||
const res = await getMenuModulesByModuleId(moduleId);
|
||||
if (!res?.error) {
|
||||
const menuIds = (res?.data?.data || []).map((mm: any) => mm.menu_id || mm.menuId).filter((id: any) => id);
|
||||
setFormData((prev) => ({ ...prev, menuIds }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading module menus:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDialog = async (module?: MasterModule) => {
|
||||
await loadMenus();
|
||||
|
||||
if (module) {
|
||||
setEditingModule(module);
|
||||
setFormData({
|
||||
name: module.name,
|
||||
description: module.description,
|
||||
pathUrl: module.pathUrl,
|
||||
actionType: module.actionType || "",
|
||||
statusId: module.statusId,
|
||||
menuIds: [],
|
||||
});
|
||||
await loadModuleMenus(module.id);
|
||||
} else {
|
||||
setEditingModule(null);
|
||||
setFormData({
|
||||
name: "",
|
||||
description: "",
|
||||
pathUrl: "",
|
||||
actionType: "",
|
||||
statusId: 1,
|
||||
menuIds: [],
|
||||
});
|
||||
}
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const { menuIds, ...moduleData } = formData;
|
||||
|
||||
if (editingModule) {
|
||||
const res = await updateMasterModule(editingModule.id, moduleData);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to update module",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update menu associations
|
||||
if (menuIds && menuIds.length > 0) {
|
||||
// Create associations for each selected menu
|
||||
for (const menuId of menuIds) {
|
||||
try {
|
||||
await createMenuModulesBatch({
|
||||
menuId,
|
||||
moduleIds: [editingModule.id],
|
||||
});
|
||||
} catch (err) {
|
||||
// Ignore duplicate errors, backend should handle it
|
||||
console.log("Menu association may already exist:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Module updated successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
setIsDialogOpen(false);
|
||||
} else {
|
||||
const res = await createMasterModule(moduleData);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to create module",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the created module ID from response
|
||||
const createdModuleId = res?.data?.data?.id || res?.data?.id;
|
||||
|
||||
// Create menu associations if menuIds provided
|
||||
if (createdModuleId && menuIds && menuIds.length > 0) {
|
||||
for (const menuId of menuIds) {
|
||||
await createMenuModulesBatch({
|
||||
menuId,
|
||||
moduleIds: [createdModuleId],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Module created successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving module:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (module: MasterModule) => {
|
||||
const result = await Swal.fire({
|
||||
title: "Delete Module?",
|
||||
text: `Are you sure you want to delete "${module.name}"? This action cannot be undone.`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, delete it",
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const res = await deleteMasterModule(module.id);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to delete module",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Deleted!",
|
||||
text: "Module has been deleted.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting module:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
return null; // Will redirect in useEffect
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteBreadcrumb />
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Module Management</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage system modules and their configurations
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex items-center gap-2" onClick={() => handleOpenDialog()}>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Module
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingModule ? `Edit Module: ${editingModule.name}` : "Create New Module"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
label="Module Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="e.g., View Articles, Create Content"
|
||||
value={formData.name}
|
||||
onChange={(value) => setFormData({ ...formData, name: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Description"
|
||||
name="description"
|
||||
type="text"
|
||||
placeholder="Brief description of the module"
|
||||
value={formData.description}
|
||||
onChange={(value) => setFormData({ ...formData, description: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Path URL"
|
||||
name="pathUrl"
|
||||
type="text"
|
||||
placeholder="e.g., /api/articles, /api/content"
|
||||
value={formData.pathUrl}
|
||||
onChange={(value) => setFormData({ ...formData, pathUrl: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Action Type"
|
||||
name="actionType"
|
||||
type="select"
|
||||
placeholder="Select action type"
|
||||
value={formData.actionType}
|
||||
onChange={(value) => setFormData({ ...formData, actionType: value })}
|
||||
options={[
|
||||
{ value: "view", label: "View" },
|
||||
{ value: "create", label: "Create" },
|
||||
{ value: "update", label: "Update" },
|
||||
{ value: "delete", label: "Delete" },
|
||||
{ value: "approve", label: "Approve" },
|
||||
{ value: "reject", label: "Reject" },
|
||||
]}
|
||||
required
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="menuIds">Menus (Optional)</Label>
|
||||
<p className="text-sm text-gray-500 mb-2">
|
||||
Select which menus this module belongs to. A module can belong to multiple menus.
|
||||
</p>
|
||||
<div className="border rounded-md p-4 max-h-48 overflow-y-auto">
|
||||
{menus.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{menus.map((menu) => (
|
||||
<div key={menu.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`menu-${menu.id}`}
|
||||
checked={formData.menuIds.includes(menu.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setFormData({
|
||||
...formData,
|
||||
menuIds: [...formData.menuIds, menu.id],
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
...formData,
|
||||
menuIds: formData.menuIds.filter((id) => id !== menu.id),
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`menu-${menu.id}`}
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
{menu.name}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">No menus available</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FormField
|
||||
label="Status ID"
|
||||
name="statusId"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
value={formData.statusId}
|
||||
onChange={(value) => setFormData({ ...formData, statusId: Number(value) || 1 })}
|
||||
required
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
{editingModule ? "Update" : "Create"} Module
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading modules...</p>
|
||||
</div>
|
||||
) : modules.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{modules.map((module) => (
|
||||
<Card key={module.id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="truncate">{module.name}</span>
|
||||
{module.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>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="text-sm text-gray-600">{module.description}</div>
|
||||
<div className="text-xs text-gray-500 font-mono bg-gray-50 p-2 rounded">
|
||||
{module.pathUrl}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
|
||||
{module.actionType}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={() => handleOpenDialog(module)}
|
||||
>
|
||||
<EditIcon className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleDelete(module)}
|
||||
>
|
||||
<DeleteIcon className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<ModuleIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No Modules Found</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Create your first module to define system capabilities
|
||||
</p>
|
||||
<Button onClick={() => handleOpenDialog()}>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create Module
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2,7 +2,13 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { PlusIcon, ModuleIcon, EditIcon, DeleteIcon } from "@/components/icons";
|
||||
import {
|
||||
MasterModule,
|
||||
|
|
@ -50,11 +56,11 @@ export default function ModulesSettingsPage() {
|
|||
if (module) {
|
||||
setEditingModule(module);
|
||||
setFormData({
|
||||
name: module.name,
|
||||
description: module.description,
|
||||
pathUrl: module.pathUrl,
|
||||
actionType: module.actionType,
|
||||
statusId: module.statusId,
|
||||
name: module.name ?? "",
|
||||
description: module.description ?? "",
|
||||
pathUrl: module.pathUrl ?? "",
|
||||
actionType: module.actionType ?? "",
|
||||
statusId: module.statusId ?? 1,
|
||||
});
|
||||
} else {
|
||||
setEditingModule(null);
|
||||
|
|
@ -80,8 +86,8 @@ export default function ModulesSettingsPage() {
|
|||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
|
|
@ -90,8 +96,8 @@ export default function ModulesSettingsPage() {
|
|||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
await loadData();
|
||||
setIsDialogOpen(false);
|
||||
|
|
@ -105,8 +111,8 @@ export default function ModulesSettingsPage() {
|
|||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
|
|
@ -115,8 +121,8 @@ export default function ModulesSettingsPage() {
|
|||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
await loadData();
|
||||
setIsDialogOpen(false);
|
||||
|
|
@ -130,8 +136,8 @@ export default function ModulesSettingsPage() {
|
|||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -145,8 +151,8 @@ export default function ModulesSettingsPage() {
|
|||
confirmButtonText: "Yes, delete it",
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
|
|
@ -159,8 +165,8 @@ export default function ModulesSettingsPage() {
|
|||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
|
|
@ -169,8 +175,8 @@ export default function ModulesSettingsPage() {
|
|||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
await loadData();
|
||||
}
|
||||
|
|
@ -182,8 +188,8 @@ export default function ModulesSettingsPage() {
|
|||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -195,14 +201,19 @@ export default function ModulesSettingsPage() {
|
|||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Modules Settings</h1>
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
Modules Settings
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage system modules and their configurations
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex items-center gap-2" onClick={() => handleOpenDialog()}>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => handleOpenDialog()}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Module
|
||||
</Button>
|
||||
|
|
@ -210,7 +221,9 @@ export default function ModulesSettingsPage() {
|
|||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingModule ? `Edit Module: ${editingModule.name}` : "Create New Module"}
|
||||
{editingModule
|
||||
? `Edit Module: ${editingModule.name}`
|
||||
: "Create New Module"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
|
|
@ -220,7 +233,9 @@ export default function ModulesSettingsPage() {
|
|||
type="text"
|
||||
placeholder="e.g., View Articles, Create Content"
|
||||
value={formData.name}
|
||||
onChange={(value) => setFormData({ ...formData, name: value })}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, name: value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
|
|
@ -229,7 +244,9 @@ export default function ModulesSettingsPage() {
|
|||
type="text"
|
||||
placeholder="Brief description of the module"
|
||||
value={formData.description}
|
||||
onChange={(value) => setFormData({ ...formData, description: value })}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, description: value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
|
|
@ -238,7 +255,9 @@ export default function ModulesSettingsPage() {
|
|||
type="text"
|
||||
placeholder="e.g., /api/articles, /api/content"
|
||||
value={formData.pathUrl}
|
||||
onChange={(value) => setFormData({ ...formData, pathUrl: value })}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, pathUrl: value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
|
|
@ -247,7 +266,9 @@ export default function ModulesSettingsPage() {
|
|||
type="select"
|
||||
placeholder="Select action type"
|
||||
value={formData.actionType}
|
||||
onChange={(value) => setFormData({ ...formData, actionType: value })}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, actionType: value })
|
||||
}
|
||||
options={[
|
||||
{ value: "view", label: "View" },
|
||||
{ value: "create", label: "Create" },
|
||||
|
|
@ -264,7 +285,9 @@ export default function ModulesSettingsPage() {
|
|||
type="number"
|
||||
placeholder="1"
|
||||
value={formData.statusId}
|
||||
onChange={(value) => setFormData({ ...formData, statusId: Number(value) || 1 })}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, statusId: Number(value) || 1 })
|
||||
}
|
||||
required
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
|
|
@ -291,7 +314,10 @@ export default function ModulesSettingsPage() {
|
|||
) : modules.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{modules.map((module) => (
|
||||
<Card key={module.id} className="hover:shadow-lg transition-shadow">
|
||||
<Card
|
||||
key={module.id}
|
||||
className="hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="truncate">{module.name}</span>
|
||||
|
|
@ -308,7 +334,9 @@ export default function ModulesSettingsPage() {
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 mb-4">
|
||||
<div className="text-sm text-gray-600">{module.description}</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
{module.description}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 font-mono bg-gray-50 p-2 rounded">
|
||||
{module.pathUrl}
|
||||
</div>
|
||||
|
|
@ -347,7 +375,9 @@ export default function ModulesSettingsPage() {
|
|||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<ModuleIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No Modules Found</h3>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||
No Modules Found
|
||||
</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Create your first module to define system capabilities
|
||||
</p>
|
||||
|
|
@ -363,4 +393,3 @@ export default function ModulesSettingsPage() {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,25 @@ import {
|
|||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import { getUserLevelDetail } from "@/service/tenant";
|
||||
import {
|
||||
getUserLevelMenuAccessesByUserLevelId,
|
||||
UserLevelMenuAccess,
|
||||
} from "@/service/user-level-menu-accesses";
|
||||
import {
|
||||
getUserLevelMenuActionAccessesByUserLevelIdAndMenuId,
|
||||
UserLevelMenuActionAccess,
|
||||
} from "@/service/user-level-menu-action-accesses";
|
||||
import {
|
||||
getMenuActionsByMenuId,
|
||||
MenuAction,
|
||||
} from "@/service/menu-actions";
|
||||
import {
|
||||
getMasterMenus,
|
||||
MasterMenu,
|
||||
} from "@/service/menu-modules";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
const useTableColumns = (onEdit?: (data: any) => void) => {
|
||||
const MySwal = withReactContent(Swal);
|
||||
|
|
@ -192,11 +211,68 @@ const useTableColumns = (onEdit?: (data: any) => void) => {
|
|||
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
||||
const [detailData, setDetailData] = React.useState<any>(null);
|
||||
|
||||
const [menuAccesses, setMenuAccesses] = React.useState<UserLevelMenuAccess[]>([]);
|
||||
const [actionAccesses, setActionAccesses] = React.useState<Record<number, UserLevelMenuActionAccess[]>>({});
|
||||
const [menus, setMenus] = React.useState<MasterMenu[]>([]);
|
||||
const [menuActionsMap, setMenuActionsMap] = React.useState<Record<number, MenuAction[]>>({});
|
||||
const [isLoadingDetail, setIsLoadingDetail] = React.useState(false);
|
||||
|
||||
const handleView = async (id: number) => {
|
||||
setIsLoadingDetail(true);
|
||||
try {
|
||||
// Load basic user level data
|
||||
const res = await getUserLevelDetail(id);
|
||||
if (!res?.error) {
|
||||
setDetailData(res?.data?.data);
|
||||
|
||||
// Load menus
|
||||
const menusRes = await getMasterMenus({ limit: 100 });
|
||||
if (!menusRes?.error) {
|
||||
const menusData = (menusRes?.data?.data || []).map((menu: any) => ({
|
||||
...menu,
|
||||
moduleId: menu.module_id || menu.moduleId,
|
||||
parentMenuId: menu.parent_menu_id !== undefined ? menu.parent_menu_id : menu.parentMenuId,
|
||||
statusId: menu.status_id || menu.statusId,
|
||||
isActive: menu.is_active !== undefined ? menu.is_active : menu.isActive,
|
||||
}));
|
||||
setMenus(menusData);
|
||||
|
||||
// Load actions for each menu
|
||||
const actionsMap: Record<number, MenuAction[]> = {};
|
||||
for (const menu of menusData) {
|
||||
try {
|
||||
const actionsRes = await getMenuActionsByMenuId(menu.id);
|
||||
if (!actionsRes?.error) {
|
||||
actionsMap[menu.id] = actionsRes?.data?.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading actions for menu ${menu.id}:`, error);
|
||||
}
|
||||
}
|
||||
setMenuActionsMap(actionsMap);
|
||||
}
|
||||
|
||||
// Load menu accesses
|
||||
const menuAccessRes = await getUserLevelMenuAccessesByUserLevelId(id);
|
||||
if (!menuAccessRes?.error) {
|
||||
const accesses = menuAccessRes?.data?.data || [];
|
||||
setMenuAccesses(accesses);
|
||||
|
||||
// Load action accesses for each menu
|
||||
const actionAccessesMap: Record<number, UserLevelMenuActionAccess[]> = {};
|
||||
for (const menuAccess of accesses.filter((a: UserLevelMenuAccess) => a.canAccess)) {
|
||||
try {
|
||||
const actionRes = await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(id, menuAccess.menuId);
|
||||
if (!actionRes?.error) {
|
||||
actionAccessesMap[menuAccess.menuId] = actionRes?.data?.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading action accesses for menu ${menuAccess.menuId}:`, error);
|
||||
}
|
||||
}
|
||||
setActionAccesses(actionAccessesMap);
|
||||
}
|
||||
|
||||
setIsDialogOpen(true);
|
||||
} else {
|
||||
error(res?.message || "Gagal memuat detail user level");
|
||||
|
|
@ -204,6 +280,8 @@ const useTableColumns = (onEdit?: (data: any) => void) => {
|
|||
} catch (err) {
|
||||
console.error("View error:", err);
|
||||
error("Terjadi kesalahan saat memuat data.");
|
||||
} finally {
|
||||
setIsLoadingDetail(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -318,35 +396,200 @@ const useTableColumns = (onEdit?: (data: any) => void) => {
|
|||
</DropdownMenuContent>
|
||||
{/* ✅ Dialog Detail User Level */}
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogContent size="md" className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Detail User Level</DialogTitle>
|
||||
<DialogDescription>
|
||||
Informasi lengkap dari user level ID: {detailData?.id}
|
||||
Informasi lengkap dari user level: {detailData?.name || detailData?.aliasName}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{detailData ? (
|
||||
<div className="space-y-3 mt-4">
|
||||
<p>
|
||||
<span className="font-medium">Name:</span>{" "}
|
||||
{detailData.aliasName}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">Group:</span>{" "}
|
||||
{detailData.group}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">Parent Level:</span>{" "}
|
||||
{detailData.parentLevelId || "-"}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">Created At:</span>{" "}
|
||||
{detailData.createdAt
|
||||
? new Date(detailData.createdAt).toLocaleString("id-ID")
|
||||
: "-"}
|
||||
</p>
|
||||
{isLoadingDetail ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<span className="ml-2 text-gray-600">Memuat data...</span>
|
||||
</div>
|
||||
) : detailData ? (
|
||||
<Tabs defaultValue="basic" className="w-full mt-4">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="basic">Basic Information</TabsTrigger>
|
||||
<TabsTrigger value="menus">Menu Access</TabsTrigger>
|
||||
<TabsTrigger value="actions">Action Access</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Basic Information Tab */}
|
||||
<TabsContent value="basic" className="space-y-4 mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>User Level Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">ID:</span>
|
||||
<p className="text-base font-mono">{detailData.id}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Name:</span>
|
||||
<p className="text-base">{detailData.name || detailData.aliasName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Alias Name:</span>
|
||||
<p className="text-base font-mono">{detailData.aliasName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Level Number:</span>
|
||||
<p className="text-base">{detailData.levelNumber}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Group:</span>
|
||||
<p className="text-base">{detailData.group || "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Parent Level ID:</span>
|
||||
<p className="text-base">{detailData.parentLevelId || "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Province ID:</span>
|
||||
<p className="text-base">{detailData.provinceId || "-"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Is Approval Active:</span>
|
||||
<Badge className={detailData.isApprovalActive ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}>
|
||||
{detailData.isApprovalActive ? "Yes" : "No"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Is Active:</span>
|
||||
<Badge className={detailData.isActive ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}>
|
||||
{detailData.isActive ? "Active" : "Inactive"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Created At:</span>
|
||||
<p className="text-base text-sm">
|
||||
{detailData.createdAt
|
||||
? new Date(detailData.createdAt).toLocaleString("id-ID")
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium text-gray-600">Updated At:</span>
|
||||
<p className="text-base text-sm">
|
||||
{detailData.updatedAt
|
||||
? new Date(detailData.updatedAt).toLocaleString("id-ID")
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Menu Access Tab */}
|
||||
<TabsContent value="menus" className="space-y-4 mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Menu Access Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{menuAccesses.filter((a: UserLevelMenuAccess) => a.canAccess).length > 0 ? (
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{menuAccesses
|
||||
.filter((a: UserLevelMenuAccess) => a.canAccess)
|
||||
.map((access) => {
|
||||
const menu = menus.find((m) => m.id === access.menuId);
|
||||
return (
|
||||
<div
|
||||
key={access.id}
|
||||
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{menu?.name || `Menu ID: ${access.menuId}`}</div>
|
||||
<div className="text-sm text-gray-500">{menu?.description || "-"}</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
Group: {menu?.group || "-"} • Status: {access.canAccess ? "Accessible" : "No Access"}
|
||||
</div>
|
||||
</div>
|
||||
<Badge className={access.canAccess ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}>
|
||||
{access.canAccess ? "Accessible" : "No Access"}
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No menu access configured
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Action Access Tab */}
|
||||
<TabsContent value="actions" className="space-y-4 mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Action Access Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{Object.keys(actionAccesses).length > 0 ? (
|
||||
<div className="space-y-4 max-h-96 overflow-y-auto">
|
||||
{Object.entries(actionAccesses).map(([menuId, actions]) => {
|
||||
const menu = menus.find((m) => m.id === Number(menuId));
|
||||
const accessibleActions = actions.filter((a: UserLevelMenuActionAccess) => a.canAccess);
|
||||
|
||||
if (accessibleActions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card key={menuId} className="border-l-4 border-l-blue-500">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">{menu?.name || `Menu ID: ${menuId}`}</CardTitle>
|
||||
<p className="text-sm text-gray-500">{menu?.description || "-"}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{accessibleActions.map((actionAccess) => {
|
||||
const action = menuActionsMap[Number(menuId)]?.find(
|
||||
(a) => a.actionCode === actionAccess.actionCode
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={actionAccess.id}
|
||||
className="flex items-start gap-3 p-2 border rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">
|
||||
{action?.actionName || actionAccess.actionCode}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Code: {actionAccess.actionCode}
|
||||
{action?.pathUrl && ` • Path: ${action.pathUrl}`}
|
||||
{action?.httpMethod && ` • Method: ${action.httpMethod}`}
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="bg-green-100 text-green-800">
|
||||
Allowed
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No action access configured
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
<p className="text-gray-500 mt-4">Memuat data...</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ import {
|
|||
} from "@tanstack/react-table";
|
||||
import TablePagination from "@/components/table/table-pagination";
|
||||
import useTableColumns from "./columns";
|
||||
import TenantUpdateForm from "@/components/form/tenant/tenant-update-form";
|
||||
import { errorAutoClose, successAutoClose } from "@/lib/swal";
|
||||
import { close, loading } from "@/config/swal";
|
||||
import DetailTenant from "@/components/form/tenant/tenant-detail-update-form";
|
||||
|
|
@ -879,19 +878,21 @@ function TenantSettingsContentTable() {
|
|||
</DialogHeader>
|
||||
|
||||
{editingUserLevel ? (
|
||||
<TenantUpdateForm
|
||||
id={editingUserLevel.id}
|
||||
<UserLevelsForm
|
||||
mode="single"
|
||||
initialData={{
|
||||
// id: editingUserLevel.id,
|
||||
name: editingUserLevel.name,
|
||||
aliasName: editingUserLevel.aliasName,
|
||||
levelNumber: editingUserLevel.levelNumber,
|
||||
parentLevelId: editingUserLevel.parentLevelId || 0,
|
||||
provinceId: editingUserLevel.provinceId || 0,
|
||||
provinceId: editingUserLevel.provinceId,
|
||||
group: editingUserLevel.group || "",
|
||||
isApprovalActive: editingUserLevel.isApprovalActive,
|
||||
isActive: editingUserLevel.isActive,
|
||||
}}
|
||||
onSuccess={async () => {
|
||||
onSave={async (data) => {
|
||||
// The form handles the update internally
|
||||
setIsEditDialogOpen(false);
|
||||
setEditingUserLevel(null);
|
||||
await loadData();
|
||||
|
|
|
|||
|
|
@ -475,6 +475,7 @@ function TenantSettingsContent() {
|
|||
<UserLevelsForm
|
||||
mode="single"
|
||||
initialData={editingUserLevel ? {
|
||||
// id: editingUserLevel.id,
|
||||
name: editingUserLevel.name,
|
||||
aliasName: editingUserLevel.aliasName,
|
||||
levelNumber: editingUserLevel.levelNumber,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,977 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
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 {
|
||||
ChevronLeftIcon,
|
||||
SaveIcon,
|
||||
SettingsIcon,
|
||||
UsersIcon,
|
||||
WorkflowIcon,
|
||||
} from "@/components/icons";
|
||||
import {
|
||||
Tenant,
|
||||
TenantUpdateRequest,
|
||||
getTenantById,
|
||||
updateTenant,
|
||||
} from "@/service/tenant";
|
||||
import { getTenantList } from "@/service/tenant";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import Swal from "sweetalert2";
|
||||
import { FormField } from "@/components/form/common/FormField";
|
||||
import { getCookiesDecrypt } from "@/lib/utils";
|
||||
import { ApprovalWorkflowForm } from "@/components/form/ApprovalWorkflowForm";
|
||||
import { UserLevelsForm } from "@/components/form/UserLevelsForm";
|
||||
import {
|
||||
CreateApprovalWorkflowWithClientSettingsRequest,
|
||||
UserLevelsCreateRequest,
|
||||
UserLevel,
|
||||
getUserLevels,
|
||||
getApprovalWorkflowComprehensiveDetails,
|
||||
ComprehensiveWorkflowResponse,
|
||||
createUserLevel,
|
||||
} from "@/service/approval-workflows";
|
||||
import TenantCompanyUpdateForm from "@/components/form/tenant/tenant-detail-update-form";
|
||||
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 "@/app/[locale]/(admin)/admin/settings/tenant/component/columns";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { PlusIcon, EditIcon, DeleteIcon } from "@/components/icons";
|
||||
import { errorAutoClose, successAutoClose } from "@/lib/swal";
|
||||
import { close, loading } from "@/config/swal";
|
||||
|
||||
export default function EditTenantPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const tenantId = params?.id as string;
|
||||
const [totalData, setTotalData] = useState<number>(0);
|
||||
const [totalPage, setTotalPage] = useState<number>(1);
|
||||
|
||||
const [tenant, setTenant] = useState<Tenant | null>(null);
|
||||
const [parentTenants, setParentTenants] = useState<Tenant[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("profile");
|
||||
|
||||
// Workflow state
|
||||
const [workflow, setWorkflow] =
|
||||
useState<ComprehensiveWorkflowResponse | null>(null);
|
||||
const [isEditingWorkflow, setIsEditingWorkflow] = useState(false);
|
||||
|
||||
// User Levels state
|
||||
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
|
||||
const [isUserLevelDialogOpen, setIsUserLevelDialogOpen] = useState(false);
|
||||
const [editingUserLevel, setEditingUserLevel] = useState<UserLevel | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
// Tenant form data
|
||||
const [formData, setFormData] = useState<TenantUpdateRequest>({
|
||||
name: "",
|
||||
description: "",
|
||||
clientType: "standalone",
|
||||
parentClientId: undefined,
|
||||
maxUsers: undefined,
|
||||
maxStorage: undefined,
|
||||
address: "",
|
||||
phoneNumber: "",
|
||||
website: "",
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has roleId = 1
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
Swal.fire({
|
||||
title: "Access Denied",
|
||||
text: "You don't have permission to access this page",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
}).then(() => {
|
||||
router.push("/admin/dashboard");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (tenantId) {
|
||||
loadData();
|
||||
}
|
||||
}, [tenantId, router]);
|
||||
|
||||
// Load workflow and user levels when switching to those tabs
|
||||
useEffect(() => {
|
||||
if (tenant && (activeTab === "workflows" || activeTab === "user-levels")) {
|
||||
loadWorkflowAndUserLevels();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeTab]);
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [tenantRes, parentRes] = await Promise.all([
|
||||
getTenantById(tenantId),
|
||||
getTenantList({ clientType: "parent_client", limit: 100 }),
|
||||
]);
|
||||
|
||||
if (tenantRes?.error || !tenantRes?.data?.data) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: tenantRes?.message || "Failed to load tenant data",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
}).then(() => {
|
||||
router.push("/admin/tenants");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tenantData = tenantRes.data.data;
|
||||
setTenant(tenantData);
|
||||
|
||||
// Set form data with all available fields
|
||||
setFormData({
|
||||
name: tenantData.name || "",
|
||||
description: tenantData.description || "",
|
||||
clientType: tenantData.clientType || "standalone",
|
||||
parentClientId: tenantData.parentClientId || undefined,
|
||||
maxUsers: tenantData.maxUsers || undefined,
|
||||
maxStorage: tenantData.maxStorage || undefined,
|
||||
address: tenantData.address || "",
|
||||
phoneNumber: tenantData.phoneNumber || "",
|
||||
website: tenantData.website || "",
|
||||
isActive:
|
||||
tenantData.isActive !== undefined ? tenantData.isActive : true,
|
||||
logoUrl: tenantData.logoUrl || undefined,
|
||||
logoImagePath: tenantData.logoImagePath || undefined,
|
||||
});
|
||||
|
||||
if (!parentRes?.error) {
|
||||
setParentTenants(parentRes?.data?.data || []);
|
||||
}
|
||||
|
||||
// Load workflow and user levels if on those tabs
|
||||
// Note: This will be loaded when tab changes via useEffect
|
||||
} catch (error) {
|
||||
console.error("Error loading tenant:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred while loading tenant data",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
}).then(() => {
|
||||
router.push("/admin/tenants");
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadWorkflowAndUserLevels = async () => {
|
||||
try {
|
||||
const [comprehensiveWorkflowRes, userLevelsRes] = await Promise.all([
|
||||
getApprovalWorkflowComprehensiveDetails(),
|
||||
getUserLevels(),
|
||||
]);
|
||||
|
||||
if (!comprehensiveWorkflowRes?.error) {
|
||||
setWorkflow(comprehensiveWorkflowRes?.data?.data || null);
|
||||
} else {
|
||||
setWorkflow(null);
|
||||
}
|
||||
if (!userLevelsRes?.error) {
|
||||
const data = userLevelsRes?.data?.data || [];
|
||||
|
||||
setUserLevels(data);
|
||||
setTotalData(data.length);
|
||||
|
||||
const pageSize = pagination.pageSize;
|
||||
setTotalPage(Math.max(1, Math.ceil(data.length / pageSize)));
|
||||
}
|
||||
|
||||
if (!userLevelsRes?.error) {
|
||||
setUserLevels(userLevelsRes?.data?.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading workflow and user levels:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveTenantInfo = async () => {
|
||||
if (!tenantId) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const updateData: TenantUpdateRequest = {
|
||||
name: formData.name,
|
||||
description: formData.description || undefined,
|
||||
clientType: formData.clientType,
|
||||
parentClientId: formData.parentClientId || undefined,
|
||||
maxUsers: formData.maxUsers,
|
||||
maxStorage: formData.maxStorage,
|
||||
address: formData.address || undefined,
|
||||
phoneNumber: formData.phoneNumber || undefined,
|
||||
website: formData.website || undefined,
|
||||
isActive: formData.isActive,
|
||||
};
|
||||
|
||||
const res = await updateTenant(tenantId, updateData);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to update tenant",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Tenant updated successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
await loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving tenant:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWorkflowSave = async (
|
||||
data: CreateApprovalWorkflowWithClientSettingsRequest,
|
||||
) => {
|
||||
setIsEditingWorkflow(false);
|
||||
await loadWorkflowAndUserLevels();
|
||||
};
|
||||
|
||||
const handleUserLevelSave = async (data: UserLevelsCreateRequest) => {
|
||||
try {
|
||||
loading();
|
||||
const response = await createUserLevel(data);
|
||||
close();
|
||||
|
||||
if (response?.error) {
|
||||
errorAutoClose(response.message || "Failed to create user level.");
|
||||
return;
|
||||
}
|
||||
|
||||
successAutoClose("User level created successfully.");
|
||||
setIsUserLevelDialogOpen(false);
|
||||
setEditingUserLevel(null);
|
||||
setTimeout(async () => {
|
||||
await loadWorkflowAndUserLevels();
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
close();
|
||||
errorAutoClose("An error occurred while creating user level.");
|
||||
console.error("Error creating user level:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditUserLevel = (userLevel: UserLevel) => {
|
||||
setEditingUserLevel(userLevel);
|
||||
setIsUserLevelDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteUserLevel = async (userLevel: UserLevel) => {
|
||||
const result = await Swal.fire({
|
||||
title: "Delete User Level?",
|
||||
text: `Are you sure you want to delete "${userLevel.name}"? This action cannot be undone.`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, delete it",
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: "swal-z-index-9999",
|
||||
},
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
// TODO: Implement delete API call
|
||||
console.log("Delete user level:", userLevel.id);
|
||||
await loadWorkflowAndUserLevels();
|
||||
} catch (error) {
|
||||
console.error("Error deleting user level:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const columns = React.useMemo(
|
||||
() => useTableColumns((data) => handleEditUserLevel(data)),
|
||||
[],
|
||||
);
|
||||
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const table = useReactTable({
|
||||
data: userLevels,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
state: {
|
||||
pagination,
|
||||
},
|
||||
});
|
||||
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<SiteBreadcrumb />
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading tenant data...</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!tenant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteBreadcrumb />
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => router.push("/admin/tenants")}
|
||||
>
|
||||
<ChevronLeftIcon className="h-4 w-4 mr-2" />
|
||||
Back
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
Edit Tenant: {tenant.name}
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage tenant information, workflows, and user levels
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="profile" className="flex items-center gap-2">
|
||||
<SettingsIcon className="h-4 w-4" />
|
||||
Tenant Information
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="workflows" className="flex items-center gap-2">
|
||||
<WorkflowIcon className="h-4 w-4" />
|
||||
Approval Workflows
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="user-levels"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<UsersIcon className="h-4 w-4" />
|
||||
User Levels
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Tenant Information Tab */}
|
||||
<TabsContent value="profile" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Tenant Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<FormField
|
||||
label="Tenant Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="e.g., Company ABC, Organization XYZ"
|
||||
value={formData.name}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, name: value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Description"
|
||||
name="description"
|
||||
type="textarea"
|
||||
placeholder="Brief description of the tenant"
|
||||
value={formData.description || ""}
|
||||
onChange={(value) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
description: value || undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<FormField
|
||||
label="Client Type"
|
||||
name="clientType"
|
||||
type="select"
|
||||
placeholder="Select client type"
|
||||
value={formData.clientType}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, clientType: value as any })
|
||||
}
|
||||
options={[
|
||||
{ value: "standalone", label: "Standalone" },
|
||||
{ value: "parent_client", label: "Parent Client" },
|
||||
{ value: "sub_client", label: "Sub Client" },
|
||||
]}
|
||||
required
|
||||
/>
|
||||
{formData.clientType === "sub_client" && (
|
||||
<FormField
|
||||
label="Parent Tenant"
|
||||
name="parentClientId"
|
||||
type="select"
|
||||
placeholder="Select parent tenant"
|
||||
value={formData.parentClientId || "none"}
|
||||
onChange={(value) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
parentClientId: value === "none" ? undefined : value,
|
||||
})
|
||||
}
|
||||
options={[
|
||||
{ value: "none", label: "No Parent Tenant" },
|
||||
...parentTenants
|
||||
.filter((t) => t.id !== tenantId)
|
||||
.map((t) => ({
|
||||
value: t.id,
|
||||
label: t.name,
|
||||
})),
|
||||
]}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
label="Max Users"
|
||||
name="maxUsers"
|
||||
type="number"
|
||||
placeholder="e.g., 100"
|
||||
value={formData.maxUsers?.toString() || ""}
|
||||
onChange={(value) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
maxUsers: value ? Number(value) : undefined,
|
||||
})
|
||||
}
|
||||
helpText="Maximum number of users allowed"
|
||||
/>
|
||||
<FormField
|
||||
label="Max Storage (MB)"
|
||||
name="maxStorage"
|
||||
type="number"
|
||||
placeholder="e.g., 10000"
|
||||
value={formData.maxStorage?.toString() || ""}
|
||||
onChange={(value) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
maxStorage: value ? Number(value) : undefined,
|
||||
})
|
||||
}
|
||||
helpText="Maximum storage in MB"
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
label="Address"
|
||||
name="address"
|
||||
type="textarea"
|
||||
placeholder="Tenant address"
|
||||
value={formData.address || ""}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, address: value || undefined })
|
||||
}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
label="Phone Number"
|
||||
name="phoneNumber"
|
||||
type="tel"
|
||||
placeholder="e.g., +62 123 456 7890"
|
||||
value={formData.phoneNumber || ""}
|
||||
onChange={(value) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
phoneNumber: value || undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<FormField
|
||||
label="Website"
|
||||
name="website"
|
||||
type="url"
|
||||
placeholder="e.g., https://example.com"
|
||||
value={formData.website || ""}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, website: value || undefined })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
label="Status"
|
||||
name="isActive"
|
||||
type="select"
|
||||
placeholder="Select status"
|
||||
value={formData.isActive ? "active" : "inactive"}
|
||||
onChange={(value) =>
|
||||
setFormData({ ...formData, isActive: value === "active" })
|
||||
}
|
||||
options={[
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "inactive", label: "Inactive" },
|
||||
]}
|
||||
required
|
||||
/>
|
||||
<div className="flex justify-end pt-4 border-t">
|
||||
<Button
|
||||
onClick={handleSaveTenantInfo}
|
||||
disabled={isSaving}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<SaveIcon className="h-4 w-4" />
|
||||
{isSaving ? "Saving..." : "Save Tenant Information"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Approval Workflows Tab */}
|
||||
<TabsContent value="workflows" className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold">
|
||||
Approval Workflow Setup
|
||||
</h2>
|
||||
{workflow && !isEditingWorkflow && (
|
||||
<Button
|
||||
onClick={() => setIsEditingWorkflow(true)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<SettingsIcon className="h-4 w-4" />
|
||||
Edit Workflow
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditingWorkflow ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Setup Approval Workflow</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsEditingWorkflow(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ApprovalWorkflowForm
|
||||
initialData={
|
||||
workflow
|
||||
? {
|
||||
name: workflow.workflow.name,
|
||||
description: workflow.workflow.description,
|
||||
isDefault: workflow.workflow.isDefault,
|
||||
isActive: workflow.workflow.isActive,
|
||||
requiresApproval:
|
||||
workflow.workflow.requiresApproval,
|
||||
autoPublish: workflow.workflow.autoPublish,
|
||||
steps:
|
||||
workflow.steps?.map((step) => ({
|
||||
stepOrder: step.stepOrder,
|
||||
stepName: step.stepName,
|
||||
requiredUserLevelId: step.requiredUserLevelId,
|
||||
canSkip: step.canSkip,
|
||||
autoApproveAfterHours:
|
||||
step.autoApproveAfterHours,
|
||||
isActive: step.isActive,
|
||||
conditionType: step.conditionType,
|
||||
conditionValue: step.conditionValue,
|
||||
})) || [],
|
||||
clientApprovalSettings: {
|
||||
approvalExemptCategories:
|
||||
workflow.clientSettings
|
||||
.exemptCategoriesDetails || [],
|
||||
approvalExemptRoles:
|
||||
workflow.clientSettings.exemptRolesDetails ||
|
||||
[],
|
||||
approvalExemptUsers:
|
||||
workflow.clientSettings.exemptUsersDetails ||
|
||||
[],
|
||||
autoPublishArticles:
|
||||
workflow.clientSettings.autoPublishArticles,
|
||||
isActive: workflow.clientSettings.isActive,
|
||||
requireApprovalFor:
|
||||
workflow.clientSettings.requireApprovalFor ||
|
||||
[],
|
||||
requiresApproval:
|
||||
workflow.clientSettings.requiresApproval,
|
||||
skipApprovalFor:
|
||||
workflow.clientSettings.skipApprovalFor || [],
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
workflowId={workflow?.workflow.id}
|
||||
onSave={handleWorkflowSave}
|
||||
onCancel={() => setIsEditingWorkflow(false)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : workflow ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>{workflow.workflow.name}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{workflow.workflow.isDefault && (
|
||||
<span className="px-2 py-1 text-xs bg-blue-100 text-blue-800 rounded-full">
|
||||
Default
|
||||
</span>
|
||||
)}
|
||||
{workflow.workflow.isActive ? (
|
||||
<span className="px-2 py-1 text-xs bg-green-100 text-green-800 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-800 rounded-full">
|
||||
Inactive
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
{workflow.workflow.description}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{workflow.workflow.totalSteps}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Total Steps</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{workflow.workflow.activeSteps}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Active Steps</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div
|
||||
className={`text-2xl font-bold ${workflow.workflow.requiresApproval ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{workflow.workflow.requiresApproval ? "Yes" : "No"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">
|
||||
Requires Approval
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 rounded-lg">
|
||||
<div
|
||||
className={`text-2xl font-bold ${workflow.workflow.autoPublish ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{workflow.workflow.autoPublish ? "Yes" : "No"}
|
||||
</div>
|
||||
<div className="text-sm text-gray-600">Auto Publish</div>
|
||||
</div>
|
||||
</div>
|
||||
{workflow.steps && workflow.steps.length > 0 && (
|
||||
<div>
|
||||
<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-600">
|
||||
Required Level: {step.requiredUserLevelName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{step.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>
|
||||
))}
|
||||
</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 Found
|
||||
</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
No approval workflow has been set up for this tenant yet.
|
||||
</p>
|
||||
<Button onClick={() => setIsEditingWorkflow(true)}>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create 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 Management</h2>
|
||||
<Dialog
|
||||
open={isUserLevelDialogOpen}
|
||||
onOpenChange={setIsUserLevelDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setEditingUserLevel(null);
|
||||
setIsUserLevelDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create User Level
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingUserLevel
|
||||
? `Edit User Level: ${editingUserLevel.name}`
|
||||
: "Create New User Level"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<UserLevelsForm
|
||||
mode="single"
|
||||
initialData={
|
||||
editingUserLevel
|
||||
? {
|
||||
// id: editingUserLevel.id,
|
||||
name: editingUserLevel.name,
|
||||
aliasName: editingUserLevel.aliasName,
|
||||
levelNumber: editingUserLevel.levelNumber,
|
||||
parentLevelId: editingUserLevel.parentLevelId || 0,
|
||||
provinceId: editingUserLevel.provinceId,
|
||||
group: editingUserLevel.group || "",
|
||||
isApprovalActive: editingUserLevel.isApprovalActive,
|
||||
isActive: editingUserLevel.isActive,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onSave={handleUserLevelSave}
|
||||
onCancel={() => {
|
||||
setIsUserLevelDialogOpen(false);
|
||||
setEditingUserLevel(null);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{userLevels.length > 0 ? (
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{table
|
||||
.getHeaderGroups()
|
||||
.map((headerGroup) =>
|
||||
headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
)),
|
||||
)}
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleEditUserLevel(row.original)
|
||||
}
|
||||
>
|
||||
<EditIcon className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() =>
|
||||
handleDeleteUserLevel(row.original)
|
||||
}
|
||||
>
|
||||
<DeleteIcon className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length + 1}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="p-4">
|
||||
<TablePagination
|
||||
table={table}
|
||||
totalData={totalData}
|
||||
totalPage={totalPage}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<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 user hierarchy
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditingUserLevel(null);
|
||||
setIsUserLevelDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create User Level
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,463 @@
|
|||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { PlusIcon, EditIcon, DeleteIcon } from "@/components/icons";
|
||||
import {
|
||||
Tenant,
|
||||
TenantCreateRequest,
|
||||
getTenantList,
|
||||
createTenant,
|
||||
deleteTenant,
|
||||
} from "@/service/tenant";
|
||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||
import Swal from "sweetalert2";
|
||||
import { FormField } from "@/components/form/common/FormField";
|
||||
import { getCookiesDecrypt } from "@/lib/utils";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
|
||||
export default function TenantsManagementPage() {
|
||||
const router = useRouter();
|
||||
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||
const [parentTenants, setParentTenants] = useState<Tenant[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [formData, setFormData] = useState<TenantCreateRequest>({
|
||||
name: "",
|
||||
description: "",
|
||||
clientType: "standalone",
|
||||
parentClientId: undefined,
|
||||
maxUsers: undefined,
|
||||
maxStorage: undefined,
|
||||
address: "",
|
||||
phoneNumber: "",
|
||||
website: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Check if user has roleId = 1
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
Swal.fire({
|
||||
title: "Access Denied",
|
||||
text: "You don't have permission to access this page",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
}).then(() => {
|
||||
router.push("/admin/dashboard");
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
loadData();
|
||||
}, [router]);
|
||||
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [tenantsRes, parentRes] = await Promise.all([
|
||||
getTenantList({ limit: 100 }),
|
||||
getTenantList({ clientType: "parent_client", limit: 100 }),
|
||||
]);
|
||||
|
||||
if (!tenantsRes?.error) {
|
||||
setTenants(tenantsRes?.data?.data || []);
|
||||
}
|
||||
if (!parentRes?.error) {
|
||||
setParentTenants(parentRes?.data?.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading tenants:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenDialog = () => {
|
||||
setFormData({
|
||||
name: "",
|
||||
description: "",
|
||||
clientType: "standalone",
|
||||
parentClientId: undefined,
|
||||
maxUsers: undefined,
|
||||
maxStorage: undefined,
|
||||
address: "",
|
||||
phoneNumber: "",
|
||||
website: "",
|
||||
});
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const createData: TenantCreateRequest = {
|
||||
name: formData.name,
|
||||
description: formData.description || undefined,
|
||||
clientType: formData.clientType,
|
||||
parentClientId: formData.parentClientId || undefined,
|
||||
maxUsers: formData.maxUsers,
|
||||
maxStorage: formData.maxStorage,
|
||||
address: formData.address || undefined,
|
||||
phoneNumber: formData.phoneNumber || undefined,
|
||||
website: formData.website || undefined,
|
||||
};
|
||||
|
||||
const res = await createTenant(createData);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to create tenant",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "Tenant created successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
setIsDialogOpen(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving tenant:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (tenant: Tenant) => {
|
||||
const result = await Swal.fire({
|
||||
title: "Delete Tenant?",
|
||||
text: `Are you sure you want to delete "${tenant.name}"? This action cannot be undone.`,
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Yes, delete it",
|
||||
cancelButtonText: "Cancel",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
const res = await deleteTenant(tenant.id);
|
||||
if (res?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: res?.message || "Failed to delete tenant",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Swal.fire({
|
||||
title: "Deleted!",
|
||||
text: "Tenant has been deleted.",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
await loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting tenant:", error);
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: "An unexpected error occurred",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const roleId = getCookiesDecrypt("urie");
|
||||
if (Number(roleId) !== 1) {
|
||||
return null; // Will redirect in useEffect
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SiteBreadcrumb />
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Tenant Management</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage system tenants and their configurations
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="flex items-center gap-2" onClick={() => handleOpenDialog()}>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Create Tenant
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
{/* @ts-ignore - DialogContent accepts children */}
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Tenant</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
label="Tenant Name"
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="e.g., Company ABC, Organization XYZ"
|
||||
value={formData.name}
|
||||
onChange={(value) => setFormData({ ...formData, name: value })}
|
||||
required
|
||||
/>
|
||||
<FormField
|
||||
label="Description"
|
||||
name="description"
|
||||
type="textarea"
|
||||
placeholder="Brief description of the tenant"
|
||||
value={formData.description}
|
||||
onChange={(value) => setFormData({ ...formData, description: value })}
|
||||
/>
|
||||
<FormField
|
||||
label="Client Type"
|
||||
name="clientType"
|
||||
type="select"
|
||||
placeholder="Select client type"
|
||||
value={formData.clientType}
|
||||
onChange={(value) => setFormData({ ...formData, clientType: value as any })}
|
||||
options={[
|
||||
{ value: "standalone", label: "Standalone" },
|
||||
{ value: "parent_client", label: "Parent Client" },
|
||||
{ value: "sub_client", label: "Sub Client" },
|
||||
]}
|
||||
required
|
||||
/>
|
||||
{formData.clientType === "sub_client" && (
|
||||
<FormField
|
||||
label="Parent Tenant"
|
||||
name="parentClientId"
|
||||
type="select"
|
||||
placeholder="Select parent tenant"
|
||||
value={formData.parentClientId || "none"}
|
||||
onChange={(value) => setFormData({ ...formData, parentClientId: value === "none" ? undefined : value })}
|
||||
options={[
|
||||
{ value: "none", label: "Select a parent tenant" },
|
||||
...parentTenants.map((tenant) => ({
|
||||
value: tenant.id,
|
||||
label: tenant.name,
|
||||
})),
|
||||
]}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
label="Max Users"
|
||||
name="maxUsers"
|
||||
type="number"
|
||||
placeholder="e.g., 100"
|
||||
value={formData.maxUsers?.toString() || ""}
|
||||
onChange={(value) => setFormData({ ...formData, maxUsers: value ? Number(value) : undefined })}
|
||||
helpText="Maximum number of users allowed"
|
||||
/>
|
||||
<FormField
|
||||
label="Max Storage (MB)"
|
||||
name="maxStorage"
|
||||
type="number"
|
||||
placeholder="e.g., 10000"
|
||||
value={formData.maxStorage?.toString() || ""}
|
||||
onChange={(value) => setFormData({ ...formData, maxStorage: value ? Number(value) : undefined })}
|
||||
helpText="Maximum storage in MB"
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
label="Address"
|
||||
name="address"
|
||||
type="textarea"
|
||||
placeholder="Tenant address"
|
||||
value={formData.address}
|
||||
onChange={(value) => setFormData({ ...formData, address: value })}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
label="Phone Number"
|
||||
name="phoneNumber"
|
||||
type="tel"
|
||||
placeholder="e.g., +62 123 456 7890"
|
||||
value={formData.phoneNumber}
|
||||
onChange={(value) => setFormData({ ...formData, phoneNumber: value })}
|
||||
/>
|
||||
<FormField
|
||||
label="Website"
|
||||
name="website"
|
||||
type="url"
|
||||
placeholder="e.g., https://example.com"
|
||||
value={formData.website}
|
||||
onChange={(value) => setFormData({ ...formData, website: value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
Create Tenant
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading tenants...</p>
|
||||
</div>
|
||||
) : tenants.length > 0 ? (
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Parent</TableHead>
|
||||
<TableHead>Address</TableHead>
|
||||
<TableHead>Phone</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tenants.map((tenant) => {
|
||||
const parentTenant = tenants.find((t) => t.id === tenant.parentClientId);
|
||||
return (
|
||||
<TableRow key={tenant.id}>
|
||||
<TableCell className="font-medium">{tenant.name}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
tenant.clientType === "parent_client"
|
||||
? "bg-blue-100 text-blue-800"
|
||||
: tenant.clientType === "sub_client"
|
||||
? "bg-purple-100 text-purple-800"
|
||||
: "bg-gray-100 text-gray-800"
|
||||
}`}>
|
||||
{tenant.clientType.replace("_", " ")}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>{parentTenant?.name || "-"}</TableCell>
|
||||
<TableCell className="max-w-xs truncate">{tenant.address || "-"}</TableCell>
|
||||
<TableCell>{tenant.phoneNumber || "-"}</TableCell>
|
||||
<TableCell>
|
||||
{tenant.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>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/admin/tenants/${tenant.id}/edit`)}
|
||||
>
|
||||
<EditIcon className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={() => handleDelete(tenant)}
|
||||
>
|
||||
<DeleteIcon className="h-4 w-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-12">
|
||||
<div className="text-center">
|
||||
<div className="h-12 w-12 text-gray-400 mx-auto mb-4 flex items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
className="w-12 h-12"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">No Tenants Found</h3>
|
||||
<p className="text-gray-500 mb-4">
|
||||
Create your first tenant to get started
|
||||
</p>
|
||||
<Button onClick={() => handleOpenDialog()}>
|
||||
<PlusIcon className="h-4 w-4 mr-2" />
|
||||
Create Tenant
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,7 +1,142 @@
|
|||
@import "tailwindcss";
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* ================================
|
||||
GLOBAL CSS VARIABLE (LIGHT MODE)
|
||||
================================ */
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 215.3 19.3% 34.5%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--success: 154 52% 55%;
|
||||
--warning: 16 93% 70%;
|
||||
--info: 185 96% 51%;
|
||||
|
||||
--sidebar: 0 0% 100%;
|
||||
--sidebar-foreground: 215 20% 65%;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
DARK MODE VARIABLES
|
||||
================================ */
|
||||
.dark {
|
||||
--background: 222.2 47.4% 11.2%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 215 27.9% 16.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 215.3 25% 26.7%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
|
||||
--sidebar: 215 27.9% 16.9%;
|
||||
--sidebar-foreground: 214.3 31.8% 91.4%;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
BASE LAYER
|
||||
================================ */
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================
|
||||
GLOBAL UTILITIES
|
||||
================================ */
|
||||
@layer utilities {
|
||||
/* SweetAlert z-index fix */
|
||||
.swal-z-index-9999 {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
/* Scrollbar hide */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Input group helpers */
|
||||
.input-group :not(:first-child) input {
|
||||
border-top-left-radius: 0 !important;
|
||||
border-bottom-left-radius: 0 !important;
|
||||
}
|
||||
|
||||
.input-group.merged :not(:first-child) input {
|
||||
border-left-width: 0 !important;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.input-group :not(:last-child) input {
|
||||
border-top-right-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
}
|
||||
|
||||
.input-group.merged :not(:last-child) input {
|
||||
border-right-width: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* @import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
/* SweetAlert2 z-index fix */
|
||||
.swal-z-index-9999 {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
|
@ -297,4 +432,4 @@
|
|||
.no-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
|
@ -19,7 +19,7 @@ interface FormFieldProps {
|
|||
showPasswordToggle?: boolean;
|
||||
onPasswordToggle?: () => void;
|
||||
showPassword?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const FormField: React.FC<FormFieldProps> = ({
|
||||
label,
|
||||
|
|
|
|||
|
|
@ -12,19 +12,30 @@ import {
|
|||
UserLevel,
|
||||
Province,
|
||||
createUserLevel,
|
||||
updateUserLevel,
|
||||
getUserLevels,
|
||||
getProvinces,
|
||||
} from "@/service/approval-workflows";
|
||||
import {
|
||||
MasterModule,
|
||||
getMasterModules,
|
||||
MasterMenu,
|
||||
getMasterMenus,
|
||||
} from "@/service/menu-modules";
|
||||
import {
|
||||
getUserLevelModuleAccessesByUserLevelId,
|
||||
createUserLevelModuleAccessesBatch,
|
||||
deleteUserLevelModuleAccess,
|
||||
UserLevelModuleAccess,
|
||||
} from "@/service/user-level-module-accesses";
|
||||
getUserLevelMenuAccessesByUserLevelId,
|
||||
createUserLevelMenuAccessesBatch,
|
||||
deleteUserLevelMenuAccess,
|
||||
UserLevelMenuAccess,
|
||||
} from "@/service/user-level-menu-accesses";
|
||||
import {
|
||||
getUserLevelMenuActionAccessesByUserLevelIdAndMenuId,
|
||||
createUserLevelMenuActionAccessesBatch,
|
||||
deleteUserLevelMenuActionAccess,
|
||||
UserLevelMenuActionAccess,
|
||||
} from "@/service/user-level-menu-action-accesses";
|
||||
import {
|
||||
getMenuActionsByMenuId,
|
||||
MenuAction,
|
||||
} from "@/service/menu-actions";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
interface UserLevelsFormProps {
|
||||
|
|
@ -57,9 +68,11 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
const [bulkFormData, setBulkFormData] = useState<UserLevelsCreateRequest[]>([]);
|
||||
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
|
||||
const [provinces, setProvinces] = useState<Province[]>([]);
|
||||
const [modules, setModules] = useState<MasterModule[]>([]);
|
||||
const [selectedModuleIds, setSelectedModuleIds] = useState<number[]>([]);
|
||||
const [userLevelModuleAccesses, setUserLevelModuleAccesses] = useState<UserLevelModuleAccess[]>([]);
|
||||
const [menus, setMenus] = useState<MasterMenu[]>([]);
|
||||
const [selectedMenuIds, setSelectedMenuIds] = useState<number[]>([]);
|
||||
const [userLevelMenuAccesses, setUserLevelMenuAccesses] = useState<UserLevelMenuAccess[]>([]);
|
||||
const [menuActionsMap, setMenuActionsMap] = useState<Record<number, MenuAction[]>>({});
|
||||
const [selectedActionAccesses, setSelectedActionAccesses] = useState<Record<number, string[]>>({});
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [expandedHierarchy, setExpandedHierarchy] = useState(false);
|
||||
|
|
@ -75,15 +88,38 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [userLevelsRes, provincesRes, modulesRes] = await Promise.all([
|
||||
const [userLevelsRes, provincesRes, menusRes] = await Promise.all([
|
||||
getUserLevels(),
|
||||
getProvinces(),
|
||||
getMasterModules({ limit: 100 }),
|
||||
getMasterMenus({ limit: 100 }),
|
||||
]);
|
||||
|
||||
if (!userLevelsRes?.error) setUserLevels(userLevelsRes?.data?.data || []);
|
||||
if (!provincesRes?.error) setProvinces(provincesRes?.data?.data || []);
|
||||
if (!modulesRes?.error) setModules(modulesRes?.data?.data || []);
|
||||
if (!menusRes?.error) {
|
||||
const menusData = (menusRes?.data?.data || []).map((menu: any) => ({
|
||||
...menu,
|
||||
moduleId: menu.module_id || menu.moduleId,
|
||||
parentMenuId: menu.parent_menu_id !== undefined ? menu.parent_menu_id : menu.parentMenuId,
|
||||
statusId: menu.status_id || menu.statusId,
|
||||
isActive: menu.is_active !== undefined ? menu.is_active : menu.isActive,
|
||||
}));
|
||||
setMenus(menusData);
|
||||
|
||||
// Load actions for each menu
|
||||
const actionsMap: Record<number, MenuAction[]> = {};
|
||||
for (const menu of menusData) {
|
||||
try {
|
||||
const actionsRes = await getMenuActionsByMenuId(menu.id);
|
||||
if (!actionsRes?.error) {
|
||||
actionsMap[menu.id] = actionsRes?.data?.data || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading actions for menu ${menu.id}:`, error);
|
||||
}
|
||||
}
|
||||
setMenuActionsMap(actionsMap);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading form data:", error);
|
||||
} finally {
|
||||
|
|
@ -95,22 +131,41 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadModuleAccesses = async () => {
|
||||
const loadAccesses = async () => {
|
||||
if (initialData && (initialData as any).id) {
|
||||
const userLevelId = (initialData as any).id;
|
||||
try {
|
||||
const res = await getUserLevelModuleAccessesByUserLevelId((initialData as any).id);
|
||||
if (!res?.error) {
|
||||
const accesses = res?.data?.data || [];
|
||||
setUserLevelModuleAccesses(accesses);
|
||||
setSelectedModuleIds(accesses.filter((a: UserLevelModuleAccess) => a.canAccess).map((a: UserLevelModuleAccess) => a.moduleId));
|
||||
// Load menu accesses
|
||||
const menuRes = await getUserLevelMenuAccessesByUserLevelId(userLevelId);
|
||||
if (!menuRes?.error) {
|
||||
const menuAccesses = menuRes?.data?.data || [];
|
||||
setUserLevelMenuAccesses(menuAccesses);
|
||||
setSelectedMenuIds(menuAccesses.filter((a: UserLevelMenuAccess) => a.canAccess).map((a: UserLevelMenuAccess) => a.menuId));
|
||||
|
||||
// Load action accesses for each menu
|
||||
const actionAccesses: Record<number, string[]> = {};
|
||||
for (const menuAccess of menuAccesses.filter((a: UserLevelMenuAccess) => a.canAccess)) {
|
||||
try {
|
||||
const actionRes = await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(userLevelId, menuAccess.menuId);
|
||||
if (!actionRes?.error) {
|
||||
const actions = actionRes?.data?.data || [];
|
||||
actionAccesses[menuAccess.menuId] = actions
|
||||
.filter((a: UserLevelMenuActionAccess) => a.canAccess)
|
||||
.map((a: UserLevelMenuActionAccess) => a.actionCode);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading action accesses for menu ${menuAccess.menuId}:`, error);
|
||||
}
|
||||
}
|
||||
setSelectedActionAccesses(actionAccesses);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading module accesses:", error);
|
||||
console.error("Error loading accesses:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadModuleAccesses();
|
||||
loadAccesses();
|
||||
}, [initialData]);
|
||||
|
||||
const validateForm = (data: UserLevelsCreateRequest): Record<string, string> => {
|
||||
|
|
@ -348,13 +403,22 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
try {
|
||||
if (onSave) {
|
||||
if (currentMode === "single") {
|
||||
// Check if editing or creating
|
||||
const isEditing = !!(initialData as any)?.id;
|
||||
const userLevelId = (initialData as any)?.id;
|
||||
|
||||
// Save user level first
|
||||
const userLevelResponse = await createUserLevel(formData);
|
||||
let userLevelResponse;
|
||||
if (isEditing) {
|
||||
userLevelResponse = await updateUserLevel(userLevelId, formData);
|
||||
} else {
|
||||
userLevelResponse = await createUserLevel(formData);
|
||||
}
|
||||
|
||||
if (userLevelResponse?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: userLevelResponse?.message || "Failed to create user level",
|
||||
text: userLevelResponse?.message || `Failed to ${isEditing ? 'update' : 'create'} user level`,
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
|
|
@ -365,27 +429,59 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
return;
|
||||
}
|
||||
|
||||
// Get the created user level ID
|
||||
const createdUserLevelId = userLevelResponse?.data?.data?.id || (initialData as any)?.id;
|
||||
// Get the user level ID
|
||||
const createdUserLevelId = userLevelResponse?.data?.data?.id || userLevelId;
|
||||
|
||||
if (createdUserLevelId && selectedModuleIds.length > 0) {
|
||||
// Delete existing module accesses if editing
|
||||
// Save menu accesses
|
||||
if (createdUserLevelId && selectedMenuIds.length > 0) {
|
||||
// Delete existing menu accesses if editing
|
||||
if ((initialData as any)?.id) {
|
||||
for (const access of userLevelModuleAccesses) {
|
||||
await deleteUserLevelModuleAccess(access.id);
|
||||
for (const access of userLevelMenuAccesses) {
|
||||
await deleteUserLevelMenuAccess(access.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new module accesses in batch
|
||||
const moduleAccessResponse = await createUserLevelModuleAccessesBatch({
|
||||
// Create new menu accesses in batch
|
||||
const menuAccessResponse = await createUserLevelMenuAccessesBatch({
|
||||
userLevelId: createdUserLevelId,
|
||||
moduleIds: selectedModuleIds,
|
||||
canAccess: true,
|
||||
menuIds: selectedMenuIds,
|
||||
});
|
||||
|
||||
if (moduleAccessResponse?.error) {
|
||||
console.error("Error saving module accesses:", moduleAccessResponse?.message);
|
||||
// Don't fail the whole operation, just log the error
|
||||
if (menuAccessResponse?.error) {
|
||||
console.error("Error saving menu accesses:", menuAccessResponse?.message);
|
||||
} else {
|
||||
// Save action accesses for each menu
|
||||
for (const menuId of selectedMenuIds) {
|
||||
const actionCodes = selectedActionAccesses[menuId] || [];
|
||||
|
||||
// Delete existing action accesses for this menu if editing
|
||||
if ((initialData as any)?.id) {
|
||||
try {
|
||||
const existingActionsRes = await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(createdUserLevelId, menuId);
|
||||
if (!existingActionsRes?.error) {
|
||||
const existingActions = existingActionsRes?.data?.data || [];
|
||||
for (const action of existingActions) {
|
||||
await deleteUserLevelMenuActionAccess(action.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error deleting existing action accesses for menu ${menuId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new action accesses in batch
|
||||
if (actionCodes.length > 0) {
|
||||
const actionAccessResponse = await createUserLevelMenuActionAccessesBatch({
|
||||
userLevelId: createdUserLevelId,
|
||||
menuId: menuId,
|
||||
actionCodes: actionCodes,
|
||||
});
|
||||
|
||||
if (actionAccessResponse?.error) {
|
||||
console.error(`Error saving action accesses for menu ${menuId}:`, actionAccessResponse?.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -430,57 +526,97 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
}, 1000); // Small delay to let user see the success message
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (currentMode === "single") {
|
||||
const response = await createUserLevel(formData);
|
||||
console.log("Create Response: ", response);
|
||||
|
||||
if (response?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: response?.message || "Failed to create user level",
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Get the created user level ID
|
||||
const createdUserLevelId = response?.data?.data?.id || (initialData as any)?.id;
|
||||
} else {
|
||||
if (currentMode === "single") {
|
||||
// Check if editing or creating
|
||||
const isEditing = !!(initialData as any)?.id;
|
||||
const userLevelId = (initialData as any)?.id;
|
||||
|
||||
// Save module accesses if any selected
|
||||
if (createdUserLevelId && selectedModuleIds.length > 0) {
|
||||
// Delete existing module accesses if editing
|
||||
let response;
|
||||
if (isEditing) {
|
||||
response = await updateUserLevel(userLevelId, formData);
|
||||
} else {
|
||||
response = await createUserLevel(formData);
|
||||
}
|
||||
console.log(`${isEditing ? 'Update' : 'Create'} Response: `, response);
|
||||
|
||||
if (response?.error) {
|
||||
Swal.fire({
|
||||
title: "Error",
|
||||
text: response?.message || `Failed to ${isEditing ? 'update' : 'create'} user level`,
|
||||
icon: "error",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Get the user level ID
|
||||
const createdUserLevelId = response?.data?.data?.id || userLevelId;
|
||||
|
||||
// Save menu accesses
|
||||
if (createdUserLevelId && selectedMenuIds.length > 0) {
|
||||
// Delete existing menu accesses if editing
|
||||
if ((initialData as any)?.id) {
|
||||
for (const access of userLevelModuleAccesses) {
|
||||
await deleteUserLevelModuleAccess(access.id);
|
||||
for (const access of userLevelMenuAccesses) {
|
||||
await deleteUserLevelMenuAccess(access.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new module accesses in batch
|
||||
const moduleAccessResponse = await createUserLevelModuleAccessesBatch({
|
||||
// Create new menu accesses in batch
|
||||
const menuAccessResponse = await createUserLevelMenuAccessesBatch({
|
||||
userLevelId: createdUserLevelId,
|
||||
moduleIds: selectedModuleIds,
|
||||
canAccess: true,
|
||||
menuIds: selectedMenuIds,
|
||||
});
|
||||
|
||||
if (moduleAccessResponse?.error) {
|
||||
console.error("Error saving module accesses:", moduleAccessResponse?.message);
|
||||
// Don't fail the whole operation, just log the error
|
||||
if (menuAccessResponse?.error) {
|
||||
console.error("Error saving menu accesses:", menuAccessResponse?.message);
|
||||
} else {
|
||||
// Save action accesses for each menu
|
||||
for (const menuId of selectedMenuIds) {
|
||||
const actionCodes = selectedActionAccesses[menuId] || [];
|
||||
|
||||
// Delete existing action accesses for this menu if editing
|
||||
if ((initialData as any)?.id) {
|
||||
try {
|
||||
const existingActionsRes = await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(createdUserLevelId, menuId);
|
||||
if (!existingActionsRes?.error) {
|
||||
const existingActions = existingActionsRes?.data?.data || [];
|
||||
for (const action of existingActions) {
|
||||
await deleteUserLevelMenuActionAccess(action.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error deleting existing action accesses for menu ${menuId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Create new action accesses in batch
|
||||
if (actionCodes.length > 0) {
|
||||
const actionAccessResponse = await createUserLevelMenuActionAccessesBatch({
|
||||
userLevelId: createdUserLevelId,
|
||||
menuId: menuId,
|
||||
actionCodes: actionCodes,
|
||||
});
|
||||
|
||||
if (actionAccessResponse?.error) {
|
||||
console.error(`Error saving action accesses for menu ${menuId}:`, actionAccessResponse?.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: "Success",
|
||||
text: "User level created successfully",
|
||||
text: isEditing ? "User level updated successfully" : "User level created successfully",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
customClass: {
|
||||
popup: 'swal-z-index-9999'
|
||||
}
|
||||
}).then(() => {
|
||||
// Refresh page after successful creation
|
||||
// Refresh page after successful save
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
|
@ -609,8 +745,8 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="basic" disabled={isLoadingData}>Basic Information</TabsTrigger>
|
||||
<TabsTrigger value="modules" disabled={isLoadingData || mode === "bulk"}>Module Access</TabsTrigger>
|
||||
{/* <TabsTrigger value="hierarchy" disabled={isLoadingData}>Hierarchy</TabsTrigger> */}
|
||||
<TabsTrigger value="menus" disabled={isLoadingData || mode === "bulk"}>Menu Access</TabsTrigger>
|
||||
<TabsTrigger value="actions" disabled={isLoadingData || mode === "bulk"}>Action Access</TabsTrigger>
|
||||
<TabsTrigger value="bulk" disabled={isLoadingData}>Bulk Operations</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
|
|
@ -758,40 +894,51 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
</Card>
|
||||
</TabsContent> */}
|
||||
|
||||
{/* Module Access Tab */}
|
||||
<TabsContent value="modules" className="space-y-6">
|
||||
{/* Menu Access Tab */}
|
||||
<TabsContent value="menus" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Module Access Configuration</CardTitle>
|
||||
<CardTitle>Menu Access Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
Select which modules this user level can access. Only selected modules will be accessible to users with this level.
|
||||
Select which menus this user level can access. Users with this level will only see selected menus in the navigation.
|
||||
</p>
|
||||
{modules.length > 0 ? (
|
||||
{menus.length > 0 ? (
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto border rounded-lg p-4">
|
||||
{modules.map((module) => (
|
||||
{menus.map((menu) => (
|
||||
<label
|
||||
key={module.id}
|
||||
key={menu.id}
|
||||
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-gray-50 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedModuleIds.includes(module.id)}
|
||||
checked={selectedMenuIds.includes(menu.id)}
|
||||
onChange={() => {
|
||||
setSelectedModuleIds((prev) =>
|
||||
prev.includes(module.id)
|
||||
? prev.filter((id) => id !== module.id)
|
||||
: [...prev, module.id]
|
||||
);
|
||||
setSelectedMenuIds((prev) => {
|
||||
const newMenuIds = prev.includes(menu.id)
|
||||
? prev.filter((id) => id !== menu.id)
|
||||
: [...prev, menu.id];
|
||||
|
||||
// If menu is deselected, remove its action accesses
|
||||
if (prev.includes(menu.id) && !newMenuIds.includes(menu.id)) {
|
||||
setSelectedActionAccesses((prevActions) => {
|
||||
const newActions = { ...prevActions };
|
||||
delete newActions[menu.id];
|
||||
return newActions;
|
||||
});
|
||||
}
|
||||
|
||||
return newMenuIds;
|
||||
});
|
||||
}}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium">{module.name}</div>
|
||||
<div className="text-sm text-gray-500">{module.description}</div>
|
||||
<div className="font-medium">{menu.name}</div>
|
||||
<div className="text-sm text-gray-500">{menu.description}</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
{module.pathUrl} • {module.actionType}
|
||||
Group: {menu.group} • Module ID: {menu.moduleId}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
|
@ -799,7 +946,87 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
|||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No modules available
|
||||
No menus available
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Action Access Tab */}
|
||||
<TabsContent value="actions" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Action Access Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
Configure which actions users with this level can perform in each menu. First select menus in the "Menu Access" tab.
|
||||
</p>
|
||||
{selectedMenuIds.length > 0 ? (
|
||||
<div className="space-y-4 max-h-96 overflow-y-auto">
|
||||
{selectedMenuIds.map((menuId) => {
|
||||
const menu = menus.find((m) => m.id === menuId);
|
||||
const actions = menuActionsMap[menuId] || [];
|
||||
const selectedActions = selectedActionAccesses[menuId] || [];
|
||||
|
||||
if (!menu) return null;
|
||||
|
||||
return (
|
||||
<Card key={menuId} className="border-l-4 border-l-blue-500">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">{menu.name}</CardTitle>
|
||||
<p className="text-sm text-gray-500">{menu.description}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{actions.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{actions.map((action) => (
|
||||
<label
|
||||
key={action.id}
|
||||
className="flex items-start gap-3 p-2 border rounded-lg hover:bg-gray-50 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedActions.includes(action.actionCode)}
|
||||
onChange={() => {
|
||||
setSelectedActionAccesses((prev) => {
|
||||
const current = prev[menuId] || [];
|
||||
const newActions = current.includes(action.actionCode)
|
||||
? current.filter((code) => code !== action.actionCode)
|
||||
: [...current, action.actionCode];
|
||||
return {
|
||||
...prev,
|
||||
[menuId]: newActions,
|
||||
};
|
||||
});
|
||||
}}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{action.actionName}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Code: {action.actionCode}
|
||||
{action.pathUrl && ` • Path: ${action.pathUrl}`}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-4 text-gray-500 text-sm">
|
||||
No actions available for this menu
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<p className="mb-2">No menus selected</p>
|
||||
<p className="text-sm">Please select menus in the "Menu Access" tab first</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -3052,35 +3052,4 @@ export const RotateCcwIcon = ({ size = 24, width, height, ...props }: IconSvgPro
|
|||
<path d="M1 4v6h6" />
|
||||
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const EditIcon = ({
|
||||
size = 24,
|
||||
width,
|
||||
height,
|
||||
...props
|
||||
}: IconSvgProps) => (
|
||||
<svg
|
||||
width={size || width}
|
||||
height={size || height}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
|
@ -199,6 +199,16 @@ export default function MediaUpdate() {
|
|||
const typeId = parseInt(getTypeIdByContentType(contentType));
|
||||
setCurrentTypeId(typeId.toString());
|
||||
|
||||
// const response = await listArticles(
|
||||
// 1,
|
||||
// 10,
|
||||
// typeId,
|
||||
// undefined,
|
||||
// undefined,
|
||||
// section === "latest" ? "createdAt" : "viewCount",
|
||||
// slug
|
||||
// );
|
||||
|
||||
const response = await listArticles(
|
||||
1,
|
||||
10,
|
||||
|
|
@ -206,13 +216,13 @@ export default function MediaUpdate() {
|
|||
undefined,
|
||||
undefined,
|
||||
section === "latest" ? "createdAt" : "viewCount",
|
||||
slug
|
||||
slug || undefined, // ⬅️ jangan kirim undefined string
|
||||
);
|
||||
|
||||
let hasil: any[] = [];
|
||||
|
||||
if (response?.error) {
|
||||
console.error("Articles API failed, fallback ke old API");
|
||||
// console.error("Articles API failed, fallback ke old API");
|
||||
const fallbackRes = await listData(
|
||||
typeId.toString(),
|
||||
"",
|
||||
|
|
@ -221,7 +231,7 @@ export default function MediaUpdate() {
|
|||
0,
|
||||
"",
|
||||
"",
|
||||
""
|
||||
"",
|
||||
);
|
||||
hasil = fallbackRes?.data?.data?.content || [];
|
||||
} else {
|
||||
|
|
@ -263,14 +273,14 @@ export default function MediaUpdate() {
|
|||
const ids = new Set<number>(
|
||||
(Array.isArray(bookmarks) ? bookmarks : [])
|
||||
.map((b: any) => Number(b.articleId ?? b.id ?? b.article?.id))
|
||||
.filter((x) => !isNaN(x))
|
||||
.filter((x) => !isNaN(x)),
|
||||
);
|
||||
|
||||
const gabungan = new Set([...localSet, ...ids]);
|
||||
setBookmarkedIds(gabungan);
|
||||
localStorage.setItem(
|
||||
"bookmarkedIds",
|
||||
JSON.stringify(Array.from(gabungan))
|
||||
JSON.stringify(Array.from(gabungan)),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -308,7 +318,7 @@ export default function MediaUpdate() {
|
|||
setBookmarkedIds(updated);
|
||||
localStorage.setItem(
|
||||
"bookmarkedIds",
|
||||
JSON.stringify(Array.from(updated))
|
||||
JSON.stringify(Array.from(updated)),
|
||||
);
|
||||
|
||||
MySwal.fire({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { checkUserLevelMenuAccess } from "@/service/user-level-menu-accesses";
|
||||
import { getCookie } from "cookies-next";
|
||||
|
||||
interface UseMenuAccessResult {
|
||||
hasAccess: boolean;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook untuk mengecek apakah user level memiliki akses ke menu tertentu
|
||||
* @param menuId - ID menu yang ingin dicek
|
||||
* @returns {hasAccess, loading, error}
|
||||
*/
|
||||
export function useMenuAccess(menuId: number | null | undefined): UseMenuAccessResult {
|
||||
const [hasAccess, setHasAccess] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuId) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const checkAccess = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Get user level from cookie or context
|
||||
// Assuming userLevelId is stored in cookie or context
|
||||
const userLevelId = getCookie("userLevelId");
|
||||
if (!userLevelId) {
|
||||
setHasAccess(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await checkUserLevelMenuAccess(Number(userLevelId), menuId);
|
||||
if (response?.data?.data?.has_access) {
|
||||
setHasAccess(true);
|
||||
} else {
|
||||
setHasAccess(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error("Failed to check menu access"));
|
||||
setHasAccess(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAccess();
|
||||
}, [menuId]);
|
||||
|
||||
return { hasAccess, loading, error };
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { checkUserLevelMenuActionAccess } from "@/service/user-level-menu-action-accesses";
|
||||
import { getCookie } from "cookies-next";
|
||||
|
||||
interface UseMenuActionAccessResult {
|
||||
hasAccess: boolean;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook untuk mengecek apakah user level memiliki akses ke action tertentu di dalam menu
|
||||
* @param menuId - ID menu yang ingin dicek
|
||||
* @param actionCode - Kode action yang ingin dicek (view, create, edit, delete, etc)
|
||||
* @returns {hasAccess, loading, error}
|
||||
*/
|
||||
export function useMenuActionAccess(
|
||||
menuId: number | null | undefined,
|
||||
actionCode: string | null | undefined
|
||||
): UseMenuActionAccessResult {
|
||||
const [hasAccess, setHasAccess] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuId || !actionCode) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const checkAccess = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Get user level from cookie or context
|
||||
const userLevelId = getCookie("userLevelId");
|
||||
if (!userLevelId) {
|
||||
setHasAccess(false);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await checkUserLevelMenuActionAccess(Number(userLevelId), menuId, actionCode);
|
||||
if (response?.data?.data?.has_access) {
|
||||
setHasAccess(true);
|
||||
} else {
|
||||
setHasAccess(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error("Failed to check menu action access"));
|
||||
setHasAccess(false);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAccess();
|
||||
}, [menuId, actionCode]);
|
||||
|
||||
return { hasAccess, loading, error };
|
||||
}
|
||||
|
||||
86
lib/menus.ts
86
lib/menus.ts
|
|
@ -221,32 +221,66 @@ export function getMenuList(pathname: string, t: any): Group[] {
|
|||
},
|
||||
]
|
||||
: []),
|
||||
// ...(roleId === 2
|
||||
// ? [
|
||||
// {
|
||||
// groupLabel: "Settings",
|
||||
// id: "settings",
|
||||
// menus: [
|
||||
// {
|
||||
// id: "settings",
|
||||
// href: "/admin/settings",
|
||||
// label: "Settings",
|
||||
// active: pathname.includes("/settings"),
|
||||
// icon: "heroicons:cog-6-tooth",
|
||||
// submenus: [
|
||||
// {
|
||||
// href: "/admin/categories",
|
||||
// label: "Categories",
|
||||
// active: pathname.includes("/categories"),
|
||||
// icon: "ic:outline-image",
|
||||
// children: [],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ]
|
||||
// : []),
|
||||
...(Number(roleId) === 1
|
||||
? [
|
||||
{
|
||||
groupLabel: "",
|
||||
id: "management-user",
|
||||
menus: [
|
||||
{
|
||||
id: "management-user-menu",
|
||||
href: "/admin/management-user",
|
||||
label: "Management User",
|
||||
active: pathname.includes("/management-user"),
|
||||
icon: "clarity:users-solid",
|
||||
submenus: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupLabel: "",
|
||||
id: "tenant",
|
||||
menus: [
|
||||
{
|
||||
id: "tenant",
|
||||
href: "/admin/tenants",
|
||||
label: "Tenant",
|
||||
active: pathname.includes("/tenant"),
|
||||
icon: "material-symbols:domain",
|
||||
submenus: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupLabel: "",
|
||||
id: "menu-management",
|
||||
menus: [
|
||||
{
|
||||
id: "menu-management",
|
||||
href: "/admin/settings/menu-management",
|
||||
label: "Menu Management",
|
||||
active: pathname === "/admin/settings/menu-management",
|
||||
icon: "heroicons:bars-3",
|
||||
submenus: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
// {
|
||||
// groupLabel: "",
|
||||
// id: "module-management",
|
||||
// menus: [
|
||||
// {
|
||||
// id: "module-management",
|
||||
// href: "/admin/settings/module-management",
|
||||
// label: "Module Management",
|
||||
// active: pathname === "/admin/settings/module-management",
|
||||
// icon: "heroicons:puzzle-piece",
|
||||
// submenus: [],
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
]
|
||||
: []),
|
||||
];
|
||||
return menusSelected;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
|
|
@ -3,13 +3,38 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"dev:turbo": "cross-env CSS_TRANSFORMER_WASM=1 next dev --turbopack",
|
||||
"build": "cross-env CSS_TRANSFORMER_WASM=1 next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ckeditor/ckeditor5-alignment": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-autoformat": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-basic-styles": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-block-quote": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-cloud-services": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-code-block": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-core": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-editor-classic": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-essentials": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-font": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-heading": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-image": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-indent": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-link": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-list": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-media-embed": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-paragraph": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-paste-from-office": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-react": "^10.0.0",
|
||||
"@ckeditor/ckeditor5-source-editing": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-table": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-typing": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-undo": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-upload": "^47.4.0",
|
||||
"@ckeditor/ckeditor5-utils": "^47.4.0",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
"@dnd-kit/modifiers": "^7.0.0",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
|
|
@ -74,6 +99,7 @@
|
|||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"cookie": "^1.0.2",
|
||||
"cookies-next": "^6.1.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"dayjs": "^1.11.11",
|
||||
|
|
@ -88,6 +114,7 @@
|
|||
"js-cookie": "^3.0.5",
|
||||
"jspdf": "^3.0.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"lightningcss": "^1.30.2",
|
||||
"lucide-react": "^0.525.0",
|
||||
"moment": "^2.30.1",
|
||||
"next": "^15.3.5",
|
||||
|
|
@ -140,7 +167,6 @@
|
|||
"devDependencies": {
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@next/bundle-analyzer": "^15.0.3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
|
|
@ -158,6 +184,7 @@
|
|||
"@types/react-geocode": "^0.2.4",
|
||||
"@types/rtl-detect": "^1.0.3",
|
||||
"@types/sizzle": "^2.3.10",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"cross-env": "^7.0.3",
|
||||
"d3-shape": "^3.2.0",
|
||||
"eslint": "^8",
|
||||
|
|
@ -165,8 +192,11 @@
|
|||
"jest": "^30.0.4",
|
||||
"jest-environment-jsdom": "^30.0.4",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^4",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-win32-x64-msvc": "^1.30.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
// /** @type {import('postcss-load-config').Config} */
|
||||
// const config = {
|
||||
// plugins: {
|
||||
// '@tailwindcss/postcss': {},
|
||||
// },
|
||||
// };
|
||||
|
||||
// export default config;
|
||||
|
||||
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import axios from "axios";
|
||||
|
||||
// Use environment variable for API URL, default to localhost:8080 for local development
|
||||
const baseURL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080/api/";
|
||||
const baseURL = process.env.NEXT_PUBLIC_API_URL || "https://kontenhumas.com/api/";
|
||||
|
||||
const axiosBaseInstance = axios.create({
|
||||
baseURL,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import Cookies from "js-cookie";
|
|||
import { getCsrfToken, login } from "../auth";
|
||||
|
||||
// Use environment variable for API URL, default to localhost:8080 for local development
|
||||
const baseURL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080/api/";
|
||||
const baseURL = process.env.NEXT_PUBLIC_API_URL || "https://kontenhumas.com/api/";
|
||||
|
||||
const refreshToken = Cookies.get("refresh_token");
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import {
|
||||
httpPostInterceptor,
|
||||
httpGetInterceptor,
|
||||
httpPutInterceptor,
|
||||
httpDeleteInterceptor,
|
||||
} from "./http-config/http-interceptor-service";
|
||||
|
||||
// Types
|
||||
export interface MenuAction {
|
||||
id: number;
|
||||
menuId: number;
|
||||
actionCode: string;
|
||||
actionName: string;
|
||||
description?: string;
|
||||
pathUrl?: string;
|
||||
httpMethod?: string;
|
||||
position?: number;
|
||||
isActive: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface MenuActionCreateRequest {
|
||||
menuId: number;
|
||||
actionCode: string;
|
||||
actionName: string;
|
||||
description?: string;
|
||||
pathUrl?: string;
|
||||
httpMethod?: string;
|
||||
position?: number;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface MenuActionBatchCreateRequest {
|
||||
menuId: number;
|
||||
actionCodes: string[];
|
||||
}
|
||||
|
||||
export interface MenuActionUpdateRequest {
|
||||
menuId: number;
|
||||
actionCode: string;
|
||||
actionName: string;
|
||||
description?: string;
|
||||
pathUrl?: string;
|
||||
httpMethod?: string;
|
||||
position?: number;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
// API Functions
|
||||
export async function getMenuActions(params?: {
|
||||
menuId?: number;
|
||||
actionCode?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.menuId) queryParams.append("menu_id", params.menuId.toString());
|
||||
if (params?.actionCode) queryParams.append("action_code", params.actionCode);
|
||||
if (params?.page) queryParams.append("page", params.page.toString());
|
||||
if (params?.limit) queryParams.append("limit", params.limit.toString());
|
||||
|
||||
const url = `menu-actions${queryParams.toString() ? `?${queryParams.toString()}` : ""}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function getMenuActionById(id: number) {
|
||||
const url = `menu-actions/${id}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function getMenuActionsByMenuId(menuId: number) {
|
||||
const url = `menu-actions/menu/${menuId}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function createMenuAction(data: MenuActionCreateRequest) {
|
||||
const url = "menu-actions";
|
||||
return httpPostInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function createMenuActionsBatch(data: MenuActionBatchCreateRequest) {
|
||||
const url = "menu-actions/batch";
|
||||
return httpPostInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function updateMenuAction(id: number, data: MenuActionUpdateRequest) {
|
||||
const url = `menu-actions/${id}`;
|
||||
return httpPutInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function deleteMenuAction(id: number) {
|
||||
const url = `menu-actions/${id}`;
|
||||
return httpDeleteInterceptor(url);
|
||||
}
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ export interface MasterModule {
|
|||
name: string;
|
||||
description: string;
|
||||
pathUrl: string;
|
||||
actionType: string;
|
||||
actionType?: string;
|
||||
statusId: number;
|
||||
isActive: boolean;
|
||||
createdAt?: string;
|
||||
|
|
@ -147,7 +147,7 @@ export async function getMasterMenuById(id: number) {
|
|||
export async function createMasterMenu(data: {
|
||||
name: string;
|
||||
description: string;
|
||||
moduleId: number;
|
||||
moduleId?: number;
|
||||
group: string;
|
||||
statusId: number;
|
||||
parentMenuId?: number;
|
||||
|
|
@ -160,7 +160,7 @@ export async function createMasterMenu(data: {
|
|||
export async function updateMasterMenu(id: number, data: {
|
||||
name: string;
|
||||
description: string;
|
||||
moduleId: number;
|
||||
moduleId?: number;
|
||||
group: string;
|
||||
statusId: number;
|
||||
parentMenuId?: number;
|
||||
|
|
@ -203,8 +203,9 @@ export async function createMasterModule(data: {
|
|||
name: string;
|
||||
description: string;
|
||||
pathUrl: string;
|
||||
actionType: string;
|
||||
actionType?: string;
|
||||
statusId: number;
|
||||
menuIds?: number[];
|
||||
}) {
|
||||
const url = "master-modules";
|
||||
return httpPostInterceptor(url, data);
|
||||
|
|
@ -214,8 +215,9 @@ export async function updateMasterModule(id: number, data: {
|
|||
name: string;
|
||||
description: string;
|
||||
pathUrl: string;
|
||||
actionType: string;
|
||||
actionType?: string;
|
||||
statusId: number;
|
||||
menuIds?: number[];
|
||||
}) {
|
||||
const url = `master-modules/${id}`;
|
||||
return httpPutInterceptor(url, data);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { httpDeleteInterceptor, httpGetInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-service";
|
||||
import {
|
||||
httpDeleteInterceptor,
|
||||
httpGetInterceptor,
|
||||
httpPutInterceptor,
|
||||
httpPostInterceptor,
|
||||
} from "./http-config/http-interceptor-service";
|
||||
|
||||
export async function deleteUserLevel(id: number) {
|
||||
const url = `user-levels/${id}`;
|
||||
|
|
@ -14,9 +19,106 @@ export async function getUserLevelDetail(id: number) {
|
|||
const url = `user-levels/${id}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
export async function getTenantList() {
|
||||
const url = `clients?limit=1000`;
|
||||
|
||||
// Tenant/Client Types
|
||||
export interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
clientType: "parent_client" | "sub_client" | "standalone";
|
||||
parentClientId?: string;
|
||||
logoUrl?: string;
|
||||
logoImagePath?: string;
|
||||
address?: string;
|
||||
phoneNumber?: string;
|
||||
website?: string;
|
||||
maxUsers?: number;
|
||||
maxStorage?: number;
|
||||
currentUsers?: number;
|
||||
currentStorage?: number;
|
||||
settings?: string;
|
||||
isActive?: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
parentClient?: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
subClients?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
subClientCount?: number;
|
||||
}
|
||||
|
||||
export interface TenantCreateRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
clientType: "parent_client" | "sub_client" | "standalone";
|
||||
parentClientId?: string;
|
||||
maxUsers?: number;
|
||||
maxStorage?: number;
|
||||
settings?: string;
|
||||
address?: string;
|
||||
phoneNumber?: string;
|
||||
website?: string;
|
||||
}
|
||||
|
||||
export interface TenantUpdateRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
clientType?: "parent_client" | "sub_client" | "standalone";
|
||||
parentClientId?: string;
|
||||
maxUsers?: number;
|
||||
maxStorage?: number;
|
||||
settings?: string;
|
||||
isActive?: boolean;
|
||||
logoUrl?: string;
|
||||
logoImagePath?: string;
|
||||
address?: string;
|
||||
phoneNumber?: string;
|
||||
website?: string;
|
||||
}
|
||||
|
||||
// Tenant API Functions
|
||||
export async function getTenantList(params?: {
|
||||
name?: string;
|
||||
clientType?: string;
|
||||
parentClientId?: string;
|
||||
isActive?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.name) queryParams.append("name", params.name);
|
||||
if (params?.clientType) queryParams.append("clientType", params.clientType);
|
||||
if (params?.parentClientId) queryParams.append("parentClientId", params.parentClientId);
|
||||
if (params?.isActive !== undefined) queryParams.append("isActive", params.isActive.toString());
|
||||
if (params?.page) queryParams.append("page", params.page.toString());
|
||||
if (params?.limit) queryParams.append("limit", params.limit.toString());
|
||||
|
||||
const url = `clients${queryParams.toString() ? `?${queryParams.toString()}` : ""}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function getTenantById(id: string) {
|
||||
const url = `clients/${id}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function createTenant(data: TenantCreateRequest) {
|
||||
const url = "clients";
|
||||
return httpPostInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function updateTenant(id: string, data: TenantUpdateRequest) {
|
||||
const url = `clients/${id}`;
|
||||
return httpPutInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function deleteTenant(id: string) {
|
||||
const url = `clients/${id}`;
|
||||
return httpDeleteInterceptor(url);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import {
|
||||
httpPostInterceptor,
|
||||
httpGetInterceptor,
|
||||
httpPutInterceptor,
|
||||
httpDeleteInterceptor,
|
||||
} from "./http-config/http-interceptor-service";
|
||||
|
||||
// Types
|
||||
export interface UserLevelMenuAccess {
|
||||
id: number;
|
||||
userLevelId: number;
|
||||
menuId: number;
|
||||
canAccess: boolean;
|
||||
isActive: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface UserLevelMenuAccessCreateRequest {
|
||||
userLevelId: number;
|
||||
menuId: number;
|
||||
canAccess?: boolean;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface UserLevelMenuAccessBatchCreateRequest {
|
||||
userLevelId: number;
|
||||
menuIds: number[];
|
||||
}
|
||||
|
||||
export interface UserLevelMenuAccessUpdateRequest {
|
||||
userLevelId?: number;
|
||||
menuId?: number;
|
||||
canAccess?: boolean;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
// API Functions
|
||||
export async function getUserLevelMenuAccesses(params?: {
|
||||
userLevelId?: number;
|
||||
menuId?: number;
|
||||
canAccess?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.userLevelId) queryParams.append("user_level_id", params.userLevelId.toString());
|
||||
if (params?.menuId) queryParams.append("menu_id", params.menuId.toString());
|
||||
if (params?.canAccess !== undefined) queryParams.append("can_access", params.canAccess.toString());
|
||||
if (params?.page) queryParams.append("page", params.page.toString());
|
||||
if (params?.limit) queryParams.append("limit", params.limit.toString());
|
||||
|
||||
const url = `user-level-menu-accesses${queryParams.toString() ? `?${queryParams.toString()}` : ""}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function getUserLevelMenuAccessById(id: number) {
|
||||
const url = `user-level-menu-accesses/${id}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function getUserLevelMenuAccessesByUserLevelId(userLevelId: number) {
|
||||
const url = `user-level-menu-accesses/user-level/${userLevelId}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function getUserLevelMenuAccessesByMenuId(menuId: number) {
|
||||
const url = `user-level-menu-accesses/menu/${menuId}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function checkUserLevelMenuAccess(userLevelId: number, menuId: number) {
|
||||
const url = `user-level-menu-accesses/check/${userLevelId}/${menuId}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function createUserLevelMenuAccess(data: UserLevelMenuAccessCreateRequest) {
|
||||
const url = "user-level-menu-accesses";
|
||||
return httpPostInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function createUserLevelMenuAccessesBatch(data: UserLevelMenuAccessBatchCreateRequest) {
|
||||
const url = "user-level-menu-accesses/batch";
|
||||
return httpPostInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function updateUserLevelMenuAccess(id: number, data: UserLevelMenuAccessUpdateRequest) {
|
||||
const url = `user-level-menu-accesses/${id}`;
|
||||
return httpPutInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function deleteUserLevelMenuAccess(id: number) {
|
||||
const url = `user-level-menu-accesses/${id}`;
|
||||
return httpDeleteInterceptor(url);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import {
|
||||
httpPostInterceptor,
|
||||
httpGetInterceptor,
|
||||
httpPutInterceptor,
|
||||
httpDeleteInterceptor,
|
||||
} from "./http-config/http-interceptor-service";
|
||||
|
||||
// Types
|
||||
export interface UserLevelMenuActionAccess {
|
||||
id: number;
|
||||
userLevelId: number;
|
||||
menuId: number;
|
||||
actionCode: string;
|
||||
canAccess: boolean;
|
||||
isActive: boolean;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface UserLevelMenuActionAccessCreateRequest {
|
||||
userLevelId: number;
|
||||
menuId: number;
|
||||
actionCode: string;
|
||||
canAccess?: boolean;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface UserLevelMenuActionAccessBatchCreateRequest {
|
||||
userLevelId: number;
|
||||
menuId: number;
|
||||
actionCodes: string[];
|
||||
}
|
||||
|
||||
export interface UserLevelMenuActionAccessUpdateRequest {
|
||||
userLevelId?: number;
|
||||
menuId?: number;
|
||||
actionCode?: string;
|
||||
canAccess?: boolean;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
// API Functions
|
||||
export async function getUserLevelMenuActionAccesses(params?: {
|
||||
userLevelId?: number;
|
||||
menuId?: number;
|
||||
actionCode?: string;
|
||||
canAccess?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.userLevelId) queryParams.append("user_level_id", params.userLevelId.toString());
|
||||
if (params?.menuId) queryParams.append("menu_id", params.menuId.toString());
|
||||
if (params?.actionCode) queryParams.append("action_code", params.actionCode);
|
||||
if (params?.canAccess !== undefined) queryParams.append("can_access", params.canAccess.toString());
|
||||
if (params?.page) queryParams.append("page", params.page.toString());
|
||||
if (params?.limit) queryParams.append("limit", params.limit.toString());
|
||||
|
||||
const url = `user-level-menu-action-accesses${queryParams.toString() ? `?${queryParams.toString()}` : ""}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function getUserLevelMenuActionAccessById(id: number) {
|
||||
const url = `user-level-menu-action-accesses/${id}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function getUserLevelMenuActionAccessesByUserLevelId(userLevelId: number) {
|
||||
const url = `user-level-menu-action-accesses/user-level/${userLevelId}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(userLevelId: number, menuId: number) {
|
||||
const url = `user-level-menu-action-accesses/user-level/${userLevelId}/menu/${menuId}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function getUserLevelMenuActionAccessesByMenuId(menuId: number) {
|
||||
const url = `user-level-menu-action-accesses/menu/${menuId}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function checkUserLevelMenuActionAccess(userLevelId: number, menuId: number, actionCode: string) {
|
||||
const url = `user-level-menu-action-accesses/check/${userLevelId}/${menuId}/${actionCode}`;
|
||||
return httpGetInterceptor(url);
|
||||
}
|
||||
|
||||
export async function createUserLevelMenuActionAccess(data: UserLevelMenuActionAccessCreateRequest) {
|
||||
const url = "user-level-menu-action-accesses";
|
||||
return httpPostInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function createUserLevelMenuActionAccessesBatch(data: UserLevelMenuActionAccessBatchCreateRequest) {
|
||||
const url = "user-level-menu-action-accesses/batch";
|
||||
return httpPostInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function updateUserLevelMenuActionAccess(id: number, data: UserLevelMenuActionAccessUpdateRequest) {
|
||||
const url = `user-level-menu-action-accesses/${id}`;
|
||||
return httpPutInterceptor(url, data);
|
||||
}
|
||||
|
||||
export async function deleteUserLevelMenuActionAccess(id: number) {
|
||||
const url = `user-level-menu-action-accesses/${id}`;
|
||||
return httpDeleteInterceptor(url);
|
||||
}
|
||||
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
||||
import type Editor from './editor/editor.js';
|
||||
export declare const DEFAULT_GROUP_ID: "common";
|
||||
/**
|
||||
* A common namespace for various accessibility features of the editor.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* @module core/command
|
||||
*/
|
||||
import { type DecoratedMethodEvent } from '@ckeditor/ckeditor5-utils';
|
||||
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
||||
import type Editor from './editor/editor.js';
|
||||
declare const Command_base: {
|
||||
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
||||
prototype: import("@ckeditor/ckeditor5-utils").Observable;
|
||||
|
|
|
|||
2
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-core/src/commandcollection.d.ts
generated
vendored
2
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-core/src/commandcollection.d.ts
generated
vendored
|
|
@ -2,7 +2,7 @@
|
|||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
import type Command from '@ckeditor/ckeditor5-core/src/command.js';
|
||||
import type Command from './command.js';
|
||||
/**
|
||||
* Collection of commands. Its instance is available in {@link module:core/editor/editor~Editor#commands `editor.commands`}.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@
|
|||
* @module core/context
|
||||
*/
|
||||
import { Config, Collection, Locale, type LocaleTranslate } from '@ckeditor/ckeditor5-utils';
|
||||
import PluginCollection from '@ckeditor/ckeditor5-core/src/plugincollection.js';
|
||||
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
||||
import type { LoadedPlugins, PluginConstructor } from '@ckeditor/ckeditor5-core/src/plugin.js';
|
||||
import type { EditorConfig } from '@ckeditor/ckeditor5-core/src/editor/editorconfig.js';
|
||||
import PluginCollection from './plugincollection.js';
|
||||
import type Editor from './editor/editor.js';
|
||||
import type { LoadedPlugins, PluginConstructor } from './plugin.js';
|
||||
import type { EditorConfig } from './editor/editorconfig.js';
|
||||
/**
|
||||
* Provides a common, higher-level environment for solutions that use multiple {@link module:core/editor/editor~Editor editors}
|
||||
* or plugins that work outside the editor. Use it instead of {@link module:core/editor/editor~Editor.create `Editor.create()`}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@
|
|||
* @module core/contextplugin
|
||||
*/
|
||||
import { type Collection, type Config, type Locale, type LocaleTranslate } from '@ckeditor/ckeditor5-utils';
|
||||
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
||||
import type { EditorConfig } from '@ckeditor/ckeditor5-core/src/editor/editorconfig.js';
|
||||
import type Context from '@ckeditor/ckeditor5-core/src/context.js';
|
||||
import type { PluginDependencies, PluginInterface } from '@ckeditor/ckeditor5-core/src/plugin.js';
|
||||
import type PluginCollection from '@ckeditor/ckeditor5-core/src/plugincollection.js';
|
||||
import type Editor from './editor/editor.js';
|
||||
import type { EditorConfig } from './editor/editorconfig.js';
|
||||
import type Context from './context.js';
|
||||
import type { PluginDependencies, PluginInterface } from './plugin.js';
|
||||
import type PluginCollection from './plugincollection.js';
|
||||
declare const ContextPlugin_base: {
|
||||
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
||||
prototype: import("@ckeditor/ckeditor5-utils").Observable;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* @module core/editingkeystrokehandler
|
||||
*/
|
||||
import { KeystrokeHandler, type PriorityString } from '@ckeditor/ckeditor5-utils';
|
||||
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
||||
import type Editor from './editor/editor.js';
|
||||
/**
|
||||
* A keystroke handler for editor editing. Its instance is available
|
||||
* in {@link module:core/editor/editor~Editor#keystrokes} so plugins
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@
|
|||
import { Config, type Locale, type LocaleTranslate } from '@ckeditor/ckeditor5-utils';
|
||||
import { Conversion, DataController, EditingController, Model } from '@ckeditor/ckeditor5-engine';
|
||||
import type { EditorUI } from '@ckeditor/ckeditor5-ui';
|
||||
import Context from '@ckeditor/ckeditor5-core/src/context.js';
|
||||
import PluginCollection from '@ckeditor/ckeditor5-core/src/plugincollection.js';
|
||||
import CommandCollection, { type CommandsMap } from '@ckeditor/ckeditor5-core/src/commandcollection.js';
|
||||
import EditingKeystrokeHandler from '@ckeditor/ckeditor5-core/src/editingkeystrokehandler.js';
|
||||
import Accessibility from '@ckeditor/ckeditor5-core/src/accessibility.js';
|
||||
import type { LoadedPlugins, PluginConstructor } from '@ckeditor/ckeditor5-core/src/plugin.js';
|
||||
import type { EditorConfig } from '@ckeditor/ckeditor5-core/src/editor/editorconfig.js';
|
||||
import Context from '../context.js';
|
||||
import PluginCollection from '../plugincollection.js';
|
||||
import CommandCollection, { type CommandsMap } from '../commandcollection.js';
|
||||
import EditingKeystrokeHandler from '../editingkeystrokehandler.js';
|
||||
import Accessibility from '../accessibility.js';
|
||||
import type { LoadedPlugins, PluginConstructor } from '../plugin.js';
|
||||
import type { EditorConfig } from './editorconfig.js';
|
||||
declare const Editor_base: {
|
||||
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
||||
prototype: import("@ckeditor/ckeditor5-utils").Observable;
|
||||
|
|
|
|||
6
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-core/src/editor/editorconfig.d.ts
generated
vendored
6
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-core/src/editor/editorconfig.d.ts
generated
vendored
|
|
@ -6,9 +6,9 @@
|
|||
* @module core/editor/editorconfig
|
||||
*/
|
||||
import type { ArrayOrItem, Translations } from '@ckeditor/ckeditor5-utils';
|
||||
import type Context from '@ckeditor/ckeditor5-core/src/context.js';
|
||||
import type { PluginConstructor } from '@ckeditor/ckeditor5-core/src/plugin.js';
|
||||
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
||||
import type Context from '../context.js';
|
||||
import type { PluginConstructor } from '../plugin.js';
|
||||
import type Editor from './editor.js';
|
||||
import type { MenuBarConfig } from '@ckeditor/ckeditor5-ui';
|
||||
/**
|
||||
* CKEditor configuration options.
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
import type { default as Editor } from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
||||
import type { ElementApi } from '@ckeditor/ckeditor5-core/src/editor/utils/elementapimixin.js';
|
||||
import type { default as Editor } from '../editor.js';
|
||||
import type { ElementApi } from './elementapimixin.js';
|
||||
/**
|
||||
* Checks if the editor is initialized on a `<textarea>` element that belongs to a form. If yes, it updates the editor's element
|
||||
* content before submitting the form.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
/**
|
||||
* @module core/editor/utils/dataapimixin
|
||||
*/
|
||||
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
||||
import type Editor from '../editor.js';
|
||||
import type { Constructor } from '@ckeditor/ckeditor5-utils';
|
||||
/**
|
||||
* Implementation of the {@link module:core/editor/utils/dataapimixin~DataApi}.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* @module core/editor/utils/elementapimixin
|
||||
*/
|
||||
import { type Constructor, type Mixed } from '@ckeditor/ckeditor5-utils';
|
||||
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
||||
import type Editor from '../editor.js';
|
||||
/**
|
||||
* Implementation of the {@link module:core/editor/utils/elementapimixin~ElementApi}.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
import type { default as Editor } from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
||||
import type { default as Editor } from '../editor.js';
|
||||
/**
|
||||
* Marks the source element on which the editor was initialized. This prevents other editor instances from using this element.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -5,23 +5,23 @@
|
|||
/**
|
||||
* @module core
|
||||
*/
|
||||
export { default as Plugin, type PluginDependencies, type PluginConstructor } from '@ckeditor/ckeditor5-core/src/plugin.js';
|
||||
export { default as Command, type CommandExecuteEvent } from '@ckeditor/ckeditor5-core/src/command.js';
|
||||
export { default as MultiCommand } from '@ckeditor/ckeditor5-core/src/multicommand.js';
|
||||
export type { CommandsMap } from '@ckeditor/ckeditor5-core/src/commandcollection.js';
|
||||
export type { PluginsMap, default as PluginCollection } from '@ckeditor/ckeditor5-core/src/plugincollection.js';
|
||||
export { default as Context, type ContextConfig } from '@ckeditor/ckeditor5-core/src/context.js';
|
||||
export { default as ContextPlugin, type ContextPluginDependencies } from '@ckeditor/ckeditor5-core/src/contextplugin.js';
|
||||
export { type EditingKeystrokeCallback } from '@ckeditor/ckeditor5-core/src/editingkeystrokehandler.js';
|
||||
export type { PartialBy, NonEmptyArray } from '@ckeditor/ckeditor5-core/src/typings.js';
|
||||
export { default as Editor, type EditorReadyEvent, type EditorDestroyEvent } from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
||||
export type { EditorConfig, LanguageConfig, ToolbarConfig, ToolbarConfigItem, UiConfig } from '@ckeditor/ckeditor5-core/src/editor/editorconfig.js';
|
||||
export { default as attachToForm } from '@ckeditor/ckeditor5-core/src/editor/utils/attachtoform.js';
|
||||
export { default as DataApiMixin, type DataApi } from '@ckeditor/ckeditor5-core/src/editor/utils/dataapimixin.js';
|
||||
export { default as ElementApiMixin, type ElementApi } from '@ckeditor/ckeditor5-core/src/editor/utils/elementapimixin.js';
|
||||
export { default as secureSourceElement } from '@ckeditor/ckeditor5-core/src/editor/utils/securesourceelement.js';
|
||||
export { default as PendingActions, type PendingAction } from '@ckeditor/ckeditor5-core/src/pendingactions.js';
|
||||
export type { KeystrokeInfos as KeystrokeInfoDefinitions, KeystrokeInfoGroup as KeystrokeInfoGroupDefinition, KeystrokeInfoCategory as KeystrokeInfoCategoryDefinition, KeystrokeInfoDefinition as KeystrokeInfoDefinition } from '@ckeditor/ckeditor5-core/src/accessibility.js';
|
||||
export { default as Plugin, type PluginDependencies, type PluginConstructor } from './plugin.js';
|
||||
export { default as Command, type CommandExecuteEvent } from './command.js';
|
||||
export { default as MultiCommand } from './multicommand.js';
|
||||
export type { CommandsMap } from './commandcollection.js';
|
||||
export type { PluginsMap, default as PluginCollection } from './plugincollection.js';
|
||||
export { default as Context, type ContextConfig } from './context.js';
|
||||
export { default as ContextPlugin, type ContextPluginDependencies } from './contextplugin.js';
|
||||
export { type EditingKeystrokeCallback } from './editingkeystrokehandler.js';
|
||||
export type { PartialBy, NonEmptyArray } from './typings.js';
|
||||
export { default as Editor, type EditorReadyEvent, type EditorDestroyEvent } from './editor/editor.js';
|
||||
export type { EditorConfig, LanguageConfig, ToolbarConfig, ToolbarConfigItem, UiConfig } from './editor/editorconfig.js';
|
||||
export { default as attachToForm } from './editor/utils/attachtoform.js';
|
||||
export { default as DataApiMixin, type DataApi } from './editor/utils/dataapimixin.js';
|
||||
export { default as ElementApiMixin, type ElementApi } from './editor/utils/elementapimixin.js';
|
||||
export { default as secureSourceElement } from './editor/utils/securesourceelement.js';
|
||||
export { default as PendingActions, type PendingAction } from './pendingactions.js';
|
||||
export type { KeystrokeInfos as KeystrokeInfoDefinitions, KeystrokeInfoGroup as KeystrokeInfoGroupDefinition, KeystrokeInfoCategory as KeystrokeInfoCategoryDefinition, KeystrokeInfoDefinition as KeystrokeInfoDefinition } from './accessibility.js';
|
||||
export declare const icons: {
|
||||
bold: string;
|
||||
cancel: string;
|
||||
|
|
@ -86,4 +86,4 @@ export declare const icons: {
|
|||
outdent: string;
|
||||
table: string;
|
||||
};
|
||||
import '@ckeditor/ckeditor5-core/src/augmentation.js';
|
||||
import './augmentation.js';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
/**
|
||||
* @module core/multicommand
|
||||
*/
|
||||
import Command from '@ckeditor/ckeditor5-core/src/command.js';
|
||||
import Command from './command.js';
|
||||
import { type PriorityString } from '@ckeditor/ckeditor5-utils';
|
||||
/**
|
||||
* A CKEditor command that aggregates other commands.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
/**
|
||||
* @module core/pendingactions
|
||||
*/
|
||||
import ContextPlugin from '@ckeditor/ckeditor5-core/src/contextplugin.js';
|
||||
import ContextPlugin from './contextplugin.js';
|
||||
import { type CollectionAddEvent, type CollectionRemoveEvent, type Observable } from '@ckeditor/ckeditor5-utils';
|
||||
/**
|
||||
* The list of pending editor actions.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
||||
import type Editor from './editor/editor.js';
|
||||
declare const Plugin_base: {
|
||||
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
||||
prototype: import("@ckeditor/ckeditor5-utils").Observable;
|
||||
|
|
|
|||
2
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-core/src/plugincollection.d.ts
generated
vendored
2
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-core/src/plugincollection.d.ts
generated
vendored
|
|
@ -2,7 +2,7 @@
|
|||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
import type { LoadedPlugins, PluginClassConstructor, PluginConstructor, PluginInterface } from '@ckeditor/ckeditor5-core/src/plugin.js';
|
||||
import type { LoadedPlugins, PluginClassConstructor, PluginConstructor, PluginInterface } from './plugin.js';
|
||||
declare const PluginCollection_base: {
|
||||
new (): import("@ckeditor/ckeditor5-utils").Emitter;
|
||||
prototype: import("@ckeditor/ckeditor5-utils").Emitter;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,13 @@
|
|||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
declare const Collection_base: {
|
||||
<<<<<<< HEAD
|
||||
new (): import("@ckeditor/ckeditor5-utils/src/emittermixin.js").Emitter;
|
||||
prototype: import("@ckeditor/ckeditor5-utils/src/emittermixin.js").Emitter;
|
||||
=======
|
||||
new (): import("./emittermixin.js").Emitter;
|
||||
prototype: import("./emittermixin.js").Emitter;
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
};
|
||||
/**
|
||||
* Collections are ordered sets of objects. Items in the collection can be retrieved by their indexes
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@
|
|||
*/
|
||||
declare function diff<T>(a: ArrayLike<T>, b: ArrayLike<T>, cmp?: (a: T, b: T) => boolean): Array<DiffResult>;
|
||||
declare namespace diff {
|
||||
<<<<<<< HEAD
|
||||
var fastDiff: typeof import("@ckeditor/ckeditor5-utils/src/fastdiff.js").default;
|
||||
=======
|
||||
var fastDiff: typeof import("./fastdiff.js").default;
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
}
|
||||
export default diff;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
import type { DiffResult } from '@ckeditor/ckeditor5-utils/src/diff.js';
|
||||
=======
|
||||
import type { DiffResult } from './diff.js';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
/**
|
||||
* @module utils/difftochanges
|
||||
*/
|
||||
|
|
|
|||
6
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-utils/src/dom/emittermixin.d.ts
generated
vendored
6
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-utils/src/dom/emittermixin.d.ts
generated
vendored
|
|
@ -5,9 +5,15 @@
|
|||
/**
|
||||
* @module utils/dom/emittermixin
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
import { type Emitter, type CallbackOptions, type BaseEvent, type GetCallback } from '@ckeditor/ckeditor5-utils/src/emittermixin.js';
|
||||
import type EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo.js';
|
||||
import type { Constructor, Mixed } from '@ckeditor/ckeditor5-utils/src/mix.js';
|
||||
=======
|
||||
import { type Emitter, type CallbackOptions, type BaseEvent, type GetCallback } from '../emittermixin.js';
|
||||
import type EventInfo from '../eventinfo.js';
|
||||
import type { Constructor, Mixed } from '../mix.js';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
/**
|
||||
* Mixin that injects the DOM events API into its host. It provides the API
|
||||
* compatible with {@link module:utils/emittermixin~Emitter}.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
import Rect, { type RectSource } from '@ckeditor/ckeditor5-utils/src/dom/rect.js';
|
||||
=======
|
||||
import Rect, { type RectSource } from './rect.js';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
/**
|
||||
* Calculates the `position: absolute` coordinates of a given element so it can be positioned with respect to the
|
||||
* target in the visually most efficient way, taking various restrictions like viewport or limiter geometry
|
||||
|
|
|
|||
|
|
@ -5,10 +5,17 @@
|
|||
/**
|
||||
* @module utils/emittermixin
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo.js';
|
||||
import { type PriorityString } from '@ckeditor/ckeditor5-utils/src/priorities.js';
|
||||
import type { Constructor, Mixed } from '@ckeditor/ckeditor5-utils/src/mix.js';
|
||||
import '@ckeditor/ckeditor5-utils/src/version.js';
|
||||
=======
|
||||
import EventInfo from './eventinfo.js';
|
||||
import { type PriorityString } from './priorities.js';
|
||||
import type { Constructor, Mixed } from './mix.js';
|
||||
import './version.js';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
/**
|
||||
* Mixin that injects the {@link ~Emitter events API} into its host.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -2,8 +2,13 @@
|
|||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
import type { DiffResult } from '@ckeditor/ckeditor5-utils/src/diff.js';
|
||||
import type { Change } from '@ckeditor/ckeditor5-utils/src/difftochanges.js';
|
||||
=======
|
||||
import type { DiffResult } from './diff.js';
|
||||
import type { Change } from './difftochanges.js';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
/**
|
||||
* @module utils/fastdiff
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,10 +2,17 @@
|
|||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
declare const FocusTracker_base: import("@ckeditor/ckeditor5-utils/src/mix.js").Mixed<{
|
||||
new (): import("@ckeditor/ckeditor5-utils/src/observablemixin.js").Observable;
|
||||
prototype: import("@ckeditor/ckeditor5-utils/src/observablemixin.js").Observable;
|
||||
}, import("@ckeditor/ckeditor5-utils/src/dom/emittermixin.js").DomEmitter>;
|
||||
=======
|
||||
declare const FocusTracker_base: import("./mix.js").Mixed<{
|
||||
new (): import("./observablemixin.js").Observable;
|
||||
prototype: import("./observablemixin.js").Observable;
|
||||
}, import("./dom/emittermixin.js").DomEmitter>;
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
/**
|
||||
* Allows observing a group of `Element`s whether at least one of them is focused.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
/**
|
||||
* @module utils
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
export { default as env } from '@ckeditor/ckeditor5-utils/src/env.js';
|
||||
export { default as diff, type DiffResult } from '@ckeditor/ckeditor5-utils/src/diff.js';
|
||||
export { default as fastDiff } from '@ckeditor/ckeditor5-utils/src/fastdiff.js';
|
||||
|
|
@ -62,3 +63,62 @@ export { default as verifyLicense } from '@ckeditor/ckeditor5-utils/src/verifyli
|
|||
export { default as wait } from '@ckeditor/ckeditor5-utils/src/wait.js';
|
||||
export * from '@ckeditor/ckeditor5-utils/src/unicode.js';
|
||||
export { default as version, releaseDate } from '@ckeditor/ckeditor5-utils/src/version.js';
|
||||
=======
|
||||
export { default as env } from './env.js';
|
||||
export { default as diff, type DiffResult } from './diff.js';
|
||||
export { default as fastDiff } from './fastdiff.js';
|
||||
export { default as diffToChanges } from './difftochanges.js';
|
||||
export { default as mix } from './mix.js';
|
||||
export type { Constructor, Mixed } from './mix.js';
|
||||
export { default as EmitterMixin, type Emitter, type BaseEvent, type CallbackOptions, type EmitterMixinDelegateChain, type GetCallback, type GetCallbackOptions, type GetEventInfo, type GetNameOrEventInfo } from './emittermixin.js';
|
||||
export { default as EventInfo } from './eventinfo.js';
|
||||
export { default as ObservableMixin, type Observable, type DecoratedMethodEvent, type ObservableChangeEvent, type ObservableSetEvent } from './observablemixin.js';
|
||||
export { default as CKEditorError, logError, logWarning } from './ckeditorerror.js';
|
||||
export { default as ElementReplacer } from './elementreplacer.js';
|
||||
export { default as abortableDebounce, type AbortableFunc } from './abortabledebounce.js';
|
||||
export { default as count } from './count.js';
|
||||
export { default as compareArrays } from './comparearrays.js';
|
||||
export { default as createElement } from './dom/createelement.js';
|
||||
export { default as Config } from './config.js';
|
||||
export { default as isIterable } from './isiterable.js';
|
||||
export { default as DomEmitterMixin, type DomEmitter } from './dom/emittermixin.js';
|
||||
export { default as findClosestScrollableAncestor } from './dom/findclosestscrollableancestor.js';
|
||||
export { default as global } from './dom/global.js';
|
||||
export { default as getAncestors } from './dom/getancestors.js';
|
||||
export { default as getDataFromElement } from './dom/getdatafromelement.js';
|
||||
export { default as getBorderWidths } from './dom/getborderwidths.js';
|
||||
export { default as isText } from './dom/istext.js';
|
||||
export { default as Rect, type RectSource } from './dom/rect.js';
|
||||
export { default as ResizeObserver } from './dom/resizeobserver.js';
|
||||
export { default as setDataInElement } from './dom/setdatainelement.js';
|
||||
export { default as toUnit } from './dom/tounit.js';
|
||||
export { default as indexOf } from './dom/indexof.js';
|
||||
export { default as insertAt } from './dom/insertat.js';
|
||||
export { default as isComment } from './dom/iscomment.js';
|
||||
export { default as isNode } from './dom/isnode.js';
|
||||
export { default as isRange } from './dom/isrange.js';
|
||||
export { default as isValidAttributeName } from './dom/isvalidattributename.js';
|
||||
export { default as isVisible } from './dom/isvisible.js';
|
||||
export { getOptimalPosition, type Options as PositionOptions, type PositioningFunction, type DomPoint } from './dom/position.js';
|
||||
export { default as remove } from './dom/remove.js';
|
||||
export * from './dom/scroll.js';
|
||||
export * from './keyboard.js';
|
||||
export * from './language.js';
|
||||
export { default as Locale, type LocaleTranslate, type Translations } from './locale.js';
|
||||
export { default as Collection, type CollectionAddEvent, type CollectionChangeEvent, type CollectionRemoveEvent } from './collection.js';
|
||||
export { default as first } from './first.js';
|
||||
export { default as FocusTracker } from './focustracker.js';
|
||||
export { default as KeystrokeHandler } from './keystrokehandler.js';
|
||||
export { default as toArray, type ArrayOrItem, type ReadonlyArrayOrItem } from './toarray.js';
|
||||
export { default as toMap } from './tomap.js';
|
||||
export { default as priorities, type PriorityString } from './priorities.js';
|
||||
export { default as retry, exponentialDelay } from './retry.js';
|
||||
export { default as insertToPriorityArray } from './inserttopriorityarray.js';
|
||||
export { default as spliceArray } from './splicearray.js';
|
||||
export { default as uid } from './uid.js';
|
||||
export { default as delay, type DelayedFunc } from './delay.js';
|
||||
export { default as verifyLicense } from './verifylicense.js';
|
||||
export { default as wait } from './wait.js';
|
||||
export * from './unicode.js';
|
||||
export { default as version, releaseDate } from './version.js';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
import { type PriorityString } from '@ckeditor/ckeditor5-utils/src/priorities.js';
|
||||
=======
|
||||
import { type PriorityString } from './priorities.js';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
/**
|
||||
* @module utils/inserttopriorityarray
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@
|
|||
*
|
||||
* @module utils/keyboard
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
import type { LanguageDirection } from '@ckeditor/ckeditor5-utils/src/language.js';
|
||||
=======
|
||||
import type { LanguageDirection } from './language.js';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
/**
|
||||
* An object with `keyName => keyCode` pairs for a set of known keys.
|
||||
*
|
||||
|
|
|
|||
6
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-utils/src/keystrokehandler.d.ts
generated
vendored
6
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-utils/src/keystrokehandler.d.ts
generated
vendored
|
|
@ -2,9 +2,15 @@
|
|||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
import type { Emitter } from '@ckeditor/ckeditor5-utils/src/emittermixin.js';
|
||||
import { type KeystrokeInfo } from '@ckeditor/ckeditor5-utils/src/keyboard.js';
|
||||
import type { PriorityString } from '@ckeditor/ckeditor5-utils/src/priorities.js';
|
||||
=======
|
||||
import type { Emitter } from './emittermixin.js';
|
||||
import { type KeystrokeInfo } from './keyboard.js';
|
||||
import type { PriorityString } from './priorities.js';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
/**
|
||||
* Keystroke handler allows registering callbacks for given keystrokes.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -5,9 +5,15 @@
|
|||
/**
|
||||
* @module utils/locale
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
import { type ArrayOrItem } from '@ckeditor/ckeditor5-utils/src/toarray.js';
|
||||
import { type Message } from '@ckeditor/ckeditor5-utils/src/translation-service.js';
|
||||
import { type LanguageDirection } from '@ckeditor/ckeditor5-utils/src/language.js';
|
||||
=======
|
||||
import { type ArrayOrItem } from './toarray.js';
|
||||
import { type Message } from './translation-service.js';
|
||||
import { type LanguageDirection } from './language.js';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
/**
|
||||
* Represents the localization services.
|
||||
*/
|
||||
|
|
|
|||
5
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-utils/src/observablemixin.d.ts
generated
vendored
5
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-utils/src/observablemixin.d.ts
generated
vendored
|
|
@ -5,8 +5,13 @@
|
|||
/**
|
||||
* @module utils/observablemixin
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
import { type Emitter } from '@ckeditor/ckeditor5-utils/src/emittermixin.js';
|
||||
import type { Constructor, Mixed } from '@ckeditor/ckeditor5-utils/src/mix.js';
|
||||
=======
|
||||
import { type Emitter } from './emittermixin.js';
|
||||
import type { Constructor, Mixed } from './mix.js';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
/**
|
||||
* A mixin that injects the "observable properties" and data binding functionality described in the
|
||||
* {@link ~Observable} interface.
|
||||
|
|
|
|||
5
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-utils/src/translation-service.d.ts
generated
vendored
5
vendor/ckeditor5/node_modules/@ckeditor/ckeditor5-utils/src/translation-service.d.ts
generated
vendored
|
|
@ -5,8 +5,13 @@
|
|||
/**
|
||||
* @module utils/translation-service
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
import type { Translations } from '@ckeditor/ckeditor5-utils/src/locale.js';
|
||||
import { type ArrayOrItem } from '@ckeditor/ckeditor5-utils/src/toarray.js';
|
||||
=======
|
||||
import type { Translations } from './locale.js';
|
||||
import { type ArrayOrItem } from './toarray.js';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
declare global {
|
||||
var CKEDITOR_TRANSLATIONS: Translations;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,17 @@
|
|||
/**
|
||||
* @module widget/widgetresize
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
import Resizer from '@ckeditor/ckeditor5-widget/src/widgetresize/resizer.js';
|
||||
import { Plugin, type Editor } from '@ckeditor/ckeditor5-core';
|
||||
import { type Element, type ViewContainerElement } from '@ckeditor/ckeditor5-engine';
|
||||
import '@ckeditor/ckeditor5-widget/theme/widgetresize.css';
|
||||
=======
|
||||
import Resizer from './widgetresize/resizer.js';
|
||||
import { Plugin, type Editor } from '@ckeditor/ckeditor5-core';
|
||||
import { type Element, type ViewContainerElement } from '@ckeditor/ckeditor5-engine';
|
||||
import '../theme/widgetresize.css';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
/**
|
||||
* The widget resize feature plugin.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -3,8 +3,13 @@
|
|||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
import { Rect, type DecoratedMethodEvent } from '@ckeditor/ckeditor5-utils';
|
||||
<<<<<<< HEAD
|
||||
import ResizeState from '@ckeditor/ckeditor5-widget/src/widgetresize/resizerstate.js';
|
||||
import type { ResizerOptions } from '@ckeditor/ckeditor5-widget/src/widgetresize.js';
|
||||
=======
|
||||
import ResizeState from './resizerstate.js';
|
||||
import type { ResizerOptions } from '../widgetresize.js';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
declare const Resizer_base: {
|
||||
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
||||
prototype: import("@ckeditor/ckeditor5-utils").Observable;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@
|
|||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
<<<<<<< HEAD
|
||||
import type { ResizerOptions } from '@ckeditor/ckeditor5-widget/src/widgetresize.js';
|
||||
=======
|
||||
import type { ResizerOptions } from '../widgetresize.js';
|
||||
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||
declare const ResizeState_base: {
|
||||
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
||||
prototype: import("@ckeditor/ckeditor5-utils").Observable;
|
||||
|
|
|
|||
Loading…
Reference in New Issue