feat: navbar buttons depended on selected project

This commit is contained in:
2025-10-05 19:54:02 +04:00
parent b35961329e
commit 665625557d
15 changed files with 233 additions and 145 deletions

View File

@ -1,16 +1,15 @@
"use client"; "use client";
import { FC } from "react"; import { FC } from "react";
import { IconEdit, IconFilter, IconPlus } from "@tabler/icons-react"; import { IconFilter } from "@tabler/icons-react";
import { Flex, Group, Indicator } from "@mantine/core"; import { Button, Divider, Flex, Group, Indicator } from "@mantine/core";
import { modals } from "@mantine/modals"; import { modals } from "@mantine/modals";
import style from "@/app/deals/components/desktop/ViewSelectButton/ViewSelectButton.module.css";
import ViewSelector from "@/app/deals/components/desktop/ViewSelector/ViewSelector"; import ViewSelector from "@/app/deals/components/desktop/ViewSelector/ViewSelector";
import { useDealsContext } from "@/app/deals/contexts/DealsContext"; import { useDealsContext } from "@/app/deals/contexts/DealsContext";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext"; import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import { DealsFiltersForm } from "@/app/deals/hooks/useDealsFilters"; import { DealsFiltersForm } from "@/app/deals/hooks/useDealsFilters";
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect"; import SmallPageBlock from "@/components/layout/SmallPageBlock/SmallPageBlock";
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
import { useDrawersContext } from "@/drawers/DrawersContext";
import useIsMobile from "@/hooks/utils/useIsMobile"; import useIsMobile from "@/hooks/utils/useIsMobile";
export type View = "board" | "table" | "schedule"; export type View = "board" | "table" | "schedule";
@ -22,37 +21,11 @@ type Props = {
const TopToolPanel: FC<Props> = ({ view, setView }) => { const TopToolPanel: FC<Props> = ({ view, setView }) => {
const { dealsFiltersForm, isChangedFilters } = useDealsContext(); const { dealsFiltersForm, isChangedFilters } = useDealsContext();
const { projects, setSelectedProjectId, selectedProject, projectsCrud } = const { selectedProject } = useProjectsContext();
useProjectsContext();
const { openDrawer } = useDrawersContext();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
if (isMobile) return; if (isMobile) return;
const onCreateClick = () => {
modals.openContextModal({
modal: "enterNameModal",
title: "Создание проекта",
withCloseButton: true,
innerProps: {
onChange: projectsCrud.onCreate,
},
});
};
const onEditClick = () => {
if (!selectedProject) return;
openDrawer({
key: "projectEditorDrawer",
props: {
value: selectedProject,
onChange: value => projectsCrud.onUpdate(value.id, value),
onDelete: projectsCrud.onDelete,
},
});
};
const viewFiltersModalMap = { const viewFiltersModalMap = {
table: "dealsTableFiltersModal", table: "dealsTableFiltersModal",
board: "dealsBoardFiltersModal", board: "dealsBoardFiltersModal",
@ -75,11 +48,12 @@ const TopToolPanel: FC<Props> = ({ view, setView }) => {
}; };
return ( return (
<Group justify={"space-between"}> <Group>
<ViewSelector <ViewSelector
value={view} value={view}
onChange={setView} onChange={setView}
/> />
<Divider orientation={"vertical"} />
<Flex <Flex
wrap={"nowrap"} wrap={"nowrap"}
align={"center"} align={"center"}
@ -87,24 +61,19 @@ const TopToolPanel: FC<Props> = ({ view, setView }) => {
<Indicator <Indicator
zIndex={100} zIndex={100}
disabled={!isChangedFilters} disabled={!isChangedFilters}
offset={3} offset={5}
size={8}> size={8}>
<ActionIconWithTip onClick={onFiltersClick}> <SmallPageBlock
style={{ borderRadius: "var(--mantine-radius-xl)" }}>
<Button
unstyled
onClick={onFiltersClick}
radius="xl"
className={style.container}>
<IconFilter /> <IconFilter />
</ActionIconWithTip> </Button>
</SmallPageBlock>
</Indicator> </Indicator>
<ActionIconWithTip onClick={onEditClick}>
<IconEdit />
</ActionIconWithTip>
<ActionIconWithTip onClick={onCreateClick}>
<IconPlus />
</ActionIconWithTip>
<ProjectSelect
data={projects}
value={selectedProject}
onChange={value => value && setSelectedProjectId(value.id)}
style={{ width: 250 }}
/>
</Flex> </Flex>
</Group> </Group>
); );

View File

@ -7,7 +7,6 @@ import {
import { Center, Loader } from "@mantine/core"; import { Center, Loader } from "@mantine/core";
import PageBody from "@/app/deals/components/shared/PageBody/PageBody"; import PageBody from "@/app/deals/components/shared/PageBody/PageBody";
import { BoardsContextProvider } from "@/app/deals/contexts/BoardsContext"; import { BoardsContextProvider } from "@/app/deals/contexts/BoardsContext";
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
import { StatusesContextProvider } from "@/app/deals/contexts/StatusesContext"; import { StatusesContextProvider } from "@/app/deals/contexts/StatusesContext";
import PageContainer from "@/components/layout/PageContainer/PageContainer"; import PageContainer from "@/components/layout/PageContainer/PageContainer";
import { import {
@ -35,7 +34,6 @@ export default async function DealsPage() {
const Providers = combineProviders( const Providers = combineProviders(
[HydrationBoundary, { state: dehydrate(queryClient) }], [HydrationBoundary, { state: dehydrate(queryClient) }],
[ProjectsContextProvider],
[BoardsContextProvider], [BoardsContextProvider],
[StatusesContextProvider] [StatusesContextProvider]
); );

View File

@ -16,6 +16,7 @@ import { theme } from "@/theme";
import "@/app/global.css"; import "@/app/global.css";
import { ModalsProvider } from "@mantine/modals"; import { ModalsProvider } from "@mantine/modals";
import { Notifications } from "@mantine/notifications"; import { Notifications } from "@mantine/notifications";
import { ProjectsContextProvider } from "@/app/deals/contexts/ProjectsContext";
import AppShellFooterWrapper from "@/components/layout/AppShellWrappers/AppShellFooterWrapper"; import AppShellFooterWrapper from "@/components/layout/AppShellWrappers/AppShellFooterWrapper";
import AppShellMainWrapper from "@/components/layout/AppShellWrappers/AppShellMainWrapper"; import AppShellMainWrapper from "@/components/layout/AppShellWrappers/AppShellMainWrapper";
import AppShellNavbarWrapper from "@/components/layout/AppShellWrappers/AppShellNavbarWrapper"; import AppShellNavbarWrapper from "@/components/layout/AppShellWrappers/AppShellNavbarWrapper";
@ -68,11 +69,12 @@ export default function RootLayout({ children }: Props) {
labels={{ confirm: "Да", cancel: "Нет" }} labels={{ confirm: "Да", cancel: "Нет" }}
modals={modals}> modals={modals}>
<DrawersContextProvider> <DrawersContextProvider>
<ProjectsContextProvider>
<AppShell <AppShell
layout={"alt"} layout={"alt"}
withBorder={false} withBorder={false}
navbar={{ navbar={{
width: 145, width: 250,
breakpoint: "sm", breakpoint: "sm",
collapsed: { collapsed: {
desktop: false, desktop: false,
@ -89,6 +91,7 @@ export default function RootLayout({ children }: Props) {
<Footer /> <Footer />
</AppShellFooterWrapper> </AppShellFooterWrapper>
</AppShell> </AppShell>
</ProjectsContextProvider>
</DrawersContextProvider> </DrawersContextProvider>
</ModalsProvider> </ModalsProvider>
</ReduxProvider> </ReduxProvider>

View File

@ -10,7 +10,7 @@
.link { .link {
width: 100%; width: 100%;
height: rem(50px); height: rem(50px);
border-radius: var(--mantine-radius-md); border-radius: var(--mantine-radius-lg);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -1,39 +1,11 @@
import {
IconColumns,
IconFileBarcode,
IconLayoutKanban,
IconUsers,
} from "@tabler/icons-react";
import { Box, Flex } from "@mantine/core"; import { Box, Flex } from "@mantine/core";
import ProjectSelectContainer from "@/components/layout/Navbar/components/ProjectSelectContainer";
import PageBlock from "@/components/layout/PageBlock/PageBlock"; import PageBlock from "@/components/layout/PageBlock/PageBlock";
import { ColorSchemeToggle } from "@/components/ui/ColorSchemeToggle/ColorSchemeToggle"; import { ColorSchemeToggle } from "@/components/ui/ColorSchemeToggle/ColorSchemeToggle";
import Logo from "@/components/ui/Logo/Logo"; import Logo from "@/components/ui/Logo/Logo";
import NavbarLinks from "./NavbarLinks"; import NavbarLinks from "./components/NavbarLinks";
import styles from "./Navbar.module.css"; import styles from "./Navbar.module.css";
const linksData = [
{
icon: IconLayoutKanban,
label: "Сделки",
href: "/deals",
},
{
icon: IconUsers,
label: "Клиенты",
href: "/clients",
},
{
icon: IconColumns,
label: "Услуги",
href: "/services",
},
{
icon: IconFileBarcode,
label: "Шаблоны штрихкодов",
href: "/barcode-templates",
},
];
const Navbar = () => { const Navbar = () => {
return ( return (
<Box className={styles.background}> <Box className={styles.background}>
@ -47,11 +19,13 @@ const Navbar = () => {
direction={"column"} direction={"column"}
h={"100%"}> h={"100%"}>
<Logo title={"Fulfillment & Delivery"} /> <Logo title={"Fulfillment & Delivery"} />
<ProjectSelectContainer />
<Flex <Flex
mt={"xs"}
direction={"column"} direction={"column"}
justify={"space-between"} justify={"space-between"}
h={"100%"}> h={"100%"}>
<NavbarLinks linksData={linksData} /> <NavbarLinks />
<ColorSchemeToggle /> <ColorSchemeToggle />
</Flex> </Flex>
</Flex> </Flex>

View File

@ -1,40 +0,0 @@
import { IconPlus } from "@tabler/icons-react";
import { Center, Stack, Tooltip } from "@mantine/core";
import NavbarClickable from "./NavbarClickable";
type LinkData = {
icon: typeof IconPlus;
label: string;
href: string;
};
type Props = {
linksData: LinkData[];
};
const NavbarLinks = ({ linksData }: Props) => {
return (
<Stack gap={"xs"}>
{linksData.map((linkData, index) => (
<NavbarClickable
href={linkData.href}
key={linkData.label}>
<Tooltip
key={index}
display={!linkData.label ? "none" : "flex"}
label={linkData.label}
position="right"
transitionProps={{ duration: 0 }}>
<Center
w={"100%"}
h={"100%"}>
<linkData.icon />
</Center>
</Tooltip>
</NavbarClickable>
))}
</Stack>
);
};
export default NavbarLinks;

View File

@ -3,7 +3,7 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import styles from "./Navbar.module.css"; import styles from "../Navbar.module.css";
type Props = { type Props = {
href: string; href: string;

View File

@ -0,0 +1,51 @@
"use client";
import { useEffect, useMemo } from "react";
import { usePathname, useRouter } from "next/navigation";
import { Group, Stack, Text } from "@mantine/core";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import ThemeIcon from "@/components/ui/ThemeIcon/ThemeIcon";
import linksData from "../data/linksData";
import NavbarClickable from "./NavbarClickable";
const NavbarLinks = () => {
const pathname = usePathname();
const router = useRouter();
const { modulesSet, selectedProject } = useProjectsContext();
const filteredLinks = useMemo(
() =>
linksData.filter(
link => !link.moduleName || modulesSet.has(link.moduleName)
),
[modulesSet]
);
useEffect(() => {
if (pathname !== "/deals") router.push("/deals");
}, [selectedProject]);
return (
<Stack gap={"xs"}>
{filteredLinks.map((linkData, index) => (
<NavbarClickable
href={linkData.href}
key={linkData.label}>
<Group
key={index}
px={"xs"}
w={"100%"}
justify={"left"}
wrap={"nowrap"}
gap={"xs"}>
<ThemeIcon size={"sm"}>
<linkData.icon />
</ThemeIcon>
<Text>{linkData.label}</Text>
</Group>
</NavbarClickable>
))}
</Stack>
);
};
export default NavbarLinks;

View File

@ -0,0 +1,40 @@
"use client";
import { IconEdit, IconPlus } from "@tabler/icons-react";
import { Group, HoverCard } from "@mantine/core";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import useProjectActions from "@/components/layout/Navbar/hooks/useProjectActions";
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
const ProjectSelectContainer = () => {
const { projects, selectedProject, setSelectedProjectId } =
useProjectsContext();
const { onCreateClick, onChangeClick } = useProjectActions();
return (
<HoverCard zIndex={150} position={"right"}>
<HoverCard.Target>
<ProjectSelect
data={projects}
value={selectedProject}
onChange={value => value && setSelectedProjectId(value.id)}
/>
</HoverCard.Target>
<HoverCard.Dropdown
p={"xs"}
bdrs={"xl"}>
<Group gap={"xs"}>
<ActionIconWithTip onClick={onChangeClick}>
<IconEdit />
</ActionIconWithTip>
<ActionIconWithTip onClick={onCreateClick}>
<IconPlus />
</ActionIconWithTip>
</Group>
</HoverCard.Dropdown>
</HoverCard>
);
};
export default ProjectSelectContainer;

View File

@ -0,0 +1,37 @@
import {
IconColumns,
IconFileBarcode,
IconLayoutKanban,
IconUsers,
} from "@tabler/icons-react";
import LinkData from "@/components/layout/Navbar/types/LinkData";
import { ModuleNames } from "@/modules/modules";
const linksData: LinkData[] = [
{
icon: IconLayoutKanban,
label: "Сделки",
href: "/deals",
moduleName: undefined,
},
{
icon: IconUsers,
label: "Клиенты",
href: "/clients",
moduleName: ModuleNames.CLIENTS,
},
{
icon: IconColumns,
label: "Услуги",
href: "/services",
moduleName: ModuleNames.FULFILLMENT_BASE,
},
{
icon: IconFileBarcode,
label: "Шаблоны штрихкодов",
href: "/barcode-templates",
moduleName: ModuleNames.FULFILLMENT_BASE,
},
];
export default linksData;

View File

@ -0,0 +1,39 @@
import { modals } from "@mantine/modals";
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
import { useDrawersContext } from "@/drawers/DrawersContext";
const useProjectActions = () => {
const { selectedProject, projectsCrud } = useProjectsContext();
const { openDrawer } = useDrawersContext();
const onCreateClick = () => {
modals.openContextModal({
modal: "enterNameModal",
title: "Создание проекта",
withCloseButton: true,
innerProps: {
onChange: projectsCrud.onCreate,
},
});
};
const onChangeClick = () => {
if (!selectedProject) return;
openDrawer({
key: "projectEditorDrawer",
props: {
value: selectedProject,
onChange: value => projectsCrud.onUpdate(value.id, value),
onDelete: projectsCrud.onDelete,
},
});
};
return {
onCreateClick,
onChangeClick,
};
};
export default useProjectActions;

View File

@ -0,0 +1,11 @@
import { IconPlus } from "@tabler/icons-react";
import { ModuleNames } from "@/modules/modules";
type LinkData = {
icon: typeof IconPlus;
label: string;
href: string;
moduleName?: ModuleNames;
};
export default LinkData;

View File

@ -1,3 +1,5 @@
"use client";
import { FC } from "react"; import { FC } from "react";
import { IMaskInput } from "react-imask"; import { IMaskInput } from "react-imask";
import { import {

View File

@ -1,3 +1,5 @@
"use client";
import { FC, useEffect, useMemo, useState } from "react"; import { FC, useEffect, useMemo, useState } from "react";
import { Text } from "@mantine/core"; import { Text } from "@mantine/core";
import useClientsList from "@/app/clients/hooks/useClientsList"; import useClientsList from "@/app/clients/hooks/useClientsList";

View File

@ -1,3 +1,5 @@
"use client";
import { FC, useEffect, useRef, useState } from "react"; import { FC, useEffect, useRef, useState } from "react";
import { isNull } from "lodash"; import { isNull } from "lodash";
import type { Swiper as SwiperClass } from "swiper/types"; import type { Swiper as SwiperClass } from "swiper/types";