user management
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import type {
|
||||
AdminDiagnosticsDto,
|
||||
AdminPermissionOptionDto,
|
||||
AdminRoleDto,
|
||||
AdminRoleInput,
|
||||
AdminUserDto,
|
||||
AdminUserInput,
|
||||
ApiResponse,
|
||||
CompanyProfileDto,
|
||||
CompanyProfileInput,
|
||||
@@ -131,6 +136,27 @@ export const api = {
|
||||
getAdminDiagnostics(token: string) {
|
||||
return request<AdminDiagnosticsDto>("/api/v1/admin/diagnostics", undefined, token);
|
||||
},
|
||||
getAdminPermissions(token: string) {
|
||||
return request<AdminPermissionOptionDto[]>("/api/v1/admin/permissions", undefined, token);
|
||||
},
|
||||
getAdminRoles(token: string) {
|
||||
return request<AdminRoleDto[]>("/api/v1/admin/roles", undefined, token);
|
||||
},
|
||||
createAdminRole(token: string, payload: AdminRoleInput) {
|
||||
return request<AdminRoleDto>("/api/v1/admin/roles", { method: "POST", body: JSON.stringify(payload) }, token);
|
||||
},
|
||||
updateAdminRole(token: string, roleId: string, payload: AdminRoleInput) {
|
||||
return request<AdminRoleDto>(`/api/v1/admin/roles/${roleId}`, { method: "PUT", body: JSON.stringify(payload) }, token);
|
||||
},
|
||||
getAdminUsers(token: string) {
|
||||
return request<AdminUserDto[]>("/api/v1/admin/users", undefined, token);
|
||||
},
|
||||
createAdminUser(token: string, payload: AdminUserInput) {
|
||||
return request<AdminUserDto>("/api/v1/admin/users", { method: "POST", body: JSON.stringify(payload) }, token);
|
||||
},
|
||||
updateAdminUser(token: string, userId: string, payload: AdminUserInput) {
|
||||
return request<AdminUserDto>(`/api/v1/admin/users/${userId}`, { method: "PUT", body: JSON.stringify(payload) }, token);
|
||||
},
|
||||
getCompanyProfile(token: string) {
|
||||
return request<CompanyProfileDto>("/api/v1/company-profile", undefined, token);
|
||||
},
|
||||
|
||||
@@ -9,39 +9,111 @@ import { ProtectedRoute } from "./components/ProtectedRoute";
|
||||
import { AuthProvider } from "./auth/AuthProvider";
|
||||
import { DashboardPage } from "./modules/dashboard/DashboardPage";
|
||||
import { LoginPage } from "./modules/login/LoginPage";
|
||||
import { AdminDiagnosticsPage } from "./modules/settings/AdminDiagnosticsPage";
|
||||
import { CompanySettingsPage } from "./modules/settings/CompanySettingsPage";
|
||||
import { CrmDetailPage } from "./modules/crm/CrmDetailPage";
|
||||
import { CrmFormPage } from "./modules/crm/CrmFormPage";
|
||||
import { CustomersPage } from "./modules/crm/CustomersPage";
|
||||
import { VendorsPage } from "./modules/crm/VendorsPage";
|
||||
import { GanttPage } from "./modules/gantt/GanttPage";
|
||||
import { InventoryDetailPage } from "./modules/inventory/InventoryDetailPage";
|
||||
import { InventoryFormPage } from "./modules/inventory/InventoryFormPage";
|
||||
import { InventoryItemsPage } from "./modules/inventory/InventoryItemsPage";
|
||||
import { ManufacturingPage } from "./modules/manufacturing/ManufacturingPage";
|
||||
import { WorkOrderDetailPage } from "./modules/manufacturing/WorkOrderDetailPage";
|
||||
import { WorkOrderFormPage } from "./modules/manufacturing/WorkOrderFormPage";
|
||||
import { PurchaseDetailPage } from "./modules/purchasing/PurchaseDetailPage";
|
||||
import { PurchaseFormPage } from "./modules/purchasing/PurchaseFormPage";
|
||||
import { PurchaseListPage } from "./modules/purchasing/PurchaseListPage";
|
||||
import { ProjectDetailPage } from "./modules/projects/ProjectDetailPage";
|
||||
import { ProjectFormPage } from "./modules/projects/ProjectFormPage";
|
||||
import { ProjectsPage } from "./modules/projects/ProjectsPage";
|
||||
import { WarehouseDetailPage } from "./modules/inventory/WarehouseDetailPage";
|
||||
import { WarehouseFormPage } from "./modules/inventory/WarehouseFormPage";
|
||||
import { WarehousesPage } from "./modules/inventory/WarehousesPage";
|
||||
import { SalesDetailPage } from "./modules/sales/SalesDetailPage";
|
||||
import { SalesFormPage } from "./modules/sales/SalesFormPage";
|
||||
import { SalesListPage } from "./modules/sales/SalesListPage";
|
||||
import { ShipmentDetailPage } from "./modules/shipping/ShipmentDetailPage";
|
||||
import { ShipmentFormPage } from "./modules/shipping/ShipmentFormPage";
|
||||
import { ShipmentListPage } from "./modules/shipping/ShipmentListPage";
|
||||
import { ThemeProvider } from "./theme/ThemeProvider";
|
||||
import "./index.css";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const CompanySettingsPage = React.lazy(() =>
|
||||
import("./modules/settings/CompanySettingsPage").then((module) => ({ default: module.CompanySettingsPage }))
|
||||
);
|
||||
const AdminDiagnosticsPage = React.lazy(() =>
|
||||
import("./modules/settings/AdminDiagnosticsPage").then((module) => ({ default: module.AdminDiagnosticsPage }))
|
||||
);
|
||||
const UserManagementPage = React.lazy(() =>
|
||||
import("./modules/settings/UserManagementPage").then((module) => ({ default: module.UserManagementPage }))
|
||||
);
|
||||
const CustomersPage = React.lazy(() =>
|
||||
import("./modules/crm/CustomersPage").then((module) => ({ default: module.CustomersPage }))
|
||||
);
|
||||
const VendorsPage = React.lazy(() =>
|
||||
import("./modules/crm/VendorsPage").then((module) => ({ default: module.VendorsPage }))
|
||||
);
|
||||
const CrmDetailPage = React.lazy(() =>
|
||||
import("./modules/crm/CrmDetailPage").then((module) => ({ default: module.CrmDetailPage }))
|
||||
);
|
||||
const CrmFormPage = React.lazy(() =>
|
||||
import("./modules/crm/CrmFormPage").then((module) => ({ default: module.CrmFormPage }))
|
||||
);
|
||||
const InventoryItemsPage = React.lazy(() =>
|
||||
import("./modules/inventory/InventoryItemsPage").then((module) => ({ default: module.InventoryItemsPage }))
|
||||
);
|
||||
const InventoryDetailPage = React.lazy(() =>
|
||||
import("./modules/inventory/InventoryDetailPage").then((module) => ({ default: module.InventoryDetailPage }))
|
||||
);
|
||||
const InventoryFormPage = React.lazy(() =>
|
||||
import("./modules/inventory/InventoryFormPage").then((module) => ({ default: module.InventoryFormPage }))
|
||||
);
|
||||
const WarehousesPage = React.lazy(() =>
|
||||
import("./modules/inventory/WarehousesPage").then((module) => ({ default: module.WarehousesPage }))
|
||||
);
|
||||
const WarehouseDetailPage = React.lazy(() =>
|
||||
import("./modules/inventory/WarehouseDetailPage").then((module) => ({ default: module.WarehouseDetailPage }))
|
||||
);
|
||||
const WarehouseFormPage = React.lazy(() =>
|
||||
import("./modules/inventory/WarehouseFormPage").then((module) => ({ default: module.WarehouseFormPage }))
|
||||
);
|
||||
const ProjectsPage = React.lazy(() =>
|
||||
import("./modules/projects/ProjectsPage").then((module) => ({ default: module.ProjectsPage }))
|
||||
);
|
||||
const ProjectDetailPage = React.lazy(() =>
|
||||
import("./modules/projects/ProjectDetailPage").then((module) => ({ default: module.ProjectDetailPage }))
|
||||
);
|
||||
const ProjectFormPage = React.lazy(() =>
|
||||
import("./modules/projects/ProjectFormPage").then((module) => ({ default: module.ProjectFormPage }))
|
||||
);
|
||||
const ManufacturingPage = React.lazy(() =>
|
||||
import("./modules/manufacturing/ManufacturingPage").then((module) => ({ default: module.ManufacturingPage }))
|
||||
);
|
||||
const WorkOrderDetailPage = React.lazy(() =>
|
||||
import("./modules/manufacturing/WorkOrderDetailPage").then((module) => ({ default: module.WorkOrderDetailPage }))
|
||||
);
|
||||
const WorkOrderFormPage = React.lazy(() =>
|
||||
import("./modules/manufacturing/WorkOrderFormPage").then((module) => ({ default: module.WorkOrderFormPage }))
|
||||
);
|
||||
const PurchaseListPage = React.lazy(() =>
|
||||
import("./modules/purchasing/PurchaseListPage").then((module) => ({ default: module.PurchaseListPage }))
|
||||
);
|
||||
const PurchaseDetailPage = React.lazy(() =>
|
||||
import("./modules/purchasing/PurchaseDetailPage").then((module) => ({ default: module.PurchaseDetailPage }))
|
||||
);
|
||||
const PurchaseFormPage = React.lazy(() =>
|
||||
import("./modules/purchasing/PurchaseFormPage").then((module) => ({ default: module.PurchaseFormPage }))
|
||||
);
|
||||
const SalesListPage = React.lazy(() =>
|
||||
import("./modules/sales/SalesListPage").then((module) => ({ default: module.SalesListPage }))
|
||||
);
|
||||
const SalesDetailPage = React.lazy(() =>
|
||||
import("./modules/sales/SalesDetailPage").then((module) => ({ default: module.SalesDetailPage }))
|
||||
);
|
||||
const SalesFormPage = React.lazy(() =>
|
||||
import("./modules/sales/SalesFormPage").then((module) => ({ default: module.SalesFormPage }))
|
||||
);
|
||||
const ShipmentListPage = React.lazy(() =>
|
||||
import("./modules/shipping/ShipmentListPage").then((module) => ({ default: module.ShipmentListPage }))
|
||||
);
|
||||
const ShipmentDetailPage = React.lazy(() =>
|
||||
import("./modules/shipping/ShipmentDetailPage").then((module) => ({ default: module.ShipmentDetailPage }))
|
||||
);
|
||||
const ShipmentFormPage = React.lazy(() =>
|
||||
import("./modules/shipping/ShipmentFormPage").then((module) => ({ default: module.ShipmentFormPage }))
|
||||
);
|
||||
const GanttPage = React.lazy(() =>
|
||||
import("./modules/gantt/GanttPage").then((module) => ({ default: module.GanttPage }))
|
||||
);
|
||||
|
||||
function RouteFallback() {
|
||||
return (
|
||||
<div className="rounded-[28px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">
|
||||
Loading module...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function lazyElement(element: React.ReactNode) {
|
||||
return <React.Suspense fallback={<RouteFallback />}>{element}</React.Suspense>;
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{ path: "/login", element: <LoginPage /> },
|
||||
{
|
||||
@@ -53,125 +125,128 @@ const router = createBrowserRouter([
|
||||
{ path: "/", element: <DashboardPage /> },
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.companyRead]} />,
|
||||
children: [{ path: "/settings/company", element: <CompanySettingsPage /> }],
|
||||
children: [{ path: "/settings/company", element: lazyElement(<CompanySettingsPage />) }],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.adminManage]} />,
|
||||
children: [{ path: "/settings/admin-diagnostics", element: <AdminDiagnosticsPage /> }],
|
||||
children: [
|
||||
{ path: "/settings/admin-diagnostics", element: lazyElement(<AdminDiagnosticsPage />) },
|
||||
{ path: "/settings/users", element: lazyElement(<UserManagementPage />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.crmRead]} />,
|
||||
children: [
|
||||
{ path: "/crm/customers", element: <CustomersPage /> },
|
||||
{ path: "/crm/customers/:customerId", element: <CrmDetailPage entity="customer" /> },
|
||||
{ path: "/crm/vendors", element: <VendorsPage /> },
|
||||
{ path: "/crm/vendors/:vendorId", element: <CrmDetailPage entity="vendor" /> },
|
||||
{ path: "/crm/customers", element: lazyElement(<CustomersPage />) },
|
||||
{ path: "/crm/customers/:customerId", element: lazyElement(<CrmDetailPage entity="customer" />) },
|
||||
{ path: "/crm/vendors", element: lazyElement(<VendorsPage />) },
|
||||
{ path: "/crm/vendors/:vendorId", element: lazyElement(<CrmDetailPage entity="vendor" />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.inventoryRead]} />,
|
||||
children: [
|
||||
{ path: "/inventory/items", element: <InventoryItemsPage /> },
|
||||
{ path: "/inventory/items/:itemId", element: <InventoryDetailPage /> },
|
||||
{ path: "/inventory/warehouses", element: <WarehousesPage /> },
|
||||
{ path: "/inventory/warehouses/:warehouseId", element: <WarehouseDetailPage /> },
|
||||
{ path: "/inventory/items", element: lazyElement(<InventoryItemsPage />) },
|
||||
{ path: "/inventory/items/:itemId", element: lazyElement(<InventoryDetailPage />) },
|
||||
{ path: "/inventory/warehouses", element: lazyElement(<WarehousesPage />) },
|
||||
{ path: "/inventory/warehouses/:warehouseId", element: lazyElement(<WarehouseDetailPage />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.projectsRead]} />,
|
||||
children: [
|
||||
{ path: "/projects", element: <ProjectsPage /> },
|
||||
{ path: "/projects/:projectId", element: <ProjectDetailPage /> },
|
||||
{ path: "/projects", element: lazyElement(<ProjectsPage />) },
|
||||
{ path: "/projects/:projectId", element: lazyElement(<ProjectDetailPage />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.manufacturingRead]} />,
|
||||
children: [
|
||||
{ path: "/manufacturing/work-orders", element: <ManufacturingPage /> },
|
||||
{ path: "/manufacturing/work-orders/:workOrderId", element: <WorkOrderDetailPage /> },
|
||||
{ path: "/manufacturing/work-orders", element: lazyElement(<ManufacturingPage />) },
|
||||
{ path: "/manufacturing/work-orders/:workOrderId", element: lazyElement(<WorkOrderDetailPage />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={["purchasing.read"]} />,
|
||||
children: [
|
||||
{ path: "/purchasing/orders", element: <PurchaseListPage /> },
|
||||
{ path: "/purchasing/orders/:orderId", element: <PurchaseDetailPage /> },
|
||||
{ path: "/purchasing/orders", element: lazyElement(<PurchaseListPage />) },
|
||||
{ path: "/purchasing/orders/:orderId", element: lazyElement(<PurchaseDetailPage />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.salesRead]} />,
|
||||
children: [
|
||||
{ path: "/sales/quotes", element: <SalesListPage entity="quote" /> },
|
||||
{ path: "/sales/quotes/:quoteId", element: <SalesDetailPage entity="quote" /> },
|
||||
{ path: "/sales/orders", element: <SalesListPage entity="order" /> },
|
||||
{ path: "/sales/orders/:orderId", element: <SalesDetailPage entity="order" /> },
|
||||
{ path: "/sales/quotes", element: lazyElement(<SalesListPage entity="quote" />) },
|
||||
{ path: "/sales/quotes/:quoteId", element: lazyElement(<SalesDetailPage entity="quote" />) },
|
||||
{ path: "/sales/orders", element: lazyElement(<SalesListPage entity="order" />) },
|
||||
{ path: "/sales/orders/:orderId", element: lazyElement(<SalesDetailPage entity="order" />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.shippingRead]} />,
|
||||
children: [
|
||||
{ path: "/shipping/shipments", element: <ShipmentListPage /> },
|
||||
{ path: "/shipping/shipments/:shipmentId", element: <ShipmentDetailPage /> },
|
||||
{ path: "/shipping/shipments", element: lazyElement(<ShipmentListPage />) },
|
||||
{ path: "/shipping/shipments/:shipmentId", element: lazyElement(<ShipmentDetailPage />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.crmWrite]} />,
|
||||
children: [
|
||||
{ path: "/crm/customers/new", element: <CrmFormPage entity="customer" mode="create" /> },
|
||||
{ path: "/crm/customers/:customerId/edit", element: <CrmFormPage entity="customer" mode="edit" /> },
|
||||
{ path: "/crm/vendors/new", element: <CrmFormPage entity="vendor" mode="create" /> },
|
||||
{ path: "/crm/vendors/:vendorId/edit", element: <CrmFormPage entity="vendor" mode="edit" /> },
|
||||
{ path: "/crm/customers/new", element: lazyElement(<CrmFormPage entity="customer" mode="create" />) },
|
||||
{ path: "/crm/customers/:customerId/edit", element: lazyElement(<CrmFormPage entity="customer" mode="edit" />) },
|
||||
{ path: "/crm/vendors/new", element: lazyElement(<CrmFormPage entity="vendor" mode="create" />) },
|
||||
{ path: "/crm/vendors/:vendorId/edit", element: lazyElement(<CrmFormPage entity="vendor" mode="edit" />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.projectsWrite]} />,
|
||||
children: [
|
||||
{ path: "/projects/new", element: <ProjectFormPage mode="create" /> },
|
||||
{ path: "/projects/:projectId/edit", element: <ProjectFormPage mode="edit" /> },
|
||||
{ path: "/projects/new", element: lazyElement(<ProjectFormPage mode="create" />) },
|
||||
{ path: "/projects/:projectId/edit", element: lazyElement(<ProjectFormPage mode="edit" />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.manufacturingWrite]} />,
|
||||
children: [
|
||||
{ path: "/manufacturing/work-orders/new", element: <WorkOrderFormPage mode="create" /> },
|
||||
{ path: "/manufacturing/work-orders/:workOrderId/edit", element: <WorkOrderFormPage mode="edit" /> },
|
||||
{ path: "/manufacturing/work-orders/new", element: lazyElement(<WorkOrderFormPage mode="create" />) },
|
||||
{ path: "/manufacturing/work-orders/:workOrderId/edit", element: lazyElement(<WorkOrderFormPage mode="edit" />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={["purchasing.write"]} />,
|
||||
children: [
|
||||
{ path: "/purchasing/orders/new", element: <PurchaseFormPage mode="create" /> },
|
||||
{ path: "/purchasing/orders/:orderId/edit", element: <PurchaseFormPage mode="edit" /> },
|
||||
{ path: "/purchasing/orders/new", element: lazyElement(<PurchaseFormPage mode="create" />) },
|
||||
{ path: "/purchasing/orders/:orderId/edit", element: lazyElement(<PurchaseFormPage mode="edit" />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.salesWrite]} />,
|
||||
children: [
|
||||
{ path: "/sales/quotes/new", element: <SalesFormPage entity="quote" mode="create" /> },
|
||||
{ path: "/sales/quotes/:quoteId/edit", element: <SalesFormPage entity="quote" mode="edit" /> },
|
||||
{ path: "/sales/orders/new", element: <SalesFormPage entity="order" mode="create" /> },
|
||||
{ path: "/sales/orders/:orderId/edit", element: <SalesFormPage entity="order" mode="edit" /> },
|
||||
{ path: "/sales/quotes/new", element: lazyElement(<SalesFormPage entity="quote" mode="create" />) },
|
||||
{ path: "/sales/quotes/:quoteId/edit", element: lazyElement(<SalesFormPage entity="quote" mode="edit" />) },
|
||||
{ path: "/sales/orders/new", element: lazyElement(<SalesFormPage entity="order" mode="create" />) },
|
||||
{ path: "/sales/orders/:orderId/edit", element: lazyElement(<SalesFormPage entity="order" mode="edit" />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.shippingWrite]} />,
|
||||
children: [
|
||||
{ path: "/shipping/shipments/new", element: <ShipmentFormPage mode="create" /> },
|
||||
{ path: "/shipping/shipments/:shipmentId/edit", element: <ShipmentFormPage mode="edit" /> },
|
||||
{ path: "/shipping/shipments/new", element: lazyElement(<ShipmentFormPage mode="create" />) },
|
||||
{ path: "/shipping/shipments/:shipmentId/edit", element: lazyElement(<ShipmentFormPage mode="edit" />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.inventoryWrite]} />,
|
||||
children: [
|
||||
{ path: "/inventory/items/new", element: <InventoryFormPage mode="create" /> },
|
||||
{ path: "/inventory/items/:itemId/edit", element: <InventoryFormPage mode="edit" /> },
|
||||
{ path: "/inventory/warehouses/new", element: <WarehouseFormPage mode="create" /> },
|
||||
{ path: "/inventory/warehouses/:warehouseId/edit", element: <WarehouseFormPage mode="edit" /> },
|
||||
{ path: "/inventory/items/new", element: lazyElement(<InventoryFormPage mode="create" />) },
|
||||
{ path: "/inventory/items/:itemId/edit", element: lazyElement(<InventoryFormPage mode="edit" />) },
|
||||
{ path: "/inventory/warehouses/new", element: lazyElement(<WarehouseFormPage mode="create" />) },
|
||||
{ path: "/inventory/warehouses/:warehouseId/edit", element: lazyElement(<WarehouseFormPage mode="edit" />) },
|
||||
],
|
||||
},
|
||||
{
|
||||
element: <ProtectedRoute requiredPermissions={[permissions.ganttRead]} />,
|
||||
children: [{ path: "/planning/gantt", element: <GanttPage /> }],
|
||||
children: [{ path: "/planning/gantt", element: lazyElement(<GanttPage />) }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -90,6 +90,9 @@ export function AdminDiagnosticsPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link to="/settings/users" className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
|
||||
User management
|
||||
</Link>
|
||||
<Link to="/settings/company" className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
|
||||
Company settings
|
||||
</Link>
|
||||
|
||||
@@ -151,12 +151,17 @@ export function CompanySettingsPage() {
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Admin</p>
|
||||
<h3 className="mt-2 text-lg font-bold text-text">Diagnostics and audit trail</h3>
|
||||
<p className="mt-2 text-sm text-muted">Review runtime footprint and recent change activity from the admin diagnostics surface.</p>
|
||||
<h3 className="mt-2 text-lg font-bold text-text">Admin access and diagnostics</h3>
|
||||
<p className="mt-2 text-sm text-muted">Manage users, roles, and system diagnostics from the linked admin surfaces.</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link to="/settings/users" className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
|
||||
User management
|
||||
</Link>
|
||||
<Link to="/settings/admin-diagnostics" className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
|
||||
Open diagnostics
|
||||
</Link>
|
||||
</div>
|
||||
<Link to="/settings/admin-diagnostics" className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
|
||||
Open diagnostics
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
363
client/src/modules/settings/UserManagementPage.tsx
Normal file
363
client/src/modules/settings/UserManagementPage.tsx
Normal file
@@ -0,0 +1,363 @@
|
||||
import type { AdminPermissionOptionDto, AdminRoleDto, AdminRoleInput, AdminUserDto, AdminUserInput } from "@mrp/shared";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useAuth } from "../../auth/AuthProvider";
|
||||
import { api } from "../../lib/api";
|
||||
|
||||
const emptyUserForm: AdminUserInput = {
|
||||
email: "",
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
isActive: true,
|
||||
roleIds: [],
|
||||
password: "",
|
||||
};
|
||||
|
||||
const emptyRoleForm: AdminRoleInput = {
|
||||
name: "",
|
||||
description: "",
|
||||
permissionKeys: [],
|
||||
};
|
||||
|
||||
export function UserManagementPage() {
|
||||
const { token } = useAuth();
|
||||
const [users, setUsers] = useState<AdminUserDto[]>([]);
|
||||
const [roles, setRoles] = useState<AdminRoleDto[]>([]);
|
||||
const [permissions, setPermissions] = useState<AdminPermissionOptionDto[]>([]);
|
||||
const [selectedUserId, setSelectedUserId] = useState<string>("new");
|
||||
const [selectedRoleId, setSelectedRoleId] = useState<string>("new");
|
||||
const [userForm, setUserForm] = useState<AdminUserInput>(emptyUserForm);
|
||||
const [roleForm, setRoleForm] = useState<AdminRoleInput>(emptyRoleForm);
|
||||
const [status, setStatus] = useState("Loading admin access controls...");
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
|
||||
let active = true;
|
||||
|
||||
Promise.all([api.getAdminUsers(token), api.getAdminRoles(token), api.getAdminPermissions(token)])
|
||||
.then(([nextUsers, nextRoles, nextPermissions]) => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
setUsers(nextUsers);
|
||||
setRoles(nextRoles);
|
||||
setPermissions(nextPermissions);
|
||||
setStatus("User management loaded.");
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
setStatus(error.message || "Unable to load admin access controls.");
|
||||
});
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedUserId === "new") {
|
||||
setUserForm(emptyUserForm);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedUser = users.find((user) => user.id === selectedUserId);
|
||||
if (!selectedUser) {
|
||||
setUserForm(emptyUserForm);
|
||||
return;
|
||||
}
|
||||
|
||||
setUserForm({
|
||||
email: selectedUser.email,
|
||||
firstName: selectedUser.firstName,
|
||||
lastName: selectedUser.lastName,
|
||||
isActive: selectedUser.isActive,
|
||||
roleIds: selectedUser.roleIds,
|
||||
password: "",
|
||||
});
|
||||
}, [selectedUserId, users]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRoleId === "new") {
|
||||
setRoleForm(emptyRoleForm);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedRole = roles.find((role) => role.id === selectedRoleId);
|
||||
if (!selectedRole) {
|
||||
setRoleForm(emptyRoleForm);
|
||||
return;
|
||||
}
|
||||
|
||||
setRoleForm({
|
||||
name: selectedRole.name,
|
||||
description: selectedRole.description,
|
||||
permissionKeys: selectedRole.permissionKeys,
|
||||
});
|
||||
}, [roles, selectedRoleId]);
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const authToken = token;
|
||||
|
||||
async function refreshData(nextStatus: string) {
|
||||
const [nextUsers, nextRoles, nextPermissions] = await Promise.all([
|
||||
api.getAdminUsers(authToken),
|
||||
api.getAdminRoles(authToken),
|
||||
api.getAdminPermissions(authToken),
|
||||
]);
|
||||
setUsers(nextUsers);
|
||||
setRoles(nextRoles);
|
||||
setPermissions(nextPermissions);
|
||||
setStatus(nextStatus);
|
||||
}
|
||||
|
||||
async function handleUserSave(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (selectedUserId === "new") {
|
||||
const createdUser = await api.createAdminUser(authToken, userForm);
|
||||
await refreshData(`Created user ${createdUser.email}.`);
|
||||
setSelectedUserId(createdUser.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedUser = await api.updateAdminUser(authToken, selectedUserId, userForm);
|
||||
await refreshData(`Updated user ${updatedUser.email}.`);
|
||||
setSelectedUserId(updatedUser.id);
|
||||
}
|
||||
|
||||
async function handleRoleSave(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (selectedRoleId === "new") {
|
||||
const createdRole = await api.createAdminRole(authToken, roleForm);
|
||||
await refreshData(`Created role ${createdRole.name}.`);
|
||||
setSelectedRoleId(createdRole.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedRole = await api.updateAdminRole(authToken, selectedRoleId, roleForm);
|
||||
await refreshData(`Updated role ${updatedRole.name}.`);
|
||||
setSelectedRoleId(updatedRole.id);
|
||||
}
|
||||
|
||||
function toggleUserRole(roleId: string) {
|
||||
setUserForm((current) => ({
|
||||
...current,
|
||||
roleIds: current.roleIds.includes(roleId)
|
||||
? current.roleIds.filter((currentRoleId) => currentRoleId !== roleId)
|
||||
: [...current.roleIds, roleId],
|
||||
}));
|
||||
}
|
||||
|
||||
function toggleRolePermission(permissionKey: string) {
|
||||
setRoleForm((current) => ({
|
||||
...current,
|
||||
permissionKeys: current.permissionKeys.includes(permissionKey)
|
||||
? current.permissionKeys.filter((currentPermissionKey) => currentPermissionKey !== permissionKey)
|
||||
: [...current.permissionKeys, permissionKey],
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">User Management</p>
|
||||
<h3 className="mt-2 text-lg font-bold text-text">Accounts, roles, and permission assignment</h3>
|
||||
<p className="mt-2 max-w-3xl text-sm text-muted">
|
||||
Manage user accounts and the role-permission model from one admin surface so onboarding and access control stay tied together.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link to="/settings/company" className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
|
||||
Company settings
|
||||
</Link>
|
||||
<Link to="/settings/admin-diagnostics" className="rounded-2xl border border-line/70 px-3 py-2 text-sm font-semibold text-text">
|
||||
Diagnostics
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-6 xl:grid-cols-2">
|
||||
<form className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5" onSubmit={handleUserSave}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Users</p>
|
||||
<h3 className="mt-2 text-lg font-bold text-text">Account generation and role assignment</h3>
|
||||
</div>
|
||||
<select
|
||||
value={selectedUserId}
|
||||
onChange={(event) => setSelectedUserId(event.target.value)}
|
||||
className="rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none"
|
||||
>
|
||||
<option value="new">New user</option>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.firstName} {user.lastName} ({user.email})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-4 md:grid-cols-2">
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">Email</span>
|
||||
<input
|
||||
value={userForm.email}
|
||||
onChange={(event) => setUserForm((current) => ({ ...current, email: event.target.value }))}
|
||||
className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-text outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">Password</span>
|
||||
<input
|
||||
type="password"
|
||||
value={userForm.password ?? ""}
|
||||
onChange={(event) => setUserForm((current) => ({ ...current, password: event.target.value }))}
|
||||
placeholder={selectedUserId === "new" ? "Required for new user" : "Leave blank to keep current password"}
|
||||
className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-text outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">First name</span>
|
||||
<input
|
||||
value={userForm.firstName}
|
||||
onChange={(event) => setUserForm((current) => ({ ...current, firstName: event.target.value }))}
|
||||
className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-text outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">Last name</span>
|
||||
<input
|
||||
value={userForm.lastName}
|
||||
onChange={(event) => setUserForm((current) => ({ ...current, lastName: event.target.value }))}
|
||||
className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-text outline-none"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="mt-4 flex items-center gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3 text-sm text-text">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={userForm.isActive}
|
||||
onChange={(event) => setUserForm((current) => ({ ...current, isActive: event.target.checked }))}
|
||||
/>
|
||||
User can sign in
|
||||
</label>
|
||||
|
||||
<div className="mt-5">
|
||||
<p className="text-sm font-semibold text-text">Assigned roles</p>
|
||||
<div className="mt-3 grid gap-3">
|
||||
{roles.map((role) => (
|
||||
<label key={role.id} className="flex items-start gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3 text-sm text-text">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={userForm.roleIds.includes(role.id)}
|
||||
onChange={() => toggleUserRole(role.id)}
|
||||
/>
|
||||
<span>
|
||||
<span className="block font-semibold">{role.name}</span>
|
||||
<span className="block text-xs text-muted">{role.description || "No description"}</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex items-center justify-between gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
||||
<span className="text-sm text-muted">{status}</span>
|
||||
<button type="submit" className="rounded-2xl bg-brand px-4 py-2 text-sm font-semibold text-white">
|
||||
{selectedUserId === "new" ? "Create user" : "Save user"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5" onSubmit={handleRoleSave}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Roles</p>
|
||||
<h3 className="mt-2 text-lg font-bold text-text">Permission assignment administration</h3>
|
||||
</div>
|
||||
<select
|
||||
value={selectedRoleId}
|
||||
onChange={(event) => setSelectedRoleId(event.target.value)}
|
||||
className="rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none"
|
||||
>
|
||||
<option value="new">New role</option>
|
||||
{roles.map((role) => (
|
||||
<option key={role.id} value={role.id}>
|
||||
{role.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-4">
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">Role name</span>
|
||||
<input
|
||||
value={roleForm.name}
|
||||
onChange={(event) => setRoleForm((current) => ({ ...current, name: event.target.value }))}
|
||||
className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-text outline-none"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="mb-2 block text-sm font-semibold text-text">Description</span>
|
||||
<textarea
|
||||
value={roleForm.description}
|
||||
onChange={(event) => setRoleForm((current) => ({ ...current, description: event.target.value }))}
|
||||
rows={3}
|
||||
className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-text outline-none"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<p className="text-sm font-semibold text-text">Role permissions</p>
|
||||
<div className="mt-3 grid gap-3 md:grid-cols-2">
|
||||
{permissions.map((permission) => (
|
||||
<label key={permission.key} className="flex items-start gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3 text-sm text-text">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={roleForm.permissionKeys.includes(permission.key)}
|
||||
onChange={() => toggleRolePermission(permission.key)}
|
||||
/>
|
||||
<span>
|
||||
<span className="block font-semibold">{permission.key}</span>
|
||||
<span className="block text-xs text-muted">{permission.description}</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-3 md:grid-cols-3">
|
||||
{roles.map((role) => (
|
||||
<div key={role.id} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
||||
<p className="text-sm font-semibold text-text">{role.name}</p>
|
||||
<p className="mt-1 text-xs text-muted">{role.userCount} assigned users</p>
|
||||
<p className="mt-2 text-xs text-muted">{role.permissionKeys.length} permissions</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex items-center justify-between gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
||||
<span className="text-sm text-muted">{status}</span>
|
||||
<button type="submit" className="rounded-2xl bg-brand px-4 py-2 text-sm font-semibold text-white">
|
||||
{selectedRoleId === "new" ? "Create role" : "Save role"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,24 @@ import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes("@svar-ui/react-gantt")) {
|
||||
return "gantt-vendor";
|
||||
}
|
||||
if (id.includes("@tanstack/react-query")) {
|
||||
return "query-vendor";
|
||||
}
|
||||
if (id.includes("react-router-dom") || id.includes("react-dom") || id.includes(`${path.sep}react${path.sep}`)) {
|
||||
return "react-vendor";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "src"),
|
||||
@@ -16,4 +34,3 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user