ignore folder

This commit is contained in:
2026-04-21 06:30:48 +07:00
commit ca00b36f19
70 changed files with 3871 additions and 0 deletions

View File

@ -0,0 +1,85 @@
"use client";
import { useEffect, useState } from "react";
import { getAudit } from "@/services/audit";
import { usePermissions } from "@/hooks/usePermissions";
import { useApiStore } from "@/store/uiStore";
import { useLocaleStore } from "@/store/uiStore";
import DataTable from "@/components/ui/Table";
import PageHeader from "@/components/ui/PageHeader";
import EmptyState from "@/components/ui/EmptyState";
import Spinner from "@/components/ui/Spinner";
import { AuditItem } from "@/types/api";
import { t } from "@/lib/locale";
export default function AuditPage() {
const permissions = usePermissions();
const addToast = useApiStore((s) => s.addToast);
const locale = useLocaleStore((s) => s.locale);
const [items, setItems] = useState<AuditItem[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const load = async () => {
if (!permissions.isAdmin) {
setLoading(false);
return;
}
setLoading(true);
try {
const data = await getAudit(50);
setItems(data);
} catch (error) {
addToast((error as { message?: string })?.message || t("loadFailed", locale), "error");
} finally {
setLoading(false);
}
};
load();
}, [permissions.isAdmin, addToast, locale]);
if (!permissions.isAdmin) {
return (
<div className="alert alert-warning">
{t("forbidden", locale)}
</div>
);
}
return (
<main className="vstack gap-3">
<PageHeader
title={t("audit", locale)}
breadcrumb={[
{ label: t("dashboard", locale), href: "/dashboard" },
{ label: t("audit", locale), href: "/dashboard/audit" }
]}
/>
{loading && <Spinner label={t("loading", locale)} />}
{!loading && items.length === 0 ? (
<EmptyState
title={t("noData", locale)}
description={t("noData", locale)}
/>
) : (
<div className="card page-card">
<div className="card-body">
<DataTable
columns={[
{ key: "id", header: "Id" },
{ key: "actor", header: "Actor" },
{ key: "action", header: "Action" },
{ key: "resourceType", header: "Resource Type" },
{ key: "resourceId", header: "Resource Id" },
{ key: "outcome", header: "Outcome" },
{ key: "correlationId", header: "Correlation" },
{ key: "createdAt", header: "Created" }
]}
data={items}
/>
</div>
</div>
)}
</main>
);
}

View File

@ -0,0 +1,32 @@
"use client";
import { useEffect, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import type { ReactNode } from "react";
import DashboardShell from "@/components/layout/DashboardShell";
import { useAuthStore } from "@/store/authStore";
import { useTenantStore } from "@/store/tenantStore";
export default function DashboardLayout({ children }: { children: ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const token = useAuthStore((s) => s.accessToken);
const hydrateAuth = useAuthStore((s) => s.hydrate);
const hydrateTenant = useTenantStore((s) => s.hydrate);
const [ready, setReady] = useState(false);
useEffect(() => {
hydrateAuth();
hydrateTenant();
setReady(true);
}, [hydrateAuth, hydrateTenant]);
useEffect(() => {
if (!ready) return;
if (!token && !pathname.startsWith("/login")) {
router.replace("/login");
}
}, [token, pathname, ready, router]);
return <DashboardShell>{children}</DashboardShell>;
}

View File

@ -0,0 +1,112 @@
"use client";
import { useEffect, useState } from "react";
import { getModules, toggleModule } from "@/services/modules";
import { usePermissions } from "@/hooks/usePermissions";
import { useApiStore, useLocaleStore } from "@/store/uiStore";
import DataTable from "@/components/ui/Table";
import { ModuleItem } from "@/types/api";
import { t } from "@/lib/locale";
import PageHeader from "@/components/ui/PageHeader";
import ConfirmDialog from "@/components/ui/ConfirmDialog";
export default function ModulesPage() {
const locale = useLocaleStore((s) => s.locale);
const permissions = usePermissions();
const addToast = useApiStore((s) => s.addToast);
const [modules, setModules] = useState<ModuleItem[]>([]);
const [pendingModule, setPendingModule] = useState<ModuleItem | null>(null);
const [confirmLoading, setConfirmLoading] = useState(false);
useEffect(() => {
const loadModules = async () => {
if (!permissions.isAdmin) return;
try {
const rows = await getModules();
setModules(rows);
} catch (error) {
addToast((error as { message?: string })?.message || t("loadFailed", locale), "error");
}
};
loadModules();
}, [permissions.isAdmin, addToast, locale]);
const onToggle = (row: ModuleItem) => {
setPendingModule(row);
};
const onConfirmToggle = async () => {
if (!pendingModule) return;
setConfirmLoading(true);
try {
await toggleModule(pendingModule.code, { enabled: !pendingModule.enabled });
setModules((prev) =>
prev.map((item) =>
item.code === pendingModule.code ? { ...item, enabled: !pendingModule.enabled } : item
)
);
addToast(t("moduleToggled", locale), "success");
} catch (error) {
addToast((error as { message?: string })?.message || t("actionFailed", locale), "error");
} finally {
setPendingModule(null);
setConfirmLoading(false);
}
};
if (!permissions.isAdmin) {
return <div className="alert alert-warning">{t("forbidden", locale)}</div>;
}
return (
<main className="vstack gap-3">
<PageHeader
title={t("modules", locale)}
breadcrumb={[
{ label: t("dashboard", locale), href: "/dashboard" },
{ label: t("modules", locale), href: "/dashboard/modules" }
]}
/>
<div className="card page-card">
<div className="card-body">
<DataTable
columns={[
{ key: "code", header: "Code" },
{ key: "name", header: "Name" },
{
key: "enabled",
header: "Enabled",
render: (row) => (
<span className="badge bg-azure">
{row.enabled ? t("enabled", locale) : t("disabled", locale)}
</span>
)
},
{
key: "actions",
header: t("actions", locale),
render: (row) => (
<button className="btn btn-outline-primary btn-sm" onClick={() => onToggle(row)}>
{row.enabled ? t("disable", locale) : t("enable", locale)}
</button>
)
}
]}
data={modules}
/>
</div>
</div>
<ConfirmDialog
open={!!pendingModule}
title={pendingModule ? `${pendingModule.enabled ? t("disable", locale) : t("enable", locale)} ${pendingModule.code}` : ""}
message={t("moduleToggled", locale)}
onConfirm={onConfirmToggle}
onCancel={() => setPendingModule(null)}
confirmLabel={t("save", locale)}
cancelLabel="Cancel"
loading={confirmLoading}
/>
</main>
);
}

84
app/(dashboard)/page.tsx Normal file
View File

@ -0,0 +1,84 @@
"use client";
import { useEffect, useState } from "react";
import { getWorkflowRequests } from "@/services/workflow";
import { getAudit } from "@/services/audit";
import { useApiStore } from "@/store/uiStore";
import { t } from "@/lib/locale";
import { useLocaleStore } from "@/store/uiStore";
import PageHeader from "@/components/ui/PageHeader";
import Spinner from "@/components/ui/Spinner";
type DashboardSummary = {
pendingWorkflows: number;
totalAudits: number;
recentAuditCount: number;
};
export default function DashboardPage() {
const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [loading, setLoading] = useState(false);
const addToast = useApiStore((s) => s.addToast);
const locale = useLocaleStore((s) => s.locale);
useEffect(() => {
const load = async () => {
setLoading(true);
try {
const [pending, audits] = await Promise.all([
getWorkflowRequests({ status: "PENDING", limit: 200 }),
getAudit(50)
]);
setSummary({
pendingWorkflows: pending.length,
totalAudits: audits.length,
recentAuditCount: audits.slice(0, 5).length
});
} catch (error) {
addToast("Failed to load dashboard summary", "error");
} finally {
setLoading(false);
}
};
load();
}, [addToast]);
return (
<main className="vstack gap-3">
<PageHeader
title={t("dashboard", locale)}
description={t("checkerWorkload", locale)}
breadcrumb={[
{ label: t("dashboard", locale), href: "/dashboard" }
]}
/>
{loading && <Spinner label={t("loading", locale)} />}
<div className="row g-3">
<section className="col-12 col-md-4">
<div className="card page-card">
<div className="card-body">
<h3 className="card-title h5">{t("pendingWorkflows", locale)}</h3>
<p className="display-6 fw-bold">{summary?.pendingWorkflows ?? "--"}</p>
</div>
</div>
</section>
<section className="col-12 col-md-4">
<div className="card page-card">
<div className="card-body">
<h3 className="card-title h5">{t("auditSnapshots", locale)}</h3>
<p className="display-6 fw-bold">{summary?.totalAudits ?? "--"}</p>
</div>
</div>
</section>
<section className="col-12 col-md-4">
<div className="card page-card">
<div className="card-body">
<h3 className="card-title h5">{t("recentAudits", locale)}</h3>
<p className="display-6 fw-bold">{summary?.recentAuditCount ?? "--"}</p>
</div>
</div>
</section>
</div>
</main>
);
}

View File

@ -0,0 +1,135 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { createRoleRequest, updateRolePermissionRequest } from "@/services/roles";
import { getWorkflowRequests } from "@/services/workflow";
import { useApiStore, useLocaleStore } from "@/store/uiStore";
import { usePermissions } from "@/hooks/usePermissions";
import { WorkflowRequestItem, WorkflowResourceType } from "@/types/api";
import RoleCreateForm from "@/components/role/RoleCreateForm";
import RolePermissionForm from "@/components/role/RolePermissionForm";
import { t } from "@/lib/locale";
import DataTable from "@/components/ui/Table";
import StatusBadge from "@/components/workflow/StatusBadge";
import PageHeader from "@/components/ui/PageHeader";
import EmptyState from "@/components/ui/EmptyState";
const RESOURCE_TYPE = WorkflowResourceType.ROLE_MANAGEMENT;
export default function RolesPage() {
const [requests, setRequests] = useState<WorkflowRequestItem[]>([]);
const [loading, setLoading] = useState(false);
const addToast = useApiStore((s) => s.addToast);
const locale = useLocaleStore((s) => s.locale);
const permissions = usePermissions();
const permissionCatalog = useMemo(
() =>
Array.from(
new Set([
...permissions.permissions,
"USER_MANAGE",
"WORKFLOW_APPROVE",
"ROLE_MANAGE",
"USER_ROLE_ADMIN"
])
).sort(),
[permissions.permissions]
);
const load = async () => {
setLoading(true);
try {
const data = await getWorkflowRequests({ resourceType: RESOURCE_TYPE, limit: 100 });
setRequests(data);
} catch (error) {
addToast((error as { message?: string })?.message || t("loadFailed", locale), "error");
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, []);
const handleCreate = async (values: { code: string; name: string; permissionCodes: string[] }) => {
try {
await createRoleRequest(values);
addToast(t("roleCreateCreated", locale), "success");
load();
} catch (error) {
addToast((error as { message?: string })?.message || t("createFailed", locale), "error");
}
};
const handleUpdate = async (values: { code: string; permissionCodes: string[] }) => {
try {
await updateRolePermissionRequest(values);
addToast(t("rolePermissionUpdated", locale), "success");
load();
} catch (error) {
addToast((error as { message?: string })?.message || t("updateFailed", locale), "error");
}
};
return (
<main className="vstack gap-4">
<PageHeader
title={t("roles", locale)}
breadcrumb={[
{ label: t("dashboard", locale), href: "/dashboard" },
{ label: t("roles", locale), href: "/dashboard/roles" }
]}
/>
<section className="card page-card">
<div className="card-header fw-bold">{t("createRoleRequest", locale)}</div>
<div className="card-body">
<RoleCreateForm
disabled={!(permissions.canManageRoles || permissions.isAdminOrManager)}
onSubmit={handleCreate}
/>
</div>
</section>
<section className="card page-card">
<div className="card-header fw-bold">{t("updateRolePermissions", locale)}</div>
<div className="card-body">
<RolePermissionForm
disabled={!(permissions.canManageRoles || permissions.isAdminOrManager)}
permissionCatalog={permissionCatalog}
onSubmit={handleUpdate}
/>
</div>
</section>
<section className="card page-card">
<div className="card-header fw-bold">{t("recentRoleRequests", locale)}</div>
<div className="card-body">
{requests.length === 0 && !loading && (
<EmptyState
title={t("noRoleRequests", locale)}
description={t("noData", locale)}
/>
)}
<DataTable
loading={loading}
noDataText={t("noRoleRequests", locale)}
columns={[
{ key: "id", header: "Request Id" },
{ key: "resourceId", header: "Role Code" },
{ key: "makerUsername", header: "Maker" },
{
key: "status",
header: "Status",
render: (row) => <StatusBadge status={row.status} />
},
{ key: "createdAt", header: "Created" },
{ key: "payload", header: "Payload", render: (row) => <code>{row.payload}</code> }
]}
data={requests}
/>
</div>
</section>
</main>
);
}

View File

@ -0,0 +1,75 @@
"use client";
import { useState } from "react";
import { logout } from "@/services/auth";
import { useAuthStore } from "@/store/authStore";
import { useLocaleStore } from "@/store/uiStore";
import { useTenantStore } from "@/store/tenantStore";
import { t } from "@/lib/locale";
import PageHeader from "@/components/ui/PageHeader";
import FileUpload from "@/components/ui/FileUpload";
export default function SettingsPage() {
const locale = useLocaleStore((s) => s.locale);
const setLocale = useLocaleStore((s) => s.setLocale);
const currentTenant = useTenantStore((s) => s.tenantId);
const clearAuth = useAuthStore((s) => s.clearAuth);
const [_, setUploadedFiles] = useState<string[]>([]);
const authMode =
process.env.NEXT_PUBLIC_AUTH_MODE?.toLowerCase?.() === "ldap" ? "LDAP" : "LOCAL";
const handleLogout = async () => {
await logout();
clearAuth();
window.location.replace("/login");
};
const onUpload = (files: File[]) => {
setUploadedFiles(files.map((file) => file.name));
};
return (
<main className="vstack gap-3">
<PageHeader
title={t("settings", locale)}
breadcrumb={[
{ label: t("dashboard", locale), href: "/dashboard" },
{ label: t("settings", locale), href: "/dashboard/settings" }
]}
/>
<section className="card page-card">
<div className="card-body vstack gap-3">
<div>
<label className="form-label">Locale</label>
<select
className="form-select"
value={locale}
onChange={(e) => setLocale(e.target.value as "en" | "id")}
>
<option value="en">en-US</option>
<option value="id">id-ID</option>
</select>
</div>
<div>
<strong>{t("tenant", locale)}:</strong> {currentTenant}
</div>
<div>
<strong>Auth Mode:</strong> {authMode}
</div>
<div>
<FileUpload
label="Upload attachment (optional)"
accept=".txt,.csv,.json"
onFileSelect={onUpload}
helperText="Optional local file preview upload for admin workflows."
/>
</div>
<button className="btn btn-outline-danger w-auto" onClick={handleLogout}>
{t("logout", locale)}
</button>
</div>
</section>
</main>
);
}

View File

@ -0,0 +1,145 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { createUserRequest, updateUserRolesRequest } from "@/services/users";
import { getWorkflowRequests } from "@/services/workflow";
import { useApiStore } from "@/store/uiStore";
import { useLocaleStore } from "@/store/uiStore";
import { usePermissions } from "@/hooks/usePermissions";
import UserCreateForm from "@/components/user/UserCreateForm";
import UserRoleUpdateForm from "@/components/user/RoleUpdateForm";
import DataTable from "@/components/ui/Table";
import PageHeader from "@/components/ui/PageHeader";
import EmptyState from "@/components/ui/EmptyState";
import { WorkflowRequestItem, WorkflowResourceType } from "@/types/api";
import { t } from "@/lib/locale";
import StatusBadge from "@/components/workflow/StatusBadge";
const RESOURCE_TYPE = WorkflowResourceType.USER_MANAGEMENT;
export default function UsersPage() {
const [requests, setRequests] = useState<WorkflowRequestItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const { canManageUsers, isAdminOrManager } = usePermissions();
const addToast = useApiStore((s) => s.addToast);
const locale = useLocaleStore((s) => s.locale);
const authMode =
process.env.NEXT_PUBLIC_AUTH_MODE?.toLowerCase?.() === "ldap" ? "LDAP" : "LOCAL";
const loadRequests = async () => {
setIsLoading(true);
try {
const data = await getWorkflowRequests({ resourceType: RESOURCE_TYPE, limit: 100 });
setRequests(data);
} catch (error: unknown) {
addToast((error as { message?: string })?.message || t("loadFailed", locale), "error");
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadRequests();
}, []);
const rows = useMemo(
() =>
requests.map((item) => ({
...item,
actionLabel: item.status === "PENDING" ? t("pending", locale) : t("done", locale)
})),
[requests, locale]
);
const onCreate = async (payload: {
username: string;
password?: string;
ldapDn?: string;
enabled?: boolean;
roleCodes: string[];
}) => {
try {
await createUserRequest(payload);
addToast(t("userRequestCreated", locale), "success");
loadRequests();
} catch (error) {
addToast((error as { message?: string })?.message || t("createFailed", locale), "error");
}
};
const onUpdateRoles = async (payload: { username: string; roleCodes: string[] }) => {
try {
await updateUserRolesRequest(payload);
addToast(t("userRoleRequestCreated", locale), "success");
loadRequests();
} catch (error) {
addToast((error as { message?: string })?.message || t("updateFailed", locale), "error");
}
};
return (
<main className="vstack gap-4">
<PageHeader
title={t("users", locale)}
description={`${t("ldapModeIndicator", locale)}: ${authMode}`}
breadcrumb={[
{ label: t("dashboard", locale), href: "/dashboard" },
{ label: t("users", locale), href: "/dashboard/users" }
]}
/>
<section className="row g-4">
<div className="col-12 col-lg-6">
<div className="card page-card">
<div className="card-header fw-bold">{t("createUserRequest", locale)}</div>
<div className="card-body">
<UserCreateForm
disabled={!(canManageUsers || isAdminOrManager)}
onSubmit={onCreate}
authMode={authMode}
/>
</div>
</div>
</div>
<div className="col-12 col-lg-6">
<div className="card page-card">
<div className="card-header fw-bold">{t("updateUserRolesRequest", locale)}</div>
<div className="card-body">
<UserRoleUpdateForm disabled={!(canManageUsers || isAdminOrManager)} onSubmit={onUpdateRoles} />
</div>
</div>
</div>
</section>
<section className="card page-card">
<div className="card-header fw-bold">{t("recentRequests", locale)}</div>
<div className="card-body">
{requests.length === 0 && !isLoading && (
<EmptyState
title={t("noUserRequests", locale)}
description={t("noData", locale)}
/>
)}
<DataTable
columns={[
{ key: "id", header: "Request ID" },
{ key: "resourceId", header: "Username" },
{ key: "makerUsername", header: "Maker" },
{
key: "status",
header: "Status",
render: (item) => <StatusBadge status={item.status} />
},
{ key: "createdAt", header: "Created" },
{ key: "updatedAt", header: "Updated" },
{ key: "actionLabel", header: "State" }
]}
data={rows}
loading={isLoading}
noDataText={t("noUserRequests", locale)}
/>
</div>
</section>
</main>
);
}

View File

@ -0,0 +1,197 @@
"use client";
import { FormEvent, useEffect, useMemo, useState } from "react";
import {
approveWorkflowRequest,
getWorkflowRequests,
rejectWorkflowRequest,
WorkflowFilters
} from "@/services/workflow";
import { useApiStore } from "@/store/uiStore";
import { useLocaleStore } from "@/store/uiStore";
import { t } from "@/lib/locale";
import DataTable from "@/components/ui/Table";
import StatusBadge from "@/components/workflow/StatusBadge";
import ApprovalActionModal from "@/components/workflow/ApprovalActionModal";
import PageHeader from "@/components/ui/PageHeader";
import EmptyState from "@/components/ui/EmptyState";
import { WorkflowRequestItem, WorkflowStatus } from "@/types/api";
const DEFAULT_FILTERS: WorkflowFilters = {
status: "",
resourceType: "",
makerUsername: "",
limit: 50
};
export default function WorkflowPage() {
const [filters, setFilters] = useState<WorkflowFilters>(DEFAULT_FILTERS);
const [requests, setRequests] = useState<WorkflowRequestItem[]>([]);
const [loading, setLoading] = useState(false);
const [approvalRequest, setApprovalRequest] = useState<WorkflowRequestItem | null>(null);
const [actionType, setActionType] = useState<"approve" | "reject">("approve");
const [isOpen, setIsOpen] = useState(false);
const addToast = useApiStore((s) => s.addToast);
const locale = useLocaleStore((s) => s.locale);
const filtered = useMemo(
() =>
requests.filter((item) => {
const matchesStatus = !filters.status || item.status === filters.status;
const matchesType = !filters.resourceType || item.resourceType === filters.resourceType;
const matchesMaker = !filters.makerUsername || item.makerUsername.includes(filters.makerUsername);
return matchesStatus && matchesType && matchesMaker;
}),
[filters, requests]
);
const load = async () => {
setLoading(true);
try {
const response = await getWorkflowRequests(filters);
setRequests(response);
} catch (error) {
addToast((error as { message?: string })?.message || t("loadFailed", locale), "error");
} finally {
setLoading(false);
}
};
useEffect(() => {
load();
}, [filters]);
const onSubmitAction = async (notes?: string, checkerRole?: string) => {
if (!approvalRequest) return;
try {
if (actionType === "approve") {
await approveWorkflowRequest(approvalRequest.id, { notes, checkerRole });
addToast(t("requestApproved", locale), "success");
} else {
await rejectWorkflowRequest(approvalRequest.id, { notes, checkerRole });
addToast(t("requestRejected", locale), "success");
}
setIsOpen(false);
load();
} catch (error) {
addToast((error as { message?: string })?.message || t("actionFailed", locale), "error");
}
};
const submitFilters = (e: FormEvent) => {
e.preventDefault();
load();
};
return (
<main className="vstack gap-3">
<PageHeader
title={t("workflow", locale)}
breadcrumb={[
{ label: t("dashboard", locale), href: "/dashboard" },
{ label: t("workflow", locale), href: "/dashboard/workflow" }
]}
/>
<form className="card page-card card-body" onSubmit={submitFilters}>
<div className="form-grid">
<select
className="form-select"
value={filters.status ?? ""}
onChange={(e) => setFilters((prev) => ({ ...prev, status: e.target.value as WorkflowStatus | "" }))}
>
<option value="">{t("allStatuses", locale)}</option>
<option value="DRAFT">DRAFT</option>
<option value="PENDING">PENDING</option>
<option value="APPROVED">APPROVED</option>
<option value="REJECTED">REJECTED</option>
</select>
<input
className="form-control"
placeholder="Resource type"
value={filters.resourceType ?? ""}
onChange={(e) => setFilters((prev) => ({ ...prev, resourceType: e.target.value }))}
/>
<input
className="form-control"
placeholder="Maker username"
value={filters.makerUsername ?? ""}
onChange={(e) => setFilters((prev) => ({ ...prev, makerUsername: e.target.value }))}
/>
<input
className="form-control"
type="number"
min={1}
max={200}
value={filters.limit ?? 50}
onChange={(e) => setFilters((prev) => ({ ...prev, limit: Number(e.target.value) }))}
/>
<button className="btn btn-outline-primary">Apply</button>
</div>
</form>
<section className="card page-card">
<div className="card-body">
{filtered.length === 0 && !loading && (
<EmptyState
title={t("noData", locale)}
description={t("noData", locale)}
/>
)}
<DataTable
loading={loading}
columns={[
{ key: "id", header: "Request Id" },
{ key: "resourceType", header: "Resource Type" },
{ key: "resourceId", header: "Resource Id" },
{ key: "makerUsername", header: "Maker" },
{
key: "status",
header: "Status",
render: (row) => <StatusBadge status={row.status} />
},
{ key: "currentStep", header: "Current Step" },
{ key: "requiredSteps", header: "Required Steps" },
{ key: "updatedAt", header: "Updated At" },
{
key: "actions",
header: "Actions",
render: (row) => (
<div className="d-flex gap-2">
<button
className="btn btn-success btn-sm"
onClick={() => {
setApprovalRequest(row);
setActionType("approve");
setIsOpen(true);
}}
>
{t("approve", locale)}
</button>
<button
className="btn btn-danger btn-sm"
onClick={() => {
setApprovalRequest(row);
setActionType("reject");
setIsOpen(true);
}}
>
{t("reject", locale)}
</button>
</div>
)
}
]}
data={filtered}
/>
</div>
</section>
<ApprovalActionModal
isOpen={isOpen}
mode={actionType}
onSubmit={onSubmitAction}
onClose={() => setIsOpen(false)}
title={actionType === "approve" ? t("approveRequest", locale) : t("rejectRequest", locale)}
/>
</main>
);
}