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
|
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
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
.env.local
|
||||||
|
|
|
||||||
20
Dockerfile
20
Dockerfile
|
|
@ -4,6 +4,16 @@ FROM node:23.5.0-alpine
|
||||||
# Mengatur port
|
# Mengatur port
|
||||||
ENV PORT 3000
|
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
|
# Install pnpm secara global
|
||||||
RUN npm install -g pnpm
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
|
|
@ -16,17 +26,19 @@ COPY package.json ./
|
||||||
# Menyalin direktori ckeditor5 jika diperlukan
|
# Menyalin direktori ckeditor5 jika diperlukan
|
||||||
COPY vendor/ckeditor5 ./vendor/ckeditor5
|
COPY vendor/ckeditor5 ./vendor/ckeditor5
|
||||||
|
|
||||||
# Menyalin env
|
|
||||||
COPY .env .env
|
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN pnpm install
|
RUN pnpm install
|
||||||
# RUN pnpm install --frozen-lockfile
|
# 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 . .
|
COPY . .
|
||||||
|
|
||||||
# Build aplikasi
|
# 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
|
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm next build
|
||||||
|
|
||||||
# Expose port untuk server
|
# 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 React, { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { 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 { PlusIcon, ModuleIcon, EditIcon, DeleteIcon } from "@/components/icons";
|
||||||
import {
|
import {
|
||||||
MasterModule,
|
MasterModule,
|
||||||
|
|
@ -50,11 +56,11 @@ export default function ModulesSettingsPage() {
|
||||||
if (module) {
|
if (module) {
|
||||||
setEditingModule(module);
|
setEditingModule(module);
|
||||||
setFormData({
|
setFormData({
|
||||||
name: module.name,
|
name: module.name ?? "",
|
||||||
description: module.description,
|
description: module.description ?? "",
|
||||||
pathUrl: module.pathUrl,
|
pathUrl: module.pathUrl ?? "",
|
||||||
actionType: module.actionType,
|
actionType: module.actionType ?? "",
|
||||||
statusId: module.statusId,
|
statusId: module.statusId ?? 1,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setEditingModule(null);
|
setEditingModule(null);
|
||||||
|
|
@ -80,8 +86,8 @@ export default function ModulesSettingsPage() {
|
||||||
icon: "error",
|
icon: "error",
|
||||||
confirmButtonText: "OK",
|
confirmButtonText: "OK",
|
||||||
customClass: {
|
customClass: {
|
||||||
popup: 'swal-z-index-9999'
|
popup: "swal-z-index-9999",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
|
|
@ -90,8 +96,8 @@ export default function ModulesSettingsPage() {
|
||||||
icon: "success",
|
icon: "success",
|
||||||
confirmButtonText: "OK",
|
confirmButtonText: "OK",
|
||||||
customClass: {
|
customClass: {
|
||||||
popup: 'swal-z-index-9999'
|
popup: "swal-z-index-9999",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
await loadData();
|
await loadData();
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
|
|
@ -105,8 +111,8 @@ export default function ModulesSettingsPage() {
|
||||||
icon: "error",
|
icon: "error",
|
||||||
confirmButtonText: "OK",
|
confirmButtonText: "OK",
|
||||||
customClass: {
|
customClass: {
|
||||||
popup: 'swal-z-index-9999'
|
popup: "swal-z-index-9999",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
|
|
@ -115,8 +121,8 @@ export default function ModulesSettingsPage() {
|
||||||
icon: "success",
|
icon: "success",
|
||||||
confirmButtonText: "OK",
|
confirmButtonText: "OK",
|
||||||
customClass: {
|
customClass: {
|
||||||
popup: 'swal-z-index-9999'
|
popup: "swal-z-index-9999",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
await loadData();
|
await loadData();
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
|
|
@ -130,8 +136,8 @@ export default function ModulesSettingsPage() {
|
||||||
icon: "error",
|
icon: "error",
|
||||||
confirmButtonText: "OK",
|
confirmButtonText: "OK",
|
||||||
customClass: {
|
customClass: {
|
||||||
popup: 'swal-z-index-9999'
|
popup: "swal-z-index-9999",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -145,8 +151,8 @@ export default function ModulesSettingsPage() {
|
||||||
confirmButtonText: "Yes, delete it",
|
confirmButtonText: "Yes, delete it",
|
||||||
cancelButtonText: "Cancel",
|
cancelButtonText: "Cancel",
|
||||||
customClass: {
|
customClass: {
|
||||||
popup: 'swal-z-index-9999'
|
popup: "swal-z-index-9999",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.isConfirmed) {
|
if (result.isConfirmed) {
|
||||||
|
|
@ -159,8 +165,8 @@ export default function ModulesSettingsPage() {
|
||||||
icon: "error",
|
icon: "error",
|
||||||
confirmButtonText: "OK",
|
confirmButtonText: "OK",
|
||||||
customClass: {
|
customClass: {
|
||||||
popup: 'swal-z-index-9999'
|
popup: "swal-z-index-9999",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
|
|
@ -169,8 +175,8 @@ export default function ModulesSettingsPage() {
|
||||||
icon: "success",
|
icon: "success",
|
||||||
confirmButtonText: "OK",
|
confirmButtonText: "OK",
|
||||||
customClass: {
|
customClass: {
|
||||||
popup: 'swal-z-index-9999'
|
popup: "swal-z-index-9999",
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
await loadData();
|
await loadData();
|
||||||
}
|
}
|
||||||
|
|
@ -182,8 +188,8 @@ export default function ModulesSettingsPage() {
|
||||||
icon: "error",
|
icon: "error",
|
||||||
confirmButtonText: "OK",
|
confirmButtonText: "OK",
|
||||||
customClass: {
|
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="container mx-auto p-6 space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<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">
|
<p className="text-gray-600 mt-2">
|
||||||
Manage system modules and their configurations
|
Manage system modules and their configurations
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<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" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
Create Module
|
Create Module
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -210,7 +221,9 @@ export default function ModulesSettingsPage() {
|
||||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{editingModule ? `Edit Module: ${editingModule.name}` : "Create New Module"}
|
{editingModule
|
||||||
|
? `Edit Module: ${editingModule.name}`
|
||||||
|
: "Create New Module"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
@ -220,7 +233,9 @@ export default function ModulesSettingsPage() {
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g., View Articles, Create Content"
|
placeholder="e.g., View Articles, Create Content"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(value) => setFormData({ ...formData, name: value })}
|
onChange={(value) =>
|
||||||
|
setFormData({ ...formData, name: value })
|
||||||
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
|
|
@ -229,7 +244,9 @@ export default function ModulesSettingsPage() {
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Brief description of the module"
|
placeholder="Brief description of the module"
|
||||||
value={formData.description}
|
value={formData.description}
|
||||||
onChange={(value) => setFormData({ ...formData, description: value })}
|
onChange={(value) =>
|
||||||
|
setFormData({ ...formData, description: value })
|
||||||
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
|
|
@ -238,7 +255,9 @@ export default function ModulesSettingsPage() {
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="e.g., /api/articles, /api/content"
|
placeholder="e.g., /api/articles, /api/content"
|
||||||
value={formData.pathUrl}
|
value={formData.pathUrl}
|
||||||
onChange={(value) => setFormData({ ...formData, pathUrl: value })}
|
onChange={(value) =>
|
||||||
|
setFormData({ ...formData, pathUrl: value })
|
||||||
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
|
|
@ -247,7 +266,9 @@ export default function ModulesSettingsPage() {
|
||||||
type="select"
|
type="select"
|
||||||
placeholder="Select action type"
|
placeholder="Select action type"
|
||||||
value={formData.actionType}
|
value={formData.actionType}
|
||||||
onChange={(value) => setFormData({ ...formData, actionType: value })}
|
onChange={(value) =>
|
||||||
|
setFormData({ ...formData, actionType: value })
|
||||||
|
}
|
||||||
options={[
|
options={[
|
||||||
{ value: "view", label: "View" },
|
{ value: "view", label: "View" },
|
||||||
{ value: "create", label: "Create" },
|
{ value: "create", label: "Create" },
|
||||||
|
|
@ -264,7 +285,9 @@ export default function ModulesSettingsPage() {
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="1"
|
placeholder="1"
|
||||||
value={formData.statusId}
|
value={formData.statusId}
|
||||||
onChange={(value) => setFormData({ ...formData, statusId: Number(value) || 1 })}
|
onChange={(value) =>
|
||||||
|
setFormData({ ...formData, statusId: Number(value) || 1 })
|
||||||
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
<div className="flex items-center justify-end gap-2 pt-4 border-t">
|
||||||
|
|
@ -291,7 +314,10 @@ export default function ModulesSettingsPage() {
|
||||||
) : modules.length > 0 ? (
|
) : modules.length > 0 ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{modules.map((module) => (
|
{modules.map((module) => (
|
||||||
<Card key={module.id} className="hover:shadow-lg transition-shadow">
|
<Card
|
||||||
|
key={module.id}
|
||||||
|
className="hover:shadow-lg transition-shadow"
|
||||||
|
>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center justify-between">
|
<CardTitle className="flex items-center justify-between">
|
||||||
<span className="truncate">{module.name}</span>
|
<span className="truncate">{module.name}</span>
|
||||||
|
|
@ -308,7 +334,9 @@ export default function ModulesSettingsPage() {
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-2 mb-4">
|
<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">
|
<div className="text-xs text-gray-500 font-mono bg-gray-50 p-2 rounded">
|
||||||
{module.pathUrl}
|
{module.pathUrl}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -347,7 +375,9 @@ export default function ModulesSettingsPage() {
|
||||||
<CardContent className="flex items-center justify-center py-12">
|
<CardContent className="flex items-center justify-center py-12">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<ModuleIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
<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">
|
<p className="text-gray-500 mb-4">
|
||||||
Create your first module to define system capabilities
|
Create your first module to define system capabilities
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -363,4 +393,3 @@ export default function ModulesSettingsPage() {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,25 @@ import {
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { getUserLevelDetail } from "@/service/tenant";
|
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 useTableColumns = (onEdit?: (data: any) => void) => {
|
||||||
const MySwal = withReactContent(Swal);
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
@ -192,11 +211,68 @@ const useTableColumns = (onEdit?: (data: any) => void) => {
|
||||||
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
||||||
const [detailData, setDetailData] = React.useState<any>(null);
|
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) => {
|
const handleView = async (id: number) => {
|
||||||
|
setIsLoadingDetail(true);
|
||||||
try {
|
try {
|
||||||
|
// Load basic user level data
|
||||||
const res = await getUserLevelDetail(id);
|
const res = await getUserLevelDetail(id);
|
||||||
if (!res?.error) {
|
if (!res?.error) {
|
||||||
setDetailData(res?.data?.data);
|
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);
|
setIsDialogOpen(true);
|
||||||
} else {
|
} else {
|
||||||
error(res?.message || "Gagal memuat detail user level");
|
error(res?.message || "Gagal memuat detail user level");
|
||||||
|
|
@ -204,6 +280,8 @@ const useTableColumns = (onEdit?: (data: any) => void) => {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("View error:", err);
|
console.error("View error:", err);
|
||||||
error("Terjadi kesalahan saat memuat data.");
|
error("Terjadi kesalahan saat memuat data.");
|
||||||
|
} finally {
|
||||||
|
setIsLoadingDetail(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -318,35 +396,200 @@ const useTableColumns = (onEdit?: (data: any) => void) => {
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
{/* ✅ Dialog Detail User Level */}
|
{/* ✅ Dialog Detail User Level */}
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogContent className="max-w-lg">
|
<DialogContent size="md" className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Detail User Level</DialogTitle>
|
<DialogTitle>Detail User Level</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Informasi lengkap dari user level ID: {detailData?.id}
|
Informasi lengkap dari user level: {detailData?.name || detailData?.aliasName}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{detailData ? (
|
{isLoadingDetail ? (
|
||||||
<div className="space-y-3 mt-4">
|
<div className="flex items-center justify-center py-8">
|
||||||
<p>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
<span className="font-medium">Name:</span>{" "}
|
<span className="ml-2 text-gray-600">Memuat data...</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>
|
|
||||||
</div>
|
</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>
|
<p className="text-gray-500 mt-4">Memuat data...</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ import {
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import TablePagination from "@/components/table/table-pagination";
|
import TablePagination from "@/components/table/table-pagination";
|
||||||
import useTableColumns from "./columns";
|
import useTableColumns from "./columns";
|
||||||
import TenantUpdateForm from "@/components/form/tenant/tenant-update-form";
|
|
||||||
import { errorAutoClose, successAutoClose } from "@/lib/swal";
|
import { errorAutoClose, successAutoClose } from "@/lib/swal";
|
||||||
import { close, loading } from "@/config/swal";
|
import { close, loading } from "@/config/swal";
|
||||||
import DetailTenant from "@/components/form/tenant/tenant-detail-update-form";
|
import DetailTenant from "@/components/form/tenant/tenant-detail-update-form";
|
||||||
|
|
@ -879,19 +878,21 @@ function TenantSettingsContentTable() {
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{editingUserLevel ? (
|
{editingUserLevel ? (
|
||||||
<TenantUpdateForm
|
<UserLevelsForm
|
||||||
id={editingUserLevel.id}
|
mode="single"
|
||||||
initialData={{
|
initialData={{
|
||||||
|
// id: editingUserLevel.id,
|
||||||
name: editingUserLevel.name,
|
name: editingUserLevel.name,
|
||||||
aliasName: editingUserLevel.aliasName,
|
aliasName: editingUserLevel.aliasName,
|
||||||
levelNumber: editingUserLevel.levelNumber,
|
levelNumber: editingUserLevel.levelNumber,
|
||||||
parentLevelId: editingUserLevel.parentLevelId || 0,
|
parentLevelId: editingUserLevel.parentLevelId || 0,
|
||||||
provinceId: editingUserLevel.provinceId || 0,
|
provinceId: editingUserLevel.provinceId,
|
||||||
group: editingUserLevel.group || "",
|
group: editingUserLevel.group || "",
|
||||||
isApprovalActive: editingUserLevel.isApprovalActive,
|
isApprovalActive: editingUserLevel.isApprovalActive,
|
||||||
isActive: editingUserLevel.isActive,
|
isActive: editingUserLevel.isActive,
|
||||||
}}
|
}}
|
||||||
onSuccess={async () => {
|
onSave={async (data) => {
|
||||||
|
// The form handles the update internally
|
||||||
setIsEditDialogOpen(false);
|
setIsEditDialogOpen(false);
|
||||||
setEditingUserLevel(null);
|
setEditingUserLevel(null);
|
||||||
await loadData();
|
await loadData();
|
||||||
|
|
|
||||||
|
|
@ -475,6 +475,7 @@ function TenantSettingsContent() {
|
||||||
<UserLevelsForm
|
<UserLevelsForm
|
||||||
mode="single"
|
mode="single"
|
||||||
initialData={editingUserLevel ? {
|
initialData={editingUserLevel ? {
|
||||||
|
// id: editingUserLevel.id,
|
||||||
name: editingUserLevel.name,
|
name: editingUserLevel.name,
|
||||||
aliasName: editingUserLevel.aliasName,
|
aliasName: editingUserLevel.aliasName,
|
||||||
levelNumber: editingUserLevel.levelNumber,
|
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";
|
@import "tw-animate-css";
|
||||||
|
|
||||||
/* SweetAlert2 z-index fix */
|
|
||||||
.swal-z-index-9999 {
|
.swal-z-index-9999 {
|
||||||
z-index: 9999 !important;
|
z-index: 9999 !important;
|
||||||
}
|
}
|
||||||
|
|
@ -297,4 +432,4 @@
|
||||||
.no-scrollbar::-webkit-scrollbar-thumb {
|
.no-scrollbar::-webkit-scrollbar-thumb {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
|
|
@ -19,7 +19,7 @@ interface FormFieldProps {
|
||||||
showPasswordToggle?: boolean;
|
showPasswordToggle?: boolean;
|
||||||
onPasswordToggle?: () => void;
|
onPasswordToggle?: () => void;
|
||||||
showPassword?: boolean;
|
showPassword?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FormField: React.FC<FormFieldProps> = ({
|
export const FormField: React.FC<FormFieldProps> = ({
|
||||||
label,
|
label,
|
||||||
|
|
|
||||||
|
|
@ -12,19 +12,30 @@ import {
|
||||||
UserLevel,
|
UserLevel,
|
||||||
Province,
|
Province,
|
||||||
createUserLevel,
|
createUserLevel,
|
||||||
|
updateUserLevel,
|
||||||
getUserLevels,
|
getUserLevels,
|
||||||
getProvinces,
|
getProvinces,
|
||||||
} from "@/service/approval-workflows";
|
} from "@/service/approval-workflows";
|
||||||
import {
|
import {
|
||||||
MasterModule,
|
MasterMenu,
|
||||||
getMasterModules,
|
getMasterMenus,
|
||||||
} from "@/service/menu-modules";
|
} from "@/service/menu-modules";
|
||||||
import {
|
import {
|
||||||
getUserLevelModuleAccessesByUserLevelId,
|
getUserLevelMenuAccessesByUserLevelId,
|
||||||
createUserLevelModuleAccessesBatch,
|
createUserLevelMenuAccessesBatch,
|
||||||
deleteUserLevelModuleAccess,
|
deleteUserLevelMenuAccess,
|
||||||
UserLevelModuleAccess,
|
UserLevelMenuAccess,
|
||||||
} from "@/service/user-level-module-accesses";
|
} 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";
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
interface UserLevelsFormProps {
|
interface UserLevelsFormProps {
|
||||||
|
|
@ -57,9 +68,11 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
||||||
const [bulkFormData, setBulkFormData] = useState<UserLevelsCreateRequest[]>([]);
|
const [bulkFormData, setBulkFormData] = useState<UserLevelsCreateRequest[]>([]);
|
||||||
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
|
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
|
||||||
const [provinces, setProvinces] = useState<Province[]>([]);
|
const [provinces, setProvinces] = useState<Province[]>([]);
|
||||||
const [modules, setModules] = useState<MasterModule[]>([]);
|
const [menus, setMenus] = useState<MasterMenu[]>([]);
|
||||||
const [selectedModuleIds, setSelectedModuleIds] = useState<number[]>([]);
|
const [selectedMenuIds, setSelectedMenuIds] = useState<number[]>([]);
|
||||||
const [userLevelModuleAccesses, setUserLevelModuleAccesses] = useState<UserLevelModuleAccess[]>([]);
|
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 [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [expandedHierarchy, setExpandedHierarchy] = useState(false);
|
const [expandedHierarchy, setExpandedHierarchy] = useState(false);
|
||||||
|
|
@ -75,15 +88,38 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const [userLevelsRes, provincesRes, modulesRes] = await Promise.all([
|
const [userLevelsRes, provincesRes, menusRes] = await Promise.all([
|
||||||
getUserLevels(),
|
getUserLevels(),
|
||||||
getProvinces(),
|
getProvinces(),
|
||||||
getMasterModules({ limit: 100 }),
|
getMasterMenus({ limit: 100 }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!userLevelsRes?.error) setUserLevels(userLevelsRes?.data?.data || []);
|
if (!userLevelsRes?.error) setUserLevels(userLevelsRes?.data?.data || []);
|
||||||
if (!provincesRes?.error) setProvinces(provincesRes?.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) {
|
} catch (error) {
|
||||||
console.error("Error loading form data:", error);
|
console.error("Error loading form data:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -95,22 +131,41 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadModuleAccesses = async () => {
|
const loadAccesses = async () => {
|
||||||
if (initialData && (initialData as any).id) {
|
if (initialData && (initialData as any).id) {
|
||||||
|
const userLevelId = (initialData as any).id;
|
||||||
try {
|
try {
|
||||||
const res = await getUserLevelModuleAccessesByUserLevelId((initialData as any).id);
|
// Load menu accesses
|
||||||
if (!res?.error) {
|
const menuRes = await getUserLevelMenuAccessesByUserLevelId(userLevelId);
|
||||||
const accesses = res?.data?.data || [];
|
if (!menuRes?.error) {
|
||||||
setUserLevelModuleAccesses(accesses);
|
const menuAccesses = menuRes?.data?.data || [];
|
||||||
setSelectedModuleIds(accesses.filter((a: UserLevelModuleAccess) => a.canAccess).map((a: UserLevelModuleAccess) => a.moduleId));
|
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) {
|
} catch (error) {
|
||||||
console.error("Error loading module accesses:", error);
|
console.error("Error loading accesses:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadModuleAccesses();
|
loadAccesses();
|
||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
|
|
||||||
const validateForm = (data: UserLevelsCreateRequest): Record<string, string> => {
|
const validateForm = (data: UserLevelsCreateRequest): Record<string, string> => {
|
||||||
|
|
@ -348,13 +403,22 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
||||||
try {
|
try {
|
||||||
if (onSave) {
|
if (onSave) {
|
||||||
if (currentMode === "single") {
|
if (currentMode === "single") {
|
||||||
|
// Check if editing or creating
|
||||||
|
const isEditing = !!(initialData as any)?.id;
|
||||||
|
const userLevelId = (initialData as any)?.id;
|
||||||
|
|
||||||
// Save user level first
|
// 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) {
|
if (userLevelResponse?.error) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text: userLevelResponse?.message || "Failed to create user level",
|
text: userLevelResponse?.message || `Failed to ${isEditing ? 'update' : 'create'} user level`,
|
||||||
icon: "error",
|
icon: "error",
|
||||||
confirmButtonText: "OK",
|
confirmButtonText: "OK",
|
||||||
customClass: {
|
customClass: {
|
||||||
|
|
@ -365,27 +429,59 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the created user level ID
|
// Get the user level ID
|
||||||
const createdUserLevelId = userLevelResponse?.data?.data?.id || (initialData as any)?.id;
|
const createdUserLevelId = userLevelResponse?.data?.data?.id || userLevelId;
|
||||||
|
|
||||||
if (createdUserLevelId && selectedModuleIds.length > 0) {
|
// Save menu accesses
|
||||||
// Delete existing module accesses if editing
|
if (createdUserLevelId && selectedMenuIds.length > 0) {
|
||||||
|
// Delete existing menu accesses if editing
|
||||||
if ((initialData as any)?.id) {
|
if ((initialData as any)?.id) {
|
||||||
for (const access of userLevelModuleAccesses) {
|
for (const access of userLevelMenuAccesses) {
|
||||||
await deleteUserLevelModuleAccess(access.id);
|
await deleteUserLevelMenuAccess(access.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new module accesses in batch
|
// Create new menu accesses in batch
|
||||||
const moduleAccessResponse = await createUserLevelModuleAccessesBatch({
|
const menuAccessResponse = await createUserLevelMenuAccessesBatch({
|
||||||
userLevelId: createdUserLevelId,
|
userLevelId: createdUserLevelId,
|
||||||
moduleIds: selectedModuleIds,
|
menuIds: selectedMenuIds,
|
||||||
canAccess: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (moduleAccessResponse?.error) {
|
if (menuAccessResponse?.error) {
|
||||||
console.error("Error saving module accesses:", moduleAccessResponse?.message);
|
console.error("Error saving menu accesses:", menuAccessResponse?.message);
|
||||||
// Don't fail the whole operation, just log the error
|
} 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
|
}, 1000); // Small delay to let user see the success message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (currentMode === "single") {
|
if (currentMode === "single") {
|
||||||
const response = await createUserLevel(formData);
|
// Check if editing or creating
|
||||||
console.log("Create Response: ", response);
|
const isEditing = !!(initialData as any)?.id;
|
||||||
|
const userLevelId = (initialData as any)?.id;
|
||||||
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;
|
|
||||||
|
|
||||||
// Save module accesses if any selected
|
let response;
|
||||||
if (createdUserLevelId && selectedModuleIds.length > 0) {
|
if (isEditing) {
|
||||||
// Delete existing module accesses if editing
|
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) {
|
if ((initialData as any)?.id) {
|
||||||
for (const access of userLevelModuleAccesses) {
|
for (const access of userLevelMenuAccesses) {
|
||||||
await deleteUserLevelModuleAccess(access.id);
|
await deleteUserLevelMenuAccess(access.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new module accesses in batch
|
// Create new menu accesses in batch
|
||||||
const moduleAccessResponse = await createUserLevelModuleAccessesBatch({
|
const menuAccessResponse = await createUserLevelMenuAccessesBatch({
|
||||||
userLevelId: createdUserLevelId,
|
userLevelId: createdUserLevelId,
|
||||||
moduleIds: selectedModuleIds,
|
menuIds: selectedMenuIds,
|
||||||
canAccess: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (moduleAccessResponse?.error) {
|
if (menuAccessResponse?.error) {
|
||||||
console.error("Error saving module accesses:", moduleAccessResponse?.message);
|
console.error("Error saving menu accesses:", menuAccessResponse?.message);
|
||||||
// Don't fail the whole operation, just log the error
|
} 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({
|
Swal.fire({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
text: "User level created successfully",
|
text: isEditing ? "User level updated successfully" : "User level created successfully",
|
||||||
icon: "success",
|
icon: "success",
|
||||||
confirmButtonText: "OK",
|
confirmButtonText: "OK",
|
||||||
customClass: {
|
customClass: {
|
||||||
popup: 'swal-z-index-9999'
|
popup: 'swal-z-index-9999'
|
||||||
}
|
}
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// Refresh page after successful creation
|
// Refresh page after successful save
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -609,8 +745,8 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
||||||
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
|
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-4">
|
<TabsList className="grid w-full grid-cols-4">
|
||||||
<TabsTrigger value="basic" disabled={isLoadingData}>Basic Information</TabsTrigger>
|
<TabsTrigger value="basic" disabled={isLoadingData}>Basic Information</TabsTrigger>
|
||||||
<TabsTrigger value="modules" disabled={isLoadingData || mode === "bulk"}>Module Access</TabsTrigger>
|
<TabsTrigger value="menus" disabled={isLoadingData || mode === "bulk"}>Menu Access</TabsTrigger>
|
||||||
{/* <TabsTrigger value="hierarchy" disabled={isLoadingData}>Hierarchy</TabsTrigger> */}
|
<TabsTrigger value="actions" disabled={isLoadingData || mode === "bulk"}>Action Access</TabsTrigger>
|
||||||
<TabsTrigger value="bulk" disabled={isLoadingData}>Bulk Operations</TabsTrigger>
|
<TabsTrigger value="bulk" disabled={isLoadingData}>Bulk Operations</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
|
|
@ -758,40 +894,51 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent> */}
|
</TabsContent> */}
|
||||||
|
|
||||||
{/* Module Access Tab */}
|
{/* Menu Access Tab */}
|
||||||
<TabsContent value="modules" className="space-y-6">
|
<TabsContent value="menus" className="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Module Access Configuration</CardTitle>
|
<CardTitle>Menu Access Configuration</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<p className="text-sm text-gray-600">
|
<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>
|
</p>
|
||||||
{modules.length > 0 ? (
|
{menus.length > 0 ? (
|
||||||
<div className="space-y-2 max-h-96 overflow-y-auto border rounded-lg p-4">
|
<div className="space-y-2 max-h-96 overflow-y-auto border rounded-lg p-4">
|
||||||
{modules.map((module) => (
|
{menus.map((menu) => (
|
||||||
<label
|
<label
|
||||||
key={module.id}
|
key={menu.id}
|
||||||
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-gray-50 cursor-pointer"
|
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-gray-50 cursor-pointer"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedModuleIds.includes(module.id)}
|
checked={selectedMenuIds.includes(menu.id)}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setSelectedModuleIds((prev) =>
|
setSelectedMenuIds((prev) => {
|
||||||
prev.includes(module.id)
|
const newMenuIds = prev.includes(menu.id)
|
||||||
? prev.filter((id) => id !== module.id)
|
? prev.filter((id) => id !== menu.id)
|
||||||
: [...prev, module.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"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="font-medium">{module.name}</div>
|
<div className="font-medium">{menu.name}</div>
|
||||||
<div className="text-sm text-gray-500">{module.description}</div>
|
<div className="text-sm text-gray-500">{menu.description}</div>
|
||||||
<div className="text-xs text-gray-400 mt-1">
|
<div className="text-xs text-gray-400 mt-1">
|
||||||
{module.pathUrl} • {module.actionType}
|
Group: {menu.group} • Module ID: {menu.moduleId}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -799,7 +946,87 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-8 text-gray-500">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
|
||||||
|
|
@ -3052,35 +3052,4 @@ export const RotateCcwIcon = ({ size = 24, width, height, ...props }: IconSvgPro
|
||||||
<path d="M1 4v6h6" />
|
<path d="M1 4v6h6" />
|
||||||
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
||||||
</svg>
|
</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));
|
const typeId = parseInt(getTypeIdByContentType(contentType));
|
||||||
setCurrentTypeId(typeId.toString());
|
setCurrentTypeId(typeId.toString());
|
||||||
|
|
||||||
|
// const response = await listArticles(
|
||||||
|
// 1,
|
||||||
|
// 10,
|
||||||
|
// typeId,
|
||||||
|
// undefined,
|
||||||
|
// undefined,
|
||||||
|
// section === "latest" ? "createdAt" : "viewCount",
|
||||||
|
// slug
|
||||||
|
// );
|
||||||
|
|
||||||
const response = await listArticles(
|
const response = await listArticles(
|
||||||
1,
|
1,
|
||||||
10,
|
10,
|
||||||
|
|
@ -206,13 +216,13 @@ export default function MediaUpdate() {
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
section === "latest" ? "createdAt" : "viewCount",
|
section === "latest" ? "createdAt" : "viewCount",
|
||||||
slug
|
slug || undefined, // ⬅️ jangan kirim undefined string
|
||||||
);
|
);
|
||||||
|
|
||||||
let hasil: any[] = [];
|
let hasil: any[] = [];
|
||||||
|
|
||||||
if (response?.error) {
|
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(
|
const fallbackRes = await listData(
|
||||||
typeId.toString(),
|
typeId.toString(),
|
||||||
"",
|
"",
|
||||||
|
|
@ -221,7 +231,7 @@ export default function MediaUpdate() {
|
||||||
0,
|
0,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
""
|
"",
|
||||||
);
|
);
|
||||||
hasil = fallbackRes?.data?.data?.content || [];
|
hasil = fallbackRes?.data?.data?.content || [];
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -263,14 +273,14 @@ export default function MediaUpdate() {
|
||||||
const ids = new Set<number>(
|
const ids = new Set<number>(
|
||||||
(Array.isArray(bookmarks) ? bookmarks : [])
|
(Array.isArray(bookmarks) ? bookmarks : [])
|
||||||
.map((b: any) => Number(b.articleId ?? b.id ?? b.article?.id))
|
.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]);
|
const gabungan = new Set([...localSet, ...ids]);
|
||||||
setBookmarkedIds(gabungan);
|
setBookmarkedIds(gabungan);
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"bookmarkedIds",
|
"bookmarkedIds",
|
||||||
JSON.stringify(Array.from(gabungan))
|
JSON.stringify(Array.from(gabungan)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -308,7 +318,7 @@ export default function MediaUpdate() {
|
||||||
setBookmarkedIds(updated);
|
setBookmarkedIds(updated);
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"bookmarkedIds",
|
"bookmarkedIds",
|
||||||
JSON.stringify(Array.from(updated))
|
JSON.stringify(Array.from(updated)),
|
||||||
);
|
);
|
||||||
|
|
||||||
MySwal.fire({
|
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
|
...(Number(roleId) === 1
|
||||||
// ? [
|
? [
|
||||||
// {
|
{
|
||||||
// groupLabel: "Settings",
|
groupLabel: "",
|
||||||
// id: "settings",
|
id: "management-user",
|
||||||
// menus: [
|
menus: [
|
||||||
// {
|
{
|
||||||
// id: "settings",
|
id: "management-user-menu",
|
||||||
// href: "/admin/settings",
|
href: "/admin/management-user",
|
||||||
// label: "Settings",
|
label: "Management User",
|
||||||
// active: pathname.includes("/settings"),
|
active: pathname.includes("/management-user"),
|
||||||
// icon: "heroicons:cog-6-tooth",
|
icon: "clarity:users-solid",
|
||||||
// submenus: [
|
submenus: [],
|
||||||
// {
|
},
|
||||||
// href: "/admin/categories",
|
],
|
||||||
// label: "Categories",
|
},
|
||||||
// active: pathname.includes("/categories"),
|
{
|
||||||
// icon: "ic:outline-image",
|
groupLabel: "",
|
||||||
// children: [],
|
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;
|
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",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"dev:turbo": "cross-env CSS_TRANSFORMER_WASM=1 next dev --turbopack",
|
||||||
|
"build": "cross-env CSS_TRANSFORMER_WASM=1 next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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-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/core": "^6.1.0",
|
||||||
"@dnd-kit/modifiers": "^7.0.0",
|
"@dnd-kit/modifiers": "^7.0.0",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
|
|
@ -74,6 +99,7 @@
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
|
"cookies-next": "^6.1.1",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
|
|
@ -88,6 +114,7 @@
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"jspdf": "^3.0.1",
|
"jspdf": "^3.0.1",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"lightningcss": "^1.30.2",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"next": "^15.3.5",
|
"next": "^15.3.5",
|
||||||
|
|
@ -140,7 +167,6 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@next/bundle-analyzer": "^15.0.3",
|
"@next/bundle-analyzer": "^15.0.3",
|
||||||
"@tailwindcss/postcss": "^4",
|
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
|
@ -158,6 +184,7 @@
|
||||||
"@types/react-geocode": "^0.2.4",
|
"@types/react-geocode": "^0.2.4",
|
||||||
"@types/rtl-detect": "^1.0.3",
|
"@types/rtl-detect": "^1.0.3",
|
||||||
"@types/sizzle": "^2.3.10",
|
"@types/sizzle": "^2.3.10",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"d3-shape": "^3.2.0",
|
"d3-shape": "^3.2.0",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
|
|
@ -165,8 +192,11 @@
|
||||||
"jest": "^30.0.4",
|
"jest": "^30.0.4",
|
||||||
"jest-environment-jsdom": "^30.0.4",
|
"jest-environment-jsdom": "^30.0.4",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^3.4.14",
|
||||||
"tw-animate-css": "^1.3.5",
|
"tw-animate-css": "^1.3.5",
|
||||||
"typescript": "^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} */
|
/** @type {import('postcss-load-config').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
'@tailwindcss/postcss': {},
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
// Use environment variable for API URL, default to localhost:8080 for local development
|
// 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({
|
const axiosBaseInstance = axios.create({
|
||||||
baseURL,
|
baseURL,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import Cookies from "js-cookie";
|
||||||
import { getCsrfToken, login } from "../auth";
|
import { getCsrfToken, login } from "../auth";
|
||||||
|
|
||||||
// Use environment variable for API URL, default to localhost:8080 for local development
|
// 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");
|
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;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
pathUrl: string;
|
pathUrl: string;
|
||||||
actionType: string;
|
actionType?: string;
|
||||||
statusId: number;
|
statusId: number;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdAt?: string;
|
createdAt?: string;
|
||||||
|
|
@ -147,7 +147,7 @@ export async function getMasterMenuById(id: number) {
|
||||||
export async function createMasterMenu(data: {
|
export async function createMasterMenu(data: {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
moduleId: number;
|
moduleId?: number;
|
||||||
group: string;
|
group: string;
|
||||||
statusId: number;
|
statusId: number;
|
||||||
parentMenuId?: number;
|
parentMenuId?: number;
|
||||||
|
|
@ -160,7 +160,7 @@ export async function createMasterMenu(data: {
|
||||||
export async function updateMasterMenu(id: number, data: {
|
export async function updateMasterMenu(id: number, data: {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
moduleId: number;
|
moduleId?: number;
|
||||||
group: string;
|
group: string;
|
||||||
statusId: number;
|
statusId: number;
|
||||||
parentMenuId?: number;
|
parentMenuId?: number;
|
||||||
|
|
@ -203,8 +203,9 @@ export async function createMasterModule(data: {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
pathUrl: string;
|
pathUrl: string;
|
||||||
actionType: string;
|
actionType?: string;
|
||||||
statusId: number;
|
statusId: number;
|
||||||
|
menuIds?: number[];
|
||||||
}) {
|
}) {
|
||||||
const url = "master-modules";
|
const url = "master-modules";
|
||||||
return httpPostInterceptor(url, data);
|
return httpPostInterceptor(url, data);
|
||||||
|
|
@ -214,8 +215,9 @@ export async function updateMasterModule(id: number, data: {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
pathUrl: string;
|
pathUrl: string;
|
||||||
actionType: string;
|
actionType?: string;
|
||||||
statusId: number;
|
statusId: number;
|
||||||
|
menuIds?: number[];
|
||||||
}) {
|
}) {
|
||||||
const url = `master-modules/${id}`;
|
const url = `master-modules/${id}`;
|
||||||
return httpPutInterceptor(url, data);
|
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) {
|
export async function deleteUserLevel(id: number) {
|
||||||
const url = `user-levels/${id}`;
|
const url = `user-levels/${id}`;
|
||||||
|
|
@ -14,9 +19,106 @@ export async function getUserLevelDetail(id: number) {
|
||||||
const url = `user-levels/${id}`;
|
const url = `user-levels/${id}`;
|
||||||
return httpGetInterceptor(url);
|
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);
|
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": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
* @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
|
* 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";
|
export declare const DEFAULT_GROUP_ID: "common";
|
||||||
/**
|
/**
|
||||||
* A common namespace for various accessibility features of the editor.
|
* A common namespace for various accessibility features of the editor.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* @module core/command
|
* @module core/command
|
||||||
*/
|
*/
|
||||||
import { type DecoratedMethodEvent } from '@ckeditor/ckeditor5-utils';
|
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: {
|
declare const Command_base: {
|
||||||
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
||||||
prototype: 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.
|
* @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
|
* 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`}.
|
* Collection of commands. Its instance is available in {@link module:core/editor/editor~Editor#commands `editor.commands`}.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@
|
||||||
* @module core/context
|
* @module core/context
|
||||||
*/
|
*/
|
||||||
import { Config, Collection, Locale, type LocaleTranslate } from '@ckeditor/ckeditor5-utils';
|
import { Config, Collection, Locale, type LocaleTranslate } from '@ckeditor/ckeditor5-utils';
|
||||||
import PluginCollection from '@ckeditor/ckeditor5-core/src/plugincollection.js';
|
import PluginCollection from './plugincollection.js';
|
||||||
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
import type Editor from './editor/editor.js';
|
||||||
import type { LoadedPlugins, PluginConstructor } from '@ckeditor/ckeditor5-core/src/plugin.js';
|
import type { LoadedPlugins, PluginConstructor } from './plugin.js';
|
||||||
import type { EditorConfig } from '@ckeditor/ckeditor5-core/src/editor/editorconfig.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}
|
* 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()`}
|
* 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
|
* @module core/contextplugin
|
||||||
*/
|
*/
|
||||||
import { type Collection, type Config, type Locale, type LocaleTranslate } from '@ckeditor/ckeditor5-utils';
|
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 Editor from './editor/editor.js';
|
||||||
import type { EditorConfig } from '@ckeditor/ckeditor5-core/src/editor/editorconfig.js';
|
import type { EditorConfig } from './editor/editorconfig.js';
|
||||||
import type Context from '@ckeditor/ckeditor5-core/src/context.js';
|
import type Context from './context.js';
|
||||||
import type { PluginDependencies, PluginInterface } from '@ckeditor/ckeditor5-core/src/plugin.js';
|
import type { PluginDependencies, PluginInterface } from './plugin.js';
|
||||||
import type PluginCollection from '@ckeditor/ckeditor5-core/src/plugincollection.js';
|
import type PluginCollection from './plugincollection.js';
|
||||||
declare const ContextPlugin_base: {
|
declare const ContextPlugin_base: {
|
||||||
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
||||||
prototype: import("@ckeditor/ckeditor5-utils").Observable;
|
prototype: import("@ckeditor/ckeditor5-utils").Observable;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* @module core/editingkeystrokehandler
|
* @module core/editingkeystrokehandler
|
||||||
*/
|
*/
|
||||||
import { KeystrokeHandler, type PriorityString } from '@ckeditor/ckeditor5-utils';
|
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
|
* A keystroke handler for editor editing. Its instance is available
|
||||||
* in {@link module:core/editor/editor~Editor#keystrokes} so plugins
|
* 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 { Config, type Locale, type LocaleTranslate } from '@ckeditor/ckeditor5-utils';
|
||||||
import { Conversion, DataController, EditingController, Model } from '@ckeditor/ckeditor5-engine';
|
import { Conversion, DataController, EditingController, Model } from '@ckeditor/ckeditor5-engine';
|
||||||
import type { EditorUI } from '@ckeditor/ckeditor5-ui';
|
import type { EditorUI } from '@ckeditor/ckeditor5-ui';
|
||||||
import Context from '@ckeditor/ckeditor5-core/src/context.js';
|
import Context from '../context.js';
|
||||||
import PluginCollection from '@ckeditor/ckeditor5-core/src/plugincollection.js';
|
import PluginCollection from '../plugincollection.js';
|
||||||
import CommandCollection, { type CommandsMap } from '@ckeditor/ckeditor5-core/src/commandcollection.js';
|
import CommandCollection, { type CommandsMap } from '../commandcollection.js';
|
||||||
import EditingKeystrokeHandler from '@ckeditor/ckeditor5-core/src/editingkeystrokehandler.js';
|
import EditingKeystrokeHandler from '../editingkeystrokehandler.js';
|
||||||
import Accessibility from '@ckeditor/ckeditor5-core/src/accessibility.js';
|
import Accessibility from '../accessibility.js';
|
||||||
import type { LoadedPlugins, PluginConstructor } from '@ckeditor/ckeditor5-core/src/plugin.js';
|
import type { LoadedPlugins, PluginConstructor } from '../plugin.js';
|
||||||
import type { EditorConfig } from '@ckeditor/ckeditor5-core/src/editor/editorconfig.js';
|
import type { EditorConfig } from './editorconfig.js';
|
||||||
declare const Editor_base: {
|
declare const Editor_base: {
|
||||||
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
||||||
prototype: 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
|
* @module core/editor/editorconfig
|
||||||
*/
|
*/
|
||||||
import type { ArrayOrItem, Translations } from '@ckeditor/ckeditor5-utils';
|
import type { ArrayOrItem, Translations } from '@ckeditor/ckeditor5-utils';
|
||||||
import type Context from '@ckeditor/ckeditor5-core/src/context.js';
|
import type Context from '../context.js';
|
||||||
import type { PluginConstructor } from '@ckeditor/ckeditor5-core/src/plugin.js';
|
import type { PluginConstructor } from '../plugin.js';
|
||||||
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
import type Editor from './editor.js';
|
||||||
import type { MenuBarConfig } from '@ckeditor/ckeditor5-ui';
|
import type { MenuBarConfig } from '@ckeditor/ckeditor5-ui';
|
||||||
/**
|
/**
|
||||||
* CKEditor configuration options.
|
* CKEditor configuration options.
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
* @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
|
* 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';
|
||||||
import type { ElementApi } from '@ckeditor/ckeditor5-core/src/editor/utils/elementapimixin.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
|
* 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.
|
* content before submitting the form.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
/**
|
/**
|
||||||
* @module core/editor/utils/dataapimixin
|
* @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';
|
import type { Constructor } from '@ckeditor/ckeditor5-utils';
|
||||||
/**
|
/**
|
||||||
* Implementation of the {@link module:core/editor/utils/dataapimixin~DataApi}.
|
* Implementation of the {@link module:core/editor/utils/dataapimixin~DataApi}.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* @module core/editor/utils/elementapimixin
|
* @module core/editor/utils/elementapimixin
|
||||||
*/
|
*/
|
||||||
import { type Constructor, type Mixed } from '@ckeditor/ckeditor5-utils';
|
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}.
|
* 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.
|
* @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
|
* 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.
|
* 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
|
* @module core
|
||||||
*/
|
*/
|
||||||
export { default as Plugin, type PluginDependencies, type PluginConstructor } from '@ckeditor/ckeditor5-core/src/plugin.js';
|
export { default as Plugin, type PluginDependencies, type PluginConstructor } from './plugin.js';
|
||||||
export { default as Command, type CommandExecuteEvent } from '@ckeditor/ckeditor5-core/src/command.js';
|
export { default as Command, type CommandExecuteEvent } from './command.js';
|
||||||
export { default as MultiCommand } from '@ckeditor/ckeditor5-core/src/multicommand.js';
|
export { default as MultiCommand } from './multicommand.js';
|
||||||
export type { CommandsMap } from '@ckeditor/ckeditor5-core/src/commandcollection.js';
|
export type { CommandsMap } from './commandcollection.js';
|
||||||
export type { PluginsMap, default as PluginCollection } from '@ckeditor/ckeditor5-core/src/plugincollection.js';
|
export type { PluginsMap, default as PluginCollection } from './plugincollection.js';
|
||||||
export { default as Context, type ContextConfig } from '@ckeditor/ckeditor5-core/src/context.js';
|
export { default as Context, type ContextConfig } from './context.js';
|
||||||
export { default as ContextPlugin, type ContextPluginDependencies } from '@ckeditor/ckeditor5-core/src/contextplugin.js';
|
export { default as ContextPlugin, type ContextPluginDependencies } from './contextplugin.js';
|
||||||
export { type EditingKeystrokeCallback } from '@ckeditor/ckeditor5-core/src/editingkeystrokehandler.js';
|
export { type EditingKeystrokeCallback } from './editingkeystrokehandler.js';
|
||||||
export type { PartialBy, NonEmptyArray } from '@ckeditor/ckeditor5-core/src/typings.js';
|
export type { PartialBy, NonEmptyArray } from './typings.js';
|
||||||
export { default as Editor, type EditorReadyEvent, type EditorDestroyEvent } from '@ckeditor/ckeditor5-core/src/editor/editor.js';
|
export { default as Editor, type EditorReadyEvent, type EditorDestroyEvent } from './editor/editor.js';
|
||||||
export type { EditorConfig, LanguageConfig, ToolbarConfig, ToolbarConfigItem, UiConfig } from '@ckeditor/ckeditor5-core/src/editor/editorconfig.js';
|
export type { EditorConfig, LanguageConfig, ToolbarConfig, ToolbarConfigItem, UiConfig } from './editor/editorconfig.js';
|
||||||
export { default as attachToForm } from '@ckeditor/ckeditor5-core/src/editor/utils/attachtoform.js';
|
export { default as attachToForm } from './editor/utils/attachtoform.js';
|
||||||
export { default as DataApiMixin, type DataApi } from '@ckeditor/ckeditor5-core/src/editor/utils/dataapimixin.js';
|
export { default as DataApiMixin, type DataApi } from './editor/utils/dataapimixin.js';
|
||||||
export { default as ElementApiMixin, type ElementApi } from '@ckeditor/ckeditor5-core/src/editor/utils/elementapimixin.js';
|
export { default as ElementApiMixin, type ElementApi } from './editor/utils/elementapimixin.js';
|
||||||
export { default as secureSourceElement } from '@ckeditor/ckeditor5-core/src/editor/utils/securesourceelement.js';
|
export { default as secureSourceElement } from './editor/utils/securesourceelement.js';
|
||||||
export { default as PendingActions, type PendingAction } from '@ckeditor/ckeditor5-core/src/pendingactions.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 '@ckeditor/ckeditor5-core/src/accessibility.js';
|
export type { KeystrokeInfos as KeystrokeInfoDefinitions, KeystrokeInfoGroup as KeystrokeInfoGroupDefinition, KeystrokeInfoCategory as KeystrokeInfoCategoryDefinition, KeystrokeInfoDefinition as KeystrokeInfoDefinition } from './accessibility.js';
|
||||||
export declare const icons: {
|
export declare const icons: {
|
||||||
bold: string;
|
bold: string;
|
||||||
cancel: string;
|
cancel: string;
|
||||||
|
|
@ -86,4 +86,4 @@ export declare const icons: {
|
||||||
outdent: string;
|
outdent: string;
|
||||||
table: string;
|
table: string;
|
||||||
};
|
};
|
||||||
import '@ckeditor/ckeditor5-core/src/augmentation.js';
|
import './augmentation.js';
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
/**
|
/**
|
||||||
* @module core/multicommand
|
* @module core/multicommand
|
||||||
*/
|
*/
|
||||||
import Command from '@ckeditor/ckeditor5-core/src/command.js';
|
import Command from './command.js';
|
||||||
import { type PriorityString } from '@ckeditor/ckeditor5-utils';
|
import { type PriorityString } from '@ckeditor/ckeditor5-utils';
|
||||||
/**
|
/**
|
||||||
* A CKEditor command that aggregates other commands.
|
* A CKEditor command that aggregates other commands.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
/**
|
/**
|
||||||
* @module core/pendingactions
|
* @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';
|
import { type CollectionAddEvent, type CollectionRemoveEvent, type Observable } from '@ckeditor/ckeditor5-utils';
|
||||||
/**
|
/**
|
||||||
* The list of pending editor actions.
|
* The list of pending editor actions.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
* @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
|
* 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: {
|
declare const Plugin_base: {
|
||||||
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
||||||
prototype: 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.
|
* @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
|
* 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: {
|
declare const PluginCollection_base: {
|
||||||
new (): import("@ckeditor/ckeditor5-utils").Emitter;
|
new (): import("@ckeditor/ckeditor5-utils").Emitter;
|
||||||
prototype: 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
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||||
*/
|
*/
|
||||||
declare const Collection_base: {
|
declare const Collection_base: {
|
||||||
|
<<<<<<< HEAD
|
||||||
new (): import("@ckeditor/ckeditor5-utils/src/emittermixin.js").Emitter;
|
new (): import("@ckeditor/ckeditor5-utils/src/emittermixin.js").Emitter;
|
||||||
prototype: 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
|
* 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 function diff<T>(a: ArrayLike<T>, b: ArrayLike<T>, cmp?: (a: T, b: T) => boolean): Array<DiffResult>;
|
||||||
declare namespace diff {
|
declare namespace diff {
|
||||||
|
<<<<<<< HEAD
|
||||||
var fastDiff: typeof import("@ckeditor/ckeditor5-utils/src/fastdiff.js").default;
|
var fastDiff: typeof import("@ckeditor/ckeditor5-utils/src/fastdiff.js").default;
|
||||||
|
=======
|
||||||
|
var fastDiff: typeof import("./fastdiff.js").default;
|
||||||
|
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||||
}
|
}
|
||||||
export default diff;
|
export default diff;
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@
|
||||||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
* @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
|
* 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 '@ckeditor/ckeditor5-utils/src/diff.js';
|
||||||
|
=======
|
||||||
|
import type { DiffResult } from './diff.js';
|
||||||
|
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||||
/**
|
/**
|
||||||
* @module utils/difftochanges
|
* @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
|
* @module utils/dom/emittermixin
|
||||||
*/
|
*/
|
||||||
|
<<<<<<< HEAD
|
||||||
import { type Emitter, type CallbackOptions, type BaseEvent, type GetCallback } from '@ckeditor/ckeditor5-utils/src/emittermixin.js';
|
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 EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo.js';
|
||||||
import type { Constructor, Mixed } from '@ckeditor/ckeditor5-utils/src/mix.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
|
* Mixin that injects the DOM events API into its host. It provides the API
|
||||||
* compatible with {@link module:utils/emittermixin~Emitter}.
|
* 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.
|
* @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
|
* 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 '@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
|
* 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
|
* target in the visually most efficient way, taking various restrictions like viewport or limiter geometry
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,17 @@
|
||||||
/**
|
/**
|
||||||
* @module utils/emittermixin
|
* @module utils/emittermixin
|
||||||
*/
|
*/
|
||||||
|
<<<<<<< HEAD
|
||||||
import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo.js';
|
import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo.js';
|
||||||
import { type PriorityString } from '@ckeditor/ckeditor5-utils/src/priorities.js';
|
import { type PriorityString } from '@ckeditor/ckeditor5-utils/src/priorities.js';
|
||||||
import type { Constructor, Mixed } from '@ckeditor/ckeditor5-utils/src/mix.js';
|
import type { Constructor, Mixed } from '@ckeditor/ckeditor5-utils/src/mix.js';
|
||||||
import '@ckeditor/ckeditor5-utils/src/version.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.
|
* 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.
|
* @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
|
* 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 '@ckeditor/ckeditor5-utils/src/diff.js';
|
||||||
import type { Change } from '@ckeditor/ckeditor5-utils/src/difftochanges.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
|
* @module utils/fastdiff
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,17 @@
|
||||||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
* @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
|
* 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<{
|
declare const FocusTracker_base: import("@ckeditor/ckeditor5-utils/src/mix.js").Mixed<{
|
||||||
new (): import("@ckeditor/ckeditor5-utils/src/observablemixin.js").Observable;
|
new (): import("@ckeditor/ckeditor5-utils/src/observablemixin.js").Observable;
|
||||||
prototype: 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>;
|
}, 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.
|
* Allows observing a group of `Element`s whether at least one of them is focused.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
/**
|
/**
|
||||||
* @module utils
|
* @module utils
|
||||||
*/
|
*/
|
||||||
|
<<<<<<< HEAD
|
||||||
export { default as env } from '@ckeditor/ckeditor5-utils/src/env.js';
|
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 diff, type DiffResult } from '@ckeditor/ckeditor5-utils/src/diff.js';
|
||||||
export { default as fastDiff } from '@ckeditor/ckeditor5-utils/src/fastdiff.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 { default as wait } from '@ckeditor/ckeditor5-utils/src/wait.js';
|
||||||
export * from '@ckeditor/ckeditor5-utils/src/unicode.js';
|
export * from '@ckeditor/ckeditor5-utils/src/unicode.js';
|
||||||
export { default as version, releaseDate } from '@ckeditor/ckeditor5-utils/src/version.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.
|
* @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
|
* 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 '@ckeditor/ckeditor5-utils/src/priorities.js';
|
||||||
|
=======
|
||||||
|
import { type PriorityString } from './priorities.js';
|
||||||
|
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||||
/**
|
/**
|
||||||
* @module utils/inserttopriorityarray
|
* @module utils/inserttopriorityarray
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,11 @@
|
||||||
*
|
*
|
||||||
* @module utils/keyboard
|
* @module utils/keyboard
|
||||||
*/
|
*/
|
||||||
|
<<<<<<< HEAD
|
||||||
import type { LanguageDirection } from '@ckeditor/ckeditor5-utils/src/language.js';
|
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.
|
* 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.
|
* @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
|
* 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 { Emitter } from '@ckeditor/ckeditor5-utils/src/emittermixin.js';
|
||||||
import { type KeystrokeInfo } from '@ckeditor/ckeditor5-utils/src/keyboard.js';
|
import { type KeystrokeInfo } from '@ckeditor/ckeditor5-utils/src/keyboard.js';
|
||||||
import type { PriorityString } from '@ckeditor/ckeditor5-utils/src/priorities.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.
|
* Keystroke handler allows registering callbacks for given keystrokes.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,15 @@
|
||||||
/**
|
/**
|
||||||
* @module utils/locale
|
* @module utils/locale
|
||||||
*/
|
*/
|
||||||
|
<<<<<<< HEAD
|
||||||
import { type ArrayOrItem } from '@ckeditor/ckeditor5-utils/src/toarray.js';
|
import { type ArrayOrItem } from '@ckeditor/ckeditor5-utils/src/toarray.js';
|
||||||
import { type Message } from '@ckeditor/ckeditor5-utils/src/translation-service.js';
|
import { type Message } from '@ckeditor/ckeditor5-utils/src/translation-service.js';
|
||||||
import { type LanguageDirection } from '@ckeditor/ckeditor5-utils/src/language.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.
|
* 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
|
* @module utils/observablemixin
|
||||||
*/
|
*/
|
||||||
|
<<<<<<< HEAD
|
||||||
import { type Emitter } from '@ckeditor/ckeditor5-utils/src/emittermixin.js';
|
import { type Emitter } from '@ckeditor/ckeditor5-utils/src/emittermixin.js';
|
||||||
import type { Constructor, Mixed } from '@ckeditor/ckeditor5-utils/src/mix.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
|
* A mixin that injects the "observable properties" and data binding functionality described in the
|
||||||
* {@link ~Observable} interface.
|
* {@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
|
* @module utils/translation-service
|
||||||
*/
|
*/
|
||||||
|
<<<<<<< HEAD
|
||||||
import type { Translations } from '@ckeditor/ckeditor5-utils/src/locale.js';
|
import type { Translations } from '@ckeditor/ckeditor5-utils/src/locale.js';
|
||||||
import { type ArrayOrItem } from '@ckeditor/ckeditor5-utils/src/toarray.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 {
|
declare global {
|
||||||
var CKEDITOR_TRANSLATIONS: Translations;
|
var CKEDITOR_TRANSLATIONS: Translations;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,17 @@
|
||||||
/**
|
/**
|
||||||
* @module widget/widgetresize
|
* @module widget/widgetresize
|
||||||
*/
|
*/
|
||||||
|
<<<<<<< HEAD
|
||||||
import Resizer from '@ckeditor/ckeditor5-widget/src/widgetresize/resizer.js';
|
import Resizer from '@ckeditor/ckeditor5-widget/src/widgetresize/resizer.js';
|
||||||
import { Plugin, type Editor } from '@ckeditor/ckeditor5-core';
|
import { Plugin, type Editor } from '@ckeditor/ckeditor5-core';
|
||||||
import { type Element, type ViewContainerElement } from '@ckeditor/ckeditor5-engine';
|
import { type Element, type ViewContainerElement } from '@ckeditor/ckeditor5-engine';
|
||||||
import '@ckeditor/ckeditor5-widget/theme/widgetresize.css';
|
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.
|
* The widget resize feature plugin.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,13 @@
|
||||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||||
*/
|
*/
|
||||||
import { Rect, type DecoratedMethodEvent } from '@ckeditor/ckeditor5-utils';
|
import { Rect, type DecoratedMethodEvent } from '@ckeditor/ckeditor5-utils';
|
||||||
|
<<<<<<< HEAD
|
||||||
import ResizeState from '@ckeditor/ckeditor5-widget/src/widgetresize/resizerstate.js';
|
import ResizeState from '@ckeditor/ckeditor5-widget/src/widgetresize/resizerstate.js';
|
||||||
import type { ResizerOptions } from '@ckeditor/ckeditor5-widget/src/widgetresize.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: {
|
declare const Resizer_base: {
|
||||||
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
||||||
prototype: 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.
|
* @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
|
* 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 '@ckeditor/ckeditor5-widget/src/widgetresize.js';
|
||||||
|
=======
|
||||||
|
import type { ResizerOptions } from '../widgetresize.js';
|
||||||
|
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
|
||||||
declare const ResizeState_base: {
|
declare const ResizeState_base: {
|
||||||
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
new (): import("@ckeditor/ckeditor5-utils").Observable;
|
||||||
prototype: import("@ckeditor/ckeditor5-utils").Observable;
|
prototype: import("@ckeditor/ckeditor5-utils").Observable;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue