fix: pull main

This commit is contained in:
Sabda Yagra 2026-01-20 10:42:24 +07:00
commit 3b4e621f4b
66 changed files with 44211 additions and 2114 deletions

2
.env
View File

@ -1,2 +1,2 @@
NETIDHUB_CLIENT_KEY=b1ce6602-07ad-46c2-85eb-0cd6decfefa3
NEXT_PUBLIC_API_URL=http://localhost:8080
NEXT_PUBLIC_API_URL=https://kontenhumas.com/api

View File

@ -1,2 +0,0 @@
# API Configuration for Local Development
NEXT_PUBLIC_API_URL=http://localhost:8080/api/

1
.gitignore vendored
View File

@ -36,3 +36,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
.env.local

View File

@ -4,6 +4,16 @@ FROM node:23.5.0-alpine
# Mengatur port
ENV PORT 3000
# Build arguments untuk environment variables (build-time)
# Bisa di-override saat docker build dengan --build-arg
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_SITE_URL
# Set sebagai environment variables untuk build
# Next.js membaca NEXT_PUBLIC_* variables saat BUILD TIME
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}
# Install pnpm secara global
RUN npm install -g pnpm
@ -16,17 +26,19 @@ COPY package.json ./
# Menyalin direktori ckeditor5 jika diperlukan
COPY vendor/ckeditor5 ./vendor/ckeditor5
# Menyalin env
COPY .env .env
# Install dependencies
RUN pnpm install
# RUN pnpm install --frozen-lockfile
# Menyalin source code aplikasi
# Menyalin source code aplikasi (termasuk .env jika ada)
# PENTING: Next.js akan membaca file .env otomatis jika ada
# Tapi jika ARG di-set, ARG akan override nilai dari .env
COPY . .
# Build aplikasi
# Next.js membaca NEXT_PUBLIC_* dari:
# 1. Environment variables (ENV) - prioritas tertinggi
# 2. File .env - jika ENV tidak di-set
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm next build
# Expose port untuk server

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -2,7 +2,13 @@
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { PlusIcon, ModuleIcon, EditIcon, DeleteIcon } from "@/components/icons";
import {
MasterModule,
@ -50,11 +56,11 @@ export default function ModulesSettingsPage() {
if (module) {
setEditingModule(module);
setFormData({
name: module.name,
description: module.description,
pathUrl: module.pathUrl,
actionType: module.actionType,
statusId: module.statusId,
name: module.name ?? "",
description: module.description ?? "",
pathUrl: module.pathUrl ?? "",
actionType: module.actionType ?? "",
statusId: module.statusId ?? 1,
});
} else {
setEditingModule(null);
@ -80,8 +86,8 @@ export default function ModulesSettingsPage() {
icon: "error",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
popup: "swal-z-index-9999",
},
});
} else {
Swal.fire({
@ -90,8 +96,8 @@ export default function ModulesSettingsPage() {
icon: "success",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
popup: "swal-z-index-9999",
},
});
await loadData();
setIsDialogOpen(false);
@ -105,8 +111,8 @@ export default function ModulesSettingsPage() {
icon: "error",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
popup: "swal-z-index-9999",
},
});
} else {
Swal.fire({
@ -115,8 +121,8 @@ export default function ModulesSettingsPage() {
icon: "success",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
popup: "swal-z-index-9999",
},
});
await loadData();
setIsDialogOpen(false);
@ -130,8 +136,8 @@ export default function ModulesSettingsPage() {
icon: "error",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
popup: "swal-z-index-9999",
},
});
}
};
@ -145,8 +151,8 @@ export default function ModulesSettingsPage() {
confirmButtonText: "Yes, delete it",
cancelButtonText: "Cancel",
customClass: {
popup: 'swal-z-index-9999'
}
popup: "swal-z-index-9999",
},
});
if (result.isConfirmed) {
@ -159,8 +165,8 @@ export default function ModulesSettingsPage() {
icon: "error",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
popup: "swal-z-index-9999",
},
});
} else {
Swal.fire({
@ -169,8 +175,8 @@ export default function ModulesSettingsPage() {
icon: "success",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
popup: "swal-z-index-9999",
},
});
await loadData();
}
@ -182,8 +188,8 @@ export default function ModulesSettingsPage() {
icon: "error",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
popup: "swal-z-index-9999",
},
});
}
}
@ -195,14 +201,19 @@ export default function ModulesSettingsPage() {
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Modules Settings</h1>
<h1 className="text-3xl font-bold text-gray-900">
Modules Settings
</h1>
<p className="text-gray-600 mt-2">
Manage system modules and their configurations
</p>
</div>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button className="flex items-center gap-2" onClick={() => handleOpenDialog()}>
<Button
className="flex items-center gap-2"
onClick={() => handleOpenDialog()}
>
<PlusIcon className="h-4 w-4" />
Create Module
</Button>
@ -210,7 +221,9 @@ export default function ModulesSettingsPage() {
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{editingModule ? `Edit Module: ${editingModule.name}` : "Create New Module"}
{editingModule
? `Edit Module: ${editingModule.name}`
: "Create New Module"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
@ -220,7 +233,9 @@ export default function ModulesSettingsPage() {
type="text"
placeholder="e.g., View Articles, Create Content"
value={formData.name}
onChange={(value) => setFormData({ ...formData, name: value })}
onChange={(value) =>
setFormData({ ...formData, name: value })
}
required
/>
<FormField
@ -229,7 +244,9 @@ export default function ModulesSettingsPage() {
type="text"
placeholder="Brief description of the module"
value={formData.description}
onChange={(value) => setFormData({ ...formData, description: value })}
onChange={(value) =>
setFormData({ ...formData, description: value })
}
required
/>
<FormField
@ -238,7 +255,9 @@ export default function ModulesSettingsPage() {
type="text"
placeholder="e.g., /api/articles, /api/content"
value={formData.pathUrl}
onChange={(value) => setFormData({ ...formData, pathUrl: value })}
onChange={(value) =>
setFormData({ ...formData, pathUrl: value })
}
required
/>
<FormField
@ -247,7 +266,9 @@ export default function ModulesSettingsPage() {
type="select"
placeholder="Select action type"
value={formData.actionType}
onChange={(value) => setFormData({ ...formData, actionType: value })}
onChange={(value) =>
setFormData({ ...formData, actionType: value })
}
options={[
{ value: "view", label: "View" },
{ value: "create", label: "Create" },
@ -264,7 +285,9 @@ export default function ModulesSettingsPage() {
type="number"
placeholder="1"
value={formData.statusId}
onChange={(value) => setFormData({ ...formData, statusId: Number(value) || 1 })}
onChange={(value) =>
setFormData({ ...formData, statusId: Number(value) || 1 })
}
required
/>
<div className="flex items-center justify-end gap-2 pt-4 border-t">
@ -291,7 +314,10 @@ export default function ModulesSettingsPage() {
) : modules.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{modules.map((module) => (
<Card key={module.id} className="hover:shadow-lg transition-shadow">
<Card
key={module.id}
className="hover:shadow-lg transition-shadow"
>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="truncate">{module.name}</span>
@ -308,7 +334,9 @@ export default function ModulesSettingsPage() {
</CardHeader>
<CardContent>
<div className="space-y-2 mb-4">
<div className="text-sm text-gray-600">{module.description}</div>
<div className="text-sm text-gray-600">
{module.description}
</div>
<div className="text-xs text-gray-500 font-mono bg-gray-50 p-2 rounded">
{module.pathUrl}
</div>
@ -347,7 +375,9 @@ export default function ModulesSettingsPage() {
<CardContent className="flex items-center justify-center py-12">
<div className="text-center">
<ModuleIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No Modules Found</h3>
<h3 className="text-lg font-medium text-gray-900 mb-2">
No Modules Found
</h3>
<p className="text-gray-500 mb-4">
Create your first module to define system capabilities
</p>
@ -363,4 +393,3 @@ export default function ModulesSettingsPage() {
</>
);
}

View File

@ -24,6 +24,25 @@ import {
DialogDescription,
} from "@/components/ui/dialog";
import { getUserLevelDetail } from "@/service/tenant";
import {
getUserLevelMenuAccessesByUserLevelId,
UserLevelMenuAccess,
} from "@/service/user-level-menu-accesses";
import {
getUserLevelMenuActionAccessesByUserLevelIdAndMenuId,
UserLevelMenuActionAccess,
} from "@/service/user-level-menu-action-accesses";
import {
getMenuActionsByMenuId,
MenuAction,
} from "@/service/menu-actions";
import {
getMasterMenus,
MasterMenu,
} from "@/service/menu-modules";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
const useTableColumns = (onEdit?: (data: any) => void) => {
const MySwal = withReactContent(Swal);
@ -192,11 +211,68 @@ const useTableColumns = (onEdit?: (data: any) => void) => {
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
const [detailData, setDetailData] = React.useState<any>(null);
const [menuAccesses, setMenuAccesses] = React.useState<UserLevelMenuAccess[]>([]);
const [actionAccesses, setActionAccesses] = React.useState<Record<number, UserLevelMenuActionAccess[]>>({});
const [menus, setMenus] = React.useState<MasterMenu[]>([]);
const [menuActionsMap, setMenuActionsMap] = React.useState<Record<number, MenuAction[]>>({});
const [isLoadingDetail, setIsLoadingDetail] = React.useState(false);
const handleView = async (id: number) => {
setIsLoadingDetail(true);
try {
// Load basic user level data
const res = await getUserLevelDetail(id);
if (!res?.error) {
setDetailData(res?.data?.data);
// Load menus
const menusRes = await getMasterMenus({ limit: 100 });
if (!menusRes?.error) {
const menusData = (menusRes?.data?.data || []).map((menu: any) => ({
...menu,
moduleId: menu.module_id || menu.moduleId,
parentMenuId: menu.parent_menu_id !== undefined ? menu.parent_menu_id : menu.parentMenuId,
statusId: menu.status_id || menu.statusId,
isActive: menu.is_active !== undefined ? menu.is_active : menu.isActive,
}));
setMenus(menusData);
// Load actions for each menu
const actionsMap: Record<number, MenuAction[]> = {};
for (const menu of menusData) {
try {
const actionsRes = await getMenuActionsByMenuId(menu.id);
if (!actionsRes?.error) {
actionsMap[menu.id] = actionsRes?.data?.data || [];
}
} catch (error) {
console.error(`Error loading actions for menu ${menu.id}:`, error);
}
}
setMenuActionsMap(actionsMap);
}
// Load menu accesses
const menuAccessRes = await getUserLevelMenuAccessesByUserLevelId(id);
if (!menuAccessRes?.error) {
const accesses = menuAccessRes?.data?.data || [];
setMenuAccesses(accesses);
// Load action accesses for each menu
const actionAccessesMap: Record<number, UserLevelMenuActionAccess[]> = {};
for (const menuAccess of accesses.filter((a: UserLevelMenuAccess) => a.canAccess)) {
try {
const actionRes = await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(id, menuAccess.menuId);
if (!actionRes?.error) {
actionAccessesMap[menuAccess.menuId] = actionRes?.data?.data || [];
}
} catch (error) {
console.error(`Error loading action accesses for menu ${menuAccess.menuId}:`, error);
}
}
setActionAccesses(actionAccessesMap);
}
setIsDialogOpen(true);
} else {
error(res?.message || "Gagal memuat detail user level");
@ -204,6 +280,8 @@ const useTableColumns = (onEdit?: (data: any) => void) => {
} catch (err) {
console.error("View error:", err);
error("Terjadi kesalahan saat memuat data.");
} finally {
setIsLoadingDetail(false);
}
};
@ -318,35 +396,200 @@ const useTableColumns = (onEdit?: (data: any) => void) => {
</DropdownMenuContent>
{/* ✅ Dialog Detail User Level */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="max-w-lg">
<DialogContent size="md" className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Detail User Level</DialogTitle>
<DialogDescription>
Informasi lengkap dari user level ID: {detailData?.id}
Informasi lengkap dari user level: {detailData?.name || detailData?.aliasName}
</DialogDescription>
</DialogHeader>
{detailData ? (
<div className="space-y-3 mt-4">
<p>
<span className="font-medium">Name:</span>{" "}
{detailData.aliasName}
</p>
<p>
<span className="font-medium">Group:</span>{" "}
{detailData.group}
</p>
<p>
<span className="font-medium">Parent Level:</span>{" "}
{detailData.parentLevelId || "-"}
</p>
<p>
<span className="font-medium">Created At:</span>{" "}
{detailData.createdAt
? new Date(detailData.createdAt).toLocaleString("id-ID")
: "-"}
</p>
{isLoadingDetail ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2 text-gray-600">Memuat data...</span>
</div>
) : detailData ? (
<Tabs defaultValue="basic" className="w-full mt-4">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="basic">Basic Information</TabsTrigger>
<TabsTrigger value="menus">Menu Access</TabsTrigger>
<TabsTrigger value="actions">Action Access</TabsTrigger>
</TabsList>
{/* Basic Information Tab */}
<TabsContent value="basic" className="space-y-4 mt-4">
<Card>
<CardHeader>
<CardTitle>User Level Information</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<span className="text-sm font-medium text-gray-600">ID:</span>
<p className="text-base font-mono">{detailData.id}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-600">Name:</span>
<p className="text-base">{detailData.name || detailData.aliasName}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-600">Alias Name:</span>
<p className="text-base font-mono">{detailData.aliasName}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-600">Level Number:</span>
<p className="text-base">{detailData.levelNumber}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-600">Group:</span>
<p className="text-base">{detailData.group || "-"}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-600">Parent Level ID:</span>
<p className="text-base">{detailData.parentLevelId || "-"}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-600">Province ID:</span>
<p className="text-base">{detailData.provinceId || "-"}</p>
</div>
<div>
<span className="text-sm font-medium text-gray-600">Is Approval Active:</span>
<Badge className={detailData.isApprovalActive ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}>
{detailData.isApprovalActive ? "Yes" : "No"}
</Badge>
</div>
<div>
<span className="text-sm font-medium text-gray-600">Is Active:</span>
<Badge className={detailData.isActive ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}>
{detailData.isActive ? "Active" : "Inactive"}
</Badge>
</div>
<div>
<span className="text-sm font-medium text-gray-600">Created At:</span>
<p className="text-base text-sm">
{detailData.createdAt
? new Date(detailData.createdAt).toLocaleString("id-ID")
: "-"}
</p>
</div>
<div>
<span className="text-sm font-medium text-gray-600">Updated At:</span>
<p className="text-base text-sm">
{detailData.updatedAt
? new Date(detailData.updatedAt).toLocaleString("id-ID")
: "-"}
</p>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
{/* Menu Access Tab */}
<TabsContent value="menus" className="space-y-4 mt-4">
<Card>
<CardHeader>
<CardTitle>Menu Access Configuration</CardTitle>
</CardHeader>
<CardContent>
{menuAccesses.filter((a: UserLevelMenuAccess) => a.canAccess).length > 0 ? (
<div className="space-y-2 max-h-96 overflow-y-auto">
{menuAccesses
.filter((a: UserLevelMenuAccess) => a.canAccess)
.map((access) => {
const menu = menus.find((m) => m.id === access.menuId);
return (
<div
key={access.id}
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-gray-50"
>
<div className="flex-1">
<div className="font-medium">{menu?.name || `Menu ID: ${access.menuId}`}</div>
<div className="text-sm text-gray-500">{menu?.description || "-"}</div>
<div className="text-xs text-gray-400 mt-1">
Group: {menu?.group || "-"} Status: {access.canAccess ? "Accessible" : "No Access"}
</div>
</div>
<Badge className={access.canAccess ? "bg-green-100 text-green-800" : "bg-gray-100 text-gray-800"}>
{access.canAccess ? "Accessible" : "No Access"}
</Badge>
</div>
);
})}
</div>
) : (
<div className="text-center py-8 text-gray-500">
No menu access configured
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* Action Access Tab */}
<TabsContent value="actions" className="space-y-4 mt-4">
<Card>
<CardHeader>
<CardTitle>Action Access Configuration</CardTitle>
</CardHeader>
<CardContent>
{Object.keys(actionAccesses).length > 0 ? (
<div className="space-y-4 max-h-96 overflow-y-auto">
{Object.entries(actionAccesses).map(([menuId, actions]) => {
const menu = menus.find((m) => m.id === Number(menuId));
const accessibleActions = actions.filter((a: UserLevelMenuActionAccess) => a.canAccess);
if (accessibleActions.length === 0) return null;
return (
<Card key={menuId} className="border-l-4 border-l-blue-500">
<CardHeader className="pb-3">
<CardTitle className="text-base">{menu?.name || `Menu ID: ${menuId}`}</CardTitle>
<p className="text-sm text-gray-500">{menu?.description || "-"}</p>
</CardHeader>
<CardContent>
<div className="space-y-2">
{accessibleActions.map((actionAccess) => {
const action = menuActionsMap[Number(menuId)]?.find(
(a) => a.actionCode === actionAccess.actionCode
);
return (
<div
key={actionAccess.id}
className="flex items-start gap-3 p-2 border rounded-lg hover:bg-gray-50"
>
<div className="flex-1">
<div className="font-medium text-sm">
{action?.actionName || actionAccess.actionCode}
</div>
<div className="text-xs text-gray-500">
Code: {actionAccess.actionCode}
{action?.pathUrl && ` • Path: ${action.pathUrl}`}
{action?.httpMethod && ` • Method: ${action.httpMethod}`}
</div>
</div>
<Badge className="bg-green-100 text-green-800">
Allowed
</Badge>
</div>
);
})}
</div>
</CardContent>
</Card>
);
})}
</div>
) : (
<div className="text-center py-8 text-gray-500">
No action access configured
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
) : (
<p className="text-gray-500 mt-4">Memuat data...</p>
)}

View File

@ -55,7 +55,6 @@ import {
} from "@tanstack/react-table";
import TablePagination from "@/components/table/table-pagination";
import useTableColumns from "./columns";
import TenantUpdateForm from "@/components/form/tenant/tenant-update-form";
import { errorAutoClose, successAutoClose } from "@/lib/swal";
import { close, loading } from "@/config/swal";
import DetailTenant from "@/components/form/tenant/tenant-detail-update-form";
@ -879,19 +878,21 @@ function TenantSettingsContentTable() {
</DialogHeader>
{editingUserLevel ? (
<TenantUpdateForm
id={editingUserLevel.id}
<UserLevelsForm
mode="single"
initialData={{
// id: editingUserLevel.id,
name: editingUserLevel.name,
aliasName: editingUserLevel.aliasName,
levelNumber: editingUserLevel.levelNumber,
parentLevelId: editingUserLevel.parentLevelId || 0,
provinceId: editingUserLevel.provinceId || 0,
provinceId: editingUserLevel.provinceId,
group: editingUserLevel.group || "",
isApprovalActive: editingUserLevel.isApprovalActive,
isActive: editingUserLevel.isActive,
}}
onSuccess={async () => {
onSave={async (data) => {
// The form handles the update internally
setIsEditDialogOpen(false);
setEditingUserLevel(null);
await loadData();

View File

@ -475,6 +475,7 @@ function TenantSettingsContent() {
<UserLevelsForm
mode="single"
initialData={editingUserLevel ? {
// id: editingUserLevel.id,
name: editingUserLevel.name,
aliasName: editingUserLevel.aliasName,
levelNumber: editingUserLevel.levelNumber,

View File

@ -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>
</>
);
}

View File

@ -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>
</>
);
}

View File

@ -1,7 +1,142 @@
@import "tailwindcss";
@tailwind base;
@tailwind components;
@tailwind utilities;
/* ================================
GLOBAL CSS VARIABLE (LIGHT MODE)
================================ */
:root {
--radius: 0.625rem;
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 215.3 19.3% 34.5%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--success: 154 52% 55%;
--warning: 16 93% 70%;
--info: 185 96% 51%;
--sidebar: 0 0% 100%;
--sidebar-foreground: 215 20% 65%;
}
/* ================================
DARK MODE VARIABLES
================================ */
.dark {
--background: 222.2 47.4% 11.2%;
--foreground: 210 40% 98%;
--card: 215 27.9% 16.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 215.3 25% 26.7%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--sidebar: 215 27.9% 16.9%;
--sidebar-foreground: 214.3 31.8% 91.4%;
}
/* ================================
BASE LAYER
================================ */
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
/* ================================
GLOBAL UTILITIES
================================ */
@layer utilities {
/* SweetAlert z-index fix */
.swal-z-index-9999 {
z-index: 9999 !important;
}
/* Scrollbar hide */
.no-scrollbar::-webkit-scrollbar {
width: 0px;
}
.no-scrollbar::-webkit-scrollbar-thumb {
background-color: transparent;
}
/* Input group helpers */
.input-group :not(:first-child) input {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
.input-group.merged :not(:first-child) input {
border-left-width: 0 !important;
padding-left: 0 !important;
}
.input-group :not(:last-child) input {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
.input-group.merged :not(:last-child) input {
border-right-width: 0 !important;
padding-right: 0 !important;
}
}
/* @import "tailwindcss";
@import "tw-animate-css";
/* SweetAlert2 z-index fix */
.swal-z-index-9999 {
z-index: 9999 !important;
}
@ -297,4 +432,4 @@
.no-scrollbar::-webkit-scrollbar-thumb {
background-color: transparent;
}
}
} */

View File

@ -19,7 +19,7 @@ interface FormFieldProps {
showPasswordToggle?: boolean;
onPasswordToggle?: () => void;
showPassword?: boolean;
}
}
export const FormField: React.FC<FormFieldProps> = ({
label,

View File

@ -12,19 +12,30 @@ import {
UserLevel,
Province,
createUserLevel,
updateUserLevel,
getUserLevels,
getProvinces,
} from "@/service/approval-workflows";
import {
MasterModule,
getMasterModules,
MasterMenu,
getMasterMenus,
} from "@/service/menu-modules";
import {
getUserLevelModuleAccessesByUserLevelId,
createUserLevelModuleAccessesBatch,
deleteUserLevelModuleAccess,
UserLevelModuleAccess,
} from "@/service/user-level-module-accesses";
getUserLevelMenuAccessesByUserLevelId,
createUserLevelMenuAccessesBatch,
deleteUserLevelMenuAccess,
UserLevelMenuAccess,
} from "@/service/user-level-menu-accesses";
import {
getUserLevelMenuActionAccessesByUserLevelIdAndMenuId,
createUserLevelMenuActionAccessesBatch,
deleteUserLevelMenuActionAccess,
UserLevelMenuActionAccess,
} from "@/service/user-level-menu-action-accesses";
import {
getMenuActionsByMenuId,
MenuAction,
} from "@/service/menu-actions";
import Swal from "sweetalert2";
interface UserLevelsFormProps {
@ -57,9 +68,11 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
const [bulkFormData, setBulkFormData] = useState<UserLevelsCreateRequest[]>([]);
const [userLevels, setUserLevels] = useState<UserLevel[]>([]);
const [provinces, setProvinces] = useState<Province[]>([]);
const [modules, setModules] = useState<MasterModule[]>([]);
const [selectedModuleIds, setSelectedModuleIds] = useState<number[]>([]);
const [userLevelModuleAccesses, setUserLevelModuleAccesses] = useState<UserLevelModuleAccess[]>([]);
const [menus, setMenus] = useState<MasterMenu[]>([]);
const [selectedMenuIds, setSelectedMenuIds] = useState<number[]>([]);
const [userLevelMenuAccesses, setUserLevelMenuAccesses] = useState<UserLevelMenuAccess[]>([]);
const [menuActionsMap, setMenuActionsMap] = useState<Record<number, MenuAction[]>>({});
const [selectedActionAccesses, setSelectedActionAccesses] = useState<Record<number, string[]>>({});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [expandedHierarchy, setExpandedHierarchy] = useState(false);
@ -75,15 +88,38 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
useEffect(() => {
const loadData = async () => {
try {
const [userLevelsRes, provincesRes, modulesRes] = await Promise.all([
const [userLevelsRes, provincesRes, menusRes] = await Promise.all([
getUserLevels(),
getProvinces(),
getMasterModules({ limit: 100 }),
getMasterMenus({ limit: 100 }),
]);
if (!userLevelsRes?.error) setUserLevels(userLevelsRes?.data?.data || []);
if (!provincesRes?.error) setProvinces(provincesRes?.data?.data || []);
if (!modulesRes?.error) setModules(modulesRes?.data?.data || []);
if (!menusRes?.error) {
const menusData = (menusRes?.data?.data || []).map((menu: any) => ({
...menu,
moduleId: menu.module_id || menu.moduleId,
parentMenuId: menu.parent_menu_id !== undefined ? menu.parent_menu_id : menu.parentMenuId,
statusId: menu.status_id || menu.statusId,
isActive: menu.is_active !== undefined ? menu.is_active : menu.isActive,
}));
setMenus(menusData);
// Load actions for each menu
const actionsMap: Record<number, MenuAction[]> = {};
for (const menu of menusData) {
try {
const actionsRes = await getMenuActionsByMenuId(menu.id);
if (!actionsRes?.error) {
actionsMap[menu.id] = actionsRes?.data?.data || [];
}
} catch (error) {
console.error(`Error loading actions for menu ${menu.id}:`, error);
}
}
setMenuActionsMap(actionsMap);
}
} catch (error) {
console.error("Error loading form data:", error);
} finally {
@ -95,22 +131,41 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
}, []);
useEffect(() => {
const loadModuleAccesses = async () => {
const loadAccesses = async () => {
if (initialData && (initialData as any).id) {
const userLevelId = (initialData as any).id;
try {
const res = await getUserLevelModuleAccessesByUserLevelId((initialData as any).id);
if (!res?.error) {
const accesses = res?.data?.data || [];
setUserLevelModuleAccesses(accesses);
setSelectedModuleIds(accesses.filter((a: UserLevelModuleAccess) => a.canAccess).map((a: UserLevelModuleAccess) => a.moduleId));
// Load menu accesses
const menuRes = await getUserLevelMenuAccessesByUserLevelId(userLevelId);
if (!menuRes?.error) {
const menuAccesses = menuRes?.data?.data || [];
setUserLevelMenuAccesses(menuAccesses);
setSelectedMenuIds(menuAccesses.filter((a: UserLevelMenuAccess) => a.canAccess).map((a: UserLevelMenuAccess) => a.menuId));
// Load action accesses for each menu
const actionAccesses: Record<number, string[]> = {};
for (const menuAccess of menuAccesses.filter((a: UserLevelMenuAccess) => a.canAccess)) {
try {
const actionRes = await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(userLevelId, menuAccess.menuId);
if (!actionRes?.error) {
const actions = actionRes?.data?.data || [];
actionAccesses[menuAccess.menuId] = actions
.filter((a: UserLevelMenuActionAccess) => a.canAccess)
.map((a: UserLevelMenuActionAccess) => a.actionCode);
}
} catch (error) {
console.error(`Error loading action accesses for menu ${menuAccess.menuId}:`, error);
}
}
setSelectedActionAccesses(actionAccesses);
}
} catch (error) {
console.error("Error loading module accesses:", error);
console.error("Error loading accesses:", error);
}
}
};
loadModuleAccesses();
loadAccesses();
}, [initialData]);
const validateForm = (data: UserLevelsCreateRequest): Record<string, string> => {
@ -348,13 +403,22 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
try {
if (onSave) {
if (currentMode === "single") {
// Check if editing or creating
const isEditing = !!(initialData as any)?.id;
const userLevelId = (initialData as any)?.id;
// Save user level first
const userLevelResponse = await createUserLevel(formData);
let userLevelResponse;
if (isEditing) {
userLevelResponse = await updateUserLevel(userLevelId, formData);
} else {
userLevelResponse = await createUserLevel(formData);
}
if (userLevelResponse?.error) {
Swal.fire({
title: "Error",
text: userLevelResponse?.message || "Failed to create user level",
text: userLevelResponse?.message || `Failed to ${isEditing ? 'update' : 'create'} user level`,
icon: "error",
confirmButtonText: "OK",
customClass: {
@ -365,27 +429,59 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
return;
}
// Get the created user level ID
const createdUserLevelId = userLevelResponse?.data?.data?.id || (initialData as any)?.id;
// Get the user level ID
const createdUserLevelId = userLevelResponse?.data?.data?.id || userLevelId;
if (createdUserLevelId && selectedModuleIds.length > 0) {
// Delete existing module accesses if editing
// Save menu accesses
if (createdUserLevelId && selectedMenuIds.length > 0) {
// Delete existing menu accesses if editing
if ((initialData as any)?.id) {
for (const access of userLevelModuleAccesses) {
await deleteUserLevelModuleAccess(access.id);
for (const access of userLevelMenuAccesses) {
await deleteUserLevelMenuAccess(access.id);
}
}
// Create new module accesses in batch
const moduleAccessResponse = await createUserLevelModuleAccessesBatch({
// Create new menu accesses in batch
const menuAccessResponse = await createUserLevelMenuAccessesBatch({
userLevelId: createdUserLevelId,
moduleIds: selectedModuleIds,
canAccess: true,
menuIds: selectedMenuIds,
});
if (moduleAccessResponse?.error) {
console.error("Error saving module accesses:", moduleAccessResponse?.message);
// Don't fail the whole operation, just log the error
if (menuAccessResponse?.error) {
console.error("Error saving menu accesses:", menuAccessResponse?.message);
} else {
// Save action accesses for each menu
for (const menuId of selectedMenuIds) {
const actionCodes = selectedActionAccesses[menuId] || [];
// Delete existing action accesses for this menu if editing
if ((initialData as any)?.id) {
try {
const existingActionsRes = await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(createdUserLevelId, menuId);
if (!existingActionsRes?.error) {
const existingActions = existingActionsRes?.data?.data || [];
for (const action of existingActions) {
await deleteUserLevelMenuActionAccess(action.id);
}
}
} catch (error) {
console.error(`Error deleting existing action accesses for menu ${menuId}:`, error);
}
}
// Create new action accesses in batch
if (actionCodes.length > 0) {
const actionAccessResponse = await createUserLevelMenuActionAccessesBatch({
userLevelId: createdUserLevelId,
menuId: menuId,
actionCodes: actionCodes,
});
if (actionAccessResponse?.error) {
console.error(`Error saving action accesses for menu ${menuId}:`, actionAccessResponse?.message);
}
}
}
}
}
@ -430,57 +526,97 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
}, 1000); // Small delay to let user see the success message
}
}
} else {
if (currentMode === "single") {
const response = await createUserLevel(formData);
console.log("Create Response: ", response);
if (response?.error) {
Swal.fire({
title: "Error",
text: response?.message || "Failed to create user level",
icon: "error",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
});
} else {
// Get the created user level ID
const createdUserLevelId = response?.data?.data?.id || (initialData as any)?.id;
} else {
if (currentMode === "single") {
// Check if editing or creating
const isEditing = !!(initialData as any)?.id;
const userLevelId = (initialData as any)?.id;
// Save module accesses if any selected
if (createdUserLevelId && selectedModuleIds.length > 0) {
// Delete existing module accesses if editing
let response;
if (isEditing) {
response = await updateUserLevel(userLevelId, formData);
} else {
response = await createUserLevel(formData);
}
console.log(`${isEditing ? 'Update' : 'Create'} Response: `, response);
if (response?.error) {
Swal.fire({
title: "Error",
text: response?.message || `Failed to ${isEditing ? 'update' : 'create'} user level`,
icon: "error",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
});
} else {
// Get the user level ID
const createdUserLevelId = response?.data?.data?.id || userLevelId;
// Save menu accesses
if (createdUserLevelId && selectedMenuIds.length > 0) {
// Delete existing menu accesses if editing
if ((initialData as any)?.id) {
for (const access of userLevelModuleAccesses) {
await deleteUserLevelModuleAccess(access.id);
for (const access of userLevelMenuAccesses) {
await deleteUserLevelMenuAccess(access.id);
}
}
// Create new module accesses in batch
const moduleAccessResponse = await createUserLevelModuleAccessesBatch({
// Create new menu accesses in batch
const menuAccessResponse = await createUserLevelMenuAccessesBatch({
userLevelId: createdUserLevelId,
moduleIds: selectedModuleIds,
canAccess: true,
menuIds: selectedMenuIds,
});
if (moduleAccessResponse?.error) {
console.error("Error saving module accesses:", moduleAccessResponse?.message);
// Don't fail the whole operation, just log the error
if (menuAccessResponse?.error) {
console.error("Error saving menu accesses:", menuAccessResponse?.message);
} else {
// Save action accesses for each menu
for (const menuId of selectedMenuIds) {
const actionCodes = selectedActionAccesses[menuId] || [];
// Delete existing action accesses for this menu if editing
if ((initialData as any)?.id) {
try {
const existingActionsRes = await getUserLevelMenuActionAccessesByUserLevelIdAndMenuId(createdUserLevelId, menuId);
if (!existingActionsRes?.error) {
const existingActions = existingActionsRes?.data?.data || [];
for (const action of existingActions) {
await deleteUserLevelMenuActionAccess(action.id);
}
}
} catch (error) {
console.error(`Error deleting existing action accesses for menu ${menuId}:`, error);
}
}
// Create new action accesses in batch
if (actionCodes.length > 0) {
const actionAccessResponse = await createUserLevelMenuActionAccessesBatch({
userLevelId: createdUserLevelId,
menuId: menuId,
actionCodes: actionCodes,
});
if (actionAccessResponse?.error) {
console.error(`Error saving action accesses for menu ${menuId}:`, actionAccessResponse?.message);
}
}
}
}
}
Swal.fire({
title: "Success",
text: "User level created successfully",
text: isEditing ? "User level updated successfully" : "User level created successfully",
icon: "success",
confirmButtonText: "OK",
customClass: {
popup: 'swal-z-index-9999'
}
}).then(() => {
// Refresh page after successful creation
// Refresh page after successful save
window.location.reload();
});
}
@ -609,8 +745,8 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="basic" disabled={isLoadingData}>Basic Information</TabsTrigger>
<TabsTrigger value="modules" disabled={isLoadingData || mode === "bulk"}>Module Access</TabsTrigger>
{/* <TabsTrigger value="hierarchy" disabled={isLoadingData}>Hierarchy</TabsTrigger> */}
<TabsTrigger value="menus" disabled={isLoadingData || mode === "bulk"}>Menu Access</TabsTrigger>
<TabsTrigger value="actions" disabled={isLoadingData || mode === "bulk"}>Action Access</TabsTrigger>
<TabsTrigger value="bulk" disabled={isLoadingData}>Bulk Operations</TabsTrigger>
</TabsList>
@ -758,40 +894,51 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
</Card>
</TabsContent> */}
{/* Module Access Tab */}
<TabsContent value="modules" className="space-y-6">
{/* Menu Access Tab */}
<TabsContent value="menus" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Module Access Configuration</CardTitle>
<CardTitle>Menu Access Configuration</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-gray-600">
Select which modules this user level can access. Only selected modules will be accessible to users with this level.
Select which menus this user level can access. Users with this level will only see selected menus in the navigation.
</p>
{modules.length > 0 ? (
{menus.length > 0 ? (
<div className="space-y-2 max-h-96 overflow-y-auto border rounded-lg p-4">
{modules.map((module) => (
{menus.map((menu) => (
<label
key={module.id}
key={menu.id}
className="flex items-start gap-3 p-3 border rounded-lg hover:bg-gray-50 cursor-pointer"
>
<input
type="checkbox"
checked={selectedModuleIds.includes(module.id)}
checked={selectedMenuIds.includes(menu.id)}
onChange={() => {
setSelectedModuleIds((prev) =>
prev.includes(module.id)
? prev.filter((id) => id !== module.id)
: [...prev, module.id]
);
setSelectedMenuIds((prev) => {
const newMenuIds = prev.includes(menu.id)
? prev.filter((id) => id !== menu.id)
: [...prev, menu.id];
// If menu is deselected, remove its action accesses
if (prev.includes(menu.id) && !newMenuIds.includes(menu.id)) {
setSelectedActionAccesses((prevActions) => {
const newActions = { ...prevActions };
delete newActions[menu.id];
return newActions;
});
}
return newMenuIds;
});
}}
className="mt-1"
/>
<div className="flex-1">
<div className="font-medium">{module.name}</div>
<div className="text-sm text-gray-500">{module.description}</div>
<div className="font-medium">{menu.name}</div>
<div className="text-sm text-gray-500">{menu.description}</div>
<div className="text-xs text-gray-400 mt-1">
{module.pathUrl} {module.actionType}
Group: {menu.group} Module ID: {menu.moduleId}
</div>
</div>
</label>
@ -799,7 +946,87 @@ export const UserLevelsForm: React.FC<UserLevelsFormProps> = ({
</div>
) : (
<div className="text-center py-8 text-gray-500">
No modules available
No menus available
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* Action Access Tab */}
<TabsContent value="actions" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Action Access Configuration</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-gray-600">
Configure which actions users with this level can perform in each menu. First select menus in the "Menu Access" tab.
</p>
{selectedMenuIds.length > 0 ? (
<div className="space-y-4 max-h-96 overflow-y-auto">
{selectedMenuIds.map((menuId) => {
const menu = menus.find((m) => m.id === menuId);
const actions = menuActionsMap[menuId] || [];
const selectedActions = selectedActionAccesses[menuId] || [];
if (!menu) return null;
return (
<Card key={menuId} className="border-l-4 border-l-blue-500">
<CardHeader className="pb-3">
<CardTitle className="text-base">{menu.name}</CardTitle>
<p className="text-sm text-gray-500">{menu.description}</p>
</CardHeader>
<CardContent>
{actions.length > 0 ? (
<div className="space-y-2">
{actions.map((action) => (
<label
key={action.id}
className="flex items-start gap-3 p-2 border rounded-lg hover:bg-gray-50 cursor-pointer"
>
<input
type="checkbox"
checked={selectedActions.includes(action.actionCode)}
onChange={() => {
setSelectedActionAccesses((prev) => {
const current = prev[menuId] || [];
const newActions = current.includes(action.actionCode)
? current.filter((code) => code !== action.actionCode)
: [...current, action.actionCode];
return {
...prev,
[menuId]: newActions,
};
});
}}
className="mt-1"
/>
<div className="flex-1">
<div className="font-medium text-sm">{action.actionName}</div>
<div className="text-xs text-gray-500">
Code: {action.actionCode}
{action.pathUrl && ` • Path: ${action.pathUrl}`}
</div>
</div>
</label>
))}
</div>
) : (
<div className="text-center py-4 text-gray-500 text-sm">
No actions available for this menu
</div>
)}
</CardContent>
</Card>
);
})}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<p className="mb-2">No menus selected</p>
<p className="text-sm">Please select menus in the "Menu Access" tab first</p>
</div>
)}
</CardContent>

View File

@ -3052,35 +3052,4 @@ export const RotateCcwIcon = ({ size = 24, width, height, ...props }: IconSvgPro
<path d="M1 4v6h6" />
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
</svg>
);
export const EditIcon = ({
size = 24,
width,
height,
...props
}: IconSvgProps) => (
<svg
width={size || width}
height={size || height}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);

View File

@ -199,6 +199,16 @@ export default function MediaUpdate() {
const typeId = parseInt(getTypeIdByContentType(contentType));
setCurrentTypeId(typeId.toString());
// const response = await listArticles(
// 1,
// 10,
// typeId,
// undefined,
// undefined,
// section === "latest" ? "createdAt" : "viewCount",
// slug
// );
const response = await listArticles(
1,
10,
@ -206,13 +216,13 @@ export default function MediaUpdate() {
undefined,
undefined,
section === "latest" ? "createdAt" : "viewCount",
slug
slug || undefined, // ⬅️ jangan kirim undefined string
);
let hasil: any[] = [];
if (response?.error) {
console.error("Articles API failed, fallback ke old API");
// console.error("Articles API failed, fallback ke old API");
const fallbackRes = await listData(
typeId.toString(),
"",
@ -221,7 +231,7 @@ export default function MediaUpdate() {
0,
"",
"",
""
"",
);
hasil = fallbackRes?.data?.data?.content || [];
} else {
@ -263,14 +273,14 @@ export default function MediaUpdate() {
const ids = new Set<number>(
(Array.isArray(bookmarks) ? bookmarks : [])
.map((b: any) => Number(b.articleId ?? b.id ?? b.article?.id))
.filter((x) => !isNaN(x))
.filter((x) => !isNaN(x)),
);
const gabungan = new Set([...localSet, ...ids]);
setBookmarkedIds(gabungan);
localStorage.setItem(
"bookmarkedIds",
JSON.stringify(Array.from(gabungan))
JSON.stringify(Array.from(gabungan)),
);
}
} catch (err) {
@ -308,7 +318,7 @@ export default function MediaUpdate() {
setBookmarkedIds(updated);
localStorage.setItem(
"bookmarkedIds",
JSON.stringify(Array.from(updated))
JSON.stringify(Array.from(updated)),
);
MySwal.fire({

60
hooks/useMenuAccess.ts Normal file
View File

@ -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 };
}

View File

@ -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 };
}

View File

@ -221,32 +221,66 @@ export function getMenuList(pathname: string, t: any): Group[] {
},
]
: []),
// ...(roleId === 2
// ? [
// {
// groupLabel: "Settings",
// id: "settings",
// menus: [
// {
// id: "settings",
// href: "/admin/settings",
// label: "Settings",
// active: pathname.includes("/settings"),
// icon: "heroicons:cog-6-tooth",
// submenus: [
// {
// href: "/admin/categories",
// label: "Categories",
// active: pathname.includes("/categories"),
// icon: "ic:outline-image",
// children: [],
// },
// ],
// },
// ],
// },
// ]
// : []),
...(Number(roleId) === 1
? [
{
groupLabel: "",
id: "management-user",
menus: [
{
id: "management-user-menu",
href: "/admin/management-user",
label: "Management User",
active: pathname.includes("/management-user"),
icon: "clarity:users-solid",
submenus: [],
},
],
},
{
groupLabel: "",
id: "tenant",
menus: [
{
id: "tenant",
href: "/admin/tenants",
label: "Tenant",
active: pathname.includes("/tenant"),
icon: "material-symbols:domain",
submenus: [],
},
],
},
{
groupLabel: "",
id: "menu-management",
menus: [
{
id: "menu-management",
href: "/admin/settings/menu-management",
label: "Menu Management",
active: pathname === "/admin/settings/menu-management",
icon: "heroicons:bars-3",
submenus: [],
},
],
},
// {
// groupLabel: "",
// id: "module-management",
// menus: [
// {
// id: "module-management",
// href: "/admin/settings/module-management",
// label: "Module Management",
// active: pathname === "/admin/settings/module-management",
// icon: "heroicons:puzzle-piece",
// submenus: [],
// },
// ],
// },
]
: []),
];
return menusSelected;
}

41455
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,13 +3,38 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"dev": "next dev",
"dev:turbo": "cross-env CSS_TRANSFORMER_WASM=1 next dev --turbopack",
"build": "cross-env CSS_TRANSFORMER_WASM=1 next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@ckeditor/ckeditor5-alignment": "^47.4.0",
"@ckeditor/ckeditor5-autoformat": "^47.4.0",
"@ckeditor/ckeditor5-basic-styles": "^47.4.0",
"@ckeditor/ckeditor5-block-quote": "^47.4.0",
"@ckeditor/ckeditor5-cloud-services": "^47.4.0",
"@ckeditor/ckeditor5-code-block": "^47.4.0",
"@ckeditor/ckeditor5-core": "^47.4.0",
"@ckeditor/ckeditor5-editor-classic": "^47.4.0",
"@ckeditor/ckeditor5-essentials": "^47.4.0",
"@ckeditor/ckeditor5-font": "^47.4.0",
"@ckeditor/ckeditor5-heading": "^47.4.0",
"@ckeditor/ckeditor5-image": "^47.4.0",
"@ckeditor/ckeditor5-indent": "^47.4.0",
"@ckeditor/ckeditor5-link": "^47.4.0",
"@ckeditor/ckeditor5-list": "^47.4.0",
"@ckeditor/ckeditor5-media-embed": "^47.4.0",
"@ckeditor/ckeditor5-paragraph": "^47.4.0",
"@ckeditor/ckeditor5-paste-from-office": "^47.4.0",
"@ckeditor/ckeditor5-react": "^10.0.0",
"@ckeditor/ckeditor5-source-editing": "^47.4.0",
"@ckeditor/ckeditor5-table": "^47.4.0",
"@ckeditor/ckeditor5-typing": "^47.4.0",
"@ckeditor/ckeditor5-undo": "^47.4.0",
"@ckeditor/ckeditor5-upload": "^47.4.0",
"@ckeditor/ckeditor5-utils": "^47.4.0",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
@ -74,6 +99,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"cookie": "^1.0.2",
"cookies-next": "^6.1.1",
"crypto-js": "^4.2.0",
"date-fns": "^3.6.0",
"dayjs": "^1.11.11",
@ -88,6 +114,7 @@
"js-cookie": "^3.0.5",
"jspdf": "^3.0.1",
"leaflet": "^1.9.4",
"lightningcss": "^1.30.2",
"lucide-react": "^0.525.0",
"moment": "^2.30.1",
"next": "^15.3.5",
@ -140,7 +167,6 @@
"devDependencies": {
"@dnd-kit/utilities": "^3.2.2",
"@next/bundle-analyzer": "^15.0.3",
"@tailwindcss/postcss": "^4",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
@ -158,6 +184,7 @@
"@types/react-geocode": "^0.2.4",
"@types/rtl-detect": "^1.0.3",
"@types/sizzle": "^2.3.10",
"autoprefixer": "^10.4.19",
"cross-env": "^7.0.3",
"d3-shape": "^3.2.0",
"eslint": "^8",
@ -165,8 +192,11 @@
"jest": "^30.0.4",
"jest-environment-jsdom": "^30.0.4",
"postcss": "^8",
"tailwindcss": "^4",
"tailwindcss": "^3.4.14",
"tw-animate-css": "^1.3.5",
"typescript": "^5"
},
"optionalDependencies": {
"lightningcss-win32-x64-msvc": "^1.30.2"
}
}

View File

@ -1,7 +1,18 @@
// /** @type {import('postcss-load-config').Config} */
// const config = {
// plugins: {
// '@tailwindcss/postcss': {},
// },
// };
// export default config;
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
'@tailwindcss/postcss': {},
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -1,7 +1,7 @@
import axios from "axios";
// Use environment variable for API URL, default to localhost:8080 for local development
const baseURL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080/api/";
const baseURL = process.env.NEXT_PUBLIC_API_URL || "https://kontenhumas.com/api/";
const axiosBaseInstance = axios.create({
baseURL,

View File

@ -3,7 +3,7 @@ import Cookies from "js-cookie";
import { getCsrfToken, login } from "../auth";
// Use environment variable for API URL, default to localhost:8080 for local development
const baseURL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080/api/";
const baseURL = process.env.NEXT_PUBLIC_API_URL || "https://kontenhumas.com/api/";
const refreshToken = Cookies.get("refresh_token");

96
service/menu-actions.ts Normal file
View File

@ -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);
}

View File

@ -26,7 +26,7 @@ export interface MasterModule {
name: string;
description: string;
pathUrl: string;
actionType: string;
actionType?: string;
statusId: number;
isActive: boolean;
createdAt?: string;
@ -147,7 +147,7 @@ export async function getMasterMenuById(id: number) {
export async function createMasterMenu(data: {
name: string;
description: string;
moduleId: number;
moduleId?: number;
group: string;
statusId: number;
parentMenuId?: number;
@ -160,7 +160,7 @@ export async function createMasterMenu(data: {
export async function updateMasterMenu(id: number, data: {
name: string;
description: string;
moduleId: number;
moduleId?: number;
group: string;
statusId: number;
parentMenuId?: number;
@ -203,8 +203,9 @@ export async function createMasterModule(data: {
name: string;
description: string;
pathUrl: string;
actionType: string;
actionType?: string;
statusId: number;
menuIds?: number[];
}) {
const url = "master-modules";
return httpPostInterceptor(url, data);
@ -214,8 +215,9 @@ export async function updateMasterModule(id: number, data: {
name: string;
description: string;
pathUrl: string;
actionType: string;
actionType?: string;
statusId: number;
menuIds?: number[];
}) {
const url = `master-modules/${id}`;
return httpPutInterceptor(url, data);

View File

@ -1,4 +1,9 @@
import { httpDeleteInterceptor, httpGetInterceptor, httpPutInterceptor } from "./http-config/http-interceptor-service";
import {
httpDeleteInterceptor,
httpGetInterceptor,
httpPutInterceptor,
httpPostInterceptor,
} from "./http-config/http-interceptor-service";
export async function deleteUserLevel(id: number) {
const url = `user-levels/${id}`;
@ -14,9 +19,106 @@ export async function getUserLevelDetail(id: number) {
const url = `user-levels/${id}`;
return httpGetInterceptor(url);
}
export async function getTenantList() {
const url = `clients?limit=1000`;
// Tenant/Client Types
export interface Tenant {
id: string;
name: string;
slug: string;
description?: string;
clientType: "parent_client" | "sub_client" | "standalone";
parentClientId?: string;
logoUrl?: string;
logoImagePath?: string;
address?: string;
phoneNumber?: string;
website?: string;
maxUsers?: number;
maxStorage?: number;
currentUsers?: number;
currentStorage?: number;
settings?: string;
isActive?: boolean;
createdAt?: string;
updatedAt?: string;
parentClient?: {
id: string;
name: string;
};
subClients?: Array<{
id: string;
name: string;
}>;
subClientCount?: number;
}
export interface TenantCreateRequest {
name: string;
description?: string;
clientType: "parent_client" | "sub_client" | "standalone";
parentClientId?: string;
maxUsers?: number;
maxStorage?: number;
settings?: string;
address?: string;
phoneNumber?: string;
website?: string;
}
export interface TenantUpdateRequest {
name?: string;
description?: string;
clientType?: "parent_client" | "sub_client" | "standalone";
parentClientId?: string;
maxUsers?: number;
maxStorage?: number;
settings?: string;
isActive?: boolean;
logoUrl?: string;
logoImagePath?: string;
address?: string;
phoneNumber?: string;
website?: string;
}
// Tenant API Functions
export async function getTenantList(params?: {
name?: string;
clientType?: string;
parentClientId?: string;
isActive?: boolean;
page?: number;
limit?: number;
}) {
const queryParams = new URLSearchParams();
if (params?.name) queryParams.append("name", params.name);
if (params?.clientType) queryParams.append("clientType", params.clientType);
if (params?.parentClientId) queryParams.append("parentClientId", params.parentClientId);
if (params?.isActive !== undefined) queryParams.append("isActive", params.isActive.toString());
if (params?.page) queryParams.append("page", params.page.toString());
if (params?.limit) queryParams.append("limit", params.limit.toString());
const url = `clients${queryParams.toString() ? `?${queryParams.toString()}` : ""}`;
return httpGetInterceptor(url);
}
export async function getTenantById(id: string) {
const url = `clients/${id}`;
return httpGetInterceptor(url);
}
export async function createTenant(data: TenantCreateRequest) {
const url = "clients";
return httpPostInterceptor(url, data);
}
export async function updateTenant(id: string, data: TenantUpdateRequest) {
const url = `clients/${id}`;
return httpPutInterceptor(url, data);
}
export async function deleteTenant(id: string) {
const url = `clients/${id}`;
return httpDeleteInterceptor(url);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -16,7 +16,7 @@
"plugins": [
{
"name": "next"
}
}
],
"paths": {
"@/*": ["./*"]

View File

@ -2,7 +2,7 @@
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
import type Editor from './editor/editor.js';
export declare const DEFAULT_GROUP_ID: "common";
/**
* A common namespace for various accessibility features of the editor.

View File

@ -6,7 +6,7 @@
* @module core/command
*/
import { type DecoratedMethodEvent } from '@ckeditor/ckeditor5-utils';
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
import type Editor from './editor/editor.js';
declare const Command_base: {
new (): import("@ckeditor/ckeditor5-utils").Observable;
prototype: import("@ckeditor/ckeditor5-utils").Observable;

View File

@ -2,7 +2,7 @@
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import type Command from '@ckeditor/ckeditor5-core/src/command.js';
import type Command from './command.js';
/**
* Collection of commands. Its instance is available in {@link module:core/editor/editor~Editor#commands `editor.commands`}.
*/

View File

@ -6,10 +6,10 @@
* @module core/context
*/
import { Config, Collection, Locale, type LocaleTranslate } from '@ckeditor/ckeditor5-utils';
import PluginCollection from '@ckeditor/ckeditor5-core/src/plugincollection.js';
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
import type { LoadedPlugins, PluginConstructor } from '@ckeditor/ckeditor5-core/src/plugin.js';
import type { EditorConfig } from '@ckeditor/ckeditor5-core/src/editor/editorconfig.js';
import PluginCollection from './plugincollection.js';
import type Editor from './editor/editor.js';
import type { LoadedPlugins, PluginConstructor } from './plugin.js';
import type { EditorConfig } from './editor/editorconfig.js';
/**
* Provides a common, higher-level environment for solutions that use multiple {@link module:core/editor/editor~Editor editors}
* or plugins that work outside the editor. Use it instead of {@link module:core/editor/editor~Editor.create `Editor.create()`}

View File

@ -6,11 +6,11 @@
* @module core/contextplugin
*/
import { type Collection, type Config, type Locale, type LocaleTranslate } from '@ckeditor/ckeditor5-utils';
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
import type { EditorConfig } from '@ckeditor/ckeditor5-core/src/editor/editorconfig.js';
import type Context from '@ckeditor/ckeditor5-core/src/context.js';
import type { PluginDependencies, PluginInterface } from '@ckeditor/ckeditor5-core/src/plugin.js';
import type PluginCollection from '@ckeditor/ckeditor5-core/src/plugincollection.js';
import type Editor from './editor/editor.js';
import type { EditorConfig } from './editor/editorconfig.js';
import type Context from './context.js';
import type { PluginDependencies, PluginInterface } from './plugin.js';
import type PluginCollection from './plugincollection.js';
declare const ContextPlugin_base: {
new (): import("@ckeditor/ckeditor5-utils").Observable;
prototype: import("@ckeditor/ckeditor5-utils").Observable;

View File

@ -6,7 +6,7 @@
* @module core/editingkeystrokehandler
*/
import { KeystrokeHandler, type PriorityString } from '@ckeditor/ckeditor5-utils';
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
import type Editor from './editor/editor.js';
/**
* A keystroke handler for editor editing. Its instance is available
* in {@link module:core/editor/editor~Editor#keystrokes} so plugins

View File

@ -8,13 +8,13 @@
import { Config, type Locale, type LocaleTranslate } from '@ckeditor/ckeditor5-utils';
import { Conversion, DataController, EditingController, Model } from '@ckeditor/ckeditor5-engine';
import type { EditorUI } from '@ckeditor/ckeditor5-ui';
import Context from '@ckeditor/ckeditor5-core/src/context.js';
import PluginCollection from '@ckeditor/ckeditor5-core/src/plugincollection.js';
import CommandCollection, { type CommandsMap } from '@ckeditor/ckeditor5-core/src/commandcollection.js';
import EditingKeystrokeHandler from '@ckeditor/ckeditor5-core/src/editingkeystrokehandler.js';
import Accessibility from '@ckeditor/ckeditor5-core/src/accessibility.js';
import type { LoadedPlugins, PluginConstructor } from '@ckeditor/ckeditor5-core/src/plugin.js';
import type { EditorConfig } from '@ckeditor/ckeditor5-core/src/editor/editorconfig.js';
import Context from '../context.js';
import PluginCollection from '../plugincollection.js';
import CommandCollection, { type CommandsMap } from '../commandcollection.js';
import EditingKeystrokeHandler from '../editingkeystrokehandler.js';
import Accessibility from '../accessibility.js';
import type { LoadedPlugins, PluginConstructor } from '../plugin.js';
import type { EditorConfig } from './editorconfig.js';
declare const Editor_base: {
new (): import("@ckeditor/ckeditor5-utils").Observable;
prototype: import("@ckeditor/ckeditor5-utils").Observable;

View File

@ -6,9 +6,9 @@
* @module core/editor/editorconfig
*/
import type { ArrayOrItem, Translations } from '@ckeditor/ckeditor5-utils';
import type Context from '@ckeditor/ckeditor5-core/src/context.js';
import type { PluginConstructor } from '@ckeditor/ckeditor5-core/src/plugin.js';
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
import type Context from '../context.js';
import type { PluginConstructor } from '../plugin.js';
import type Editor from './editor.js';
import type { MenuBarConfig } from '@ckeditor/ckeditor5-ui';
/**
* CKEditor configuration options.

View File

@ -2,8 +2,8 @@
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import type { default as Editor } from '@ckeditor/ckeditor5-core/src/editor/editor.js';
import type { ElementApi } from '@ckeditor/ckeditor5-core/src/editor/utils/elementapimixin.js';
import type { default as Editor } from '../editor.js';
import type { ElementApi } from './elementapimixin.js';
/**
* Checks if the editor is initialized on a `<textarea>` element that belongs to a form. If yes, it updates the editor's element
* content before submitting the form.

View File

@ -5,7 +5,7 @@
/**
* @module core/editor/utils/dataapimixin
*/
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
import type Editor from '../editor.js';
import type { Constructor } from '@ckeditor/ckeditor5-utils';
/**
* Implementation of the {@link module:core/editor/utils/dataapimixin~DataApi}.

View File

@ -6,7 +6,7 @@
* @module core/editor/utils/elementapimixin
*/
import { type Constructor, type Mixed } from '@ckeditor/ckeditor5-utils';
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
import type Editor from '../editor.js';
/**
* Implementation of the {@link module:core/editor/utils/elementapimixin~ElementApi}.
*/

View File

@ -2,7 +2,7 @@
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import type { default as Editor } from '@ckeditor/ckeditor5-core/src/editor/editor.js';
import type { default as Editor } from '../editor.js';
/**
* Marks the source element on which the editor was initialized. This prevents other editor instances from using this element.
*

View File

@ -5,23 +5,23 @@
/**
* @module core
*/
export { default as Plugin, type PluginDependencies, type PluginConstructor } from '@ckeditor/ckeditor5-core/src/plugin.js';
export { default as Command, type CommandExecuteEvent } from '@ckeditor/ckeditor5-core/src/command.js';
export { default as MultiCommand } from '@ckeditor/ckeditor5-core/src/multicommand.js';
export type { CommandsMap } from '@ckeditor/ckeditor5-core/src/commandcollection.js';
export type { PluginsMap, default as PluginCollection } from '@ckeditor/ckeditor5-core/src/plugincollection.js';
export { default as Context, type ContextConfig } from '@ckeditor/ckeditor5-core/src/context.js';
export { default as ContextPlugin, type ContextPluginDependencies } from '@ckeditor/ckeditor5-core/src/contextplugin.js';
export { type EditingKeystrokeCallback } from '@ckeditor/ckeditor5-core/src/editingkeystrokehandler.js';
export type { PartialBy, NonEmptyArray } from '@ckeditor/ckeditor5-core/src/typings.js';
export { default as Editor, type EditorReadyEvent, type EditorDestroyEvent } from '@ckeditor/ckeditor5-core/src/editor/editor.js';
export type { EditorConfig, LanguageConfig, ToolbarConfig, ToolbarConfigItem, UiConfig } from '@ckeditor/ckeditor5-core/src/editor/editorconfig.js';
export { default as attachToForm } from '@ckeditor/ckeditor5-core/src/editor/utils/attachtoform.js';
export { default as DataApiMixin, type DataApi } from '@ckeditor/ckeditor5-core/src/editor/utils/dataapimixin.js';
export { default as ElementApiMixin, type ElementApi } from '@ckeditor/ckeditor5-core/src/editor/utils/elementapimixin.js';
export { default as secureSourceElement } from '@ckeditor/ckeditor5-core/src/editor/utils/securesourceelement.js';
export { default as PendingActions, type PendingAction } from '@ckeditor/ckeditor5-core/src/pendingactions.js';
export type { KeystrokeInfos as KeystrokeInfoDefinitions, KeystrokeInfoGroup as KeystrokeInfoGroupDefinition, KeystrokeInfoCategory as KeystrokeInfoCategoryDefinition, KeystrokeInfoDefinition as KeystrokeInfoDefinition } from '@ckeditor/ckeditor5-core/src/accessibility.js';
export { default as Plugin, type PluginDependencies, type PluginConstructor } from './plugin.js';
export { default as Command, type CommandExecuteEvent } from './command.js';
export { default as MultiCommand } from './multicommand.js';
export type { CommandsMap } from './commandcollection.js';
export type { PluginsMap, default as PluginCollection } from './plugincollection.js';
export { default as Context, type ContextConfig } from './context.js';
export { default as ContextPlugin, type ContextPluginDependencies } from './contextplugin.js';
export { type EditingKeystrokeCallback } from './editingkeystrokehandler.js';
export type { PartialBy, NonEmptyArray } from './typings.js';
export { default as Editor, type EditorReadyEvent, type EditorDestroyEvent } from './editor/editor.js';
export type { EditorConfig, LanguageConfig, ToolbarConfig, ToolbarConfigItem, UiConfig } from './editor/editorconfig.js';
export { default as attachToForm } from './editor/utils/attachtoform.js';
export { default as DataApiMixin, type DataApi } from './editor/utils/dataapimixin.js';
export { default as ElementApiMixin, type ElementApi } from './editor/utils/elementapimixin.js';
export { default as secureSourceElement } from './editor/utils/securesourceelement.js';
export { default as PendingActions, type PendingAction } from './pendingactions.js';
export type { KeystrokeInfos as KeystrokeInfoDefinitions, KeystrokeInfoGroup as KeystrokeInfoGroupDefinition, KeystrokeInfoCategory as KeystrokeInfoCategoryDefinition, KeystrokeInfoDefinition as KeystrokeInfoDefinition } from './accessibility.js';
export declare const icons: {
bold: string;
cancel: string;
@ -86,4 +86,4 @@ export declare const icons: {
outdent: string;
table: string;
};
import '@ckeditor/ckeditor5-core/src/augmentation.js';
import './augmentation.js';

View File

@ -5,7 +5,7 @@
/**
* @module core/multicommand
*/
import Command from '@ckeditor/ckeditor5-core/src/command.js';
import Command from './command.js';
import { type PriorityString } from '@ckeditor/ckeditor5-utils';
/**
* A CKEditor command that aggregates other commands.

View File

@ -5,7 +5,7 @@
/**
* @module core/pendingactions
*/
import ContextPlugin from '@ckeditor/ckeditor5-core/src/contextplugin.js';
import ContextPlugin from './contextplugin.js';
import { type CollectionAddEvent, type CollectionRemoveEvent, type Observable } from '@ckeditor/ckeditor5-utils';
/**
* The list of pending editor actions.

View File

@ -2,7 +2,7 @@
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import type Editor from '@ckeditor/ckeditor5-core/src/editor/editor.js';
import type Editor from './editor/editor.js';
declare const Plugin_base: {
new (): import("@ckeditor/ckeditor5-utils").Observable;
prototype: import("@ckeditor/ckeditor5-utils").Observable;

View File

@ -2,7 +2,7 @@
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import type { LoadedPlugins, PluginClassConstructor, PluginConstructor, PluginInterface } from '@ckeditor/ckeditor5-core/src/plugin.js';
import type { LoadedPlugins, PluginClassConstructor, PluginConstructor, PluginInterface } from './plugin.js';
declare const PluginCollection_base: {
new (): import("@ckeditor/ckeditor5-utils").Emitter;
prototype: import("@ckeditor/ckeditor5-utils").Emitter;

View File

@ -3,8 +3,13 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
declare const Collection_base: {
<<<<<<< HEAD
new (): import("@ckeditor/ckeditor5-utils/src/emittermixin.js").Emitter;
prototype: import("@ckeditor/ckeditor5-utils/src/emittermixin.js").Emitter;
=======
new (): import("./emittermixin.js").Emitter;
prototype: import("./emittermixin.js").Emitter;
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
};
/**
* Collections are ordered sets of objects. Items in the collection can be retrieved by their indexes

View File

@ -22,7 +22,11 @@
*/
declare function diff<T>(a: ArrayLike<T>, b: ArrayLike<T>, cmp?: (a: T, b: T) => boolean): Array<DiffResult>;
declare namespace diff {
<<<<<<< HEAD
var fastDiff: typeof import("@ckeditor/ckeditor5-utils/src/fastdiff.js").default;
=======
var fastDiff: typeof import("./fastdiff.js").default;
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
}
export default diff;
/**

View File

@ -2,7 +2,11 @@
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
<<<<<<< HEAD
import type { DiffResult } from '@ckeditor/ckeditor5-utils/src/diff.js';
=======
import type { DiffResult } from './diff.js';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
/**
* @module utils/difftochanges
*/

View File

@ -5,9 +5,15 @@
/**
* @module utils/dom/emittermixin
*/
<<<<<<< HEAD
import { type Emitter, type CallbackOptions, type BaseEvent, type GetCallback } from '@ckeditor/ckeditor5-utils/src/emittermixin.js';
import type EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo.js';
import type { Constructor, Mixed } from '@ckeditor/ckeditor5-utils/src/mix.js';
=======
import { type Emitter, type CallbackOptions, type BaseEvent, type GetCallback } from '../emittermixin.js';
import type EventInfo from '../eventinfo.js';
import type { Constructor, Mixed } from '../mix.js';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
/**
* Mixin that injects the DOM events API into its host. It provides the API
* compatible with {@link module:utils/emittermixin~Emitter}.

View File

@ -2,7 +2,11 @@
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
<<<<<<< HEAD
import Rect, { type RectSource } from '@ckeditor/ckeditor5-utils/src/dom/rect.js';
=======
import Rect, { type RectSource } from './rect.js';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
/**
* Calculates the `position: absolute` coordinates of a given element so it can be positioned with respect to the
* target in the visually most efficient way, taking various restrictions like viewport or limiter geometry

View File

@ -5,10 +5,17 @@
/**
* @module utils/emittermixin
*/
<<<<<<< HEAD
import EventInfo from '@ckeditor/ckeditor5-utils/src/eventinfo.js';
import { type PriorityString } from '@ckeditor/ckeditor5-utils/src/priorities.js';
import type { Constructor, Mixed } from '@ckeditor/ckeditor5-utils/src/mix.js';
import '@ckeditor/ckeditor5-utils/src/version.js';
=======
import EventInfo from './eventinfo.js';
import { type PriorityString } from './priorities.js';
import type { Constructor, Mixed } from './mix.js';
import './version.js';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
/**
* Mixin that injects the {@link ~Emitter events API} into its host.
*

View File

@ -2,8 +2,13 @@
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
<<<<<<< HEAD
import type { DiffResult } from '@ckeditor/ckeditor5-utils/src/diff.js';
import type { Change } from '@ckeditor/ckeditor5-utils/src/difftochanges.js';
=======
import type { DiffResult } from './diff.js';
import type { Change } from './difftochanges.js';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
/**
* @module utils/fastdiff
*/

View File

@ -2,10 +2,17 @@
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
<<<<<<< HEAD
declare const FocusTracker_base: import("@ckeditor/ckeditor5-utils/src/mix.js").Mixed<{
new (): import("@ckeditor/ckeditor5-utils/src/observablemixin.js").Observable;
prototype: import("@ckeditor/ckeditor5-utils/src/observablemixin.js").Observable;
}, import("@ckeditor/ckeditor5-utils/src/dom/emittermixin.js").DomEmitter>;
=======
declare const FocusTracker_base: import("./mix.js").Mixed<{
new (): import("./observablemixin.js").Observable;
prototype: import("./observablemixin.js").Observable;
}, import("./dom/emittermixin.js").DomEmitter>;
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
/**
* Allows observing a group of `Element`s whether at least one of them is focused.
*

View File

@ -5,6 +5,7 @@
/**
* @module utils
*/
<<<<<<< HEAD
export { default as env } from '@ckeditor/ckeditor5-utils/src/env.js';
export { default as diff, type DiffResult } from '@ckeditor/ckeditor5-utils/src/diff.js';
export { default as fastDiff } from '@ckeditor/ckeditor5-utils/src/fastdiff.js';
@ -62,3 +63,62 @@ export { default as verifyLicense } from '@ckeditor/ckeditor5-utils/src/verifyli
export { default as wait } from '@ckeditor/ckeditor5-utils/src/wait.js';
export * from '@ckeditor/ckeditor5-utils/src/unicode.js';
export { default as version, releaseDate } from '@ckeditor/ckeditor5-utils/src/version.js';
=======
export { default as env } from './env.js';
export { default as diff, type DiffResult } from './diff.js';
export { default as fastDiff } from './fastdiff.js';
export { default as diffToChanges } from './difftochanges.js';
export { default as mix } from './mix.js';
export type { Constructor, Mixed } from './mix.js';
export { default as EmitterMixin, type Emitter, type BaseEvent, type CallbackOptions, type EmitterMixinDelegateChain, type GetCallback, type GetCallbackOptions, type GetEventInfo, type GetNameOrEventInfo } from './emittermixin.js';
export { default as EventInfo } from './eventinfo.js';
export { default as ObservableMixin, type Observable, type DecoratedMethodEvent, type ObservableChangeEvent, type ObservableSetEvent } from './observablemixin.js';
export { default as CKEditorError, logError, logWarning } from './ckeditorerror.js';
export { default as ElementReplacer } from './elementreplacer.js';
export { default as abortableDebounce, type AbortableFunc } from './abortabledebounce.js';
export { default as count } from './count.js';
export { default as compareArrays } from './comparearrays.js';
export { default as createElement } from './dom/createelement.js';
export { default as Config } from './config.js';
export { default as isIterable } from './isiterable.js';
export { default as DomEmitterMixin, type DomEmitter } from './dom/emittermixin.js';
export { default as findClosestScrollableAncestor } from './dom/findclosestscrollableancestor.js';
export { default as global } from './dom/global.js';
export { default as getAncestors } from './dom/getancestors.js';
export { default as getDataFromElement } from './dom/getdatafromelement.js';
export { default as getBorderWidths } from './dom/getborderwidths.js';
export { default as isText } from './dom/istext.js';
export { default as Rect, type RectSource } from './dom/rect.js';
export { default as ResizeObserver } from './dom/resizeobserver.js';
export { default as setDataInElement } from './dom/setdatainelement.js';
export { default as toUnit } from './dom/tounit.js';
export { default as indexOf } from './dom/indexof.js';
export { default as insertAt } from './dom/insertat.js';
export { default as isComment } from './dom/iscomment.js';
export { default as isNode } from './dom/isnode.js';
export { default as isRange } from './dom/isrange.js';
export { default as isValidAttributeName } from './dom/isvalidattributename.js';
export { default as isVisible } from './dom/isvisible.js';
export { getOptimalPosition, type Options as PositionOptions, type PositioningFunction, type DomPoint } from './dom/position.js';
export { default as remove } from './dom/remove.js';
export * from './dom/scroll.js';
export * from './keyboard.js';
export * from './language.js';
export { default as Locale, type LocaleTranslate, type Translations } from './locale.js';
export { default as Collection, type CollectionAddEvent, type CollectionChangeEvent, type CollectionRemoveEvent } from './collection.js';
export { default as first } from './first.js';
export { default as FocusTracker } from './focustracker.js';
export { default as KeystrokeHandler } from './keystrokehandler.js';
export { default as toArray, type ArrayOrItem, type ReadonlyArrayOrItem } from './toarray.js';
export { default as toMap } from './tomap.js';
export { default as priorities, type PriorityString } from './priorities.js';
export { default as retry, exponentialDelay } from './retry.js';
export { default as insertToPriorityArray } from './inserttopriorityarray.js';
export { default as spliceArray } from './splicearray.js';
export { default as uid } from './uid.js';
export { default as delay, type DelayedFunc } from './delay.js';
export { default as verifyLicense } from './verifylicense.js';
export { default as wait } from './wait.js';
export * from './unicode.js';
export { default as version, releaseDate } from './version.js';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae

View File

@ -2,7 +2,11 @@
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
<<<<<<< HEAD
import { type PriorityString } from '@ckeditor/ckeditor5-utils/src/priorities.js';
=======
import { type PriorityString } from './priorities.js';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
/**
* @module utils/inserttopriorityarray
*/

View File

@ -7,7 +7,11 @@
*
* @module utils/keyboard
*/
<<<<<<< HEAD
import type { LanguageDirection } from '@ckeditor/ckeditor5-utils/src/language.js';
=======
import type { LanguageDirection } from './language.js';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
/**
* An object with `keyName => keyCode` pairs for a set of known keys.
*

View File

@ -2,9 +2,15 @@
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
<<<<<<< HEAD
import type { Emitter } from '@ckeditor/ckeditor5-utils/src/emittermixin.js';
import { type KeystrokeInfo } from '@ckeditor/ckeditor5-utils/src/keyboard.js';
import type { PriorityString } from '@ckeditor/ckeditor5-utils/src/priorities.js';
=======
import type { Emitter } from './emittermixin.js';
import { type KeystrokeInfo } from './keyboard.js';
import type { PriorityString } from './priorities.js';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
/**
* Keystroke handler allows registering callbacks for given keystrokes.
*

View File

@ -5,9 +5,15 @@
/**
* @module utils/locale
*/
<<<<<<< HEAD
import { type ArrayOrItem } from '@ckeditor/ckeditor5-utils/src/toarray.js';
import { type Message } from '@ckeditor/ckeditor5-utils/src/translation-service.js';
import { type LanguageDirection } from '@ckeditor/ckeditor5-utils/src/language.js';
=======
import { type ArrayOrItem } from './toarray.js';
import { type Message } from './translation-service.js';
import { type LanguageDirection } from './language.js';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
/**
* Represents the localization services.
*/

View File

@ -5,8 +5,13 @@
/**
* @module utils/observablemixin
*/
<<<<<<< HEAD
import { type Emitter } from '@ckeditor/ckeditor5-utils/src/emittermixin.js';
import type { Constructor, Mixed } from '@ckeditor/ckeditor5-utils/src/mix.js';
=======
import { type Emitter } from './emittermixin.js';
import type { Constructor, Mixed } from './mix.js';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
/**
* A mixin that injects the "observable properties" and data binding functionality described in the
* {@link ~Observable} interface.

View File

@ -5,8 +5,13 @@
/**
* @module utils/translation-service
*/
<<<<<<< HEAD
import type { Translations } from '@ckeditor/ckeditor5-utils/src/locale.js';
import { type ArrayOrItem } from '@ckeditor/ckeditor5-utils/src/toarray.js';
=======
import type { Translations } from './locale.js';
import { type ArrayOrItem } from './toarray.js';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
declare global {
var CKEDITOR_TRANSLATIONS: Translations;
}

View File

@ -5,10 +5,17 @@
/**
* @module widget/widgetresize
*/
<<<<<<< HEAD
import Resizer from '@ckeditor/ckeditor5-widget/src/widgetresize/resizer.js';
import { Plugin, type Editor } from '@ckeditor/ckeditor5-core';
import { type Element, type ViewContainerElement } from '@ckeditor/ckeditor5-engine';
import '@ckeditor/ckeditor5-widget/theme/widgetresize.css';
=======
import Resizer from './widgetresize/resizer.js';
import { Plugin, type Editor } from '@ckeditor/ckeditor5-core';
import { type Element, type ViewContainerElement } from '@ckeditor/ckeditor5-engine';
import '../theme/widgetresize.css';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
/**
* The widget resize feature plugin.
*

View File

@ -3,8 +3,13 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import { Rect, type DecoratedMethodEvent } from '@ckeditor/ckeditor5-utils';
<<<<<<< HEAD
import ResizeState from '@ckeditor/ckeditor5-widget/src/widgetresize/resizerstate.js';
import type { ResizerOptions } from '@ckeditor/ckeditor5-widget/src/widgetresize.js';
=======
import ResizeState from './resizerstate.js';
import type { ResizerOptions } from '../widgetresize.js';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
declare const Resizer_base: {
new (): import("@ckeditor/ckeditor5-utils").Observable;
prototype: import("@ckeditor/ckeditor5-utils").Observable;

View File

@ -2,7 +2,11 @@
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
<<<<<<< HEAD
import type { ResizerOptions } from '@ckeditor/ckeditor5-widget/src/widgetresize.js';
=======
import type { ResizerOptions } from '../widgetresize.js';
>>>>>>> b6bde7722e19d08349e7b7b7d23bc6a4aef9d5ae
declare const ResizeState_base: {
new (): import("@ckeditor/ckeditor5-utils").Observable;
prototype: import("@ckeditor/ckeditor5-utils").Observable;