Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 095f3dae76 |
@ -1,21 +0,0 @@
|
|||||||
.storybook
|
|
||||||
tests
|
|
||||||
__tests__
|
|
||||||
*.spec.ts
|
|
||||||
*.test.ts
|
|
||||||
node_modules
|
|
||||||
.yarn/cache
|
|
||||||
.eslint
|
|
||||||
.prettier
|
|
||||||
.stylelint
|
|
||||||
.env
|
|
||||||
.idea
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
Dockerfile
|
|
||||||
README.md
|
|
||||||
*.log
|
|
||||||
test
|
|
||||||
docs
|
|
||||||
coverage
|
|
||||||
*.map
|
|
||||||
@ -1 +1 @@
|
|||||||
NEXT_PUBLIC_API_URL=http://test.crm.logidex.ru/api
|
NEXT_PUBLIC_API_URL=http://api.example.com/api
|
||||||
48
Dockerfile
48
Dockerfile
@ -1,48 +0,0 @@
|
|||||||
FROM node:lts-alpine AS base
|
|
||||||
|
|
||||||
# Install dependencies only when needed
|
|
||||||
FROM base AS deps
|
|
||||||
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
|
||||||
RUN apk add --no-cache libc6-compat
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN corepack enable
|
|
||||||
COPY .yarn ./.yarn
|
|
||||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .yarnrc.yml ./
|
|
||||||
|
|
||||||
RUN yarn && rm -rf .yarn/cache .yarn/unplugged .yarn/build-state.yml
|
|
||||||
|
|
||||||
|
|
||||||
# Rebuild the source code only when needed
|
|
||||||
FROM base AS builder
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
|
||||||
|
|
||||||
RUN yarn build
|
|
||||||
|
|
||||||
# Production image, copy all the files and run next
|
|
||||||
FROM base AS runner
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
|
||||||
RUN adduser --system --uid 1001 nextjs
|
|
||||||
|
|
||||||
COPY --from=builder /app/public ./public
|
|
||||||
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
|
||||||
|
|
||||||
USER nextjs
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
ENV PORT=3000
|
|
||||||
|
|
||||||
ENV HOSTNAME="0.0.0.0"
|
|
||||||
CMD ["node", "server.js"]
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
const zodPath = "src/lib/client/zod.gen.ts";
|
|
||||||
let content = fs.readFileSync(zodPath, "utf8");
|
|
||||||
// Replace only for the upload schema
|
|
||||||
const zodTarget = "upload_file: z.string";
|
|
||||||
while (content.includes(zodTarget)) {
|
|
||||||
content = content.replace(zodTarget, "upload_file: z.any");
|
|
||||||
}
|
|
||||||
fs.writeFileSync(zodPath, content);
|
|
||||||
console.log("✅ Fixed zod schema for upload_file");
|
|
||||||
|
|
||||||
const utilsPath = "src/lib/client/client/utils.ts";
|
|
||||||
content = fs.readFileSync(utilsPath, "utf8");
|
|
||||||
|
|
||||||
const utilsTarget = "@ts-expect-error";
|
|
||||||
while (content.includes(utilsTarget)) {
|
|
||||||
content = content.replace(utilsTarget, "@ts-ignore");
|
|
||||||
}
|
|
||||||
content = content.replace(
|
|
||||||
"...(mergedHeaders[key] ?? []),",
|
|
||||||
"...(mergedHeaders[key] ?? []) as any,"
|
|
||||||
);
|
|
||||||
fs.writeFileSync(utilsPath, content);
|
|
||||||
console.log("✅ Fixed utils.ts");
|
|
||||||
@ -11,11 +11,9 @@ export default defineConfig({
|
|||||||
{
|
{
|
||||||
name: "zod",
|
name: "zod",
|
||||||
requests: true,
|
requests: true,
|
||||||
|
responses: true,
|
||||||
definitions: true,
|
definitions: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
dates: {
|
|
||||||
offset: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "@hey-api/sdk",
|
name: "@hey-api/sdk",
|
||||||
|
|||||||
27
package.json
27
package.json
@ -3,21 +3,16 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"generate-client": "openapi-ts && prettier --write ./src/lib/client/**/*.ts && git add ./src/lib/client & sudo npx tsc fix-client.ts && mv -f ./fix-client.js ./fix-client.cjs && sudo node ./fix-client.cjs",
|
"generate-client": "openapi-ts && prettier --write ./src/client/**/*.ts && git add ./src/client"
|
||||||
"generate-modules": "sudo npx tsc ./src/modules/modulesFileGen/modulesFileGen.ts && mv -f ./src/modules/modulesFileGen/modulesFileGen.js ./src/modules/modulesFileGen/modulesFileGen.cjs && sudo node ./src/modules/modulesFileGen/modulesFileGen.cjs"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
|
||||||
"@mantine/core": "8.1.2",
|
"@mantine/core": "8.1.2",
|
||||||
"@mantine/dates": "^8.2.7",
|
|
||||||
"@mantine/dropzone": "^8.3.1",
|
|
||||||
"@mantine/form": "^8.1.3",
|
"@mantine/form": "^8.1.3",
|
||||||
"@mantine/hooks": "8.1.2",
|
"@mantine/hooks": "8.1.2",
|
||||||
"@mantine/modals": "^8.2.1",
|
"@mantine/modals": "^8.2.1",
|
||||||
@ -27,28 +22,19 @@
|
|||||||
"@tabler/icons-react": "^3.34.0",
|
"@tabler/icons-react": "^3.34.0",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@tanstack/react-query": "^5.83.0",
|
"@tanstack/react-query": "^5.83.0",
|
||||||
"axios": "1.12.0",
|
"axios": "^1.11.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"date-fns": "^4.1.0",
|
|
||||||
"date-fns-tz": "^3.2.0",
|
|
||||||
"dayjs": "^1.11.18",
|
|
||||||
"framer-motion": "^12.23.7",
|
"framer-motion": "^12.23.7",
|
||||||
"handlebars": "^4.7.8",
|
|
||||||
"i18n-iso-countries": "^7.14.0",
|
"i18n-iso-countries": "^7.14.0",
|
||||||
"lexorank": "^1.0.5",
|
"lexorank": "^1.0.5",
|
||||||
"libphonenumber-js": "^1.12.10",
|
"libphonenumber-js": "^1.12.10",
|
||||||
"mantine-contextmenu": "^8.2.0",
|
"next": "15.3.3",
|
||||||
"mantine-datatable": "^8.2.0",
|
|
||||||
"next": "15.4.7",
|
|
||||||
"phone": "^3.1.67",
|
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "19.1.0",
|
||||||
"react-imask": "^7.6.1",
|
"react-imask": "^7.6.1",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.3",
|
||||||
"swiper": "^11.2.10",
|
|
||||||
"zod": "^4.0.14"
|
"zod": "^4.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -69,11 +55,8 @@
|
|||||||
"@types/lodash": "^4.17.20",
|
"@types/lodash": "^4.17.20",
|
||||||
"@types/node": "^22.13.11",
|
"@types/node": "^22.13.11",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
"@types/react-dom": "^19",
|
|
||||||
"@types/react-redux": "^7.1.34",
|
"@types/react-redux": "^7.1.34",
|
||||||
"@types/react-slick": "^0",
|
|
||||||
"@types/redux-persist": "^4.3.1",
|
"@types/redux-persist": "^4.3.1",
|
||||||
"@types/slick-carousel": "^1",
|
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"babel-loader": "^10.0.0",
|
"babel-loader": "^10.0.0",
|
||||||
"eslint": "^9.29.0",
|
"eslint": "^9.29.0",
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
.link {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: var(--mantine-radius-lg);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
@mixin light {
|
|
||||||
color: var(--mantine-color-gray-7);
|
|
||||||
}
|
|
||||||
@mixin dark {
|
|
||||||
color: var(--mantine-color-dark-0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Button, Stack, Text } from "@mantine/core";
|
|
||||||
import ThemeIcon from "@/components/ui/ThemeIcon/ThemeIcon";
|
|
||||||
import LinkData from "@/types/LinkData";
|
|
||||||
import styles from "./Action.module.css";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
linkData: LinkData;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Action: FC<Props> = ({ linkData }) => {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
href={linkData.href}
|
|
||||||
className={styles.link}>
|
|
||||||
<Button
|
|
||||||
w={"100%"}
|
|
||||||
h={"100px"}
|
|
||||||
variant={"default"}>
|
|
||||||
<Stack
|
|
||||||
px={"xs"}
|
|
||||||
w={"100%"}
|
|
||||||
align={"center"}
|
|
||||||
gap={"xs"}>
|
|
||||||
<ThemeIcon size={"sm"}>
|
|
||||||
<linkData.icon />
|
|
||||||
</ThemeIcon>
|
|
||||||
<Text>{linkData.label}</Text>
|
|
||||||
</Stack>
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Action;
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { RefObject, useMemo, useRef } from "react";
|
|
||||||
import { IconApps, IconList, IconTag } from "@tabler/icons-react";
|
|
||||||
import { SimpleGrid, Stack } from "@mantine/core";
|
|
||||||
import Action from "@/app/actions/components/Action/Action";
|
|
||||||
import mobileButtonsData from "@/app/actions/data/mobileButtonsData";
|
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
|
||||||
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
|
||||||
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
|
|
||||||
import BuiltInLinkData from "@/types/BuiltInLinkData";
|
|
||||||
|
|
||||||
const PageBody = () => {
|
|
||||||
const { selectedProject, setSelectedProjectId, projects, modulesSet } =
|
|
||||||
useProjectsContext();
|
|
||||||
|
|
||||||
const filteredMobileButtonsData = useMemo(
|
|
||||||
() =>
|
|
||||||
mobileButtonsData.filter(
|
|
||||||
link => !link.moduleName || modulesSet.has(link.moduleName)
|
|
||||||
),
|
|
||||||
[modulesSet]
|
|
||||||
);
|
|
||||||
|
|
||||||
const commonActionsData: RefObject<BuiltInLinkData[]> = useRef([
|
|
||||||
{
|
|
||||||
icon: IconList,
|
|
||||||
label: "Атрибуты",
|
|
||||||
href: "/attributes",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: IconApps,
|
|
||||||
label: "Модули",
|
|
||||||
href: "/modules",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: IconTag,
|
|
||||||
label: "Теги",
|
|
||||||
href: "/tags",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageBlock fullScreenMobile>
|
|
||||||
<Stack p={"xs"}>
|
|
||||||
<ProjectSelect
|
|
||||||
onChange={project =>
|
|
||||||
setSelectedProjectId(project?.id ?? null)
|
|
||||||
}
|
|
||||||
value={selectedProject}
|
|
||||||
data={projects}
|
|
||||||
/>
|
|
||||||
<SimpleGrid
|
|
||||||
type={"container"}
|
|
||||||
cols={2}>
|
|
||||||
{[
|
|
||||||
...commonActionsData.current,
|
|
||||||
...filteredMobileButtonsData,
|
|
||||||
].map((data, index) => (
|
|
||||||
<Action
|
|
||||||
linkData={data}
|
|
||||||
key={index}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</SimpleGrid>
|
|
||||||
</Stack>
|
|
||||||
</PageBlock>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageBody;
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import {
|
|
||||||
IconBox,
|
|
||||||
IconColumns,
|
|
||||||
IconFileBarcode,
|
|
||||||
IconUsers,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { ModuleNames } from "@/modules/modules";
|
|
||||||
import LinkData from "@/types/LinkData";
|
|
||||||
|
|
||||||
const mobileButtonsData: LinkData[] = [
|
|
||||||
{
|
|
||||||
icon: IconUsers,
|
|
||||||
label: "Клиенты",
|
|
||||||
href: "/clients",
|
|
||||||
moduleName: ModuleNames.CLIENTS,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: IconColumns,
|
|
||||||
label: "Услуги",
|
|
||||||
href: "/services",
|
|
||||||
moduleName: ModuleNames.FULFILLMENT_BASE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: IconBox,
|
|
||||||
label: "Товары",
|
|
||||||
href: "/products",
|
|
||||||
moduleName: ModuleNames.FULFILLMENT_BASE,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: IconFileBarcode,
|
|
||||||
label: "Шаблоны ШК",
|
|
||||||
href: "/barcode-templates",
|
|
||||||
moduleName: ModuleNames.FULFILLMENT_BASE,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default mobileButtonsData;
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import { Suspense } from "react";
|
|
||||||
import { Center, Loader } from "@mantine/core";
|
|
||||||
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
|
||||||
import PageBody from "./components/PageBody/PageBody";
|
|
||||||
|
|
||||||
export default async function ActionsPage() {
|
|
||||||
return (
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<Center h="50vh">
|
|
||||||
<Loader size="lg" />
|
|
||||||
</Center>
|
|
||||||
}>
|
|
||||||
<PageContainer>
|
|
||||||
<PageBody />
|
|
||||||
</PageContainer>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import AttributePageView from "@/app/attributes/types/view";
|
|
||||||
import BaseSegmentedControl, {
|
|
||||||
BaseSegmentedControlProps,
|
|
||||||
} from "@/components/ui/BaseSegmentedControl/BaseSegmentedControl";
|
|
||||||
|
|
||||||
type Props = Omit<BaseSegmentedControlProps<AttributePageView>, "data">;
|
|
||||||
|
|
||||||
const data = [
|
|
||||||
{
|
|
||||||
label: "Аттрибуты",
|
|
||||||
value: AttributePageView.ATTRIBUTES,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Справочники",
|
|
||||||
value: AttributePageView.SELECTS,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const AttrViewSegmentedControl: FC<Props> = props => (
|
|
||||||
<BaseSegmentedControl
|
|
||||||
data={data}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default AttrViewSegmentedControl;
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Dispatch, FC, SetStateAction, useMemo } from "react";
|
|
||||||
import { Divider, Flex, Group, TextInput } from "@mantine/core";
|
|
||||||
import AttrViewSegmentedControl from "@/app/attributes/components/AttrViewSegmentedControl";
|
|
||||||
import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext";
|
|
||||||
import AttributePageView from "@/app/attributes/types/view";
|
|
||||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
view: AttributePageView;
|
|
||||||
setView: Dispatch<SetStateAction<AttributePageView>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const AttributesHeader: FC<Props> = ({ view, setView }) => {
|
|
||||||
const {
|
|
||||||
attributesActions,
|
|
||||||
selectsActions,
|
|
||||||
attrSearch,
|
|
||||||
setAttrSearch,
|
|
||||||
selectSearch,
|
|
||||||
setSelectSearch,
|
|
||||||
} = useAttributesContext();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const attributeActions = useMemo(
|
|
||||||
() => (
|
|
||||||
<Group wrap={"nowrap"}>
|
|
||||||
<InlineButton
|
|
||||||
onClick={attributesActions.onCreate}
|
|
||||||
w={isMobile ? "100%" : "auto"}>
|
|
||||||
Создать атрибут
|
|
||||||
</InlineButton>
|
|
||||||
<TextInput
|
|
||||||
value={attrSearch}
|
|
||||||
onChange={e => setAttrSearch(e.currentTarget.value)}
|
|
||||||
w={isMobile ? "100%" : "auto"}
|
|
||||||
placeholder={"Поиск..."}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
),
|
|
||||||
[isMobile, attrSearch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectActions = useMemo(
|
|
||||||
() => (
|
|
||||||
<Group wrap={"nowrap"}>
|
|
||||||
<InlineButton
|
|
||||||
onClick={selectsActions.onCreate}
|
|
||||||
w={isMobile ? "100%" : "auto"}>
|
|
||||||
Создать справочник
|
|
||||||
</InlineButton>
|
|
||||||
<TextInput
|
|
||||||
value={selectSearch}
|
|
||||||
onChange={e => setSelectSearch(e.currentTarget.value)}
|
|
||||||
w={isMobile ? "100%" : "auto"}
|
|
||||||
placeholder={"Поиск..."}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
),
|
|
||||||
[isMobile, selectSearch]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
wrap={"nowrap"}
|
|
||||||
gap={"xs"}
|
|
||||||
align={isMobile ? "unset" : "center"}
|
|
||||||
mt={isMobile ? "xs" : ""}
|
|
||||||
mx={isMobile ? "xs" : ""}
|
|
||||||
direction={isMobile ? "column-reverse" : "row"}
|
|
||||||
justify={"space-between"}>
|
|
||||||
{view === AttributePageView.ATTRIBUTES
|
|
||||||
? attributeActions
|
|
||||||
: selectActions}
|
|
||||||
{isMobile && <Divider />}
|
|
||||||
<AttrViewSegmentedControl
|
|
||||||
value={view}
|
|
||||||
onChange={setView}
|
|
||||||
styles={{ root: { margin: 0, padding: 0 } }}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AttributesHeader;
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { FC } from "react";
|
|
||||||
import { IconMoodSad } from "@tabler/icons-react";
|
|
||||||
import { Group, Text } from "@mantine/core";
|
|
||||||
import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext";
|
|
||||||
import useAttributesTableColumns from "@/app/attributes/hooks/useAttributesTableColumns";
|
|
||||||
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
|
|
||||||
const AttributesTable: FC = () => {
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const { attributes } = useAttributesContext();
|
|
||||||
const columns = useAttributesTableColumns();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseTable
|
|
||||||
withTableBorder
|
|
||||||
columns={columns}
|
|
||||||
records={attributes}
|
|
||||||
verticalSpacing={"md"}
|
|
||||||
emptyState={
|
|
||||||
<Group mt={attributes.length === 0 ? "xl" : 0}>
|
|
||||||
<Text>Нет атрибутов</Text>
|
|
||||||
<IconMoodSad />
|
|
||||||
</Group>
|
|
||||||
}
|
|
||||||
groups={undefined}
|
|
||||||
styles={{
|
|
||||||
table: {
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
header: { zIndex: 1 },
|
|
||||||
}}
|
|
||||||
mx={isMobile ? "xs" : 0}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AttributesTable;
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import AttributesHeader from "@/app/attributes/components/AttributesHeader";
|
|
||||||
import AttributesTable from "@/app/attributes/components/AttributesTable";
|
|
||||||
import SelectsTable from "@/app/attributes/components/SelectsTable";
|
|
||||||
import AttributePageView from "@/app/attributes/types/view";
|
|
||||||
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
|
||||||
|
|
||||||
const PageBody = () => {
|
|
||||||
const [view, setView] = useState<AttributePageView>(
|
|
||||||
AttributePageView.ATTRIBUTES
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageBlock
|
|
||||||
style={{ flex: 1, minHeight: 0 }}
|
|
||||||
fullScreenMobile>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "var(--mantine-spacing-md)",
|
|
||||||
}}>
|
|
||||||
<AttributesHeader
|
|
||||||
view={view}
|
|
||||||
setView={setView}
|
|
||||||
/>
|
|
||||||
<div style={{ flex: 1, overflow: "auto" }}>
|
|
||||||
{view === AttributePageView.ATTRIBUTES ? (
|
|
||||||
<AttributesTable />
|
|
||||||
) : (
|
|
||||||
<SelectsTable />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageBlock>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageBody;
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import { IconMoodSad } from "@tabler/icons-react";
|
|
||||||
import { Group, Text } from "@mantine/core";
|
|
||||||
import { useAttributesContext } from "@/app/attributes/contexts/AttributesContext";
|
|
||||||
import useSelectsTableColumns from "@/app/attributes/hooks/useSelectsTableColumns";
|
|
||||||
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
|
|
||||||
const SelectsTable = () => {
|
|
||||||
const { selects } = useAttributesContext();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const columns = useSelectsTableColumns();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseTable
|
|
||||||
withTableBorder
|
|
||||||
columns={columns}
|
|
||||||
records={selects}
|
|
||||||
verticalSpacing={"md"}
|
|
||||||
emptyState={
|
|
||||||
<Group mt={selects.length === 0 ? "xl" : 0}>
|
|
||||||
<Text>Нет справочников</Text>
|
|
||||||
<IconMoodSad />
|
|
||||||
</Group>
|
|
||||||
}
|
|
||||||
groups={undefined}
|
|
||||||
styles={{
|
|
||||||
table: {
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
mx={isMobile ? "xs" : 0}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SelectsTable;
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Dispatch, SetStateAction } from "react";
|
|
||||||
import useFilteredAttributes from "@/app/attributes/hooks/useFilteredAttributes";
|
|
||||||
import useSelectsActions, {
|
|
||||||
SelectsActions,
|
|
||||||
} from "@/app/attributes/hooks/useSelectsActions";
|
|
||||||
import useAttributesActions, {
|
|
||||||
AttributesActions,
|
|
||||||
} from "@/app/module-editor/[moduleId]/hooks/useAttributesActions";
|
|
||||||
import useAttributesList from "@/app/module-editor/[moduleId]/hooks/useAttributesList";
|
|
||||||
import useAttrSelectsList from "@/hooks/lists/useAttrSelectsList";
|
|
||||||
import { AttributeSchema, AttrSelectSchema } from "@/lib/client";
|
|
||||||
import makeContext from "@/lib/contextFactory/contextFactory";
|
|
||||||
import useFilteredSelects from "@/app/attributes/hooks/useFilteredSelects";
|
|
||||||
|
|
||||||
type AttributesContextState = {
|
|
||||||
attributes: AttributeSchema[];
|
|
||||||
attributesActions: AttributesActions;
|
|
||||||
attrSearch: string;
|
|
||||||
setAttrSearch: Dispatch<SetStateAction<string>>;
|
|
||||||
selects: AttrSelectSchema[];
|
|
||||||
selectsActions: SelectsActions;
|
|
||||||
selectSearch: string;
|
|
||||||
setSelectSearch: Dispatch<SetStateAction<string>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useAttributesContextState = (): AttributesContextState => {
|
|
||||||
const { attributes, refetch: refetchAttributes } = useAttributesList();
|
|
||||||
const attributesActions = useAttributesActions({
|
|
||||||
refetchAttributes,
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
search: attrSearch,
|
|
||||||
setSearch: setAttrSearch,
|
|
||||||
filteredAttributes,
|
|
||||||
} = useFilteredAttributes({ attributes });
|
|
||||||
|
|
||||||
const { selects, queryKey } = useAttrSelectsList();
|
|
||||||
const selectsActions = useSelectsActions({ queryKey });
|
|
||||||
|
|
||||||
const {
|
|
||||||
search: selectSearch,
|
|
||||||
setSearch: setSelectSearch,
|
|
||||||
filteredSelects,
|
|
||||||
} = useFilteredSelects({ selects });
|
|
||||||
|
|
||||||
return {
|
|
||||||
attributes: filteredAttributes,
|
|
||||||
attributesActions,
|
|
||||||
attrSearch,
|
|
||||||
setAttrSearch,
|
|
||||||
selects: filteredSelects,
|
|
||||||
selectsActions,
|
|
||||||
selectSearch,
|
|
||||||
setSelectSearch,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const [AttributesContextProvider, useAttributesContext] =
|
|
||||||
makeContext<AttributesContextState>(useAttributesContextState, "Attribute");
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, { FC } from "react";
|
|
||||||
import { Drawer } from "@mantine/core";
|
|
||||||
import EditorBody from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/EditorBody";
|
|
||||||
import { SelectEditorContextProvider } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
|
|
||||||
import { DrawerProps } from "@/drawers/types";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
import { AttrSelectSchema, UpdateAttrSelectSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
select: AttrSelectSchema;
|
|
||||||
onSelectChange: (
|
|
||||||
values: UpdateAttrSelectSchema,
|
|
||||||
onSuccess: () => void
|
|
||||||
) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const AttrSelectEditorDrawer: FC<DrawerProps<Props>> = ({
|
|
||||||
onClose,
|
|
||||||
opened,
|
|
||||||
props,
|
|
||||||
}) => {
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
size={isMobile ? "100%" : "30%"}
|
|
||||||
title={"Редактирование справочника"}
|
|
||||||
position={"left"}
|
|
||||||
onClose={onClose}
|
|
||||||
removeScrollProps={{ allowPinchZoom: true }}
|
|
||||||
withCloseButton
|
|
||||||
opened={opened}
|
|
||||||
trapFocus={false}
|
|
||||||
styles={{
|
|
||||||
body: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
},
|
|
||||||
}}>
|
|
||||||
<SelectEditorContextProvider {...props}>
|
|
||||||
<EditorBody />
|
|
||||||
</SelectEditorContextProvider>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AttrSelectEditorDrawer;
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import { Button, Flex, TextInput } from "@mantine/core";
|
|
||||||
import { useForm } from "@mantine/form";
|
|
||||||
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
|
|
||||||
import { UpdateAttrSelectSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
const CommonInfoEditor = () => {
|
|
||||||
const { select, onSelectChange } = useSelectEditorContext();
|
|
||||||
|
|
||||||
const form = useForm<UpdateAttrSelectSchema>({
|
|
||||||
initialValues: select || {
|
|
||||||
name: "",
|
|
||||||
},
|
|
||||||
validate: {
|
|
||||||
name: name => !name && "Введите название",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={form.onSubmit(values => onSelectChange(values))}>
|
|
||||||
<Flex
|
|
||||||
gap={"xs"}
|
|
||||||
direction={"column"}>
|
|
||||||
<TextInput
|
|
||||||
label={"Название справочника"}
|
|
||||||
{...form.getInputProps("name")}
|
|
||||||
data-autofocus
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant={"default"}
|
|
||||||
type={"submit"}>
|
|
||||||
Сохранить
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CommonInfoEditor;
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
import { IconCheck } from "@tabler/icons-react";
|
|
||||||
import { Flex, TextInput } from "@mantine/core";
|
|
||||||
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
|
|
||||||
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
|
|
||||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
|
||||||
|
|
||||||
const CreateOptionButton = () => {
|
|
||||||
const {
|
|
||||||
optionsActions: {
|
|
||||||
isCreatingOption,
|
|
||||||
createOptionForm,
|
|
||||||
onStartCreating,
|
|
||||||
onFinishCreating,
|
|
||||||
},
|
|
||||||
} = useSelectEditorContext();
|
|
||||||
|
|
||||||
if (!isCreatingOption) {
|
|
||||||
return (
|
|
||||||
<Flex flex={1}>
|
|
||||||
<InlineButton
|
|
||||||
fullWidth
|
|
||||||
onClick={onStartCreating}>
|
|
||||||
Добавить опцию
|
|
||||||
</InlineButton>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
gap={"xs"}
|
|
||||||
flex={1}>
|
|
||||||
<TextInput
|
|
||||||
{...createOptionForm.getInputProps("name")}
|
|
||||||
flex={1}
|
|
||||||
placeholder={"Название"}
|
|
||||||
/>
|
|
||||||
<ActionIconWithTip
|
|
||||||
tipLabel={"Сохранить"}
|
|
||||||
onClick={onFinishCreating}>
|
|
||||||
<IconCheck />
|
|
||||||
</ActionIconWithTip>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateOptionButton;
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import { Divider, Flex } from "@mantine/core";
|
|
||||||
import CommonInfoEditor from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/CommonInfoEditor";
|
|
||||||
import CreateOptionButton from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/CreateOptionButton";
|
|
||||||
import OptionsTable from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/OptionsTable";
|
|
||||||
|
|
||||||
const EditorBody = () => {
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
gap={"xs"}
|
|
||||||
direction={"column"}>
|
|
||||||
<CommonInfoEditor />
|
|
||||||
<Divider
|
|
||||||
label={"Опции"}
|
|
||||||
my={"xs"}
|
|
||||||
/>
|
|
||||||
<CreateOptionButton />
|
|
||||||
<OptionsTable />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditorBody;
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import React, { FC, ReactNode } from "react";
|
|
||||||
import { IconCheck, IconEdit, IconTrash } from "@tabler/icons-react";
|
|
||||||
import { Divider, Flex, Group, Stack, TextInput } from "@mantine/core";
|
|
||||||
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
|
|
||||||
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
|
|
||||||
import { AttrOptionSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
option: AttrOptionSchema;
|
|
||||||
renderDraggable?: (item: AttrOptionSchema) => ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const OptionTableRow: FC<Props> = ({ option, renderDraggable }) => {
|
|
||||||
const {
|
|
||||||
optionsActions: {
|
|
||||||
onStartEditing,
|
|
||||||
onFinishEditing,
|
|
||||||
onDelete,
|
|
||||||
editingOptionsData,
|
|
||||||
setEditingOptionsData,
|
|
||||||
},
|
|
||||||
} = useSelectEditorContext();
|
|
||||||
|
|
||||||
const onChange = (
|
|
||||||
e: React.ChangeEvent<HTMLInputElement>,
|
|
||||||
optionId: number
|
|
||||||
) => {
|
|
||||||
setEditingOptionsData(prev => {
|
|
||||||
prev.set(optionId, e.currentTarget.value);
|
|
||||||
return new Map(prev);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
gap={"xs"}
|
|
||||||
mt={"xs"}>
|
|
||||||
<Group
|
|
||||||
wrap={"nowrap"}
|
|
||||||
justify={"space-between"}>
|
|
||||||
<Group wrap={"nowrap"}>
|
|
||||||
{renderDraggable && renderDraggable(option)}
|
|
||||||
{editingOptionsData.has(option.id) ? (
|
|
||||||
<TextInput
|
|
||||||
value={editingOptionsData.get(option.id)}
|
|
||||||
onChange={e => onChange(e, option.id)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
option.name
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
<Flex
|
|
||||||
justify={"center"}
|
|
||||||
gap={"xs"}>
|
|
||||||
{editingOptionsData.has(option.id) ? (
|
|
||||||
<ActionIconWithTip
|
|
||||||
onClick={() => onFinishEditing(option)}
|
|
||||||
tipLabel={"Сохранить"}>
|
|
||||||
<IconCheck />
|
|
||||||
</ActionIconWithTip>
|
|
||||||
) : (
|
|
||||||
<ActionIconWithTip
|
|
||||||
onClick={() => onStartEditing(option)}
|
|
||||||
tipLabel={"Редактировать"}>
|
|
||||||
<IconEdit />
|
|
||||||
</ActionIconWithTip>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ActionIconWithTip
|
|
||||||
color={"red"}
|
|
||||||
onClick={() => onDelete(option)}
|
|
||||||
tipLabel={"Удалить"}>
|
|
||||||
<IconTrash />
|
|
||||||
</ActionIconWithTip>
|
|
||||||
</Flex>
|
|
||||||
</Group>
|
|
||||||
<Divider />
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OptionTableRow;
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { IconGripVertical } from "@tabler/icons-react";
|
|
||||||
import { Box, Divider, Stack } from "@mantine/core";
|
|
||||||
import OptionTableRow from "@/app/attributes/drawers/AttrSelectEditorDrawer/components/OptionTableRow";
|
|
||||||
import { useSelectEditorContext } from "@/app/attributes/drawers/AttrSelectEditorDrawer/contexts/SelectEditorContext";
|
|
||||||
import SortableDnd from "@/components/dnd/SortableDnd";
|
|
||||||
|
|
||||||
const OptionsTable = () => {
|
|
||||||
const { options } = useSelectEditorContext();
|
|
||||||
const { onDragEnd } = useSelectEditorContext();
|
|
||||||
|
|
||||||
const renderDraggable = () => (
|
|
||||||
<Box p={"xs"}>
|
|
||||||
<IconGripVertical />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack gap={0}>
|
|
||||||
<Divider />
|
|
||||||
<SortableDnd
|
|
||||||
initialItems={options}
|
|
||||||
onDragEnd={onDragEnd}
|
|
||||||
renderItem={(item, renderDraggable) => (
|
|
||||||
<OptionTableRow
|
|
||||||
option={item}
|
|
||||||
renderDraggable={renderDraggable}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
renderDraggable={renderDraggable}
|
|
||||||
dragHandleStyle={{ width: "auto" }}
|
|
||||||
vertical
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OptionsTable;
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import useAttrOptionsList from "@/app/attributes/drawers/AttrSelectEditorDrawer/hooks/useAttrOptionsList";
|
|
||||||
import {
|
|
||||||
AttrOptionSchema,
|
|
||||||
AttrSelectSchema,
|
|
||||||
UpdateAttrSelectSchema,
|
|
||||||
} from "@/lib/client";
|
|
||||||
import makeContext from "@/lib/contextFactory/contextFactory";
|
|
||||||
import { notifications } from "@/lib/notifications";
|
|
||||||
import useOptionsActions, { OptionsActions } from "../hooks/useOptionsActions";
|
|
||||||
|
|
||||||
type SelectEditorContextState = {
|
|
||||||
select: AttrSelectSchema;
|
|
||||||
onSelectChange: (values: UpdateAttrSelectSchema) => void;
|
|
||||||
options: AttrOptionSchema[];
|
|
||||||
optionsActions: OptionsActions;
|
|
||||||
onDragEnd: (itemId: number, newLexorank: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
select: AttrSelectSchema;
|
|
||||||
onSelectChange: (
|
|
||||||
values: UpdateAttrSelectSchema,
|
|
||||||
onSuccess: () => void
|
|
||||||
) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useSelectEditorContextState = ({
|
|
||||||
select,
|
|
||||||
onSelectChange,
|
|
||||||
}: Props): SelectEditorContextState => {
|
|
||||||
const { options, queryKey } = useAttrOptionsList({ selectId: select.id });
|
|
||||||
|
|
||||||
const optionsActions = useOptionsActions({ queryKey, select, options });
|
|
||||||
|
|
||||||
const onSelectChangeWithMsg = (values: UpdateAttrSelectSchema) => {
|
|
||||||
onSelectChange(values, () => {
|
|
||||||
notifications.success({
|
|
||||||
message: "Название справочника сохранено",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDragEnd = (itemId: number, newLexorank: string) => {
|
|
||||||
optionsActions.onUpdate(itemId, { lexorank: newLexorank });
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
select,
|
|
||||||
onSelectChange: onSelectChangeWithMsg,
|
|
||||||
options,
|
|
||||||
optionsActions,
|
|
||||||
onDragEnd,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const [SelectEditorContextProvider, useSelectEditorContext] =
|
|
||||||
makeContext<SelectEditorContextState, Props>(
|
|
||||||
useSelectEditorContextState,
|
|
||||||
"SelectEditor"
|
|
||||||
);
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
import { LexoRank } from "lexorank";
|
|
||||||
import { useCrudOperations } from "@/hooks/cruds/baseCrud";
|
|
||||||
import {
|
|
||||||
AttrOptionSchema,
|
|
||||||
CreateAttrOptionSchema,
|
|
||||||
UpdateAttrOptionSchema,
|
|
||||||
} from "@/lib/client";
|
|
||||||
import {
|
|
||||||
createAttrOptionMutation,
|
|
||||||
deleteAttrOptionMutation,
|
|
||||||
updateAttrOptionMutation,
|
|
||||||
} from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
import { getNewLexorank } from "@/utils/lexorank/generation";
|
|
||||||
import { getMaxByLexorank } from "@/utils/lexorank/max";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
queryKey: any[];
|
|
||||||
options: AttrOptionSchema[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AttrOptionsCrud = {
|
|
||||||
onCreate: (
|
|
||||||
data: Partial<CreateAttrOptionSchema>,
|
|
||||||
onSuccess?: () => void
|
|
||||||
) => void;
|
|
||||||
onUpdate: (
|
|
||||||
optionId: number,
|
|
||||||
option: UpdateAttrOptionSchema,
|
|
||||||
onSuccess?: () => void
|
|
||||||
) => void;
|
|
||||||
onDelete: (option: AttrOptionSchema, onSuccess?: () => void) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useAttrOptionsCrud = ({
|
|
||||||
queryKey,
|
|
||||||
options,
|
|
||||||
}: Props): AttrOptionsCrud => {
|
|
||||||
return useCrudOperations<
|
|
||||||
AttrOptionSchema,
|
|
||||||
UpdateAttrOptionSchema,
|
|
||||||
CreateAttrOptionSchema
|
|
||||||
>({
|
|
||||||
key: "getAttrOptions",
|
|
||||||
queryKey,
|
|
||||||
mutations: {
|
|
||||||
create: createAttrOptionMutation(),
|
|
||||||
update: updateAttrOptionMutation(),
|
|
||||||
delete: deleteAttrOptionMutation(),
|
|
||||||
},
|
|
||||||
getCreateEntity: data => {
|
|
||||||
const lastOption = getMaxByLexorank(options);
|
|
||||||
const newLexorank = getNewLexorank(
|
|
||||||
lastOption ? LexoRank.parse(lastOption.lexorank) : null
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
name: data.name!,
|
|
||||||
selectId: data.selectId!,
|
|
||||||
lexorank: newLexorank.toString(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getUpdateEntity: (old, update) => ({
|
|
||||||
...old,
|
|
||||||
name: update.name ?? old.name,
|
|
||||||
lexorank: update.lexorank ?? old.lexorank,
|
|
||||||
}),
|
|
||||||
getDeleteConfirmTitle: () => "Удаление опции",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { AttrOptionSchema } from "@/lib/client";
|
|
||||||
import {
|
|
||||||
getAttrOptionsOptions,
|
|
||||||
getAttrOptionsQueryKey,
|
|
||||||
} from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
import { sortByLexorank } from "@/utils/lexorank/sort";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
selectId: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useAttrOptionsList = ({ selectId }: Props) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const options = { path: { selectId } };
|
|
||||||
const { data, refetch } = useQuery(getAttrOptionsOptions(options));
|
|
||||||
|
|
||||||
const queryKey = getAttrOptionsQueryKey(options);
|
|
||||||
|
|
||||||
const setOptions = (options: AttrOptionSchema[]) => {
|
|
||||||
queryClient.setQueryData(
|
|
||||||
queryKey,
|
|
||||||
(old: { items: AttrOptionSchema[] }) => ({
|
|
||||||
...old,
|
|
||||||
items: options,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
options: sortByLexorank(data?.items ?? []),
|
|
||||||
setOptions,
|
|
||||||
refetch,
|
|
||||||
queryKey,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useAttrOptionsList;
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
import { Dispatch, SetStateAction, useState } from "react";
|
|
||||||
import { useForm, UseFormReturnType } from "@mantine/form";
|
|
||||||
import {
|
|
||||||
AttrOptionSchema,
|
|
||||||
AttrSelectSchema,
|
|
||||||
CreateAttrOptionSchema,
|
|
||||||
UpdateAttrOptionSchema,
|
|
||||||
} from "@/lib/client";
|
|
||||||
import { notifications } from "@/lib/notifications";
|
|
||||||
import { useAttrOptionsCrud } from "@/app/attributes/drawers/AttrSelectEditorDrawer/hooks/useAttrOptionsCrud";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
queryKey: any[];
|
|
||||||
select: AttrSelectSchema;
|
|
||||||
options: AttrOptionSchema[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type OptionsActions = {
|
|
||||||
isCreatingOption: boolean;
|
|
||||||
createOptionForm: UseFormReturnType<CreateAttrOptionSchema>;
|
|
||||||
onStartCreating: () => void;
|
|
||||||
onFinishCreating: () => void;
|
|
||||||
editingOptionsData: Map<number, string>;
|
|
||||||
setEditingOptionsData: Dispatch<SetStateAction<Map<number, string>>>;
|
|
||||||
onStartEditing: (option: AttrOptionSchema) => void;
|
|
||||||
onFinishEditing: (option: AttrOptionSchema) => void;
|
|
||||||
onUpdate: (optionId: number, data: UpdateAttrOptionSchema) => void;
|
|
||||||
onDelete: (option: AttrOptionSchema) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useOptionsActions = ({ queryKey, select, options }: Props) => {
|
|
||||||
const [isCreatingOption, setIsCreatingOption] = useState<boolean>(false);
|
|
||||||
const [editingOptionsData, setEditingOptionsData] = useState<
|
|
||||||
Map<number, string>
|
|
||||||
>(new Map());
|
|
||||||
|
|
||||||
const createOptionForm = useForm<CreateAttrOptionSchema>({
|
|
||||||
initialValues: {
|
|
||||||
name: "",
|
|
||||||
lexorank: "",
|
|
||||||
selectId: select.id,
|
|
||||||
},
|
|
||||||
validate: {
|
|
||||||
name: name => !name && "Введите название",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const optionCrud = useAttrOptionsCrud({ queryKey, options });
|
|
||||||
|
|
||||||
const onStartCreating = () => {
|
|
||||||
setIsCreatingOption(true);
|
|
||||||
};
|
|
||||||
const onFinishCreating = () => {
|
|
||||||
if (createOptionForm.validate().hasErrors) return;
|
|
||||||
optionCrud.onCreate(createOptionForm.values, () => {
|
|
||||||
notifications.success({ message: "Опция успешно создана" });
|
|
||||||
createOptionForm.reset();
|
|
||||||
setIsCreatingOption(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onStartEditing = (option: AttrOptionSchema) => {
|
|
||||||
setEditingOptionsData(prev => {
|
|
||||||
prev.set(option.id, option.name);
|
|
||||||
return new Map(prev);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onFinishEditing = (option: AttrOptionSchema) => {
|
|
||||||
if (!editingOptionsData.has(option.id)) return;
|
|
||||||
|
|
||||||
const newName = editingOptionsData.get(option.id);
|
|
||||||
if (!newName) {
|
|
||||||
notifications.error({ message: "Название не может быть пустым" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
optionCrud.onUpdate(option.id, { ...option, name: newName }, () => {
|
|
||||||
notifications.success({ message: "Опция сохранена" });
|
|
||||||
setEditingOptionsData(prev => {
|
|
||||||
prev.delete(option.id);
|
|
||||||
return new Map(prev);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDelete = (option: AttrOptionSchema) => {
|
|
||||||
optionCrud.onDelete(option, () =>
|
|
||||||
notifications.success({ message: "Опция удалена" })
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUpdate = optionCrud.onUpdate;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isCreatingOption,
|
|
||||||
createOptionForm,
|
|
||||||
onStartCreating,
|
|
||||||
onFinishCreating,
|
|
||||||
editingOptionsData,
|
|
||||||
setEditingOptionsData,
|
|
||||||
onStartEditing,
|
|
||||||
onFinishEditing,
|
|
||||||
onDelete,
|
|
||||||
onUpdate,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useOptionsActions;
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
|
||||||
import { DataTableColumn } from "mantine-datatable";
|
|
||||||
import { Box, Center } from "@mantine/core";
|
|
||||||
import AttributeTableActions from "@/app/module-editor/[moduleId]/components/shared/AttributeTableActions/AttributeTableActions";
|
|
||||||
import AttributeDefaultValue from "@/components/ui/AttributeDefaultValue/AttributeDefaultValue";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
import { AttributeSchema } from "@/lib/client";
|
|
||||||
import { useAttributesContext } from "../contexts/AttributesContext";
|
|
||||||
|
|
||||||
const useAttributesTableColumns = () => {
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const { attributesActions } = useAttributesContext();
|
|
||||||
const renderCheck = (value: boolean) => (value ? <IconCheck /> : <IconX />);
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
{
|
|
||||||
title: "Название атрибута",
|
|
||||||
accessor: "label",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Тип",
|
|
||||||
accessor: "type.name",
|
|
||||||
render: attr =>
|
|
||||||
attr.type.type === "select"
|
|
||||||
? `Выбор "${attr.label}"`
|
|
||||||
: attr.type.name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Значение по умолчанию",
|
|
||||||
accessor: "defaultValue",
|
|
||||||
render: attr => <AttributeDefaultValue attribute={attr} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: isMobile
|
|
||||||
? "Синх. в группе"
|
|
||||||
: "Синхронизировано в группе",
|
|
||||||
accessor: "isApplicableToGroup",
|
|
||||||
render: attr => renderCheck(attr.isApplicableToGroup),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Может быть пустым",
|
|
||||||
accessor: "isNullable",
|
|
||||||
render: attr => renderCheck(attr.isNullable),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Описаниие",
|
|
||||||
accessor: "description",
|
|
||||||
render: attr => <Box>{attr.description}</Box>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "actions",
|
|
||||||
title: <Center>Действия</Center>,
|
|
||||||
width: "0%",
|
|
||||||
render: attribute => (
|
|
||||||
<AttributeTableActions
|
|
||||||
attribute={attribute}
|
|
||||||
onUpdate={attributesActions.onUpdate}
|
|
||||||
onDelete={attributesActions.onDelete}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
] as DataTableColumn<AttributeSchema>[],
|
|
||||||
[isMobile]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useAttributesTableColumns;
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
import { useMemo, useState } from "react";
|
|
||||||
import { AttributeSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
attributes: AttributeSchema[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const useFilteredAttributes = ({ attributes }: Props) => {
|
|
||||||
const [search, setSearch] = useState<string>("");
|
|
||||||
|
|
||||||
const filteredAttributes = useMemo(
|
|
||||||
() =>
|
|
||||||
attributes.filter(
|
|
||||||
attr =>
|
|
||||||
attr.type.name.includes(search) ||
|
|
||||||
attr.label.includes(search) ||
|
|
||||||
attr.description.includes(search)
|
|
||||||
),
|
|
||||||
[attributes, search]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
search,
|
|
||||||
setSearch,
|
|
||||||
filteredAttributes,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useFilteredAttributes;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { useMemo, useState } from "react";
|
|
||||||
import { AttrSelectSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
selects: AttrSelectSchema[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const useFilteredSelects = ({ selects }: Props) => {
|
|
||||||
const [search, setSearch] = useState<string>("");
|
|
||||||
|
|
||||||
const filteredSelects = useMemo(
|
|
||||||
() => selects.filter(s => s.name.includes(search)),
|
|
||||||
[selects, search]
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
search,
|
|
||||||
setSearch,
|
|
||||||
filteredSelects,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useFilteredSelects;
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
import { modals } from "@mantine/modals";
|
|
||||||
import { useDrawersContext } from "@/drawers/DrawersContext";
|
|
||||||
import { useAttrSelectsCrud } from "@/hooks/cruds/useSelectsCrud";
|
|
||||||
import { AttrSelectSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
queryKey: any[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SelectsActions = {
|
|
||||||
onCreate: () => void;
|
|
||||||
onUpdate: (select: AttrSelectSchema) => void;
|
|
||||||
onDelete: (select: AttrSelectSchema) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useSelectsActions = (props: Props): SelectsActions => {
|
|
||||||
const attrSelectsCrud = useAttrSelectsCrud(props);
|
|
||||||
const { openDrawer } = useDrawersContext();
|
|
||||||
|
|
||||||
const onCreate = () => {
|
|
||||||
modals.openContextModal({
|
|
||||||
modal: "enterNameModal",
|
|
||||||
title: "Создание справочника",
|
|
||||||
innerProps: {
|
|
||||||
onChange: values => attrSelectsCrud.onCreate(values),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUpdate = (select: AttrSelectSchema) => {
|
|
||||||
openDrawer({
|
|
||||||
key: "attrSelectEditorDrawer",
|
|
||||||
props: {
|
|
||||||
onSelectChange: (values, onSuccess) =>
|
|
||||||
attrSelectsCrud.onUpdate(select.id, values, onSuccess),
|
|
||||||
select,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDelete = attrSelectsCrud.onDelete;
|
|
||||||
|
|
||||||
return {
|
|
||||||
onCreate,
|
|
||||||
onUpdate,
|
|
||||||
onDelete,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useSelectsActions;
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import { DataTableColumn } from "mantine-datatable";
|
|
||||||
import { Center } from "@mantine/core";
|
|
||||||
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
import { AttrSelectSchema } from "@/lib/client";
|
|
||||||
import { useAttributesContext } from "../contexts/AttributesContext";
|
|
||||||
|
|
||||||
const useSelectsTableColumns = () => {
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const { selectsActions } = useAttributesContext();
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
{
|
|
||||||
title: "Название справочника",
|
|
||||||
accessor: "name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "actions",
|
|
||||||
title: <Center>Действия</Center>,
|
|
||||||
width: "0%",
|
|
||||||
render: select => (
|
|
||||||
<UpdateDeleteTableActions
|
|
||||||
onDelete={() => selectsActions.onDelete(select)}
|
|
||||||
onChange={() => selectsActions.onUpdate(select)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
] as DataTableColumn<AttrSelectSchema>[],
|
|
||||||
[isMobile]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useSelectsTableColumns;
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import { Suspense } from "react";
|
|
||||||
import { Center, Loader } from "@mantine/core";
|
|
||||||
import PageBody from "@/app/attributes/components/PageBody";
|
|
||||||
import { AttributesContextProvider } from "@/app/attributes/contexts/AttributesContext";
|
|
||||||
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
|
||||||
|
|
||||||
export default async function AttributesPage() {
|
|
||||||
return (
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<Center h="50vh">
|
|
||||||
<Loader size="lg" />
|
|
||||||
</Center>
|
|
||||||
}>
|
|
||||||
<PageContainer>
|
|
||||||
<AttributesContextProvider>
|
|
||||||
<PageBody />
|
|
||||||
</AttributesContextProvider>
|
|
||||||
</PageContainer>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
enum AttributePageView {
|
|
||||||
ATTRIBUTES,
|
|
||||||
SELECTS,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AttributePageView;
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import { Group } from "@mantine/core";
|
|
||||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onCreateClick: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BarcodeTemplatesDesktopHeader: FC<Props> = ({ onCreateClick }) => {
|
|
||||||
return (
|
|
||||||
<Group>
|
|
||||||
<InlineButton onClick={onCreateClick}>
|
|
||||||
Создать шаблон
|
|
||||||
</InlineButton>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BarcodeTemplatesDesktopHeader;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import { Box } from "@mantine/core";
|
|
||||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onCreateClick: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BarcodeTemplatesMobileHeader: FC<Props> = ({ onCreateClick }) => {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
px={"xs"}
|
|
||||||
pt={"xs"}>
|
|
||||||
<InlineButton
|
|
||||||
onClick={onCreateClick}
|
|
||||||
w={"100%"}>
|
|
||||||
Создать шаблон
|
|
||||||
</InlineButton>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BarcodeTemplatesMobileHeader;
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { FC } from "react";
|
|
||||||
import useBarcodeTemplateAttributesList from "@/app/barcode-templates/hooks/useBarcodeTemplateAttributesList";
|
|
||||||
import ObjectMultiSelect, {
|
|
||||||
ObjectMultiSelectProps,
|
|
||||||
} from "@/components/selects/ObjectMultiSelect/ObjectMultiSelect";
|
|
||||||
import { BarcodeTemplateAttributeSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = Omit<
|
|
||||||
ObjectMultiSelectProps<BarcodeTemplateAttributeSchema>,
|
|
||||||
"data" | "getLabelFn" | "getValueFn"
|
|
||||||
>;
|
|
||||||
|
|
||||||
const BarcodeTemplateAttributeMultiselect: FC<Props> = (props: Props) => {
|
|
||||||
const { barcodeTemplateAttributes } = useBarcodeTemplateAttributesList();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ObjectMultiSelect
|
|
||||||
data={barcodeTemplateAttributes}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BarcodeTemplateAttributeMultiselect;
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import useBarcodeTemplateSizesList from "@/app/barcode-templates/hooks/useBarcodeTemplateSizesList";
|
|
||||||
import ObjectSelect, {
|
|
||||||
ObjectSelectProps,
|
|
||||||
} from "@/components/selects/ObjectSelect/ObjectSelect";
|
|
||||||
import { BarcodeTemplateSizeSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = Omit<ObjectSelectProps<BarcodeTemplateSizeSchema>, "data">;
|
|
||||||
|
|
||||||
const BarcodeTemplateSizeSelect = (props: Props) => {
|
|
||||||
const { barcodeTemplateSizes } = useBarcodeTemplateSizesList();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ObjectSelect
|
|
||||||
data={barcodeTemplateSizes}
|
|
||||||
getLabelFn={size => `${size.name} (${size.width}x${size.height})`}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default BarcodeTemplateSizeSelect;
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import { useBarcodeTemplatesTableColumns } from "@/app/barcode-templates/components/shared/BarcodeTemplatesTable/columns";
|
|
||||||
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
import { BarcodeTemplateSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
items: BarcodeTemplateSchema[];
|
|
||||||
onDelete: (template: BarcodeTemplateSchema) => void;
|
|
||||||
onChange: (template: BarcodeTemplateSchema) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BarcodeTemplatesTable: FC<Props> = ({ items, ...props }) => {
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const columns = useBarcodeTemplatesTableColumns(props);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseTable
|
|
||||||
striped
|
|
||||||
withTableBorder
|
|
||||||
records={items}
|
|
||||||
columns={columns}
|
|
||||||
groups={undefined}
|
|
||||||
verticalSpacing={"md"}
|
|
||||||
mx={isMobile ? "xs" : ""}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BarcodeTemplatesTable;
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
|
||||||
import { DataTableColumn } from "mantine-datatable";
|
|
||||||
import { Center } from "@mantine/core";
|
|
||||||
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
|
|
||||||
import { BarcodeTemplateSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onDelete: (template: BarcodeTemplateSchema) => void;
|
|
||||||
onChange: (template: BarcodeTemplateSchema) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useBarcodeTemplatesTableColumns = ({
|
|
||||||
onDelete,
|
|
||||||
onChange,
|
|
||||||
}: Props) => {
|
|
||||||
return useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
{
|
|
||||||
accessor: "actions",
|
|
||||||
title: <Center>Действия</Center>,
|
|
||||||
width: "0%",
|
|
||||||
render: template => (
|
|
||||||
<UpdateDeleteTableActions
|
|
||||||
onDelete={() => onDelete(template)}
|
|
||||||
onChange={() => onChange(template)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "name",
|
|
||||||
title: "Название",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "attributes",
|
|
||||||
title: "Атрибуты",
|
|
||||||
render: template => (
|
|
||||||
<>
|
|
||||||
{template.attributes
|
|
||||||
.map(attr => attr.name)
|
|
||||||
.join(", ")}
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "size.name",
|
|
||||||
title: "Размер",
|
|
||||||
render: template => `${template.size.name} (${template.size.width}x${template.size.height})`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "isDefault",
|
|
||||||
title: "По умолчанию",
|
|
||||||
render: template =>
|
|
||||||
template.isDefault ? <IconCheck /> : <IconX />,
|
|
||||||
},
|
|
||||||
] as DataTableColumn<BarcodeTemplateSchema>[],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Stack } from "@mantine/core";
|
|
||||||
import BarcodeTemplatesDesktopHeader from "@/app/barcode-templates/components/desktop/BarcodeTemplatesDesktopHeader/BarcodeTemplatesDesktopHeader";
|
|
||||||
import BarcodeTemplatesMobileHeader from "@/app/barcode-templates/components/mobile/BarcodeTemplatesMobileHeader/BarcodeTemplatesMobileHeader";
|
|
||||||
import BarcodeTemplatesTable from "@/app/barcode-templates/components/shared/BarcodeTemplatesTable/BarcodeTemplatesTable";
|
|
||||||
import useBarcodeTemplateActions from "@/app/barcode-templates/hooks/useBarcodeTemplateActions";
|
|
||||||
import { useBarcodeTemplatesCrud } from "@/app/barcode-templates/hooks/useBarcodeTemplatesCrud";
|
|
||||||
import useBarcodeTemplatesList from "@/app/barcode-templates/hooks/useBarcodeTemplatesList";
|
|
||||||
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
|
|
||||||
const PageBody = () => {
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const { barcodeTemplates, queryKey } = useBarcodeTemplatesList();
|
|
||||||
const barcodeTemplatesCrud = useBarcodeTemplatesCrud({ queryKey });
|
|
||||||
|
|
||||||
const { onCreate, onChange } = useBarcodeTemplateActions();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack h={"100%"}>
|
|
||||||
{!isMobile && (
|
|
||||||
<PageBlock>
|
|
||||||
<BarcodeTemplatesDesktopHeader onCreateClick={onCreate} />
|
|
||||||
</PageBlock>
|
|
||||||
)}
|
|
||||||
<PageBlock
|
|
||||||
style={{ flex: 1, minHeight: 0 }}
|
|
||||||
fullScreenMobile>
|
|
||||||
<Stack
|
|
||||||
gap={"xs"}
|
|
||||||
h={"100%"}>
|
|
||||||
{isMobile && (
|
|
||||||
<BarcodeTemplatesMobileHeader
|
|
||||||
onCreateClick={onCreate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div style={{ flex: 1, overflow: "auto" }}>
|
|
||||||
<BarcodeTemplatesTable
|
|
||||||
items={barcodeTemplates}
|
|
||||||
onChange={onChange}
|
|
||||||
onDelete={barcodeTemplatesCrud.onDelete}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
</PageBlock>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageBody;
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import { modals } from "@mantine/modals";
|
|
||||||
import { useBarcodeTemplatesCrud } from "@/app/barcode-templates/hooks/useBarcodeTemplatesCrud";
|
|
||||||
import useBarcodeTemplatesList from "@/app/barcode-templates/hooks/useBarcodeTemplatesList";
|
|
||||||
import { BarcodeTemplateSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
const useBarcodeTemplateActions = () => {
|
|
||||||
const { queryKey } = useBarcodeTemplatesList();
|
|
||||||
const barcodeTemplatesCrud = useBarcodeTemplatesCrud({ queryKey });
|
|
||||||
|
|
||||||
const onChange = (template: BarcodeTemplateSchema) => {
|
|
||||||
modals.openContextModal({
|
|
||||||
modal: "barcodeTemplateEditorModal",
|
|
||||||
title: "Редактирование шаблона",
|
|
||||||
withCloseButton: false,
|
|
||||||
innerProps: {
|
|
||||||
onChange: updated =>
|
|
||||||
barcodeTemplatesCrud.onUpdate(template.id, updated),
|
|
||||||
entity: template,
|
|
||||||
isEditing: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCreate = () => {
|
|
||||||
modals.openContextModal({
|
|
||||||
modal: "barcodeTemplateEditorModal",
|
|
||||||
title: "Создание шаблона",
|
|
||||||
withCloseButton: false,
|
|
||||||
innerProps: {
|
|
||||||
onCreate: barcodeTemplatesCrud.onCreate,
|
|
||||||
isEditing: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
onChange,
|
|
||||||
onCreate,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useBarcodeTemplateActions;
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { getBarcodeTemplateAttributesOptions } from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
|
|
||||||
const useBarcodeTemplateAttributesList = () => {
|
|
||||||
const { isLoading, data, refetch } = useQuery(
|
|
||||||
getBarcodeTemplateAttributesOptions()
|
|
||||||
);
|
|
||||||
|
|
||||||
return { barcodeTemplateAttributes: data?.items ?? [], refetch, isLoading };
|
|
||||||
};
|
|
||||||
export default useBarcodeTemplateAttributesList;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { getBarcodeTemplateSizesOptions } from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
|
|
||||||
const useBarcodeTemplateSizesList = () => {
|
|
||||||
const { isLoading, data, refetch } = useQuery(
|
|
||||||
getBarcodeTemplateSizesOptions()
|
|
||||||
);
|
|
||||||
|
|
||||||
return { barcodeTemplateSizes: data?.items ?? [], refetch, isLoading };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useBarcodeTemplateSizesList;
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
import { useCrudOperations } from "@/hooks/cruds/baseCrud";
|
|
||||||
import {
|
|
||||||
BarcodeTemplateSchema,
|
|
||||||
CreateBarcodeTemplateSchema,
|
|
||||||
UpdateBarcodeTemplateSchema,
|
|
||||||
} from "@/lib/client";
|
|
||||||
import {
|
|
||||||
createBarcodeTemplateMutation,
|
|
||||||
deleteBarcodeTemplateMutation,
|
|
||||||
updateBarcodeTemplateMutation,
|
|
||||||
} from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
|
|
||||||
type UseBarcodeTemplateOperationsProps = {
|
|
||||||
queryKey: any[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BarcodeTemplateCrud = {
|
|
||||||
onCreate: (template: CreateBarcodeTemplateSchema) => void;
|
|
||||||
onUpdate: (
|
|
||||||
templateId: number,
|
|
||||||
template: UpdateBarcodeTemplateSchema
|
|
||||||
) => void;
|
|
||||||
onDelete: (template: BarcodeTemplateSchema) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useBarcodeTemplatesCrud = ({
|
|
||||||
queryKey,
|
|
||||||
}: UseBarcodeTemplateOperationsProps): BarcodeTemplateCrud => {
|
|
||||||
return useCrudOperations<
|
|
||||||
BarcodeTemplateSchema,
|
|
||||||
UpdateBarcodeTemplateSchema,
|
|
||||||
CreateBarcodeTemplateSchema
|
|
||||||
>({
|
|
||||||
key: "getBarcodeTemplates",
|
|
||||||
queryKey,
|
|
||||||
mutations: {
|
|
||||||
create: createBarcodeTemplateMutation(),
|
|
||||||
update: updateBarcodeTemplateMutation(),
|
|
||||||
delete: deleteBarcodeTemplateMutation(),
|
|
||||||
},
|
|
||||||
getUpdateEntity: (old, update) => ({
|
|
||||||
...old,
|
|
||||||
name: update.name ?? old.name,
|
|
||||||
attributes: update.attributes ?? old.attributes,
|
|
||||||
size: update.size ?? old.size,
|
|
||||||
isDefault: update.isDefault ?? old.isDefault,
|
|
||||||
}),
|
|
||||||
getDeleteConfirmTitle: () => "Удаление шаблона штрихкода",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import {
|
|
||||||
getBarcodeTemplatesOptions,
|
|
||||||
getBarcodeTemplatesQueryKey,
|
|
||||||
} from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
|
|
||||||
const useBarcodeTemplatesList = () => {
|
|
||||||
const { isLoading, data, refetch } = useQuery(getBarcodeTemplatesOptions());
|
|
||||||
|
|
||||||
const queryKey = getBarcodeTemplatesQueryKey();
|
|
||||||
|
|
||||||
return { barcodeTemplates: data?.items ?? [], queryKey, refetch, isLoading };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useBarcodeTemplatesList;
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Checkbox, Flex, TextInput } from "@mantine/core";
|
|
||||||
import { useForm } from "@mantine/form";
|
|
||||||
import { ContextModalProps } from "@mantine/modals";
|
|
||||||
import BarcodeTemplateAttributeMultiselect from "@/app/barcode-templates/components/shared/BarcodeTemplateAttributeMultiselect/BarcodeTemplateAttributeMultiselect";
|
|
||||||
import BarcodeTemplateSizeSelect from "@/app/barcode-templates/components/shared/BarcodeTemplateSizeSelect/BarcodeTemplateSizeSelect";
|
|
||||||
import {
|
|
||||||
BarcodeTemplateSchema,
|
|
||||||
CreateBarcodeTemplateSchema,
|
|
||||||
UpdateBarcodeTemplateSchema,
|
|
||||||
} from "@/lib/client";
|
|
||||||
import BaseFormModal, {
|
|
||||||
CreateEditFormProps,
|
|
||||||
} from "@/modals/base/BaseFormModal/BaseFormModal";
|
|
||||||
|
|
||||||
type Props = CreateEditFormProps<
|
|
||||||
BarcodeTemplateSchema,
|
|
||||||
CreateBarcodeTemplateSchema,
|
|
||||||
UpdateBarcodeTemplateSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
const BarcodeTemplateEditorModal = ({
|
|
||||||
context,
|
|
||||||
id,
|
|
||||||
innerProps,
|
|
||||||
}: ContextModalProps<Props>) => {
|
|
||||||
const initialValues = innerProps.isEditing
|
|
||||||
? innerProps.entity
|
|
||||||
: ({
|
|
||||||
name: "",
|
|
||||||
isDefault: false,
|
|
||||||
attributes: [],
|
|
||||||
} as Partial<CreateBarcodeTemplateSchema>);
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
initialValues,
|
|
||||||
validate: {
|
|
||||||
attributes: attributes =>
|
|
||||||
!attributes && "Необходимо добавить хотя бы один атрибут",
|
|
||||||
name: name =>
|
|
||||||
!name ||
|
|
||||||
(name.trim() === "" && "Необходимо ввести название шаблона"),
|
|
||||||
size: size => !size && "Необходимо выбрать размер шаблона",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseFormModal
|
|
||||||
{...innerProps}
|
|
||||||
closeOnSubmit
|
|
||||||
form={form}
|
|
||||||
onClose={() => context.closeContextModal(id)}>
|
|
||||||
<Flex
|
|
||||||
direction={"column"}
|
|
||||||
gap={"md"}>
|
|
||||||
<TextInput
|
|
||||||
label={"Название"}
|
|
||||||
placeholder={"Введите название шаблона"}
|
|
||||||
{...form.getInputProps("name")}
|
|
||||||
/>
|
|
||||||
<BarcodeTemplateSizeSelect
|
|
||||||
label={"Размер"}
|
|
||||||
placeholder={"Выберите размер шаблона"}
|
|
||||||
{...form.getInputProps("size")}
|
|
||||||
/>
|
|
||||||
<BarcodeTemplateAttributeMultiselect
|
|
||||||
label={"Стандартные атрибуты"}
|
|
||||||
placeholder={
|
|
||||||
!form.values.attributes?.length
|
|
||||||
? "Выберите атрибуты"
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
{...form.getInputProps("attributes")}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={"Использовать по умолчанию"}
|
|
||||||
{...form.getInputProps("isDefault", {
|
|
||||||
type: "checkbox",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</BaseFormModal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BarcodeTemplateEditorModal;
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import { Suspense } from "react";
|
|
||||||
import { Center, Loader } from "@mantine/core";
|
|
||||||
import PageBody from "@/app/barcode-templates/components/shared/PageBody/PageBody";
|
|
||||||
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
|
||||||
|
|
||||||
export default async function BarcodeTemplatesPage() {
|
|
||||||
return (
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<Center h="50vh">
|
|
||||||
<Loader size="lg" />
|
|
||||||
</Center>
|
|
||||||
}>
|
|
||||||
<PageContainer>
|
|
||||||
<PageBody />
|
|
||||||
</PageContainer>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import { Group, TextInput } from "@mantine/core";
|
|
||||||
import { useClientsContext } from "@/app/clients/contexts/ClientsContext";
|
|
||||||
import useClientsActions from "@/app/clients/hooks/utils/useClientsActions";
|
|
||||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
|
||||||
|
|
||||||
const ClientDesktopHeader: FC = () => {
|
|
||||||
const { search, setSearch } = useClientsContext();
|
|
||||||
const { onCreateClick } = useClientsActions();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group gap={"xs"}>
|
|
||||||
<InlineButton onClick={onCreateClick}>Создать клиента</InlineButton>
|
|
||||||
<TextInput
|
|
||||||
placeholder={"Поиск"}
|
|
||||||
value={search}
|
|
||||||
onChange={e => setSearch(e.target.value)}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClientDesktopHeader;
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import { Flex, TextInput } from "@mantine/core";
|
|
||||||
import { useClientsContext } from "@/app/clients/contexts/ClientsContext";
|
|
||||||
import useClientsActions from "@/app/clients/hooks/utils/useClientsActions";
|
|
||||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
|
||||||
|
|
||||||
const ClientMobileHeader: FC = () => {
|
|
||||||
const { search, setSearch } = useClientsContext();
|
|
||||||
const { onCreateClick } = useClientsActions();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
gap={"xs"}
|
|
||||||
px={"xs"}
|
|
||||||
pt={"xs"}>
|
|
||||||
<InlineButton
|
|
||||||
w={"100%"}
|
|
||||||
onClick={onCreateClick}>
|
|
||||||
Создать клиента
|
|
||||||
</InlineButton>
|
|
||||||
<TextInput
|
|
||||||
w={"100%"}
|
|
||||||
placeholder={"Поиск"}
|
|
||||||
value={search}
|
|
||||||
onChange={e => setSearch(e.target.value)}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClientMobileHeader;
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { FC } from "react";
|
|
||||||
import { useClientsTableColumns } from "@/app/clients/components/shared/ClientsTable/columns";
|
|
||||||
import { useClientsContext } from "@/app/clients/contexts/ClientsContext";
|
|
||||||
import useClientsActions from "@/app/clients/hooks/utils/useClientsActions";
|
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
|
||||||
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
|
||||||
import { useDrawersContext } from "@/drawers/DrawersContext";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
import { ClientSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
const ClientsTable: FC = () => {
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const { modulesSet } = useProjectsContext();
|
|
||||||
const { clientsCrud, clients } = useClientsContext();
|
|
||||||
const { onUpdateClick } = useClientsActions();
|
|
||||||
const { openDrawer } = useDrawersContext();
|
|
||||||
|
|
||||||
const onOpenMarketplacesList = (client: ClientSchema) => {
|
|
||||||
openDrawer({
|
|
||||||
key: "clientMarketplaceDrawer",
|
|
||||||
props: { client },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns = useClientsTableColumns({
|
|
||||||
onDelete: clientsCrud.onDelete,
|
|
||||||
onChange: onUpdateClick,
|
|
||||||
onOpenMarketplacesList,
|
|
||||||
modulesSet,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseTable
|
|
||||||
withTableBorder
|
|
||||||
records={clients}
|
|
||||||
columns={columns}
|
|
||||||
verticalSpacing={"md"}
|
|
||||||
mx={isMobile ? "xs" : 0}
|
|
||||||
groups={undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClientsTable;
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { IconBasket } from "@tabler/icons-react";
|
|
||||||
import { DataTableColumn } from "mantine-datatable";
|
|
||||||
import { Center } from "@mantine/core";
|
|
||||||
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
|
|
||||||
import { ClientSchema } from "@/lib/client";
|
|
||||||
import { ModuleNames } from "@/modules/modules";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onChange: (client: ClientSchema) => void;
|
|
||||||
onDelete: (client: ClientSchema) => void;
|
|
||||||
onOpenMarketplacesList: (client: ClientSchema) => void;
|
|
||||||
modulesSet: Set<ModuleNames>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useClientsTableColumns = ({
|
|
||||||
onChange,
|
|
||||||
onDelete,
|
|
||||||
onOpenMarketplacesList,
|
|
||||||
modulesSet,
|
|
||||||
}: Props) => {
|
|
||||||
return useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
{
|
|
||||||
accessor: "actions",
|
|
||||||
title: <Center>Действия</Center>,
|
|
||||||
width: "0%",
|
|
||||||
render: client => (
|
|
||||||
<UpdateDeleteTableActions
|
|
||||||
onDelete={() => onDelete(client)}
|
|
||||||
onChange={() => onChange(client)}
|
|
||||||
otherActions={[
|
|
||||||
{
|
|
||||||
label: "Маркетплейсы",
|
|
||||||
icon: <IconBasket />,
|
|
||||||
onClick: () =>
|
|
||||||
onOpenMarketplacesList(client),
|
|
||||||
hidden: !modulesSet.has(
|
|
||||||
ModuleNames.FULFILLMENT_BASE
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "name",
|
|
||||||
title: "Имя",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "details.telegram",
|
|
||||||
title: "Телеграм",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "details.email",
|
|
||||||
title: "Почта",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "details.phoneNumber",
|
|
||||||
title: "Телефон",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "details.inn",
|
|
||||||
title: "ИНН",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "companyName",
|
|
||||||
title: "Название компании",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "comment",
|
|
||||||
title: "Комментарий",
|
|
||||||
},
|
|
||||||
] as DataTableColumn<ClientSchema>[],
|
|
||||||
[onChange, onDelete]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { FC } from "react";
|
|
||||||
import { Stack } from "@mantine/core";
|
|
||||||
import ClientDesktopHeader from "@/app/clients/components/desktop/ClientDesktopHeader/ClientDesktopHeader";
|
|
||||||
import ClientsTable from "@/app/clients/components/shared/ClientsTable/ClientsTable";
|
|
||||||
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
import ClientMobileHeader from "@/app/clients/components/mobile/ClientMobileHeader/ClientMobileHeader";
|
|
||||||
|
|
||||||
const PageBody: FC = () => {
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack h={"100%"}>
|
|
||||||
{!isMobile && (
|
|
||||||
<PageBlock>
|
|
||||||
<ClientDesktopHeader />
|
|
||||||
</PageBlock>
|
|
||||||
)}
|
|
||||||
<PageBlock
|
|
||||||
style={{ flex: 1, minHeight: 0 }}
|
|
||||||
fullScreenMobile>
|
|
||||||
<Stack
|
|
||||||
gap={"xs"}
|
|
||||||
h={"100%"}>
|
|
||||||
{isMobile && <ClientMobileHeader />}
|
|
||||||
<div style={{ flex: 1, overflow: "auto" }}>
|
|
||||||
<ClientsTable />
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
</PageBlock>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageBody;
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Dispatch, SetStateAction } from "react";
|
|
||||||
import {
|
|
||||||
ClientsCrud,
|
|
||||||
useClientsCrud,
|
|
||||||
} from "@/app/clients/hooks/cruds/useClientsCrud";
|
|
||||||
import useClientsFilter from "@/app/clients/hooks/utils/useClientsFilter";
|
|
||||||
import { ClientSchema } from "@/lib/client";
|
|
||||||
import makeContext from "@/lib/contextFactory/contextFactory";
|
|
||||||
import useClientsList from "../hooks/lists/useClientsList";
|
|
||||||
|
|
||||||
type ClientsContextState = {
|
|
||||||
clients: ClientSchema[];
|
|
||||||
refetchClients: () => void;
|
|
||||||
search: string;
|
|
||||||
setSearch: Dispatch<SetStateAction<string>>;
|
|
||||||
clientsCrud: ClientsCrud;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useClientsContextState = (): ClientsContextState => {
|
|
||||||
const clientsList = useClientsList();
|
|
||||||
|
|
||||||
const { filteredClients, search, setSearch } =
|
|
||||||
useClientsFilter(clientsList);
|
|
||||||
|
|
||||||
const clientsCrud = useClientsCrud(clientsList);
|
|
||||||
|
|
||||||
return {
|
|
||||||
clients: filteredClients,
|
|
||||||
refetchClients: clientsList.refetch,
|
|
||||||
search,
|
|
||||||
setSearch,
|
|
||||||
clientsCrud,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const [ClientsContextProvider, useClientsContext] =
|
|
||||||
makeContext<ClientsContextState>(useClientsContextState, "Clients");
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, { FC } from "react";
|
|
||||||
import { Drawer } from "@mantine/core";
|
|
||||||
import DrawerBody from "@/app/clients/drawers/ClientMarketplacesDrawer/components/DrawerBody";
|
|
||||||
import { MarketplacesContextProvider } from "@/app/clients/drawers/ClientMarketplacesDrawer/contexts/MarketplacesContext";
|
|
||||||
import { DrawerProps } from "@/drawers/types";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
import { ClientSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
client: ClientSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ClientMarketplaceDrawer: FC<DrawerProps<Props>> = ({
|
|
||||||
opened,
|
|
||||||
onClose,
|
|
||||||
props,
|
|
||||||
}) => {
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
size={isMobile ? "100%" : "40%"}
|
|
||||||
position={"right"}
|
|
||||||
onClose={onClose}
|
|
||||||
removeScrollProps={{ allowPinchZoom: true }}
|
|
||||||
withCloseButton={isMobile}
|
|
||||||
opened={opened}
|
|
||||||
trapFocus={false}
|
|
||||||
title={isMobile ? "Маркетплейсы" : ""}
|
|
||||||
styles={{
|
|
||||||
body: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
height: isMobile ? "var(--mobile-page-height)" : "100%",
|
|
||||||
padding: isMobile ? 0 : "var(--mantine-spacing-xs)",
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
paddingBlock: 0,
|
|
||||||
},
|
|
||||||
}}>
|
|
||||||
<MarketplacesContextProvider client={props.client}>
|
|
||||||
<DrawerBody />
|
|
||||||
</MarketplacesContextProvider>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClientMarketplaceDrawer;
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import {
|
|
||||||
ActionIcon,
|
|
||||||
ComboboxItem,
|
|
||||||
ComboboxLikeRenderOptionInput,
|
|
||||||
Image,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import useBaseMarketplacesList from "@/app/clients/hooks/lists/useBaseMarketplacesList";
|
|
||||||
import ObjectSelect, {
|
|
||||||
ObjectSelectProps,
|
|
||||||
} from "@/components/selects/ObjectSelect/ObjectSelect";
|
|
||||||
import { BaseMarketplaceSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = Omit<
|
|
||||||
ObjectSelectProps<BaseMarketplaceSchema>,
|
|
||||||
"data" | "getValueFn" | "getLabelFn"
|
|
||||||
>;
|
|
||||||
|
|
||||||
const BaseMarketplaceSelect: FC<Props> = props => {
|
|
||||||
const { baseMarketplaces } = useBaseMarketplacesList();
|
|
||||||
|
|
||||||
const renderOption = (
|
|
||||||
baseMarketplace: ComboboxLikeRenderOptionInput<ComboboxItem>
|
|
||||||
) => (
|
|
||||||
<>
|
|
||||||
<ActionIcon
|
|
||||||
radius={"md"}
|
|
||||||
variant={"transparent"}>
|
|
||||||
<Image
|
|
||||||
src={
|
|
||||||
baseMarketplaces.find(
|
|
||||||
el =>
|
|
||||||
baseMarketplace.option.value ===
|
|
||||||
el.id.toString()
|
|
||||||
)?.iconUrl || ""
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ActionIcon>
|
|
||||||
{baseMarketplace.option.label}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ObjectSelect
|
|
||||||
renderOption={renderOption}
|
|
||||||
getValueFn={baseMarketplace => baseMarketplace.id.toString()}
|
|
||||||
getLabelFn={baseMarketplace => baseMarketplace.name}
|
|
||||||
data={baseMarketplaces}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default BaseMarketplaceSelect;
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Flex } from "@mantine/core";
|
|
||||||
import MarketplacesHeader from "@/app/clients/drawers/ClientMarketplacesDrawer/components/MarketplacesHeader";
|
|
||||||
import MarketplacesTable from "@/app/clients/drawers/ClientMarketplacesDrawer/components/MarketplacesTable";
|
|
||||||
|
|
||||||
const DrawerBody = () => {
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
gap={"xs"}
|
|
||||||
h={"100%"}
|
|
||||||
direction={"column"}>
|
|
||||||
<MarketplacesHeader />
|
|
||||||
<MarketplacesTable />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DrawerBody;
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import BaseMarketplaceType from "@/modules/dealModularEditorTabs/FulfillmentBase/shared/types/baseMarketplaceType";
|
|
||||||
import OzonInputs from "./components/OzonInputs";
|
|
||||||
import WildberriesInputs from "./components/WildberriesInputs";
|
|
||||||
import YandexMarketInputs from "./components/YandexMarketInputs";
|
|
||||||
import MpAuthDataInputProps from "./types/MpAuthDataInputProps";
|
|
||||||
|
|
||||||
const MarketplaceAuthDataInput: FC<MpAuthDataInputProps> = props => {
|
|
||||||
switch (props.baseMarketplace.id) {
|
|
||||||
case BaseMarketplaceType.WILDBERRIES:
|
|
||||||
return <WildberriesInputs {...props} />;
|
|
||||||
case BaseMarketplaceType.OZON:
|
|
||||||
return <OzonInputs {...props} />;
|
|
||||||
case BaseMarketplaceType.YANDEX_MARKET:
|
|
||||||
return <YandexMarketInputs {...props} />;
|
|
||||||
default:
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MarketplaceAuthDataInput;
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import { omit } from "lodash";
|
|
||||||
import { NumberInput, TextInput } from "@mantine/core";
|
|
||||||
import MpAuthDataInputProps from "../types/MpAuthDataInputProps";
|
|
||||||
|
|
||||||
const OzonInputs: FC<MpAuthDataInputProps> = props => {
|
|
||||||
const restProps = omit(props, ["baseMarketplace"]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<NumberInput
|
|
||||||
{...restProps}
|
|
||||||
label={"Client-Id"}
|
|
||||||
placeholder={"Введите Client-Id"}
|
|
||||||
value={props.value?.["Client-Id"] || undefined}
|
|
||||||
onChange={value =>
|
|
||||||
props.onChange({
|
|
||||||
...props.value,
|
|
||||||
"Client-Id": value.toString(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
{...restProps}
|
|
||||||
label={"Api-Key"}
|
|
||||||
placeholder={"Введите Api-Key"}
|
|
||||||
value={props.value?.["Api-Key"] || ""}
|
|
||||||
onChange={value =>
|
|
||||||
props.onChange({
|
|
||||||
...props.value,
|
|
||||||
"Api-Key": value.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OzonInputs;
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import { omit } from "lodash";
|
|
||||||
import { TextInput } from "@mantine/core";
|
|
||||||
import MpAuthDataInputProps from "../types/MpAuthDataInputProps";
|
|
||||||
|
|
||||||
const WildberriesInputs: FC<MpAuthDataInputProps> = props => {
|
|
||||||
const restProps = omit(props, ["baseMarketplace"]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextInput
|
|
||||||
{...restProps}
|
|
||||||
label={"Ключ авторизации"}
|
|
||||||
placeholder={"Введите ключ авторизации"}
|
|
||||||
value={props.value?.Authorization || ""}
|
|
||||||
onChange={value =>
|
|
||||||
props.onChange({
|
|
||||||
...props.value,
|
|
||||||
Authorization: value.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WildberriesInputs;
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import { omit } from "lodash";
|
|
||||||
import { TextInput } from "@mantine/core";
|
|
||||||
import MpAuthDataInputProps from "../types/MpAuthDataInputProps";
|
|
||||||
|
|
||||||
const YandexMarketInputs: FC<MpAuthDataInputProps> = props => {
|
|
||||||
const restProps = omit(props, ["baseMarketplace"]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextInput
|
|
||||||
{...restProps}
|
|
||||||
label={"Api-Key"}
|
|
||||||
placeholder={"Введите Api-Key"}
|
|
||||||
value={props.value?.["Api-Key"] || ""}
|
|
||||||
onChange={value => {
|
|
||||||
props.onChange({
|
|
||||||
...props.value,
|
|
||||||
"Api-Key": value.target.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default YandexMarketInputs;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import BaseFormInputProps from "@/utils/baseFormInputProps";
|
|
||||||
import { BaseMarketplaceSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type RestProps = {
|
|
||||||
baseMarketplace: BaseMarketplaceSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
type MarketplaceAuthData = Record<string, string>;
|
|
||||||
|
|
||||||
type MpAuthDataInputProps = BaseFormInputProps<MarketplaceAuthData> & RestProps;
|
|
||||||
|
|
||||||
export default MpAuthDataInputProps;
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { IconPlus } from "@tabler/icons-react";
|
|
||||||
import { Group } from "@mantine/core";
|
|
||||||
import useMarketplacesActions from "@/app/clients/drawers/ClientMarketplacesDrawer/hooks/useMarketplaceActions";
|
|
||||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
|
|
||||||
const MarketplacesHeader = () => {
|
|
||||||
const { onCreateClick } = useMarketplacesActions();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group>
|
|
||||||
<InlineButton
|
|
||||||
onClick={onCreateClick}
|
|
||||||
mx={isMobile ? "xs" : ""}
|
|
||||||
w={isMobile ? "100%" : "auto"}>
|
|
||||||
<IconPlus />
|
|
||||||
Создать
|
|
||||||
</InlineButton>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MarketplacesHeader;
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { IconMoodSad } from "@tabler/icons-react";
|
|
||||||
import { Group, Text } from "@mantine/core";
|
|
||||||
import { useMarketplacesContext } from "@/app/clients/drawers/ClientMarketplacesDrawer/contexts/MarketplacesContext";
|
|
||||||
import useMarketplacesActions from "@/app/clients/drawers/ClientMarketplacesDrawer/hooks/useMarketplaceActions";
|
|
||||||
import { useMarketplacesTableColumns } from "@/app/clients/drawers/ClientMarketplacesDrawer/hooks/useMarketplacesTableColumns";
|
|
||||||
import BaseTable from "@/components/ui/BaseTable/BaseTable";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
|
|
||||||
const MarketplacesTable = () => {
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const { onUpdateClick } = useMarketplacesActions();
|
|
||||||
const { marketplaces, marketplacesCrud } = useMarketplacesContext();
|
|
||||||
|
|
||||||
const columns = useMarketplacesTableColumns({
|
|
||||||
onChange: onUpdateClick,
|
|
||||||
onDelete: marketplacesCrud.onDelete,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseTable
|
|
||||||
withTableBorder
|
|
||||||
records={marketplaces}
|
|
||||||
columns={columns}
|
|
||||||
groups={undefined}
|
|
||||||
verticalSpacing={"md"}
|
|
||||||
mx={isMobile ? "xs" : ""}
|
|
||||||
styles={{
|
|
||||||
table: {
|
|
||||||
height: "100%",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
emptyState={
|
|
||||||
<Group mt={marketplaces.length === 0 ? "xl" : 0}>
|
|
||||||
<Text>Нет маркетплейсов</Text>
|
|
||||||
<IconMoodSad />
|
|
||||||
</Group>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MarketplacesTable;
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
MarketplacesCrud,
|
|
||||||
useMarketplacesCrud,
|
|
||||||
} from "@/app/clients/hooks/cruds/useMarketplacesCrud";
|
|
||||||
import useMarketplacesList from "@/app/clients/hooks/lists/useMarketplacesList";
|
|
||||||
import { ClientSchema, MarketplaceSchema } from "@/lib/client";
|
|
||||||
import makeContext from "@/lib/contextFactory/contextFactory";
|
|
||||||
|
|
||||||
type MarketplacesContextState = {
|
|
||||||
client: ClientSchema;
|
|
||||||
marketplaces: MarketplaceSchema[];
|
|
||||||
refetchMarketplaces: () => void;
|
|
||||||
marketplacesCrud: MarketplacesCrud;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
client: ClientSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useMarketplacesContextState = ({
|
|
||||||
client,
|
|
||||||
}: Props): MarketplacesContextState => {
|
|
||||||
const marketplacesList = useMarketplacesList({ clientId: client.id });
|
|
||||||
|
|
||||||
const marketplacesCrud = useMarketplacesCrud(marketplacesList);
|
|
||||||
|
|
||||||
return {
|
|
||||||
client,
|
|
||||||
marketplaces: marketplacesList.marketplaces,
|
|
||||||
refetchMarketplaces: marketplacesList.refetch,
|
|
||||||
marketplacesCrud,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const [MarketplacesContextProvider, useMarketplacesContext] =
|
|
||||||
makeContext<MarketplacesContextState, Props>(
|
|
||||||
useMarketplacesContextState,
|
|
||||||
"Marketplaces"
|
|
||||||
);
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
import { modals } from "@mantine/modals";
|
|
||||||
import { useMarketplacesContext } from "@/app/clients/drawers/ClientMarketplacesDrawer/contexts/MarketplacesContext";
|
|
||||||
import { MarketplaceSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
const useMarketplacesActions = () => {
|
|
||||||
const { marketplacesCrud, client } = useMarketplacesContext();
|
|
||||||
|
|
||||||
const onCreateClick = () => {
|
|
||||||
modals.openContextModal({
|
|
||||||
modal: "marketplaceEditorModal",
|
|
||||||
title: "Создание маркетплейса",
|
|
||||||
innerProps: {
|
|
||||||
onCreate: values =>
|
|
||||||
marketplacesCrud.onCreate({ ...values, client }),
|
|
||||||
isEditing: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUpdateClick = (marketplace: MarketplaceSchema) => {
|
|
||||||
modals.openContextModal({
|
|
||||||
modal: "marketplaceEditorModal",
|
|
||||||
title: "Редактирование маркетплейса",
|
|
||||||
innerProps: {
|
|
||||||
onChange: updates =>
|
|
||||||
marketplacesCrud.onUpdate(marketplace.id, updates),
|
|
||||||
entity: marketplace,
|
|
||||||
isEditing: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
onCreateClick,
|
|
||||||
onUpdateClick,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useMarketplacesActions;
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { DataTableColumn } from "mantine-datatable";
|
|
||||||
import { ActionIcon, Center, Flex, Image } from "@mantine/core";
|
|
||||||
import UpdateDeleteTableActions from "@/components/ui/BaseTable/components/UpdateDeleteTableActions";
|
|
||||||
import { MarketplaceSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onDelete: (mp: MarketplaceSchema) => void;
|
|
||||||
onChange: (mp: MarketplaceSchema) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useMarketplacesTableColumns = ({ onDelete, onChange }: Props) => {
|
|
||||||
return useMemo(
|
|
||||||
() =>
|
|
||||||
[
|
|
||||||
{
|
|
||||||
accessor: "actions",
|
|
||||||
title: <Center>Действия</Center>,
|
|
||||||
width: "0%",
|
|
||||||
render: mp => (
|
|
||||||
<UpdateDeleteTableActions
|
|
||||||
onDelete={() => onDelete(mp)}
|
|
||||||
onChange={() => onChange(mp)}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Маркетплейс",
|
|
||||||
accessor: "baseMarketplace",
|
|
||||||
cellsStyle: () => ({}),
|
|
||||||
render: mp => (
|
|
||||||
<Flex key={`${mp.id}mp`}>
|
|
||||||
<ActionIcon variant={"transparent"}>
|
|
||||||
<Image
|
|
||||||
src={mp.baseMarketplace?.iconUrl || ""}
|
|
||||||
/>
|
|
||||||
</ActionIcon>
|
|
||||||
</Flex>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessor: "name",
|
|
||||||
title: "Название",
|
|
||||||
},
|
|
||||||
] as DataTableColumn<MarketplaceSchema>[],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import ClientMarketplaceDrawer from "./ClientMarketplacesDrawer";
|
|
||||||
|
|
||||||
export default ClientMarketplaceDrawer;
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Stack, TextInput } from "@mantine/core";
|
|
||||||
import { useForm } from "@mantine/form";
|
|
||||||
import { ContextModalProps } from "@mantine/modals";
|
|
||||||
import BaseMarketplaceSelect from "@/app/clients/drawers/ClientMarketplacesDrawer/components/BaseMarketplaceSelect";
|
|
||||||
import MarketplaceAuthDataInput from "@/app/clients/drawers/ClientMarketplacesDrawer/components/MarketplaceAuthDataInput/MarketplaceAuthDataInput";
|
|
||||||
import {
|
|
||||||
CreateMarketplaceSchema,
|
|
||||||
MarketplaceSchema,
|
|
||||||
UpdateMarketplaceSchema,
|
|
||||||
} from "@/lib/client";
|
|
||||||
import BaseFormModal, {
|
|
||||||
CreateEditFormProps,
|
|
||||||
} from "@/modals/base/BaseFormModal/BaseFormModal";
|
|
||||||
|
|
||||||
type Props = CreateEditFormProps<
|
|
||||||
MarketplaceSchema,
|
|
||||||
CreateMarketplaceSchema,
|
|
||||||
UpdateMarketplaceSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
const MarketplaceEditorModal = ({
|
|
||||||
context,
|
|
||||||
id,
|
|
||||||
innerProps,
|
|
||||||
}: ContextModalProps<Props>) => {
|
|
||||||
const initialValues: UpdateMarketplaceSchema | CreateMarketplaceSchema =
|
|
||||||
innerProps.isEditing
|
|
||||||
? innerProps.entity
|
|
||||||
: {
|
|
||||||
name: "",
|
|
||||||
authData: {
|
|
||||||
"Authorization": "",
|
|
||||||
"Client-Id": "",
|
|
||||||
"Api-Key": "",
|
|
||||||
"CampaignId": "",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
initialValues,
|
|
||||||
validate: {
|
|
||||||
name: name =>
|
|
||||||
(!name || name.trim() === "") &&
|
|
||||||
"Необходимо ввести название маркетплейса",
|
|
||||||
baseMarketplace: baseMarketplace =>
|
|
||||||
!baseMarketplace && "Необходимо указать базовый маркетплейс",
|
|
||||||
authData: authData =>
|
|
||||||
!authData && "Необходимо указать данные авторизации",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseFormModal
|
|
||||||
{...innerProps}
|
|
||||||
closeOnSubmit
|
|
||||||
form={form}
|
|
||||||
onClose={() => context.closeContextModal(id)}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<TextInput
|
|
||||||
label={"Название"}
|
|
||||||
placeholder={"Введите название маркетплейса"}
|
|
||||||
{...form.getInputProps("name")}
|
|
||||||
/>
|
|
||||||
<BaseMarketplaceSelect
|
|
||||||
label={"Базовый маркетплейс"}
|
|
||||||
placeholder={"Выберите базовый маркетплейс"}
|
|
||||||
{...form.getInputProps("baseMarketplace")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{form.values.baseMarketplace && (
|
|
||||||
<MarketplaceAuthDataInput
|
|
||||||
baseMarketplace={form.values.baseMarketplace}
|
|
||||||
value={form.values.authData as Record<string, string>}
|
|
||||||
onChange={value =>
|
|
||||||
form.setFieldValue("authData", value)
|
|
||||||
}
|
|
||||||
error={form.getInputProps("authData").error}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</BaseFormModal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MarketplaceEditorModal;
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { useCrudOperations } from "@/hooks/cruds/baseCrud";
|
|
||||||
import {
|
|
||||||
ClientSchema,
|
|
||||||
CreateClientSchema,
|
|
||||||
UpdateClientSchema,
|
|
||||||
} from "@/lib/client";
|
|
||||||
import {
|
|
||||||
createClientMutation,
|
|
||||||
deleteClientMutation,
|
|
||||||
updateClientMutation,
|
|
||||||
} from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
|
|
||||||
type UseClientsProps = {
|
|
||||||
queryKey: any[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ClientsCrud = {
|
|
||||||
onCreate: (client: CreateClientSchema) => void;
|
|
||||||
onUpdate: (
|
|
||||||
clientId: number,
|
|
||||||
client: UpdateClientSchema,
|
|
||||||
onSuccess?: () => void
|
|
||||||
) => void;
|
|
||||||
onDelete: (client: ClientSchema) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useClientsCrud = ({ queryKey }: UseClientsProps): ClientsCrud => {
|
|
||||||
return useCrudOperations<
|
|
||||||
ClientSchema,
|
|
||||||
UpdateClientSchema,
|
|
||||||
CreateClientSchema
|
|
||||||
>({
|
|
||||||
key: "getClients",
|
|
||||||
queryKey,
|
|
||||||
mutations: {
|
|
||||||
create: createClientMutation(),
|
|
||||||
update: updateClientMutation(),
|
|
||||||
delete: deleteClientMutation(),
|
|
||||||
},
|
|
||||||
getUpdateEntity: (old, update) => ({
|
|
||||||
...old,
|
|
||||||
details: update.details ?? old.details,
|
|
||||||
name: update.name ?? old.name,
|
|
||||||
companyName: update.companyName ?? old.companyName,
|
|
||||||
comment: update.comment ?? old.comment,
|
|
||||||
}),
|
|
||||||
getDeleteConfirmTitle: () => "Удаление клиента",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
import { useCrudOperations } from "@/hooks/cruds/baseCrud";
|
|
||||||
import {
|
|
||||||
CreateMarketplaceSchema,
|
|
||||||
MarketplaceSchema,
|
|
||||||
UpdateMarketplaceSchema,
|
|
||||||
} from "@/lib/client";
|
|
||||||
import {
|
|
||||||
createMarketplaceMutation,
|
|
||||||
deleteMarketplaceMutation,
|
|
||||||
updateMarketplaceMutation,
|
|
||||||
} from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
|
|
||||||
type UseMarketplacesProps = {
|
|
||||||
queryKey: any[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type MarketplacesCrud = {
|
|
||||||
onCreate: (mp: CreateMarketplaceSchema) => void;
|
|
||||||
onUpdate: (
|
|
||||||
mpId: number,
|
|
||||||
mp: UpdateMarketplaceSchema,
|
|
||||||
onSuccess?: () => void
|
|
||||||
) => void;
|
|
||||||
onDelete: (mp: MarketplaceSchema) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useMarketplacesCrud = ({
|
|
||||||
queryKey,
|
|
||||||
}: UseMarketplacesProps): MarketplacesCrud => {
|
|
||||||
return useCrudOperations<
|
|
||||||
MarketplaceSchema,
|
|
||||||
UpdateMarketplaceSchema,
|
|
||||||
CreateMarketplaceSchema
|
|
||||||
>({
|
|
||||||
key: "getMarketplaces",
|
|
||||||
queryKey,
|
|
||||||
mutations: {
|
|
||||||
create: createMarketplaceMutation(),
|
|
||||||
update: updateMarketplaceMutation(),
|
|
||||||
delete: deleteMarketplaceMutation(),
|
|
||||||
},
|
|
||||||
getUpdateEntity: (old, update) => ({
|
|
||||||
...old,
|
|
||||||
baseMarketplace: update.baseMarketplace ?? old.baseMarketplace,
|
|
||||||
name: update.name ?? old.name,
|
|
||||||
authData: update.authData ?? old.authData,
|
|
||||||
}),
|
|
||||||
getDeleteConfirmTitle: () => "Удаление маркетплейса",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import {
|
|
||||||
getBaseMarketplacesOptions,
|
|
||||||
getBaseMarketplacesQueryKey,
|
|
||||||
} from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
|
|
||||||
const useBaseMarketplacesList = () => {
|
|
||||||
const { data, refetch } = useQuery(getBaseMarketplacesOptions());
|
|
||||||
|
|
||||||
const queryKey = getBaseMarketplacesQueryKey();
|
|
||||||
|
|
||||||
return { baseMarketplaces: data?.items ?? [], refetch, queryKey };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useBaseMarketplacesList;
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import {
|
|
||||||
getClientsOptions,
|
|
||||||
getClientsQueryKey,
|
|
||||||
} from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
includeDeleted?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useClientsList = (
|
|
||||||
{ includeDeleted = false }: Props = { includeDeleted: false }
|
|
||||||
) => {
|
|
||||||
const { data, refetch } = useQuery(
|
|
||||||
getClientsOptions({ query: { includeDeleted } })
|
|
||||||
);
|
|
||||||
const clients = useMemo(() => data?.items ?? [], [data]);
|
|
||||||
|
|
||||||
const queryKey = getClientsQueryKey();
|
|
||||||
|
|
||||||
return { clients, refetch, queryKey };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useClientsList;
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import {
|
|
||||||
getMarketplacesOptions,
|
|
||||||
getMarketplacesQueryKey,
|
|
||||||
} from "@/lib/client/@tanstack/react-query.gen";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
clientId: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useMarketplacesList = ({ clientId }: Props) => {
|
|
||||||
const { data, refetch } = useQuery(
|
|
||||||
getMarketplacesOptions({ path: { clientId } })
|
|
||||||
);
|
|
||||||
|
|
||||||
const queryKey = getMarketplacesQueryKey({ path: { clientId } });
|
|
||||||
|
|
||||||
return { marketplaces: data?.items ?? [], refetch, queryKey };
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useMarketplacesList;
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { modals } from "@mantine/modals";
|
|
||||||
import { useClientsContext } from "@/app/clients/contexts/ClientsContext";
|
|
||||||
import { ClientSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
const useClientsActions = () => {
|
|
||||||
const { clientsCrud } = useClientsContext();
|
|
||||||
|
|
||||||
const onCreateClick = () => {
|
|
||||||
modals.openContextModal({
|
|
||||||
modal: "clientEditorModal",
|
|
||||||
title: "Создание клиента",
|
|
||||||
innerProps: {
|
|
||||||
onCreate: clientsCrud.onCreate,
|
|
||||||
isEditing: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUpdateClick = (client: ClientSchema) => {
|
|
||||||
modals.openContextModal({
|
|
||||||
modal: "clientEditorModal",
|
|
||||||
title: "Редактирование клиента",
|
|
||||||
innerProps: {
|
|
||||||
onChange: updates => clientsCrud.onUpdate(client.id, updates),
|
|
||||||
entity: client,
|
|
||||||
isEditing: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
onCreateClick,
|
|
||||||
onUpdateClick,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useClientsActions;
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useDebouncedValue } from "@mantine/hooks";
|
|
||||||
import { ClientSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
clients: ClientSchema[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const useClientsFilter = ({ clients }: Props) => {
|
|
||||||
const [search, setSearch] = useState<string>("");
|
|
||||||
const [debouncedSearch] = useDebouncedValue(search, 400);
|
|
||||||
const [filteredClients, setFilteredClients] = useState<ClientSchema[]>([]);
|
|
||||||
|
|
||||||
const filterClients = () => {
|
|
||||||
if (debouncedSearch.length === 0) {
|
|
||||||
setFilteredClients(clients);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loweredSearch = debouncedSearch.toLowerCase();
|
|
||||||
const filtered = clients.filter(
|
|
||||||
client =>
|
|
||||||
client.name.toLowerCase().includes(loweredSearch) ||
|
|
||||||
client.details?.inn?.includes(loweredSearch) ||
|
|
||||||
client.details?.email?.toLowerCase().includes(loweredSearch) ||
|
|
||||||
client.details?.telegram
|
|
||||||
?.toLowerCase()
|
|
||||||
.includes(loweredSearch) ||
|
|
||||||
client.details?.phoneNumber?.includes(loweredSearch) ||
|
|
||||||
client.companyName.toLowerCase().includes(loweredSearch)
|
|
||||||
);
|
|
||||||
setFilteredClients(filtered);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
filterClients();
|
|
||||||
}, [debouncedSearch, clients]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
search,
|
|
||||||
setSearch,
|
|
||||||
filteredClients,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useClientsFilter;
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Fieldset, Stack, Textarea, TextInput } from "@mantine/core";
|
|
||||||
import { useForm } from "@mantine/form";
|
|
||||||
import { ContextModalProps } from "@mantine/modals";
|
|
||||||
import isValidInn from "@/app/clients/utils/isValidInn";
|
|
||||||
import {
|
|
||||||
ClientSchema,
|
|
||||||
CreateClientSchema,
|
|
||||||
UpdateClientSchema,
|
|
||||||
} from "@/lib/client";
|
|
||||||
import BaseFormModal, {
|
|
||||||
CreateEditFormProps,
|
|
||||||
} from "@/modals/base/BaseFormModal/BaseFormModal";
|
|
||||||
|
|
||||||
type Props = CreateEditFormProps<
|
|
||||||
ClientSchema,
|
|
||||||
CreateClientSchema,
|
|
||||||
UpdateClientSchema
|
|
||||||
>;
|
|
||||||
|
|
||||||
const ClientEditorModal = ({
|
|
||||||
context,
|
|
||||||
id,
|
|
||||||
innerProps,
|
|
||||||
}: ContextModalProps<Props>) => {
|
|
||||||
const initialValues = innerProps.isEditing
|
|
||||||
? innerProps.entity
|
|
||||||
: ({
|
|
||||||
name: "",
|
|
||||||
companyName: "",
|
|
||||||
details: {
|
|
||||||
telegram: "",
|
|
||||||
phoneNumber: "",
|
|
||||||
email: "",
|
|
||||||
inn: "",
|
|
||||||
},
|
|
||||||
comment: "",
|
|
||||||
} as CreateClientSchema);
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
initialValues,
|
|
||||||
validate: {
|
|
||||||
name: name =>
|
|
||||||
(!name || name.trim() === "") &&
|
|
||||||
"Необходимо ввести название клиента",
|
|
||||||
details: {
|
|
||||||
inn: inn => inn.length > 0 && !isValidInn(inn) && "Некорректный ИНН",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseFormModal
|
|
||||||
{...innerProps}
|
|
||||||
closeOnSubmit
|
|
||||||
form={form}
|
|
||||||
onClose={() => context.closeContextModal(id)}>
|
|
||||||
<Fieldset legend={"Основная информация"}>
|
|
||||||
<TextInput
|
|
||||||
required
|
|
||||||
label={"Название клиента"}
|
|
||||||
placeholder={"Введите название клиента"}
|
|
||||||
{...form.getInputProps("name")}
|
|
||||||
/>
|
|
||||||
</Fieldset>
|
|
||||||
<Fieldset legend={"Дополнительная информация"}>
|
|
||||||
<Stack gap={"xs"}>
|
|
||||||
<TextInput
|
|
||||||
label={"Телеграм"}
|
|
||||||
placeholder={"Введите телеграм"}
|
|
||||||
{...form.getInputProps("details.telegram")}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label={"Номер телефона"}
|
|
||||||
placeholder={"Введите номер телефона"}
|
|
||||||
{...form.getInputProps("details.phoneNumber")}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label={"Почта"}
|
|
||||||
placeholder={"Введите почту"}
|
|
||||||
{...form.getInputProps("details.email")}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label={"ИНН"}
|
|
||||||
placeholder={"Введите ИНН"}
|
|
||||||
{...form.getInputProps("details.inn")}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label={"Название компании"}
|
|
||||||
placeholder={"Введите название компании"}
|
|
||||||
{...form.getInputProps("companyName")}
|
|
||||||
/>
|
|
||||||
<Textarea
|
|
||||||
label={"Комментарий"}
|
|
||||||
placeholder={"Введите комментарий"}
|
|
||||||
{...form.getInputProps("comment")}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Fieldset>
|
|
||||||
</BaseFormModal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ClientEditorModal;
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import { Suspense } from "react";
|
|
||||||
import { Center, Loader } from "@mantine/core";
|
|
||||||
import { ClientsContextProvider } from "@/app/clients/contexts/ClientsContext";
|
|
||||||
import PageContainer from "@/components/layout/PageContainer/PageContainer";
|
|
||||||
import PageBody from "@/app/clients/components/shared/PageBody/PageBody";
|
|
||||||
|
|
||||||
export default async function ClientsPage() {
|
|
||||||
return (
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<Center h="50vh">
|
|
||||||
<Loader size="lg" />
|
|
||||||
</Center>
|
|
||||||
}>
|
|
||||||
<PageContainer>
|
|
||||||
<ClientsContextProvider>
|
|
||||||
<PageBody />
|
|
||||||
</ClientsContextProvider>
|
|
||||||
</PageContainer>
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
const isValidInn = (inn: string | null | undefined) => {
|
|
||||||
return inn && inn.match(/^(\d{12}|\d{10})$/);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default isValidInn;
|
|
||||||
13
src/app/deals/components/Board/Board.tsx
Normal file
13
src/app/deals/components/Board/Board.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React, { FC } from "react";
|
||||||
|
import { Box } from "@mantine/core";
|
||||||
|
import { BoardSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
board: BoardSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Board: FC<Props> = ({ board }) => {
|
||||||
|
return <Box miw={100} style={{ borderWidth: 1, margin: 0 }}>{board.name}</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Board;
|
||||||
65
src/app/deals/components/Boards/Boards.tsx
Normal file
65
src/app/deals/components/Boards/Boards.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { ScrollArea } from "@mantine/core";
|
||||||
|
import Board from "@/app/deals/components/Board/Board";
|
||||||
|
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||||
|
import { BoardSchema } from "@/lib/client";
|
||||||
|
import { updateBoardMutation } from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
import SortableDnd from "@/components/SortableDnd";
|
||||||
|
import { notifications } from "@/lib/notifications";
|
||||||
|
|
||||||
|
const Boards = () => {
|
||||||
|
const { boards, setSelectedBoard, refetchBoards } = useBoardsContext();
|
||||||
|
|
||||||
|
const updateBoard = useMutation({
|
||||||
|
...updateBoardMutation(),
|
||||||
|
onError: error => {
|
||||||
|
console.error(error);
|
||||||
|
notifications.error({
|
||||||
|
message: error.response?.data?.detail as string | undefined,
|
||||||
|
});
|
||||||
|
refetchBoards();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderBoard = (board: BoardSchema) => {
|
||||||
|
return <Board board={board} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = (itemId: number, newLexorank: string) => {
|
||||||
|
updateBoard.mutate({
|
||||||
|
path: {
|
||||||
|
boardId: itemId,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
board: {
|
||||||
|
lexorank: newLexorank,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectBoard = (board: BoardSchema) => {
|
||||||
|
setSelectedBoard(board);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea
|
||||||
|
offsetScrollbars={"x"}
|
||||||
|
scrollbars={"x"}
|
||||||
|
scrollbarSize={0}
|
||||||
|
w={"100%"}>
|
||||||
|
<SortableDnd
|
||||||
|
initialItems={boards}
|
||||||
|
renderItem={renderBoard}
|
||||||
|
onDragEnd={onDragEnd}
|
||||||
|
onItemClick={selectBoard}
|
||||||
|
rowStyle={{ flexWrap: "nowrap" }}
|
||||||
|
/>
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Boards;
|
||||||
12
src/app/deals/components/DealCard/DealCard.tsx
Normal file
12
src/app/deals/components/DealCard/DealCard.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Card } from "@mantine/core";
|
||||||
|
import { DealSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
deal: DealSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DealCard = ({ deal }: Props) => {
|
||||||
|
return <Card>{deal.name}</Card>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DealCard;
|
||||||
25
src/app/deals/components/DealContainer/DealContainer.tsx
Normal file
25
src/app/deals/components/DealContainer/DealContainer.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React, { FC, useMemo } from "react";
|
||||||
|
import { Box } from "@mantine/core";
|
||||||
|
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
||||||
|
import { DealSchema } from "@/lib/client";
|
||||||
|
import { SortableItem } from "@/components/SortableDnd/SortableItem";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
deal: DealSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DealContainer: FC<Props> = ({ deal }) => {
|
||||||
|
const dealBody = useMemo(() => <DealCard deal={deal} />, [deal]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<SortableItem
|
||||||
|
dragHandleStyle={{ cursor: "pointer" }}
|
||||||
|
id={deal.id}>
|
||||||
|
{dealBody}
|
||||||
|
</SortableItem>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DealContainer;
|
||||||
36
src/app/deals/components/DndOverlay/DndOverlay.tsx
Normal file
36
src/app/deals/components/DndOverlay/DndOverlay.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { defaultDropAnimation, DragOverlay } from "@dnd-kit/core";
|
||||||
|
import DealCard from "@/app/deals/components/DealCard/DealCard";
|
||||||
|
import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
|
||||||
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
activeDeal: DealSchema | null;
|
||||||
|
activeStatus: StatusSchema | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DndOverlay = ({ activeStatus, activeDeal }: Props) => {
|
||||||
|
const { deals } = useStatusesContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragOverlay dropAnimation={defaultDropAnimation}>
|
||||||
|
<div style={{ cursor: "grabbing" }}>
|
||||||
|
{activeDeal ? (
|
||||||
|
<DealCard deal={activeDeal} />
|
||||||
|
) : activeStatus ? (
|
||||||
|
<StatusColumn
|
||||||
|
id={`${activeStatus.id}-status`}
|
||||||
|
status={activeStatus}
|
||||||
|
deals={deals.filter(
|
||||||
|
deal => deal.statusId === activeStatus.id
|
||||||
|
)}
|
||||||
|
isDragging
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</DragOverlay>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DndOverlay;
|
||||||
24
src/app/deals/components/Header/Header.tsx
Normal file
24
src/app/deals/components/Header/Header.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Group } from "@mantine/core";
|
||||||
|
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||||
|
import ProjectSelect from "@/components/selects/ProjectSelect/ProjectSelect";
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const { projects, setSelectedProject, selectedProject } =
|
||||||
|
useProjectsContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
justify={"flex-end"}
|
||||||
|
w={"100%"}>
|
||||||
|
<ProjectSelect
|
||||||
|
data={projects}
|
||||||
|
value={selectedProject}
|
||||||
|
onChange={value => value && setSelectedProject(value)}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
37
src/app/deals/components/SortableItem/SortableItem.tsx
Normal file
37
src/app/deals/components/SortableItem/SortableItem.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useSortable } from "@dnd-kit/sortable";
|
||||||
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SortableItem = ({ children, id }: Props) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({ id });
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SortableItem;
|
||||||
67
src/app/deals/components/StatusColumn/StatusColumn.tsx
Normal file
67
src/app/deals/components/StatusColumn/StatusColumn.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { useDroppable } from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { Box, Stack, Text } from "@mantine/core";
|
||||||
|
import DealContainer from "@/app/deals/components/DealContainer/DealContainer";
|
||||||
|
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||||
|
import { sortByLexorank } from "@/utils/lexorank";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id: string;
|
||||||
|
status: StatusSchema;
|
||||||
|
deals: DealSchema[];
|
||||||
|
isDragging?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusColumn = ({ id, status, deals, isDragging }: Props) => {
|
||||||
|
const { setNodeRef } = useDroppable({ id });
|
||||||
|
const sortedDeals = useMemo(
|
||||||
|
() => sortByLexorank(deals.filter(deal => deal.statusId === status.id)),
|
||||||
|
[deals]
|
||||||
|
);
|
||||||
|
|
||||||
|
const columnBody = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<SortableContext
|
||||||
|
id={id}
|
||||||
|
items={sortedDeals}
|
||||||
|
strategy={verticalListSortingStrategy}>
|
||||||
|
<Stack
|
||||||
|
gap={"xs"}
|
||||||
|
ref={setNodeRef}>
|
||||||
|
{sortedDeals.map(deal => (
|
||||||
|
<DealContainer
|
||||||
|
key={deal.id}
|
||||||
|
deal={deal}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</SortableContext>
|
||||||
|
);
|
||||||
|
}, [sortedDeals]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#eee",
|
||||||
|
padding: 2,
|
||||||
|
width: "15vw",
|
||||||
|
minWidth: 150,
|
||||||
|
}}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
cursor: "grab",
|
||||||
|
userSelect: "none",
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
}}>
|
||||||
|
{status.name}
|
||||||
|
</Text>
|
||||||
|
{columnBody}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusColumn;
|
||||||
76
src/app/deals/components/StatusColumns/StatusColumns.tsx
Normal file
76
src/app/deals/components/StatusColumns/StatusColumns.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import StatusColumnsDnd from "@/app/deals/components/StatusColumnsDnd/StatusColumnsDnd";
|
||||||
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import {
|
||||||
|
updateDealMutation,
|
||||||
|
updateStatusMutation,
|
||||||
|
} from "@/lib/client/@tanstack/react-query.gen";
|
||||||
|
import { notifications } from "@/lib/notifications";
|
||||||
|
|
||||||
|
const StatusColumns = () => {
|
||||||
|
const { refetchStatuses, refetchDeals } = useStatusesContext();
|
||||||
|
|
||||||
|
const updateStatus = useMutation({
|
||||||
|
...updateStatusMutation(),
|
||||||
|
onError: error => {
|
||||||
|
console.error(error);
|
||||||
|
notifications.error({
|
||||||
|
message: error.response?.data?.detail as string | undefined,
|
||||||
|
});
|
||||||
|
refetchStatuses();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateDeals = useMutation({
|
||||||
|
...updateDealMutation(),
|
||||||
|
onError: error => {
|
||||||
|
console.error(error);
|
||||||
|
notifications.error({
|
||||||
|
message: error.response?.data?.detail as string | undefined,
|
||||||
|
});
|
||||||
|
refetchDeals();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDealDragEnd = (
|
||||||
|
dealId: number,
|
||||||
|
statusId: number,
|
||||||
|
lexorank?: string
|
||||||
|
) => {
|
||||||
|
updateDeals.mutate({
|
||||||
|
path: {
|
||||||
|
dealId,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
deal: {
|
||||||
|
statusId,
|
||||||
|
lexorank,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStatusDragEnd = (statusId: number, lexorank: string) => {
|
||||||
|
updateStatus.mutate({
|
||||||
|
path: {
|
||||||
|
statusId,
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
status: {
|
||||||
|
lexorank,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusColumnsDnd
|
||||||
|
onDealDragEnd={onDealDragEnd}
|
||||||
|
onStatusDragEnd={onStatusDragEnd}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusColumns;
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { FC } from "react";
|
||||||
|
import { closestCorners, DndContext } from "@dnd-kit/core";
|
||||||
|
import {
|
||||||
|
horizontalListSortingStrategy,
|
||||||
|
SortableContext,
|
||||||
|
} from "@dnd-kit/sortable";
|
||||||
|
import { Group, ScrollArea } from "@mantine/core";
|
||||||
|
import DndOverlay from "@/app/deals/components/DndOverlay/DndOverlay";
|
||||||
|
import StatusColumn from "@/app/deals/components/StatusColumn/StatusColumn";
|
||||||
|
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||||
|
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
|
||||||
|
import { SortableItem } from "@/components/SortableDnd/SortableItem";
|
||||||
|
import useDndSensors from "../../hooks/useSensors";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onDealDragEnd: (
|
||||||
|
dealId: number,
|
||||||
|
statusId: number,
|
||||||
|
lexorank?: string
|
||||||
|
) => void;
|
||||||
|
onStatusDragEnd: (statusId: number, lexorank: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusColumnsDnd: FC<Props> = props => {
|
||||||
|
const { deals } = useStatusesContext();
|
||||||
|
|
||||||
|
const {
|
||||||
|
sortedStatuses,
|
||||||
|
handleDragStart,
|
||||||
|
handleDragOver,
|
||||||
|
handleDragEnd,
|
||||||
|
activeStatus,
|
||||||
|
activeDeal,
|
||||||
|
} = useDealsAndStatusesDnd(props);
|
||||||
|
|
||||||
|
const sensors = useDndSensors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollArea
|
||||||
|
offsetScrollbars={"x"}
|
||||||
|
scrollbarSize={"0.5rem"}>
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCorners}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragEnd={handleDragEnd}>
|
||||||
|
<SortableContext
|
||||||
|
items={sortedStatuses.map(status => `${status.id}-status`)}
|
||||||
|
strategy={horizontalListSortingStrategy}>
|
||||||
|
<Group
|
||||||
|
gap={"xs"}
|
||||||
|
wrap={"nowrap"}
|
||||||
|
align={"start"}>
|
||||||
|
{sortedStatuses.map(status => (
|
||||||
|
<SortableItem
|
||||||
|
key={status.id}
|
||||||
|
id={`${status.id}-status`}>
|
||||||
|
<StatusColumn
|
||||||
|
id={`${status.id}-status`}
|
||||||
|
status={status}
|
||||||
|
deals={deals}
|
||||||
|
isDragging={activeStatus?.id === status.id}
|
||||||
|
/>
|
||||||
|
</SortableItem>
|
||||||
|
))}
|
||||||
|
<DndOverlay
|
||||||
|
activeStatus={activeStatus}
|
||||||
|
activeDeal={activeDeal}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</ScrollArea>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusColumnsDnd;
|
||||||
@ -1,86 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { FC } from "react";
|
|
||||||
import { IconFilter } from "@tabler/icons-react";
|
|
||||||
import { Button, Divider, Flex, Group, Indicator } from "@mantine/core";
|
|
||||||
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 { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
|
||||||
import { DealsFiltersForm } from "@/app/deals/hooks/useDealsFilters";
|
|
||||||
import SmallPageBlock from "@/components/layout/SmallPageBlock/SmallPageBlock";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
|
|
||||||
export enum View {
|
|
||||||
BOARD = "board",
|
|
||||||
TABLE = "table",
|
|
||||||
SCHEDULE = "schedule"
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
view: View;
|
|
||||||
setView: (view: View) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TopToolPanel: FC<Props> = ({ view, setView }) => {
|
|
||||||
const { dealsFiltersForm, isChangedFilters } = useDealsContext();
|
|
||||||
const { selectedProject } = useProjectsContext();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
if (isMobile) return;
|
|
||||||
|
|
||||||
const viewFiltersModalMap = {
|
|
||||||
table: "dealsTableFiltersModal",
|
|
||||||
board: "dealsBoardFiltersModal",
|
|
||||||
schedule: "dealsScheduleFiltersModal",
|
|
||||||
};
|
|
||||||
|
|
||||||
const onFiltersClick = () => {
|
|
||||||
modals.openContextModal({
|
|
||||||
modal: viewFiltersModalMap[view],
|
|
||||||
title: "Фильтры",
|
|
||||||
withCloseButton: true,
|
|
||||||
innerProps: {
|
|
||||||
value: dealsFiltersForm.values,
|
|
||||||
onChange: (values: DealsFiltersForm) =>
|
|
||||||
dealsFiltersForm.setValues(values),
|
|
||||||
project: selectedProject,
|
|
||||||
boardAndStatusEnabled: view === View.TABLE,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group>
|
|
||||||
<ViewSelector
|
|
||||||
value={view}
|
|
||||||
onChange={setView}
|
|
||||||
/>
|
|
||||||
<Divider orientation={"vertical"} />
|
|
||||||
<Flex
|
|
||||||
wrap={"nowrap"}
|
|
||||||
align={"center"}
|
|
||||||
gap={"sm"}>
|
|
||||||
<Indicator
|
|
||||||
zIndex={100}
|
|
||||||
disabled={!isChangedFilters}
|
|
||||||
offset={5}
|
|
||||||
size={8}>
|
|
||||||
<SmallPageBlock
|
|
||||||
style={{ borderRadius: "var(--mantine-radius-xl)" }}>
|
|
||||||
<Button
|
|
||||||
unstyled
|
|
||||||
onClick={onFiltersClick}
|
|
||||||
radius="xl"
|
|
||||||
className={style.container}>
|
|
||||||
<IconFilter />
|
|
||||||
</Button>
|
|
||||||
</SmallPageBlock>
|
|
||||||
</Indicator>
|
|
||||||
</Flex>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TopToolPanel;
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: var(--mantine-radius-xl);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: var(--mantine-spacing-xs);
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { FC, PropsWithChildren } from "react";
|
|
||||||
import { Button } from "@mantine/core";
|
|
||||||
import SmallPageBlock from "@/components/layout/SmallPageBlock/SmallPageBlock";
|
|
||||||
import style from "./ViewSelectButton.module.css";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
selected: boolean;
|
|
||||||
onSelect: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ViewSelectButton: FC<PropsWithChildren<Props>> = ({
|
|
||||||
selected,
|
|
||||||
onSelect,
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<SmallPageBlock
|
|
||||||
active={selected}
|
|
||||||
style={{ borderRadius: "var(--mantine-radius-xl)" }}>
|
|
||||||
<Button
|
|
||||||
unstyled
|
|
||||||
onClick={onSelect}
|
|
||||||
radius="xl"
|
|
||||||
className={style.container}>
|
|
||||||
{children}
|
|
||||||
</Button>
|
|
||||||
</SmallPageBlock>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ViewSelectButton;
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import {
|
|
||||||
IconCalendarWeekFilled,
|
|
||||||
IconLayoutDashboard,
|
|
||||||
IconMenu2,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
import { Group } from "@mantine/core";
|
|
||||||
import { View } from "@/app/deals/components/desktop/TopToolPanel/TopToolPanel";
|
|
||||||
import ViewSelectButton from "@/app/deals/components/desktop/ViewSelectButton/ViewSelectButton";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value: View;
|
|
||||||
onChange: (view: View) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ViewSelector: FC<Props> = ({ value, onChange }) => {
|
|
||||||
const views = [
|
|
||||||
{
|
|
||||||
value: View.BOARD,
|
|
||||||
icon: <IconLayoutDashboard />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: View.TABLE,
|
|
||||||
icon: <IconMenu2 />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: View.SCHEDULE,
|
|
||||||
icon: <IconCalendarWeekFilled />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group>
|
|
||||||
{views.map(view => (
|
|
||||||
<ViewSelectButton
|
|
||||||
key={view.value}
|
|
||||||
selected={value === view.value}
|
|
||||||
onSelect={() => onChange(view.value)}>
|
|
||||||
{view.icon}
|
|
||||||
</ViewSelectButton>
|
|
||||||
))}
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ViewSelector;
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import React, { FC } from "react";
|
|
||||||
import { IconCheckbox, IconDotsVertical, IconTrash } from "@tabler/icons-react";
|
|
||||||
import { Box, Menu } from "@mantine/core";
|
|
||||||
import DropdownMenuItem from "@/components/ui/DropdownMenuItem/DropdownMenuItem";
|
|
||||||
import ThemeIcon from "@/components/ui/ThemeIcon/ThemeIcon";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onDelete: () => void;
|
|
||||||
startDealsSelecting: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const GroupMenu: FC<Props> = ({ onDelete, startDealsSelecting }) => {
|
|
||||||
return (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Target>
|
|
||||||
<Box
|
|
||||||
px={"md"}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
onClick={e => e.stopPropagation()}>
|
|
||||||
<ThemeIcon size={"sm"}>
|
|
||||||
<IconDotsVertical />
|
|
||||||
</ThemeIcon>
|
|
||||||
</Box>
|
|
||||||
</Menu.Target>
|
|
||||||
<Menu.Dropdown>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={onDelete}
|
|
||||||
icon={<IconTrash />}
|
|
||||||
label={"Удалить группу"}
|
|
||||||
/>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={startDealsSelecting}
|
|
||||||
icon={<IconCheckbox />}
|
|
||||||
label={"Добавить/удалить сделки"}
|
|
||||||
/>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GroupMenu;
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { IconChevronLeft, IconSettings } from "@tabler/icons-react";
|
|
||||||
import { Box, Group, Stack, Title } from "@mantine/core";
|
|
||||||
import Boards from "@/app/deals/components/shared/Boards/Boards";
|
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
|
||||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
|
||||||
import { useDrawersContext } from "@/drawers/DrawersContext";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
|
|
||||||
const MainBlockHeader = () => {
|
|
||||||
const { setSelectedProjectId, refetchProjects, selectedProject } =
|
|
||||||
useProjectsContext();
|
|
||||||
const { refetchBoards } = useBoardsContext();
|
|
||||||
const { openDrawer } = useDrawersContext();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
const selectProjectId = async (projectId: number | null) => {
|
|
||||||
await refetchProjects();
|
|
||||||
setSelectedProjectId(projectId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openProjectsEditorDrawer = () => {
|
|
||||||
openDrawer({
|
|
||||||
key: "projectsMobileEditorDrawer",
|
|
||||||
props: {
|
|
||||||
onSelect: project => selectProjectId(project?.id ?? null),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const openBoardsEditorDrawer = () => {
|
|
||||||
if (!selectedProject) return;
|
|
||||||
openDrawer({
|
|
||||||
key: "boardsMobileEditorDrawer",
|
|
||||||
props: {
|
|
||||||
project: selectedProject,
|
|
||||||
},
|
|
||||||
onClose: refetchBoards,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
gap={0}
|
|
||||||
w={"100%"}>
|
|
||||||
{isMobile && (
|
|
||||||
<Group justify={"space-between"}>
|
|
||||||
<Box
|
|
||||||
p={"md"}
|
|
||||||
onClick={openProjectsEditorDrawer}>
|
|
||||||
<IconChevronLeft />
|
|
||||||
</Box>
|
|
||||||
<Title order={6}>{selectedProject?.name}</Title>
|
|
||||||
<Box
|
|
||||||
p={"md"}
|
|
||||||
onClick={openBoardsEditorDrawer}>
|
|
||||||
<IconSettings />
|
|
||||||
</Box>
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
<Group
|
|
||||||
wrap={"nowrap"}
|
|
||||||
gap={0}
|
|
||||||
align={"end"}>
|
|
||||||
<Boards />
|
|
||||||
<Box
|
|
||||||
flex={1}
|
|
||||||
style={{ borderBottom: "2px solid gray" }}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MainBlockHeader;
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
|
|
||||||
.board {
|
|
||||||
min-width: 50px;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: 3px;
|
|
||||||
border-top-left-radius: 15px;
|
|
||||||
border-top-right-radius: 15px;
|
|
||||||
border-bottom: 2px solid gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
.board-selected {
|
|
||||||
border: 2px solid gray;
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import React, { FC, useState } from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
import { Box, Group, Text } from "@mantine/core";
|
|
||||||
import BoardMenu from "@/app/deals/components/shared/BoardMenu/BoardMenu";
|
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
|
||||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
import { BoardSchema } from "@/lib/client";
|
|
||||||
import styles from "./Board.module.css";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
board: BoardSchema;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Board: FC<Props> = ({ board }) => {
|
|
||||||
const { selectedBoard, boardsCrud } = useBoardsContext();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Group
|
|
||||||
px={"md"}
|
|
||||||
py={"xs"}
|
|
||||||
className={classNames(
|
|
||||||
styles.board,
|
|
||||||
selectedBoard?.id === board.id && styles["board-selected"]
|
|
||||||
)}
|
|
||||||
justify={"space-between"}
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}>
|
|
||||||
<InPlaceInput
|
|
||||||
value={board.name}
|
|
||||||
onChange={value =>
|
|
||||||
boardsCrud.onUpdate(board.id, { name: value })
|
|
||||||
}
|
|
||||||
inputStyles={{
|
|
||||||
input: {
|
|
||||||
height: 24,
|
|
||||||
minHeight: 24,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
getChildren={startEditing => (
|
|
||||||
<Group wrap={"nowrap"}>
|
|
||||||
<Box>
|
|
||||||
<Text style={{ textWrap: "nowrap" }}>
|
|
||||||
{board.name}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
{!isMobile && (
|
|
||||||
<BoardMenu
|
|
||||||
isHovered={
|
|
||||||
selectedBoard?.id === board.id || isHovered
|
|
||||||
}
|
|
||||||
onDeleteBoard={boardsCrud.onDelete}
|
|
||||||
board={board}
|
|
||||||
startEditing={startEditing}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
modalTitle={"Редактирование доски"}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Board;
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
import React, { FC } from "react";
|
|
||||||
import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
|
|
||||||
import { Box, Menu } from "@mantine/core";
|
|
||||||
import DropdownMenuItem from "@/components/ui/DropdownMenuItem/DropdownMenuItem";
|
|
||||||
import ThemeIcon from "@/components/ui/ThemeIcon/ThemeIcon";
|
|
||||||
import { BoardSchema } from "@/lib/client";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
board: BoardSchema;
|
|
||||||
startEditing: () => void;
|
|
||||||
onDeleteBoard: (board: BoardSchema) => void;
|
|
||||||
isHovered?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const BoardMenu: FC<Props> = ({
|
|
||||||
board,
|
|
||||||
startEditing,
|
|
||||||
onDeleteBoard,
|
|
||||||
isHovered = true,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Menu>
|
|
||||||
<Menu.Target>
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
opacity: isHovered ? 1 : 0,
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
onClick={e => e.stopPropagation()}>
|
|
||||||
<ThemeIcon size={"sm"}>
|
|
||||||
<IconDotsVertical />
|
|
||||||
</ThemeIcon>
|
|
||||||
</Box>
|
|
||||||
</Menu.Target>
|
|
||||||
<Menu.Dropdown>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={startEditing}
|
|
||||||
icon={<IconEdit />}
|
|
||||||
label={"Переименовать"}
|
|
||||||
/>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => onDeleteBoard(board)}
|
|
||||||
icon={<IconTrash />}
|
|
||||||
label={"Удалить"}
|
|
||||||
/>
|
|
||||||
</Menu.Dropdown>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BoardMenu;
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
.container {
|
|
||||||
@media (min-width: 48em) {
|
|
||||||
max-width: calc(100vw - 210px - var(--mantine-spacing-md));
|
|
||||||
}
|
|
||||||
@media (max-width: 48em) {
|
|
||||||
max-width: 100vw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { Flex, ScrollArea } from "@mantine/core";
|
|
||||||
import Board from "@/app/deals/components/shared/Board/Board";
|
|
||||||
import CreateBoardButton from "@/app/deals/components/shared/CreateBoardButton/CreateBoardButton";
|
|
||||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
|
||||||
import SortableDnd from "@/components/dnd/SortableDnd";
|
|
||||||
import useHorizontalWheel from "@/hooks/utils/useHorizontalWheel";
|
|
||||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
|
||||||
import { BoardSchema } from "@/lib/client";
|
|
||||||
import styles from "./Boards.module.css";
|
|
||||||
|
|
||||||
const Boards = () => {
|
|
||||||
const { boards, setSelectedBoardId, boardsCrud } = useBoardsContext();
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const { ref, onWheel } = useHorizontalWheel<HTMLDivElement>();
|
|
||||||
|
|
||||||
const renderBoard = (board: BoardSchema) => <Board board={board} />;
|
|
||||||
|
|
||||||
const onDragEnd = (itemId: number, newLexorank: string) => {
|
|
||||||
boardsCrud.onUpdate(itemId, { lexorank: newLexorank });
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectBoard = (board: BoardSchema) => {
|
|
||||||
setSelectedBoardId(board.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
align={"end"}
|
|
||||||
className={styles.container}>
|
|
||||||
<ScrollArea
|
|
||||||
viewportRef={ref}
|
|
||||||
onWheel={onWheel}
|
|
||||||
offsetScrollbars={"x"}
|
|
||||||
scrollbars={"x"}
|
|
||||||
scrollbarSize={0}>
|
|
||||||
<SortableDnd
|
|
||||||
initialItems={boards}
|
|
||||||
renderItem={renderBoard}
|
|
||||||
onDragEnd={onDragEnd}
|
|
||||||
onItemClick={selectBoard}
|
|
||||||
containerStyle={{ flexWrap: "nowrap" }}
|
|
||||||
dragHandleStyle={{ cursor: "pointer" }}
|
|
||||||
disabled={isMobile}
|
|
||||||
/>
|
|
||||||
</ScrollArea>
|
|
||||||
<CreateBoardButton />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Boards;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user