3
This commit is contained in:
parent
ccb52987a0
commit
05382ed1be
@ -19,7 +19,7 @@ export default function AsideMenu({
|
|||||||
<>
|
<>
|
||||||
<AsideMenuLayer
|
<AsideMenuLayer
|
||||||
menu={props.menu}
|
menu={props.menu}
|
||||||
className={`${isAsideMobileExpanded ? 'left-0' : '-left-60 lg:left-0'} ${
|
className={`${isAsideMobileExpanded ? 'left-0' : '-left-72 lg:left-0'} ${
|
||||||
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
|
!isAsideLgActive ? 'lg:hidden xl:flex' : ''
|
||||||
}`}
|
}`}
|
||||||
onAsideLgCloseClick={props.onAsideLgClose}
|
onAsideLgCloseClick={props.onAsideLgClose}
|
||||||
|
|||||||
@ -43,11 +43,11 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
|
|||||||
const asideMenuItemInnerContents = (
|
const asideMenuItemInnerContents = (
|
||||||
<>
|
<>
|
||||||
{item.icon && (
|
{item.icon && (
|
||||||
<BaseIcon path={item.icon} className={`flex-none mx-3 ${activeClassAddon}`} size="18" />
|
<BaseIcon path={item.icon} className={`mt-0.5 flex-none ${activeClassAddon}`} size="18" />
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
className={`grow text-ellipsis line-clamp-1 ${
|
className={`min-w-0 grow break-words leading-6 ${
|
||||||
item.menu ? '' : 'pr-12'
|
item.menu ? '' : 'pr-2'
|
||||||
} ${activeClassAddon}`}
|
} ${activeClassAddon}`}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
@ -56,15 +56,15 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
|
|||||||
<BaseIcon
|
<BaseIcon
|
||||||
path={isDropdownActive ? mdiMinus : mdiPlus}
|
path={isDropdownActive ? mdiMinus : mdiPlus}
|
||||||
className={`flex-none ${activeClassAddon}`}
|
className={`flex-none ${activeClassAddon}`}
|
||||||
w="w-12"
|
w="w-8"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
const componentClass = [
|
const componentClass = [
|
||||||
'flex cursor-pointer py-1.5 ',
|
'flex items-start gap-3 cursor-pointer py-1.5',
|
||||||
isDropdownList ? 'px-6 text-sm' : '',
|
isDropdownList ? 'px-4 text-sm' : '',
|
||||||
item.color
|
item.color
|
||||||
? getButtonColor(item.color, false, true)
|
? getButtonColor(item.color, false, true)
|
||||||
: `${asideMenuItemStyle}`,
|
: `${asideMenuItemStyle}`,
|
||||||
|
|||||||
@ -29,13 +29,13 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
|
|||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
id='asideMenu'
|
id='asideMenu'
|
||||||
className={`${className} zzz lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
|
className={`${className} zzz lg:py-2 lg:pl-2 w-72 fixed flex z-40 top-0 h-screen transition-position overflow-hidden`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
|
className={`flex-1 flex flex-col overflow-hidden dark:bg-dark-900 ${asideStyle} ${corners}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex flex-row h-14 items-center justify-between ${asideBrandStyle}`}
|
className={`flex flex-row h-12 items-center justify-between ${asideBrandStyle}`}
|
||||||
>
|
>
|
||||||
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
|
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="hidden lg:inline-block xl:hidden p-3"
|
className="hidden lg:inline-block xl:hidden p-2.5"
|
||||||
onClick={handleAsideLgCloseClick}
|
onClick={handleAsideLgCloseClick}
|
||||||
>
|
>
|
||||||
<BaseIcon path={mdiClose} />
|
<BaseIcon path={mdiClose} />
|
||||||
|
|||||||
@ -37,13 +37,13 @@ export default function NavBar({ menu, className = '', contentClassName = contai
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className={`${className} top-0 inset-x-0 fixed ${bgColor} h-14 z-30 transition-position w-screen lg:w-auto dark:bg-dark-800`}
|
className={`${className} top-0 inset-x-0 fixed ${bgColor} h-12 z-30 transition-position w-screen lg:w-auto dark:bg-dark-800`}
|
||||||
>
|
>
|
||||||
<div className={`flex lg:items-stretch ${contentClassName} ${isScrolled && `border-b border-pavitra-400 dark:border-dark-700`}`}>
|
<div className={`flex lg:items-stretch ${contentClassName} ${isScrolled && `border-b border-pavitra-400 dark:border-dark-700`}`}>
|
||||||
<div className="flex flex-1 items-stretch h-14">{children}</div>
|
<div className="flex flex-1 items-stretch h-12">{children}</div>
|
||||||
{hasMenu && (
|
{hasMenu && (
|
||||||
<>
|
<>
|
||||||
<div className="flex-none items-stretch flex h-14 lg:hidden">
|
<div className="flex-none items-stretch flex h-12 lg:hidden">
|
||||||
<NavBarItemPlain onClick={handleMenuNavBarToggleClick}>
|
<NavBarItemPlain onClick={handleMenuNavBarToggleClick}>
|
||||||
<BaseIcon path={isMenuNavBarActive ? mdiClose : mdiDotsVertical} size="24" />
|
<BaseIcon path={isMenuNavBarActive ? mdiClose : mdiDotsVertical} size="24" />
|
||||||
</NavBarItemPlain>
|
</NavBarItemPlain>
|
||||||
@ -51,7 +51,7 @@ export default function NavBar({ menu, className = '', contentClassName = contai
|
|||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
isMenuNavBarActive ? 'block' : 'hidden'
|
isMenuNavBarActive ? 'block' : 'hidden'
|
||||||
} flex items-center max-h-screen-menu overflow-y-auto lg:overflow-visible absolute w-screen top-14 left-0 ${bgColor} shadow-lg lg:w-auto lg:flex lg:static lg:shadow-none dark:bg-dark-800`}
|
} flex items-center max-h-screen-menu overflow-y-auto lg:overflow-visible absolute w-screen top-12 left-0 ${bgColor} shadow-lg lg:w-auto lg:flex lg:static lg:shadow-none dark:bg-dark-800`}
|
||||||
>
|
>
|
||||||
<NavBarMenuList menu={menu} />
|
<NavBarMenuList menu={menu} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -901,11 +901,11 @@ export default function WorkspaceShell() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={`${activeConversation ? 'h-[calc(100vh-3.5rem)]' : 'min-h-[calc(100vh-3.5rem)]'} bg-[#f5f5f7] px-0 sm:px-1.5 sm:pb-1.5 sm:pt-1.5`}
|
className={`${activeConversation ? 'h-[calc(100vh-3rem)]' : 'min-h-[calc(100vh-3rem)]'} bg-[#f5f5f7] px-0 sm:px-1.5 sm:pb-1.5 sm:pt-1.5`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`grid border border-slate-200 bg-white shadow-[0_16px_48px_-40px_rgba(15,23,42,0.18)] sm:rounded-[10px] lg:grid-cols-[220px_minmax(0,1fr)] ${
|
className={`grid border border-slate-200 bg-white shadow-[0_16px_48px_-40px_rgba(15,23,42,0.18)] sm:rounded-[10px] lg:grid-cols-[220px_minmax(0,1fr)] ${
|
||||||
activeConversation ? 'h-full min-h-0 overflow-hidden' : 'min-h-[calc(100vh-3.5rem)]'
|
activeConversation ? 'h-full min-h-0 overflow-hidden' : 'min-h-[calc(100vh-3rem)]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<aside
|
<aside
|
||||||
@ -1115,7 +1115,7 @@ export default function WorkspaceShell() {
|
|||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
className="inline-flex items-center gap-1.5 rounded-[8px] border border-slate-200 bg-white px-2.5 py-1.5 text-[12px] font-medium text-slate-700"
|
className="inline-flex items-center gap-1.5 rounded-[8px] border border-slate-200 bg-white px-2.5 py-1.5 text-[12px] font-medium text-slate-700"
|
||||||
href="/profile"
|
href="/settings"
|
||||||
>
|
>
|
||||||
<BaseIcon path={mdiCogOutline} size={16} />
|
<BaseIcon path={mdiCogOutline} size={16} />
|
||||||
Settings
|
Settings
|
||||||
|
|||||||
@ -3,7 +3,7 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply pt-14 xl:pl-60 h-full;
|
@apply pt-12 xl:pl-72 h-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
|
|||||||
@ -10,14 +10,12 @@ import {
|
|||||||
mdiLogout,
|
mdiLogout,
|
||||||
} from '@mdi/js'
|
} from '@mdi/js'
|
||||||
import menuAside from '../menuAside'
|
import menuAside from '../menuAside'
|
||||||
import menuNavBar from '../menuNavBar'
|
|
||||||
import BaseIcon from '../components/BaseIcon'
|
import BaseIcon from '../components/BaseIcon'
|
||||||
import NavBar from '../components/NavBar'
|
import NavBar from '../components/NavBar'
|
||||||
import NavBarItemPlain from '../components/NavBarItemPlain'
|
import NavBarItemPlain from '../components/NavBarItemPlain'
|
||||||
import AsideMenu from '../components/AsideMenu'
|
import AsideMenu from '../components/AsideMenu'
|
||||||
import FooterBar from '../components/FooterBar'
|
import FooterBar from '../components/FooterBar'
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||||||
import Search from '../components/Search';
|
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import {findMe, logoutUser} from "../stores/authSlice";
|
import {findMe, logoutUser} from "../stores/authSlice";
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@ -44,7 +42,6 @@ export default function LayoutAuthenticated({
|
|||||||
const { token, currentUser } = useAppSelector((state) => state.auth)
|
const { token, currentUser } = useAppSelector((state) => state.auth)
|
||||||
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
|
||||||
const isWorkspaceRoute = router.pathname.startsWith('/workspace');
|
const isWorkspaceRoute = router.pathname.startsWith('/workspace');
|
||||||
const workspaceNavMenu = isWorkspaceRoute ? [] : menuNavBar;
|
|
||||||
let localToken
|
let localToken
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
// Perform localStorage action
|
// Perform localStorage action
|
||||||
@ -100,92 +97,90 @@ export default function LayoutAuthenticated({
|
|||||||
}, [router.events, dispatch])
|
}, [router.events, dispatch])
|
||||||
|
|
||||||
|
|
||||||
const layoutAsidePadding = isWorkspaceRoute ? '' : 'xl:pl-60'
|
const layoutAsidePadding = isWorkspaceRoute ? '' : 'xl:pl-72'
|
||||||
const layoutOffsetClass = isWorkspaceRoute ? '' : isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''
|
const layoutOffsetClass = isWorkspaceRoute ? '' : isAsideMobileExpanded ? 'ml-72 lg:ml-0' : ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
|
||||||
<div
|
<div
|
||||||
className={`${layoutAsidePadding} ${layoutOffsetClass} pt-14 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
|
className={`${layoutAsidePadding} ${layoutOffsetClass} pt-12 min-h-screen w-screen transition-position lg:w-auto ${bgColor} dark:bg-dark-800 dark:text-slate-100`}
|
||||||
>
|
>
|
||||||
<NavBar
|
<NavBar
|
||||||
menu={workspaceNavMenu}
|
menu={[]}
|
||||||
className={`${layoutAsidePadding} ${layoutOffsetClass}`}
|
className={`${layoutAsidePadding} ${layoutOffsetClass}`}
|
||||||
contentClassName={isWorkspaceRoute ? 'w-full' : undefined}
|
contentClassName="w-full"
|
||||||
>
|
>
|
||||||
{isWorkspaceRoute ? (
|
<div className="flex min-w-0 flex-1 items-center justify-between px-3 sm:px-5">
|
||||||
<div className="flex min-w-0 flex-1 items-center justify-between px-4 sm:px-6">
|
<div className="flex min-w-0 items-center">
|
||||||
|
{!isWorkspaceRoute && (
|
||||||
|
<>
|
||||||
|
<NavBarItemPlain
|
||||||
|
display="flex lg:hidden"
|
||||||
|
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
|
||||||
|
>
|
||||||
|
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" />
|
||||||
|
</NavBarItemPlain>
|
||||||
|
<NavBarItemPlain
|
||||||
|
display="hidden lg:flex xl:hidden"
|
||||||
|
onClick={() => setIsAsideLgActive(true)}
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiMenu} size="24" />
|
||||||
|
</NavBarItemPlain>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Link
|
<Link
|
||||||
className="truncate text-xs font-semibold uppercase tracking-[0.3em] text-slate-500 dark:text-slate-300"
|
className="truncate text-[11px] font-semibold uppercase tracking-[0.28em] text-slate-500 dark:text-slate-300"
|
||||||
href="/workspace"
|
href="/workspace"
|
||||||
>
|
>
|
||||||
AI Chat Workspace
|
AI Chat Workspace
|
||||||
</Link>
|
</Link>
|
||||||
<div className="relative hidden md:block">
|
|
||||||
<button
|
|
||||||
className="inline-flex items-center gap-2 rounded-[8px] border border-slate-200 bg-white px-3 py-1.5 text-[12px] text-slate-500"
|
|
||||||
onClick={() => setIsWorkspaceAccountMenuOpen((previous) => !previous)}
|
|
||||||
ref={workspaceAccountMenuButtonRef}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<BaseIcon path={mdiAccountCircleOutline} size="18" />
|
|
||||||
<span className="max-w-[220px] truncate">
|
|
||||||
{currentUser?.email || 'Workspace user'}
|
|
||||||
</span>
|
|
||||||
<BaseIcon path={mdiChevronDown} size="16" />
|
|
||||||
</button>
|
|
||||||
{isWorkspaceAccountMenuOpen && (
|
|
||||||
<div className="absolute right-0 top-[calc(100%+0.5rem)] z-40 min-w-[220px]">
|
|
||||||
<ClickOutside
|
|
||||||
excludedElements={[workspaceAccountMenuButtonRef]}
|
|
||||||
onClickOutside={() => setIsWorkspaceAccountMenuOpen(false)}
|
|
||||||
>
|
|
||||||
<div className="overflow-hidden rounded-[10px] border border-slate-200 bg-white p-1 shadow-[0_16px_48px_-32px_rgba(15,23,42,0.25)]">
|
|
||||||
<Link
|
|
||||||
className="flex items-center gap-2 rounded-[8px] px-3 py-2 text-[13px] text-slate-700"
|
|
||||||
href="/profile"
|
|
||||||
onClick={() => setIsWorkspaceAccountMenuOpen(false)}
|
|
||||||
>
|
|
||||||
<BaseIcon path={mdiCogOutline} size="16" />
|
|
||||||
Settings
|
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
className="flex w-full items-center gap-2 rounded-[8px] px-3 py-2 text-left text-[13px] text-slate-700"
|
|
||||||
onClick={() => {
|
|
||||||
setIsWorkspaceAccountMenuOpen(false)
|
|
||||||
dispatch(logoutUser())
|
|
||||||
router.push('/login')
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<BaseIcon path={mdiLogout} size="16" />
|
|
||||||
Log out
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</ClickOutside>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<div className="relative">
|
||||||
<>
|
<button
|
||||||
<NavBarItemPlain
|
className="inline-flex items-center gap-1.5 rounded-[8px] border border-slate-200 bg-white px-2.5 py-1 text-[11px] text-slate-500"
|
||||||
display="flex lg:hidden"
|
onClick={() => setIsWorkspaceAccountMenuOpen((previous) => !previous)}
|
||||||
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
|
ref={workspaceAccountMenuButtonRef}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" />
|
<BaseIcon path={mdiAccountCircleOutline} size="18" />
|
||||||
</NavBarItemPlain>
|
<span className="hidden max-w-[220px] truncate md:inline">
|
||||||
<NavBarItemPlain
|
{currentUser?.email || 'Workspace user'}
|
||||||
display="hidden lg:flex xl:hidden"
|
</span>
|
||||||
onClick={() => setIsAsideLgActive(true)}
|
<BaseIcon path={mdiChevronDown} size="16" />
|
||||||
>
|
</button>
|
||||||
<BaseIcon path={mdiMenu} size="24" />
|
{isWorkspaceAccountMenuOpen && (
|
||||||
</NavBarItemPlain>
|
<div className="absolute right-0 top-[calc(100%+0.5rem)] z-40 min-w-[220px]">
|
||||||
<NavBarItemPlain useMargin>
|
<ClickOutside
|
||||||
<Search />
|
excludedElements={[workspaceAccountMenuButtonRef]}
|
||||||
</NavBarItemPlain>
|
onClickOutside={() => setIsWorkspaceAccountMenuOpen(false)}
|
||||||
</>
|
>
|
||||||
)}
|
<div className="overflow-hidden rounded-[10px] border border-slate-200 bg-white p-1 shadow-[0_16px_48px_-32px_rgba(15,23,42,0.25)]">
|
||||||
|
<Link
|
||||||
|
className="flex items-center gap-2 rounded-[8px] px-3 py-2 text-[13px] text-slate-700"
|
||||||
|
href="/settings"
|
||||||
|
onClick={() => setIsWorkspaceAccountMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiCogOutline} size="16" />
|
||||||
|
Settings
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
className="flex w-full items-center gap-2 rounded-[8px] px-3 py-2 text-left text-[13px] text-slate-700"
|
||||||
|
onClick={() => {
|
||||||
|
setIsWorkspaceAccountMenuOpen(false)
|
||||||
|
dispatch(logoutUser())
|
||||||
|
router.push('/login')
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<BaseIcon path={mdiLogout} size="16" />
|
||||||
|
Log out
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ClickOutside>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</NavBar>
|
</NavBar>
|
||||||
{!isWorkspaceRoute && (
|
{!isWorkspaceRoute && (
|
||||||
<AsideMenu
|
<AsideMenu
|
||||||
|
|||||||
@ -5,7 +5,7 @@ const backofficeMenu: MenuAsideItem[] = [
|
|||||||
{
|
{
|
||||||
href: '/dashboard',
|
href: '/dashboard',
|
||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiViewDashboardOutline,
|
||||||
label: 'Dashboard',
|
label: 'Overview',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/users/users-list',
|
href: '/users/users-list',
|
||||||
@ -15,22 +15,6 @@ const backofficeMenu: MenuAsideItem[] = [
|
|||||||
icon: icon.mdiAccountGroup ?? icon.mdiTable,
|
icon: icon.mdiAccountGroup ?? icon.mdiTable,
|
||||||
permissions: 'READ_USERS'
|
permissions: 'READ_USERS'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
href: '/roles/roles-list',
|
|
||||||
label: 'Roles',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_ROLES'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/permissions/permissions-list',
|
|
||||||
label: 'Permissions',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_PERMISSIONS'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
href: '/agents/agents-list',
|
href: '/agents/agents-list',
|
||||||
label: 'Agents',
|
label: 'Agents',
|
||||||
@ -55,14 +39,6 @@ const backofficeMenu: MenuAsideItem[] = [
|
|||||||
icon: 'mdiMessageTextOutline' in icon ? icon['mdiMessageTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon: 'mdiMessageTextOutline' in icon ? icon['mdiMessageTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
||||||
permissions: 'READ_MESSAGES'
|
permissions: 'READ_MESSAGES'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
href: '/attachments/attachments-list',
|
|
||||||
label: 'Attachments',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
icon: 'mdiPaperclip' in icon ? icon['mdiPaperclip' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
|
||||||
permissions: 'READ_ATTACHMENTS'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
href: '/usage_events/usage_events-list',
|
href: '/usage_events/usage_events-list',
|
||||||
label: 'Usage events',
|
label: 'Usage events',
|
||||||
@ -72,11 +48,21 @@ const backofficeMenu: MenuAsideItem[] = [
|
|||||||
permissions: 'READ_USAGE_EVENTS'
|
permissions: 'READ_USAGE_EVENTS'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/api-docs',
|
href: '/roles/roles-list',
|
||||||
target: '_blank',
|
label: 'Roles',
|
||||||
label: 'Swagger API',
|
withDevider: true,
|
||||||
icon: icon.mdiFileCode,
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
permissions: 'READ_API_DOCS'
|
// @ts-ignore
|
||||||
|
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
|
||||||
|
permissions: 'READ_ROLES'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/permissions/permissions-list',
|
||||||
|
label: 'Permissions',
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
||||||
|
permissions: 'READ_PERMISSIONS'
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -89,7 +75,7 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
label: 'Workspace',
|
label: 'Workspace',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/profile',
|
href: '/settings',
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
icon: icon.mdiAccountCircle,
|
icon: icon.mdiAccountCircle,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,381 +1,289 @@
|
|||||||
import * as icon from '@mdi/js';
|
import {
|
||||||
import Head from 'next/head'
|
mdiAccountGroup,
|
||||||
import React from 'react'
|
mdiArrowTopRight,
|
||||||
|
mdiChartTimelineVariant,
|
||||||
|
mdiChatOutline,
|
||||||
|
mdiCogOutline,
|
||||||
|
mdiMessageTextOutline,
|
||||||
|
mdiRobot,
|
||||||
|
mdiShieldAccountOutline,
|
||||||
|
mdiShieldAccountVariantOutline,
|
||||||
|
} from '@mdi/js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { ReactElement } from 'react'
|
import Head from 'next/head';
|
||||||
import LayoutAuthenticated from '../layouts/Authenticated'
|
import Link from 'next/link';
|
||||||
import SectionMain from '../components/SectionMain'
|
import React from 'react';
|
||||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
import type { ReactElement } from 'react';
|
||||||
import BaseIcon from "../components/BaseIcon";
|
|
||||||
import { getPageTitle } from '../config'
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import { hasPermission } from "../helpers/userPermissions";
|
import BaseIcon from '../components/BaseIcon';
|
||||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
import SectionMain from '../components/SectionMain';
|
||||||
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
import { getPageTitle } from '../config';
|
||||||
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
import { hasPermission } from '../helpers/userPermissions';
|
||||||
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
|
import { useAppSelector } from '../stores/hooks';
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
type DashboardCard = {
|
||||||
const Dashboard = () => {
|
key: string;
|
||||||
const dispatch = useAppDispatch();
|
label: string;
|
||||||
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
description: string;
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
href: string;
|
||||||
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
|
countPath: string;
|
||||||
|
permission: string;
|
||||||
|
iconPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
const loadingMessage = 'Loading...';
|
const loadingMessage = 'Loading...';
|
||||||
|
|
||||||
|
const workspaceCards: DashboardCard[] = [
|
||||||
const [users, setUsers] = React.useState(loadingMessage);
|
{
|
||||||
const [roles, setRoles] = React.useState(loadingMessage);
|
key: 'users',
|
||||||
const [permissions, setPermissions] = React.useState(loadingMessage);
|
label: 'Users',
|
||||||
const [agents, setAgents] = React.useState(loadingMessage);
|
description: 'Workspace members and account access.',
|
||||||
const [conversations, setConversations] = React.useState(loadingMessage);
|
href: '/users/users-list',
|
||||||
const [messages, setMessages] = React.useState(loadingMessage);
|
countPath: '/users/count',
|
||||||
const [attachments, setAttachments] = React.useState(loadingMessage);
|
permission: 'READ_USERS',
|
||||||
const [usage_events, setUsage_events] = React.useState(loadingMessage);
|
iconPath: mdiAccountGroup,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'agents',
|
||||||
|
label: 'Agents',
|
||||||
|
description: 'Prompt presets and assistant configurations.',
|
||||||
|
href: '/agents/agents-list',
|
||||||
|
countPath: '/agents/count',
|
||||||
|
permission: 'READ_AGENTS',
|
||||||
|
iconPath: mdiRobot,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'conversations',
|
||||||
|
label: 'Conversations',
|
||||||
|
description: 'Threads created inside the workspace.',
|
||||||
|
href: '/conversations/conversations-list',
|
||||||
|
countPath: '/conversations/count',
|
||||||
|
permission: 'READ_CONVERSATIONS',
|
||||||
|
iconPath: mdiChatOutline,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'messages',
|
||||||
|
label: 'Messages',
|
||||||
|
description: 'User prompts and assistant responses.',
|
||||||
|
href: '/messages/messages-list',
|
||||||
|
countPath: '/messages/count',
|
||||||
|
permission: 'READ_MESSAGES',
|
||||||
|
iconPath: mdiMessageTextOutline,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'usage_events',
|
||||||
|
label: 'Usage events',
|
||||||
|
description: 'Tracked model activity and generation usage.',
|
||||||
|
href: '/usage_events/usage_events-list',
|
||||||
|
countPath: '/usage_events/count',
|
||||||
|
permission: 'READ_USAGE_EVENTS',
|
||||||
|
iconPath: mdiChartTimelineVariant,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const accessCards: DashboardCard[] = [
|
||||||
const [widgetsRole, setWidgetsRole] = React.useState({
|
{
|
||||||
role: { value: '', label: '' },
|
key: 'roles',
|
||||||
|
label: 'Roles',
|
||||||
|
description: 'High-level access groups for the app.',
|
||||||
|
href: '/roles/roles-list',
|
||||||
|
countPath: '/roles/count',
|
||||||
|
permission: 'READ_ROLES',
|
||||||
|
iconPath: mdiShieldAccountVariantOutline,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'permissions',
|
||||||
|
label: 'Permissions',
|
||||||
|
description: 'Low-level capabilities used by the backoffice.',
|
||||||
|
href: '/permissions/permissions-list',
|
||||||
|
countPath: '/permissions/count',
|
||||||
|
permission: 'READ_PERMISSIONS',
|
||||||
|
iconPath: mdiShieldAccountOutline,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function Dashboard() {
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
const [counts, setCounts] = React.useState<Record<string, number | string | null>>({});
|
||||||
|
|
||||||
|
const visibleWorkspaceCards = workspaceCards.filter((card) => {
|
||||||
|
return hasPermission(currentUser, card.permission);
|
||||||
|
});
|
||||||
|
const visibleAccessCards = accessCards.filter((card) => {
|
||||||
|
return hasPermission(currentUser, card.permission);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
const visibleCards = [...visibleWorkspaceCards, ...visibleAccessCards];
|
||||||
|
const nextCounts: Record<string, number | string | null> = {};
|
||||||
|
|
||||||
|
visibleCards.forEach((card) => {
|
||||||
|
nextCounts[card.key] = loadingMessage;
|
||||||
});
|
});
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
setCounts(nextCounts);
|
||||||
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
|
||||||
|
|
||||||
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
|
||||||
|
|
||||||
|
|
||||||
async function loadData() {
|
|
||||||
const entities = ['users','roles','permissions','agents','conversations','messages','attachments','usage_events',];
|
|
||||||
const fns = [setUsers,setRoles,setPermissions,setAgents,setConversations,setMessages,setAttachments,setUsage_events,];
|
|
||||||
|
|
||||||
const requests = entities.map((entity, index) => {
|
const results = await Promise.allSettled(
|
||||||
|
visibleCards.map((card) => {
|
||||||
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
|
return axios.get(card.countPath);
|
||||||
return axios.get(`/${entity.toLowerCase()}/count`);
|
}),
|
||||||
} else {
|
);
|
||||||
fns[index](null);
|
|
||||||
return Promise.resolve({data: {count: null}});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.allSettled(requests).then((results) => {
|
const resolvedCounts: Record<string, number | string | null> = {};
|
||||||
results.forEach((result, i) => {
|
results.forEach((result, index) => {
|
||||||
if (result.status === 'fulfilled') {
|
const card = visibleCards[index];
|
||||||
fns[i](result.value.data.count);
|
|
||||||
} else {
|
if (result.status === 'fulfilled') {
|
||||||
fns[i](result.reason.message);
|
resolvedCounts[card.key] = result.value.data.count;
|
||||||
}
|
} else {
|
||||||
});
|
resolvedCounts[card.key] = result.reason.message;
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setCounts(resolvedCounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!currentUser) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWidgets(roleId) {
|
|
||||||
await dispatch(fetchWidgets(roleId));
|
|
||||||
}
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!currentUser) return;
|
|
||||||
loadData().then();
|
|
||||||
setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
|
|
||||||
}, [currentUser]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
loadData().catch((error) => {
|
||||||
if (!currentUser || !widgetsRole?.role?.value) return;
|
throw error;
|
||||||
getWidgets(widgetsRole?.role?.value || '').then();
|
});
|
||||||
}, [widgetsRole?.role?.value]);
|
}, [currentUser]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>{getPageTitle('Admin')}</title>
|
||||||
{getPageTitle('Admin')}
|
|
||||||
</title>
|
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton
|
<div className="mb-5 rounded-[10px] border border-slate-200 bg-white px-5 py-5">
|
||||||
icon={icon.mdiChartTimelineVariant}
|
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
|
||||||
title='Admin'
|
Admin
|
||||||
main>
|
</p>
|
||||||
{''}
|
<h1 className="mt-2 text-[1.65rem] font-semibold tracking-[-0.04em] text-slate-900">
|
||||||
</SectionTitleLineWithButton>
|
Keep an eye on the workspace and the people using it.
|
||||||
|
</h1>
|
||||||
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
|
||||||
currentUser={currentUser}
|
This area is for review, support, and control. Product work stays in the workspace.
|
||||||
isFetchingQuery={isFetchingQuery}
|
Admin stays focused on data, access, and assistant configuration.
|
||||||
setWidgetsRole={setWidgetsRole}
|
</p>
|
||||||
widgetsRole={widgetsRole}
|
<div className="mt-4 flex flex-wrap gap-3">
|
||||||
/>}
|
<Link
|
||||||
{!!rolesWidgets.length &&
|
className="inline-flex items-center gap-2 rounded-[10px] border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700"
|
||||||
hasPermission(currentUser, 'CREATE_ROLES') && (
|
href="/workspace"
|
||||||
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
|
>
|
||||||
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
|
Open workspace
|
||||||
</p>
|
<BaseIcon path={mdiArrowTopRight} size={16} />
|
||||||
)}
|
</Link>
|
||||||
|
<Link
|
||||||
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
|
className="inline-flex items-center gap-2 rounded-[10px] border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700"
|
||||||
{(isFetchingQuery || loading) && (
|
href="/settings"
|
||||||
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
|
>
|
||||||
<BaseIcon
|
Settings
|
||||||
className={`${iconsColor} animate-spin mr-5`}
|
<BaseIcon path={mdiCogOutline} size={16} />
|
||||||
w='w-16'
|
</Link>
|
||||||
h='h-16'
|
</div>
|
||||||
size={48}
|
|
||||||
path={icon.mdiLoading}
|
|
||||||
/>{' '}
|
|
||||||
Loading widgets...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ rolesWidgets &&
|
|
||||||
rolesWidgets.map((widget) => (
|
|
||||||
<SmartWidget
|
|
||||||
key={widget.id}
|
|
||||||
userId={currentUser?.id}
|
|
||||||
widget={widget}
|
|
||||||
roleId={widgetsRole?.role?.value || ''}
|
|
||||||
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!!rolesWidgets.length && <hr className='my-6 ' />}
|
<div className="mb-7">
|
||||||
|
<div className="mb-4">
|
||||||
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
<h3 className="text-lg font-semibold text-slate-900">Workspace data</h3>
|
||||||
|
<p className="mt-1 text-sm text-slate-500">
|
||||||
|
Core entities that power the AI workspace itself.
|
||||||
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
|
</p>
|
||||||
<div
|
</div>
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 2xl:grid-cols-3">
|
||||||
|
{visibleWorkspaceCards.map((card) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={card.key}
|
||||||
|
className="rounded-[12px] border border-slate-200 bg-white px-5 py-5"
|
||||||
|
href={card.href}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between align-center">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div className="min-w-0">
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
<p className="text-sm font-medium text-slate-900">{card.label}</p>
|
||||||
Users
|
<p className="mt-2 text-sm leading-6 text-slate-500">{card.description}</p>
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{users}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={icon.mdiAccountGroup || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
|
||||||
</Link>}
|
<BaseIcon path={card.iconPath} size={20} />
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Roles
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{roles}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>}
|
<div className="mt-5 flex items-end justify-between gap-3">
|
||||||
|
<div>
|
||||||
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}>
|
<p className="text-[11px] font-medium uppercase tracking-[0.24em] text-slate-400">
|
||||||
<div
|
Count
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
</p>
|
||||||
>
|
<p className="mt-2 text-[2rem] font-semibold tracking-[-0.04em] text-slate-900">
|
||||||
<div className="flex justify-between align-center">
|
{counts[card.key] ?? loadingMessage}
|
||||||
<div>
|
</p>
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Permissions
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{permissions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={icon.mdiShieldAccountOutline || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span className="inline-flex items-center gap-1 text-sm font-medium text-slate-500">
|
||||||
</Link>}
|
Open
|
||||||
|
<BaseIcon path={mdiArrowTopRight} size={15} />
|
||||||
{hasPermission(currentUser, 'READ_AGENTS') && <Link href={'/agents/agents-list'}>
|
</span>
|
||||||
<div
|
</div>
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
</Link>
|
||||||
>
|
);
|
||||||
<div className="flex justify-between align-center">
|
})}
|
||||||
<div>
|
</div>
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Agents
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{agents}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiRobot' in icon ? icon['mdiRobot' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_CONVERSATIONS') && <Link href={'/conversations/conversations-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Conversations
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{conversations}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiChatOutline' in icon ? icon['mdiChatOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_MESSAGES') && <Link href={'/messages/messages-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Messages
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{messages}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiMessageTextOutline' in icon ? icon['mdiMessageTextOutline' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_ATTACHMENTS') && <Link href={'/attachments/attachments-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Attachments
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{attachments}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiPaperclip' in icon ? icon['mdiPaperclip' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_USAGE_EVENTS') && <Link href={'/usage_events/usage_events-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Usage events
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{usage_events}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiChartTimelineVariant' in icon ? icon['mdiChartTimelineVariant' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!!visibleAccessCards.length && (
|
||||||
|
<div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-slate-900">Access control</h3>
|
||||||
|
<p className="mt-1 text-sm text-slate-500">
|
||||||
|
Keep roles and permission mapping visible, but secondary to the product itself.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
||||||
|
{visibleAccessCards.map((card) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={card.key}
|
||||||
|
className="rounded-[12px] border border-slate-200 bg-white px-5 py-5"
|
||||||
|
href={card.href}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-sm font-medium text-slate-900">{card.label}</p>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-slate-500">{card.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
|
||||||
|
<BaseIcon path={card.iconPath} size={20} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 flex items-end justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-[11px] font-medium uppercase tracking-[0.24em] text-slate-400">
|
||||||
|
Count
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-[2rem] font-semibold tracking-[-0.04em] text-slate-900">
|
||||||
|
{counts[card.key] ?? loadingMessage}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="inline-flex items-center gap-1 text-sm font-medium text-slate-500">
|
||||||
|
Open
|
||||||
|
<BaseIcon path={mdiArrowTopRight} size={15} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Dashboard.getLayout = function getLayout(page: ReactElement) {
|
Dashboard.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutAuthenticated permission={'READ_USERS'}>{page}</LayoutAuthenticated>
|
return <LayoutAuthenticated permission="READ_USERS">{page}</LayoutAuthenticated>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Dashboard
|
export default Dashboard;
|
||||||
|
|||||||
@ -4,9 +4,7 @@ import {
|
|||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import React, { ReactElement, useEffect, useState } from 'react';
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
import { ToastContainer, toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import DatePicker from 'react-datepicker';
|
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
|
||||||
|
|
||||||
import CardBox from '../components/CardBox';
|
import CardBox from '../components/CardBox';
|
||||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
@ -19,19 +17,15 @@ import FormField from '../components/FormField';
|
|||||||
import BaseDivider from '../components/BaseDivider';
|
import BaseDivider from '../components/BaseDivider';
|
||||||
import BaseButtons from '../components/BaseButtons';
|
import BaseButtons from '../components/BaseButtons';
|
||||||
import BaseButton from '../components/BaseButton';
|
import BaseButton from '../components/BaseButton';
|
||||||
import FormCheckRadio from '../components/FormCheckRadio';
|
|
||||||
import FormCheckRadioGroup from '../components/FormCheckRadioGroup';
|
|
||||||
import FormImagePicker from '../components/FormImagePicker';
|
import FormImagePicker from '../components/FormImagePicker';
|
||||||
import { SwitchField } from '../components/SwitchField';
|
|
||||||
import { SelectField } from '../components/SelectField';
|
|
||||||
|
|
||||||
import { update, fetch } from '../stores/users/usersSlice';
|
import { update } from '../stores/users/usersSlice';
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import {findMe} from "../stores/authSlice";
|
import {findMe} from "../stores/authSlice";
|
||||||
|
|
||||||
const EditUsers = () => {
|
const ProfilePage = () => {
|
||||||
const { currentUser, isFetching, token } = useAppSelector(
|
const { currentUser } = useAppSelector(
|
||||||
(state) => state.auth,
|
(state) => state.auth,
|
||||||
);
|
);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -42,8 +36,6 @@ const EditUsers = () => {
|
|||||||
lastName: '',
|
lastName: '',
|
||||||
phoneNumber: '',
|
phoneNumber: '',
|
||||||
email: '',
|
email: '',
|
||||||
app_role: '',
|
|
||||||
disabled: false,
|
|
||||||
avatar: [],
|
avatar: [],
|
||||||
password: ''
|
password: ''
|
||||||
};
|
};
|
||||||
@ -64,19 +56,19 @@ const EditUsers = () => {
|
|||||||
const handleSubmit = async (data) => {
|
const handleSubmit = async (data) => {
|
||||||
await dispatch(update({ id: currentUser.id, data }));
|
await dispatch(update({ id: currentUser.id, data }));
|
||||||
await dispatch(findMe());
|
await dispatch(findMe());
|
||||||
await router.push('/users/users-list');
|
await router.push('/settings');
|
||||||
notify('success', 'Profile was updated!');
|
notify('success', 'Profile was updated!');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Edit profile')}</title>
|
<title>{getPageTitle('Profile')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton
|
<SectionTitleLineWithButton
|
||||||
icon={mdiChartTimelineVariant}
|
icon={mdiChartTimelineVariant}
|
||||||
title='Edit profile'
|
title='Profile'
|
||||||
main
|
main
|
||||||
>
|
>
|
||||||
{''}
|
{''}
|
||||||
@ -124,25 +116,6 @@ const EditUsers = () => {
|
|||||||
<Field name='email' placeholder='E-Mail' disabled />
|
<Field name='email' placeholder='E-Mail' disabled />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label='App Role' labelFor='app_role'>
|
|
||||||
<Field
|
|
||||||
name='app_role'
|
|
||||||
id='app_role'
|
|
||||||
component={SelectField}
|
|
||||||
options={initialValues.app_role}
|
|
||||||
itemRef={'roles'}
|
|
||||||
showField={'name'}
|
|
||||||
></Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField label='Disabled' labelFor='disabled'>
|
|
||||||
<Field
|
|
||||||
name='disabled'
|
|
||||||
id='disabled'
|
|
||||||
component={SwitchField}
|
|
||||||
></Field>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="Password"
|
label="Password"
|
||||||
>
|
>
|
||||||
@ -162,7 +135,7 @@ const EditUsers = () => {
|
|||||||
color='danger'
|
color='danger'
|
||||||
outline
|
outline
|
||||||
label='Cancel'
|
label='Cancel'
|
||||||
onClick={() => router.push('/users/users-list')}
|
onClick={() => router.push('/settings')}
|
||||||
/>
|
/>
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</Form>
|
</Form>
|
||||||
@ -173,8 +146,8 @@ const EditUsers = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
EditUsers.getLayout = function getLayout(page: ReactElement) {
|
ProfilePage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditUsers;
|
export default ProfilePage;
|
||||||
|
|||||||
131
frontend/src/pages/settings.tsx
Normal file
131
frontend/src/pages/settings.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import {
|
||||||
|
mdiAccountCircleOutline,
|
||||||
|
mdiCogOutline,
|
||||||
|
mdiChevronRight,
|
||||||
|
mdiFileCodeOutline,
|
||||||
|
mdiOpenInNew,
|
||||||
|
} from '@mdi/js';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import React, { ReactElement } from 'react';
|
||||||
|
|
||||||
|
import BaseIcon from '../components/BaseIcon';
|
||||||
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
|
import SectionMain from '../components/SectionMain';
|
||||||
|
import { getPageTitle } from '../config';
|
||||||
|
import { hasPermission } from '../helpers/userPermissions';
|
||||||
|
import { useAppSelector } from '../stores/hooks';
|
||||||
|
|
||||||
|
function SettingsPage() {
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
const canAccessAdmin = hasPermission(currentUser, 'READ_USERS');
|
||||||
|
const canAccessApiDocs = hasPermission(currentUser, 'READ_API_DOCS');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Settings')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<div className="mx-auto flex w-full max-w-4xl flex-col gap-6">
|
||||||
|
<div className="rounded-[12px] border border-slate-200 bg-white px-6 py-6">
|
||||||
|
<p className="text-[11px] font-medium uppercase tracking-[0.28em] text-slate-400">
|
||||||
|
Settings
|
||||||
|
</p>
|
||||||
|
<h1 className="mt-3 text-[2rem] font-semibold tracking-[-0.04em] text-slate-900">
|
||||||
|
Manage your account and workspace access.
|
||||||
|
</h1>
|
||||||
|
<p className="mt-3 max-w-2xl text-sm leading-6 text-slate-500">
|
||||||
|
Update your personal details, change your password, and move to admin tools when you
|
||||||
|
need deeper control over the workspace.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<Link
|
||||||
|
className="group rounded-[12px] border border-slate-200 bg-white px-5 py-5"
|
||||||
|
href="/profile"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
|
||||||
|
<BaseIcon path={mdiAccountCircleOutline} size={20} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-medium text-slate-900">Profile</h2>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-slate-500">
|
||||||
|
Edit your name, avatar, phone number, and password.
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 text-[12px] text-slate-400">
|
||||||
|
{currentUser?.email || 'Workspace user'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-0.5 text-slate-400">
|
||||||
|
<BaseIcon path={mdiChevronRight} size={18} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{canAccessAdmin && (
|
||||||
|
<Link
|
||||||
|
className="group rounded-[12px] border border-slate-200 bg-white px-5 py-5"
|
||||||
|
href="/dashboard"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
|
||||||
|
<BaseIcon path={mdiCogOutline} size={20} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-medium text-slate-900">Admin</h2>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-slate-500">
|
||||||
|
Open the workspace backoffice for users, agents, conversations, usage, and access control.
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 inline-flex items-center gap-1 text-[12px] text-slate-400">
|
||||||
|
Workspace backoffice
|
||||||
|
<BaseIcon path={mdiOpenInNew} size={14} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-0.5 text-slate-400">
|
||||||
|
<BaseIcon path={mdiChevronRight} size={18} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canAccessApiDocs && (
|
||||||
|
<Link
|
||||||
|
className="group rounded-[12px] border border-slate-200 bg-white px-5 py-5"
|
||||||
|
href="/api-docs"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<div className="mb-4 inline-flex h-10 w-10 items-center justify-center rounded-[10px] border border-slate-200 bg-slate-50 text-slate-600">
|
||||||
|
<BaseIcon path={mdiFileCodeOutline} size={20} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-medium text-slate-900">API docs</h2>
|
||||||
|
<p className="mt-2 text-sm leading-6 text-slate-500">
|
||||||
|
Open Swagger when you need routes, request formats, and backend reference details.
|
||||||
|
</p>
|
||||||
|
<p className="mt-4 inline-flex items-center gap-1 text-[12px] text-slate-400">
|
||||||
|
Developer reference
|
||||||
|
<BaseIcon path={mdiOpenInNew} size={14} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-0.5 text-slate-400">
|
||||||
|
<BaseIcon path={mdiChevronRight} size={18} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsPage;
|
||||||
Loading…
x
Reference in New Issue
Block a user