Compare commits
1 Commits
8cc11bca67
...
detached
| 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"]
|
||||
@ -11,11 +11,9 @@ export default defineConfig({
|
||||
{
|
||||
name: "zod",
|
||||
requests: true,
|
||||
responses: true,
|
||||
definitions: true,
|
||||
metadata: true,
|
||||
dates: {
|
||||
offset: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "@hey-api/sdk",
|
||||
|
||||
24
package.json
24
package.json
@ -3,21 +3,16 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbo",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"generate-client": "openapi-ts && prettier --write ./src/lib/client/**/*.ts && git add ./src/lib/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"
|
||||
"generate-client": "openapi-ts && prettier --write ./src/client/**/*.ts && git add ./src/client"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@mantine/core": "8.1.2",
|
||||
"@mantine/dates": "^8.2.7",
|
||||
"@mantine/dropzone": "^8.3.1",
|
||||
"@mantine/form": "^8.1.3",
|
||||
"@mantine/hooks": "8.1.2",
|
||||
"@mantine/modals": "^8.2.1",
|
||||
@ -27,28 +22,19 @@
|
||||
"@tabler/icons-react": "^3.34.0",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"axios": "1.12.0",
|
||||
"axios": "^1.11.0",
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"dayjs": "^1.11.15",
|
||||
"framer-motion": "^12.23.7",
|
||||
"handlebars": "^4.7.8",
|
||||
"i18n-iso-countries": "^7.14.0",
|
||||
"lexorank": "^1.0.5",
|
||||
"libphonenumber-js": "^1.12.10",
|
||||
"mantine-contextmenu": "^8.2.0",
|
||||
"mantine-datatable": "^8.2.0",
|
||||
"next": "15.4.7",
|
||||
"phone": "^3.1.67",
|
||||
"next": "15.3.3",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-imask": "^7.6.1",
|
||||
"react-redux": "^9.2.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"sharp": "^0.34.3",
|
||||
"swiper": "^11.2.10",
|
||||
"zod": "^4.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -70,9 +56,7 @@
|
||||
"@types/node": "^22.13.11",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-redux": "^7.1.34",
|
||||
"@types/react-slick": "^0",
|
||||
"@types/redux-persist": "^4.3.1",
|
||||
"@types/slick-carousel": "^1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"babel-loader": "^10.0.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,61 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RefObject, useMemo, useRef } from "react";
|
||||
import { 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: 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,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, Text } 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>
|
||||
<Text>{selectedProject?.name}</Text>
|
||||
<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;
|
||||
@ -1,10 +0,0 @@
|
||||
|
||||
.create-button {
|
||||
padding: 10px 10px 9px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
height: 45px;
|
||||
width: 100%;
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { Box, Flex, rem } from "@mantine/core";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||
import styles from "./CreateBoardButton.module.css";
|
||||
|
||||
const CreateBoardButton = () => {
|
||||
const { boardsCrud } = useBoardsContext();
|
||||
|
||||
return (
|
||||
<Flex style={{ borderBottom: "2px solid gray" }}>
|
||||
<InPlaceInput
|
||||
placeholder={"Название доски"}
|
||||
onChange={name => boardsCrud.onCreate({ name })}
|
||||
getChildren={startEditing => (
|
||||
<Box
|
||||
onClick={startEditing}
|
||||
className={styles["create-button"]}>
|
||||
<IconPlus />
|
||||
</Box>
|
||||
)}
|
||||
inputStyles={{
|
||||
wrapper: {
|
||||
marginRight: "var(--mantine-spacing-xs)",
|
||||
paddingBlock: rem(3),
|
||||
paddingLeft: "var(--mantine-spacing-xs)",
|
||||
backgroundColor:
|
||||
"light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))",
|
||||
borderTopLeftRadius: "var(--mantine-spacing-xs)",
|
||||
borderTopRightRadius: "var(--mantine-spacing-xs)",
|
||||
},
|
||||
}}
|
||||
modalTitle={"Создание доски"}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateBoardButton;
|
||||
@ -1,13 +0,0 @@
|
||||
.create-button {
|
||||
cursor: pointer;
|
||||
min-height: max-content;
|
||||
border: 1px dashed;
|
||||
@mixin light {
|
||||
background-color: var(--color-light-white-blue);
|
||||
border-color: lightblue;
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-7);
|
||||
border-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { Card, Center, Group, Text, Transition } from "@mantine/core";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import CreateCardForm, { CreateDealForm } from "./components/CreateCardForm";
|
||||
import styles from "./CreateDealButton.module.css";
|
||||
|
||||
const CreateCardButton = () => {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isTransitionEnded, setIsTransitionEnded] = useState(true);
|
||||
const { dealsCrud } = useDealsContext();
|
||||
|
||||
const onSubmit = (values: CreateDealForm) => {
|
||||
dealsCrud.onCreate(values);
|
||||
setIsCreating(prevState => !prevState);
|
||||
setIsTransitionEnded(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles["create-button"]}
|
||||
onClick={() => {
|
||||
if (isCreating) return;
|
||||
setIsCreating(prevState => !prevState);
|
||||
setIsTransitionEnded(false);
|
||||
}}>
|
||||
{!isCreating && isTransitionEnded && (
|
||||
<Center>
|
||||
<Group gap={"xs"}>
|
||||
<IconPlus />
|
||||
<Text>Добавить</Text>
|
||||
</Group>
|
||||
</Center>
|
||||
)}
|
||||
<Transition
|
||||
mounted={isCreating}
|
||||
transition={"scale-y"}
|
||||
onExited={() => setIsTransitionEnded(true)}>
|
||||
{styles => (
|
||||
<div style={styles}>
|
||||
<CreateCardForm
|
||||
onCancel={() => setIsCreating(false)}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
export default CreateCardButton;
|
||||
@ -1,77 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
import { Button, Group, Stack, TextInput } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import { ClientSchema } from "@/lib/client";
|
||||
import ClientSelect from "@/modules/dealModularEditorTabs/Clients/components/ClientSelect";
|
||||
import { ModuleNames } from "@/modules/modules";
|
||||
|
||||
export type CreateDealForm = {
|
||||
name: string;
|
||||
client?: ClientSchema;
|
||||
clientId?: number;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
onSubmit: (values: CreateDealForm) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const CreateCardForm: FC<Props> = ({ onSubmit, onCancel }) => {
|
||||
const { modulesSet } = useProjectsContext();
|
||||
|
||||
const form = useForm<CreateDealForm>({
|
||||
initialValues: {
|
||||
name: "",
|
||||
},
|
||||
validate: {
|
||||
name: value => !value && "Введите название",
|
||||
client: client =>
|
||||
modulesSet.has(ModuleNames.CLIENTS) &&
|
||||
!client &&
|
||||
"Выберите клиента",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit(values => {
|
||||
onSubmit(values);
|
||||
form.reset();
|
||||
})}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
placeholder={"Название"}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
{modulesSet.has(ModuleNames.CLIENTS) && (
|
||||
<ClientSelect
|
||||
placeholder={"Клиент"}
|
||||
{...form.getInputProps("client")}
|
||||
onChange={client => {
|
||||
form.setFieldValue("client", client);
|
||||
form.setFieldValue("clientId", client?.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Group wrap={"nowrap"}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
w={"100%"}
|
||||
onClick={onCancel}>
|
||||
<IconX />
|
||||
</Button>
|
||||
<Button
|
||||
variant={"default"}
|
||||
w={"100%"}
|
||||
type={"submit"}>
|
||||
<IconCheck />
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateCardForm;
|
||||
@ -1,23 +0,0 @@
|
||||
|
||||
.container {
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
|
||||
width: fit-content;
|
||||
@media (max-width: 48em) {
|
||||
width: 80vw;
|
||||
height: 73.5vh;
|
||||
}
|
||||
}
|
||||
|
||||
.inner-container {
|
||||
border-radius: var(--mantine-spacing-md);
|
||||
flex-wrap: nowrap;
|
||||
|
||||
@mixin light {
|
||||
background-color: var(--color-light-aqua);
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
}
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
import React from "react";
|
||||
import { IconPlus } from "@tabler/icons-react";
|
||||
import { Box, Center, Group, Text } from "@mantine/core";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import styles from "./CreateStatusButton.module.css";
|
||||
|
||||
const CreateStatusButton = () => {
|
||||
const { statusesCrud } = useStatusesContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<Box className={styles.container}>
|
||||
<Box
|
||||
p={isMobile ? "sm" : 0}
|
||||
className={styles["inner-container"]}>
|
||||
<InPlaceInput
|
||||
placeholder={"Название колонки"}
|
||||
onChange={name => statusesCrud.onCreate({ name })}
|
||||
getChildren={startEditing => (
|
||||
<Center
|
||||
p={"sm"}
|
||||
onClick={() => startEditing()}>
|
||||
<Group
|
||||
gap={"xs"}
|
||||
wrap={"nowrap"}
|
||||
align={"center"}>
|
||||
<IconPlus />
|
||||
{isMobile && <Text>Добавить</Text>}
|
||||
</Group>
|
||||
</Center>
|
||||
)}
|
||||
modalTitle={"Создание колонки"}
|
||||
inputStyles={{
|
||||
wrapper: {
|
||||
width: 250,
|
||||
paddingInline: "var(--mantine-spacing-md)",
|
||||
paddingBlock: "var(--mantine-spacing-xs)",
|
||||
},
|
||||
input: {
|
||||
width: 250,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateStatusButton;
|
||||
@ -1,81 +0,0 @@
|
||||
|
||||
.container {
|
||||
padding: 0;
|
||||
border: 1px dashed;
|
||||
@mixin light {
|
||||
background-color: var(--color-light-white-blue);
|
||||
border-color: lightblue;
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-7);
|
||||
border-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
}
|
||||
|
||||
.container-selected {
|
||||
border: 2px dashed !important;
|
||||
@mixin light {
|
||||
border-color: dodgerblue !important;
|
||||
}
|
||||
@mixin dark {
|
||||
border-color: dodgerblue !important;
|
||||
}
|
||||
}
|
||||
|
||||
.container-mainly-selected {
|
||||
border: 2px solid;
|
||||
@mixin light {
|
||||
border-color: dodgerblue !important;
|
||||
}
|
||||
@mixin dark {
|
||||
border-color: dodgerblue !important;
|
||||
}
|
||||
}
|
||||
|
||||
.container-in-group {
|
||||
padding: 0;
|
||||
border: 1px dashed;
|
||||
@mixin light {
|
||||
background-color: var(--color-light-aqua);
|
||||
border-color: lightblue;
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
border-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
}
|
||||
|
||||
.deal-data {
|
||||
padding: var(--mantine-spacing-xs);
|
||||
gap: var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.deal-id {
|
||||
border-top-right-radius: var(--mantine-radius-md);
|
||||
border-bottom-left-radius: var(--mantine-radius-md);
|
||||
padding-inline: var(--mantine-spacing-xs);
|
||||
@mixin light {
|
||||
background-color: lightblue;
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--color-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.first-tag {
|
||||
@mixin light {
|
||||
background-color: lightblue;
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: darkslateblue;
|
||||
}
|
||||
}
|
||||
|
||||
.second-tag {
|
||||
@mixin light {
|
||||
background-color: lightgray;
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-4);
|
||||
}
|
||||
}
|
||||
@ -1,110 +0,0 @@
|
||||
import { IconCategoryPlus } from "@tabler/icons-react";
|
||||
import classNames from "classnames";
|
||||
import { useContextMenu } from "mantine-contextmenu";
|
||||
import { Box, Card, Group, Stack, Text } from "@mantine/core";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import { useDrawersContext } from "@/drawers/DrawersContext";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import { DealSchema } from "@/lib/client";
|
||||
import { ModuleNames } from "@/modules/modules";
|
||||
import styles from "./DealCard.module.css";
|
||||
import DealTags from "@/components/ui/DealTags/DealTags";
|
||||
|
||||
type Props = {
|
||||
deal: DealSchema;
|
||||
isInGroup?: boolean;
|
||||
};
|
||||
|
||||
const DealCard = ({ deal, isInGroup = false }: Props) => {
|
||||
const { selectedProject, modulesSet } = useProjectsContext();
|
||||
const { dealsCrud, refetchDeals, groupDealsSelection } = useDealsContext();
|
||||
const { openDrawer } = useDrawersContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const onClick = () => {
|
||||
if (groupDealsSelection.isDealsSelecting) {
|
||||
groupDealsSelection.toggleDeal(deal);
|
||||
return;
|
||||
}
|
||||
|
||||
openDrawer({
|
||||
key: "dealEditorDrawer",
|
||||
props: {
|
||||
value: deal,
|
||||
onChange: deal => dealsCrud.onUpdate(deal.id, deal),
|
||||
onDelete: dealsCrud.onDelete,
|
||||
project: selectedProject,
|
||||
},
|
||||
onClose: refetchDeals,
|
||||
});
|
||||
};
|
||||
|
||||
const { showContextMenu } = useContextMenu();
|
||||
|
||||
const dealContextMenu =
|
||||
deal.group || isMobile
|
||||
? []
|
||||
: [
|
||||
{
|
||||
key: "startGroupForming",
|
||||
onClick: () =>
|
||||
groupDealsSelection.startSelectingWithDeal(deal.id),
|
||||
title: "Создать группу",
|
||||
icon: <IconCategoryPlus />,
|
||||
},
|
||||
];
|
||||
|
||||
const getSelectedStyles = () => {
|
||||
if (groupDealsSelection.selectedBaseDealId === deal.id) {
|
||||
return styles["container-mainly-selected"];
|
||||
}
|
||||
if (groupDealsSelection.selectedDealIds.has(deal.id)) {
|
||||
return styles["container-selected"];
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
getSelectedStyles(),
|
||||
isInGroup ? styles["container-in-group"] : styles.container
|
||||
)}
|
||||
onContextMenu={showContextMenu(dealContextMenu)}>
|
||||
<Group
|
||||
justify={"space-between"}
|
||||
wrap={"nowrap"}
|
||||
pl={"xs"}
|
||||
gap={"xs"}
|
||||
align={"start"}>
|
||||
<Text
|
||||
c={"dodgerblue"}
|
||||
mt={"xs"}>
|
||||
{deal.name}
|
||||
</Text>
|
||||
<Box className={styles["deal-id"]}>
|
||||
<Text style={{ textWrap: "nowrap" }}>ID: {deal.id}</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
<Stack className={styles["deal-data"]}>
|
||||
<Stack gap={0}>
|
||||
{modulesSet.has(ModuleNames.CLIENTS) && (
|
||||
<Text>{deal.client?.name}</Text>
|
||||
)}
|
||||
{modulesSet.has(ModuleNames.FULFILLMENT_BASE) && (
|
||||
<>
|
||||
<Text key={"price"}>{deal.totalPrice} руб.</Text>
|
||||
<Text key={"count"}>
|
||||
{deal.productsQuantity} тов.
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
{!deal.group && <DealTags dealId={deal.id} tags={deal.tags} />}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealCard;
|
||||
@ -1,22 +0,0 @@
|
||||
|
||||
.group-container {
|
||||
border: 1px dashed;
|
||||
@mixin light {
|
||||
background-color: var(--color-light-white-blue);
|
||||
border-color: lightblue;
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-7);
|
||||
border-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
}
|
||||
|
||||
.selected-group {
|
||||
border: 2px solid;
|
||||
@mixin light {
|
||||
border-color: dodgerblue;
|
||||
}
|
||||
@mixin dark {
|
||||
border-color: dodgerblue;
|
||||
}
|
||||
}
|
||||
@ -1,101 +0,0 @@
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
import { IconCheckbox, IconTrash } from "@tabler/icons-react";
|
||||
import classNames from "classnames";
|
||||
import { useContextMenu } from "mantine-contextmenu";
|
||||
import { Flex, Stack, TextInput } from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import GroupMenu from "@/app/deals/components/mobile/GroupMenu/GroupMenu";
|
||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import DealTags from "@/components/ui/DealTags/DealTags";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
||||
import styles from "./DealsGroup.module.css";
|
||||
|
||||
type Props = {
|
||||
group: GroupWithDealsSchema;
|
||||
};
|
||||
|
||||
const DealsGroup: FC<Props> = ({ group }) => {
|
||||
const [groupName, setGroupName] = useState(group.name ?? "");
|
||||
const [debouncedGroupName] = useDebouncedValue(groupName, 600);
|
||||
const {
|
||||
groupsCrud,
|
||||
groupDealsSelection: {
|
||||
startSelectingWithExistingGroup,
|
||||
selectedGroupId,
|
||||
},
|
||||
} = useDealsContext();
|
||||
const { showContextMenu } = useContextMenu();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedGroupName === group.name) return;
|
||||
groupsCrud.onUpdate(group.id, { name: debouncedGroupName });
|
||||
}, [debouncedGroupName]);
|
||||
|
||||
const dealContextMenu = isMobile
|
||||
? []
|
||||
: [
|
||||
{
|
||||
key: "delete",
|
||||
onClick: () => groupsCrud.onDelete(group.id),
|
||||
title: "Удалить группу",
|
||||
icon: <IconTrash />,
|
||||
},
|
||||
{
|
||||
key: "startDealsSelecting",
|
||||
onClick: () => startSelectingWithExistingGroup(group),
|
||||
title: "Добавить/удалить сделки",
|
||||
icon: <IconCheckbox />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className={classNames(
|
||||
styles["group-container"],
|
||||
selectedGroupId === group.id && styles["selected-group"]
|
||||
)}
|
||||
gap={"xs"}
|
||||
bdrs={"lg"}
|
||||
p={"xs"}
|
||||
onContextMenu={showContextMenu(dealContextMenu)}>
|
||||
<Flex
|
||||
mx={"xs"}
|
||||
align={"center"}
|
||||
w={"100%"}>
|
||||
<TextInput
|
||||
value={groupName}
|
||||
onChange={e => setGroupName(e.target.value)}
|
||||
variant={"unstyled"}
|
||||
onKeyDown={e => e.stopPropagation()}
|
||||
flex={1}
|
||||
/>
|
||||
{isMobile && (
|
||||
<GroupMenu
|
||||
startDealsSelecting={() =>
|
||||
startSelectingWithExistingGroup(group)
|
||||
}
|
||||
onDelete={() => groupsCrud.onDelete(group.id)}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
{group.items.map(deal => (
|
||||
<DealCard
|
||||
deal={deal}
|
||||
isInGroup
|
||||
key={deal.id}
|
||||
/>
|
||||
))}
|
||||
{group.items.length > 0 && (
|
||||
<DealTags
|
||||
groupId={group.id}
|
||||
tags={group.items[0].tags}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealsGroup;
|
||||
@ -1,94 +0,0 @@
|
||||
import { FC, useCallback } from "react";
|
||||
import { IconMoodSad } from "@tabler/icons-react";
|
||||
import { Group, Pagination, Stack, Text } from "@mantine/core";
|
||||
import useDealsTableColumns from "@/app/deals/components/shared/DealsTable/useDealsTableColumns";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
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 { DealSchema } from "@/lib/client";
|
||||
|
||||
const DealsTable: FC = () => {
|
||||
const isMobile = useIsMobile();
|
||||
const { selectedProject, modulesSet } = useProjectsContext();
|
||||
const {
|
||||
deals,
|
||||
paginationInfo,
|
||||
page,
|
||||
setPage,
|
||||
sortingForm,
|
||||
dealsCrud,
|
||||
refetchDeals,
|
||||
} = useDealsContext();
|
||||
const { openDrawer } = useDrawersContext();
|
||||
|
||||
const onEditClick = useCallback(
|
||||
(deal: DealSchema) => {
|
||||
openDrawer({
|
||||
key: "dealEditorDrawer",
|
||||
props: {
|
||||
value: deal,
|
||||
onChange: deal => dealsCrud.onUpdate(deal.id, deal),
|
||||
onDelete: dealsCrud.onDelete,
|
||||
project: selectedProject,
|
||||
},
|
||||
onClose: refetchDeals,
|
||||
});
|
||||
},
|
||||
[openDrawer, dealsCrud]
|
||||
);
|
||||
|
||||
const columns = useDealsTableColumns({ onEditClick, modulesSet });
|
||||
|
||||
return (
|
||||
<Stack
|
||||
p={isMobile ? "xs" : ""}
|
||||
gap={"xs"}
|
||||
h={"100%"}>
|
||||
<BaseTable
|
||||
withTableBorder
|
||||
records={[...deals]}
|
||||
columns={columns}
|
||||
sortStatus={{
|
||||
columnAccessor: sortingForm.values.sortingField ?? "",
|
||||
direction: sortingForm.values.sortingDirection,
|
||||
}}
|
||||
onSortStatusChange={sorting => {
|
||||
sortingForm.setFieldValue(
|
||||
"sortingField",
|
||||
sorting.columnAccessor
|
||||
);
|
||||
sortingForm.setFieldValue(
|
||||
"sortingDirection",
|
||||
sorting.direction
|
||||
);
|
||||
}}
|
||||
emptyState={
|
||||
<Group
|
||||
align={"center"}
|
||||
gap={"xs"}>
|
||||
<Text>Нет сделок</Text>
|
||||
<IconMoodSad />
|
||||
</Group>
|
||||
}
|
||||
groups={undefined}
|
||||
style={{
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
{paginationInfo && paginationInfo.totalPages > 1 && (
|
||||
<Group justify={"flex-end"}>
|
||||
<Pagination
|
||||
withEdges
|
||||
total={paginationInfo.totalPages}
|
||||
value={page}
|
||||
onChange={setPage}
|
||||
/>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default DealsTable;
|
||||
@ -1,70 +0,0 @@
|
||||
import { useMemo } from "react";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import { DataTableColumn } from "mantine-datatable";
|
||||
import ActionIconWithTip from "@/components/ui/ActionIconWithTip/ActionIconWithTip";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import { DealSchema } from "@/lib/client";
|
||||
import { ModuleNames } from "@/modules/modules";
|
||||
import { utcDateTimeToLocalString } from "@/utils/datetime";
|
||||
|
||||
type Props = {
|
||||
onEditClick: (deal: DealSchema) => void;
|
||||
modulesSet: Set<ModuleNames>;
|
||||
};
|
||||
|
||||
const useDealsTableColumns = ({ onEditClick, modulesSet }: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
accessor: "actions",
|
||||
title: isMobile ? "" : "Действия",
|
||||
sortable: false,
|
||||
textAlign: "center",
|
||||
width: "0%",
|
||||
render: deal => (
|
||||
<ActionIconWithTip
|
||||
tipLabel={"Редактировать"}
|
||||
onClick={() => onEditClick(deal)}
|
||||
variant={isMobile ? "subtle" : "default"}>
|
||||
<IconEdit />
|
||||
</ActionIconWithTip>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: "id",
|
||||
title: isMobile ? "№" : "Номер",
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
accessor: "name",
|
||||
title: "Название",
|
||||
},
|
||||
{
|
||||
title: "Дата создания",
|
||||
accessor: "createdAt",
|
||||
render: deal => utcDateTimeToLocalString(deal.createdAt),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
title: "Клиент",
|
||||
accessor: "client.name",
|
||||
hidden: !modulesSet.has(ModuleNames.CLIENTS),
|
||||
},
|
||||
{
|
||||
title: "Общая стоимость",
|
||||
accessor: "totalPrice",
|
||||
render: deal =>
|
||||
deal.totalPrice
|
||||
? `${deal.totalPrice.toLocaleString("ru")}₽`
|
||||
: "0₽",
|
||||
hidden: !modulesSet.has(ModuleNames.FULFILLMENT_BASE),
|
||||
},
|
||||
] as DataTableColumn<DealSchema>[],
|
||||
[onEditClick]
|
||||
);
|
||||
};
|
||||
|
||||
export default useDealsTableColumns;
|
||||
@ -1,94 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { FC, ReactNode } from "react";
|
||||
import DealCard from "@/app/deals/components/shared/DealCard/DealCard";
|
||||
import DealsGroup from "@/app/deals/components/shared/DealsGroup/DealsGroup";
|
||||
import StatusColumnHeader from "@/app/deals/components/shared/StatusColumnHeader/StatusColumnHeader";
|
||||
import StatusColumnWrapper from "@/app/deals/components/shared/StatusColumnWrapper/StatusColumnWrapper";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import useDealsAndStatusesDnd from "@/app/deals/hooks/useDealsAndStatusesDnd";
|
||||
import FunnelDnd from "@/components/dnd/FunnelDnd/FunnelDnd";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import { DealSchema, StatusSchema } from "@/lib/client";
|
||||
import GroupWithDealsSchema from "@/types/GroupWithDealsSchema";
|
||||
import { sortByLexorank } from "@/utils/lexorank/sort";
|
||||
|
||||
const Funnel: FC = () => {
|
||||
const { selectedBoard } = useBoardsContext();
|
||||
const { dealsWithoutGroup, groupsWithDeals } = useDealsContext();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { sortedStatuses, handleDragOver, handleDragEnd, swiperRef } =
|
||||
useDealsAndStatusesDnd();
|
||||
|
||||
return (
|
||||
<FunnelDnd<StatusSchema, DealSchema, GroupWithDealsSchema>
|
||||
containers={sortedStatuses}
|
||||
itemsAndGroups={sortByLexorank([
|
||||
...dealsWithoutGroup,
|
||||
...groupsWithDeals,
|
||||
])}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
swiperRef={swiperRef}
|
||||
getItemsByContainer={(status: StatusSchema) =>
|
||||
sortByLexorank([
|
||||
...dealsWithoutGroup.filter(
|
||||
deal => deal.status.id === status.id
|
||||
),
|
||||
...groupsWithDeals.filter(
|
||||
group => group.items[0].status.id === status.id
|
||||
),
|
||||
])
|
||||
}
|
||||
renderContainer={(
|
||||
status: StatusSchema,
|
||||
funnelColumnComponent: ReactNode,
|
||||
renderDraggable,
|
||||
index
|
||||
) => (
|
||||
<StatusColumnWrapper
|
||||
status={status}
|
||||
renderHeader={renderDraggable}
|
||||
createFormEnabled={index === 0}>
|
||||
{funnelColumnComponent}
|
||||
</StatusColumnWrapper>
|
||||
)}
|
||||
renderContainerHeader={status => (
|
||||
<StatusColumnHeader
|
||||
status={status}
|
||||
isDragging={false}
|
||||
/>
|
||||
)}
|
||||
renderItem={(deal: DealSchema) => (
|
||||
<DealCard
|
||||
key={deal.id}
|
||||
deal={deal}
|
||||
/>
|
||||
)}
|
||||
renderGroup={(group: GroupWithDealsSchema) => (
|
||||
<DealsGroup
|
||||
key={`${group.id}group`}
|
||||
group={group}
|
||||
/>
|
||||
)}
|
||||
renderContainerOverlay={(status: StatusSchema, children) => (
|
||||
<StatusColumnWrapper
|
||||
status={status}
|
||||
renderHeader={() => (
|
||||
<StatusColumnHeader
|
||||
status={status}
|
||||
isDragging
|
||||
/>
|
||||
)}>
|
||||
{children}
|
||||
</StatusColumnWrapper>
|
||||
)}
|
||||
disabledColumns={isMobile}
|
||||
isCreatingContainerEnabled={!!selectedBoard}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Funnel;
|
||||
@ -1,9 +0,0 @@
|
||||
.shadow {
|
||||
@mixin light {
|
||||
box-shadow: var(--light-shadow);
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-7);
|
||||
box-shadow: var(--dark-shadow);
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Affix, Flex, Stack, Title, Transition } from "@mantine/core";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import InlineButton from "@/components/ui/InlineButton/InlineButton";
|
||||
import styles from "./GroupDealsSelectionAffix.module.css";
|
||||
|
||||
const GroupDealsSelectionAffix = () => {
|
||||
const {
|
||||
groupDealsSelection: {
|
||||
finishDealsSelecting,
|
||||
cancelDealsSelecting,
|
||||
isDealsSelecting,
|
||||
},
|
||||
} = useDealsContext();
|
||||
|
||||
return (
|
||||
<Affix position={{ bottom: 35, right: 35 }}>
|
||||
<Transition
|
||||
transition="slide-up"
|
||||
mounted={isDealsSelecting}>
|
||||
{transitionStyles => (
|
||||
<Stack
|
||||
bdrs={"xl"}
|
||||
bd={"1px solid var(--mantine-color-default-border"}
|
||||
className={styles.shadow}
|
||||
p={"md"}
|
||||
gap={"md"}
|
||||
style={transitionStyles}>
|
||||
<Title
|
||||
order={5}
|
||||
ta={"center"}>
|
||||
Выбор сделок для группы
|
||||
</Title>
|
||||
<Flex gap={"xs"}>
|
||||
<InlineButton onClick={cancelDealsSelecting}>
|
||||
Отмена
|
||||
</InlineButton>
|
||||
<InlineButton
|
||||
variant={"filled"}
|
||||
onClick={finishDealsSelecting}>
|
||||
Сохранить
|
||||
</InlineButton>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)}
|
||||
</Transition>
|
||||
</Affix>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupDealsSelectionAffix;
|
||||
@ -1,55 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Box } from "@mantine/core";
|
||||
import TopToolPanel from "@/app/deals/components/desktop/TopToolPanel/TopToolPanel";
|
||||
import {
|
||||
BoardView,
|
||||
ScheduleView,
|
||||
TableView,
|
||||
} from "@/app/deals/components/shared/views";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { DealsContextProvider } from "@/app/deals/contexts/DealsContext";
|
||||
import { useProjectsContext } from "@/app/deals/contexts/ProjectsContext";
|
||||
import useView from "@/app/deals/hooks/useView";
|
||||
import PageBlock from "@/components/layout/PageBlock/PageBlock";
|
||||
|
||||
const PageBody = () => {
|
||||
const { selectedBoard } = useBoardsContext();
|
||||
const { selectedProject } = useProjectsContext();
|
||||
|
||||
const { view, setView } = useView();
|
||||
|
||||
const getViewContent = () => {
|
||||
switch (view) {
|
||||
case "board":
|
||||
return <BoardView />;
|
||||
case "table":
|
||||
return <TableView />;
|
||||
default:
|
||||
return <ScheduleView />;
|
||||
}
|
||||
};
|
||||
|
||||
const getContextProps = () => {
|
||||
if (view === "table") {
|
||||
return { withPagination: true, projectId: selectedProject?.id };
|
||||
}
|
||||
return { boardId: selectedBoard?.id };
|
||||
};
|
||||
|
||||
return (
|
||||
<DealsContextProvider {...getContextProps()}>
|
||||
<TopToolPanel
|
||||
view={view}
|
||||
setView={setView}
|
||||
/>
|
||||
<PageBlock
|
||||
fullScreenMobile
|
||||
style={{ flex: 1 }}>
|
||||
<Box h={"100%"}>{getViewContent()}</Box>
|
||||
</PageBlock>
|
||||
</DealsContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageBody;
|
||||
@ -1,76 +0,0 @@
|
||||
import React, { FC } from "react";
|
||||
import { Group, Text } from "@mantine/core";
|
||||
import StatusMenu from "@/app/deals/components/shared/StatusMenu/StatusMenu";
|
||||
import { useBoardsContext } from "@/app/deals/contexts/BoardsContext";
|
||||
import { useDealsContext } from "@/app/deals/contexts/DealsContext";
|
||||
import { useStatusesContext } from "@/app/deals/contexts/StatusesContext";
|
||||
import InPlaceInput from "@/components/ui/InPlaceInput/InPlaceInput";
|
||||
import { StatusSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
status: StatusSchema;
|
||||
isDragging: boolean;
|
||||
};
|
||||
|
||||
const StatusColumnHeader: FC<Props> = ({ status, isDragging }) => {
|
||||
const { statusesCrud, refetchStatuses } = useStatusesContext();
|
||||
const { selectedBoard } = useBoardsContext();
|
||||
const { groupDealsSelection } = useDealsContext();
|
||||
|
||||
const handleSave = (value: string) => {
|
||||
const newValue = value.trim();
|
||||
if (newValue && newValue !== status.name) {
|
||||
statusesCrud.onUpdate(status.id, { name: newValue });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Group
|
||||
justify={"space-between"}
|
||||
p={"sm"}
|
||||
wrap={"nowrap"}
|
||||
mb={"xs"}
|
||||
style={{
|
||||
borderBottom: `solid ${status.color} 3px`,
|
||||
}}>
|
||||
<InPlaceInput
|
||||
value={status.name}
|
||||
onChange={value => handleSave(value)}
|
||||
inputStyles={{
|
||||
input: {
|
||||
height: 25,
|
||||
minHeight: 25,
|
||||
},
|
||||
}}
|
||||
getChildren={startEditing => (
|
||||
<>
|
||||
<Text
|
||||
style={{
|
||||
cursor: "grab",
|
||||
userSelect: "none",
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}}>
|
||||
{status.name}
|
||||
</Text>
|
||||
<StatusMenu
|
||||
board={selectedBoard}
|
||||
status={status}
|
||||
handleEdit={startEditing}
|
||||
onStatusColorChange={color =>
|
||||
statusesCrud.onUpdate(status.id, { color })
|
||||
}
|
||||
refetchStatuses={refetchStatuses}
|
||||
onDeleteStatus={statusesCrud.onDelete}
|
||||
startDealsSelecting={
|
||||
groupDealsSelection.startSelecting
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
modalTitle={"Редактирование статуса"}
|
||||
/>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusColumnHeader;
|
||||
@ -1,23 +0,0 @@
|
||||
|
||||
.container {
|
||||
height: calc(100vh - 215px);
|
||||
@media (max-width: 48em) {
|
||||
width: 80vw;
|
||||
}
|
||||
}
|
||||
|
||||
.inner-container {
|
||||
border-radius: var(--mantine-spacing-lg);
|
||||
gap: 0;
|
||||
|
||||
@media (max-width: 48em) {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
@mixin light {
|
||||
background-color: var(--color-light-aqua);
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-6);
|
||||
}
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { Box, ScrollArea, Stack } from "@mantine/core";
|
||||
import CreateCardButton from "@/app/deals/components/shared/CreateDealButton/CreateDealButton";
|
||||
import { StatusSchema } from "@/lib/client";
|
||||
import styles from "./StatusColumnWrapper.module.css";
|
||||
|
||||
type Props = {
|
||||
status: StatusSchema;
|
||||
renderHeader: () => ReactNode;
|
||||
children: ReactNode;
|
||||
createFormEnabled?: boolean;
|
||||
};
|
||||
|
||||
const StatusColumnWrapper = ({
|
||||
renderHeader,
|
||||
children,
|
||||
createFormEnabled = false,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Box className={styles.container}>
|
||||
<Stack
|
||||
px={"xs"}
|
||||
pb={"xs"}
|
||||
className={styles["inner-container"]}>
|
||||
{renderHeader()}
|
||||
<ScrollArea
|
||||
offsetScrollbars={"present"}
|
||||
scrollbarSize={10}
|
||||
type={"always"}
|
||||
scrollbars={"y"}>
|
||||
<Stack
|
||||
gap={"xs"}
|
||||
mah={"calc(100vh - 285px)"}>
|
||||
{createFormEnabled && <CreateCardButton />}
|
||||
{children}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusColumnWrapper;
|
||||
@ -1,114 +0,0 @@
|
||||
import React, { FC } from "react";
|
||||
import {
|
||||
IconCheckbox,
|
||||
IconDotsVertical,
|
||||
IconEdit,
|
||||
IconExchange,
|
||||
IconPalette,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import { Box, Menu } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import statusColors from "@/app/deals/utils/statusColors";
|
||||
import DropdownMenuItem from "@/components/ui/DropdownMenuItem/DropdownMenuItem";
|
||||
import ThemeIcon from "@/components/ui/ThemeIcon/ThemeIcon";
|
||||
import { useDrawersContext } from "@/drawers/DrawersContext";
|
||||
import useIsMobile from "@/hooks/utils/useIsMobile";
|
||||
import { BoardSchema, StatusSchema } from "@/lib/client";
|
||||
|
||||
type Props = {
|
||||
status: StatusSchema;
|
||||
handleEdit: () => void;
|
||||
onStatusColorChange: (color: string) => void;
|
||||
board: BoardSchema | null;
|
||||
onDeleteStatus: (status: StatusSchema) => void;
|
||||
startDealsSelecting?: () => void;
|
||||
refetchStatuses?: () => void;
|
||||
withChangeOrderButton?: boolean;
|
||||
};
|
||||
|
||||
const StatusMenu: FC<Props> = ({
|
||||
status,
|
||||
handleEdit,
|
||||
onStatusColorChange,
|
||||
board,
|
||||
onDeleteStatus,
|
||||
startDealsSelecting,
|
||||
refetchStatuses,
|
||||
withChangeOrderButton = true,
|
||||
}) => {
|
||||
const isMobile = useIsMobile();
|
||||
const { openDrawer } = useDrawersContext();
|
||||
|
||||
const openStatusesMobileEditor = () => {
|
||||
if (!board) return;
|
||||
openDrawer({
|
||||
key: "statusesMobileEditorDrawer",
|
||||
props: {
|
||||
board,
|
||||
},
|
||||
onClose: refetchStatuses,
|
||||
});
|
||||
};
|
||||
|
||||
const openStatusColorPicker = () => {
|
||||
if (!board) return;
|
||||
modals.openContextModal({
|
||||
modal: "statusColorPickerModal",
|
||||
title: "Изменение цвета статуса",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
color: status.color,
|
||||
onChange: onStatusColorChange,
|
||||
switches: statusColors,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Box
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<ThemeIcon size={"sm"}>
|
||||
<IconDotsVertical />
|
||||
</ThemeIcon>
|
||||
</Box>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<DropdownMenuItem
|
||||
onClick={handleEdit}
|
||||
icon={<IconEdit />}
|
||||
label={"Переименовать"}
|
||||
/>
|
||||
<DropdownMenuItem
|
||||
onClick={openStatusColorPicker}
|
||||
icon={<IconPalette />}
|
||||
label={"Изменить цвет"}
|
||||
/>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDeleteStatus(status)}
|
||||
icon={<IconTrash />}
|
||||
label={"Удалить"}
|
||||
/>
|
||||
{isMobile && withChangeOrderButton && (
|
||||
<DropdownMenuItem
|
||||
onClick={openStatusesMobileEditor}
|
||||
icon={<IconExchange />}
|
||||
label={"Изменить порядок"}
|
||||
/>
|
||||
)}
|
||||
{isMobile && startDealsSelecting && (
|
||||
<DropdownMenuItem
|
||||
onClick={startDealsSelecting}
|
||||
icon={<IconCheckbox />}
|
||||
label={"Создать группу сделок"}
|
||||
/>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusMenu;
|
||||
@ -1,13 +0,0 @@
|
||||
import { Space } from "@mantine/core";
|
||||
import MainBlockHeader from "@/app/deals/components/mobile/MainBlockHeader/MainBlockHeader";
|
||||
import Funnel from "@/app/deals/components/shared/Funnel/Funnel";
|
||||
import GroupDealsSelectionAffix from "@/app/deals/components/shared/GroupDealsSelectionAffix/GroupDealsSelectionAffix";
|
||||
|
||||
export const BoardView = () => (
|
||||
<>
|
||||
<MainBlockHeader />
|
||||
<Space h="md" />
|
||||
<Funnel />
|
||||
<GroupDealsSelectionAffix />
|
||||
</>
|
||||
);
|
||||
@ -1,3 +0,0 @@
|
||||
export const ScheduleView = () => {
|
||||
return <>-</>;
|
||||
};
|
||||
@ -1,3 +0,0 @@
|
||||
import DealsTable from "../DealsTable/DealsTable";
|
||||
|
||||
export const TableView = () => <DealsTable />;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user