feat: prettier
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import {forwardRef, useContext, useRef} from "react";
|
||||
import {getRouterContext, Outlet} from "@tanstack/react-router";
|
||||
import {motion, useIsPresent} from "framer-motion";
|
||||
import {cloneDeep} from "lodash";
|
||||
import { forwardRef, useContext, useRef } from "react";
|
||||
import { getRouterContext, Outlet } from "@tanstack/react-router";
|
||||
import { motion, useIsPresent } from "framer-motion";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
const AnimatedOutlet = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
const RouterContext = getRouterContext();
|
||||
@@ -17,26 +17,24 @@ const AnimatedOutlet = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div ref={ref}
|
||||
initial={{x: "-100%"}}
|
||||
animate={{
|
||||
x: 0,
|
||||
transform: "",
|
||||
transitionEnd: {
|
||||
transform: "none"
|
||||
}
|
||||
}}
|
||||
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
ease: "circInOut",
|
||||
}}
|
||||
|
||||
>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ x: "-100%" }}
|
||||
animate={{
|
||||
x: 0,
|
||||
transform: "",
|
||||
transitionEnd: {
|
||||
transform: "none",
|
||||
},
|
||||
}}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
ease: "circInOut",
|
||||
}}>
|
||||
<RouterContext.Provider value={renderedContext.current}>
|
||||
<Outlet/>
|
||||
<Outlet />
|
||||
</RouterContext.Provider>
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
export default AnimatedOutlet
|
||||
export default AnimatedOutlet;
|
||||
|
||||
@@ -4,19 +4,18 @@ import {
|
||||
MRT_RowData,
|
||||
MRT_TableInstance,
|
||||
MRT_TableOptions,
|
||||
useMantineReactTable
|
||||
useMantineReactTable,
|
||||
} from "mantine-react-table";
|
||||
import {MRT_Localization_RU} from "mantine-react-table/locales/ru/index.cjs";
|
||||
import {forwardRef, useEffect, useImperativeHandle} from 'react';
|
||||
import { MRT_Localization_RU } from "mantine-react-table/locales/ru/index.cjs";
|
||||
import { forwardRef, useEffect, useImperativeHandle } from "react";
|
||||
|
||||
type Props<T extends Record<string, unknown>, K extends keyof T> = {
|
||||
data: T[],
|
||||
onSelectionChange?: (selectedRows: T[]) => void,
|
||||
columns: MRT_ColumnDef<T, K>[],
|
||||
restProps?: MRT_TableOptions<T>,
|
||||
striped?: boolean
|
||||
}
|
||||
|
||||
data: T[];
|
||||
onSelectionChange?: (selectedRows: T[]) => void;
|
||||
columns: MRT_ColumnDef<T, K>[];
|
||||
restProps?: MRT_TableOptions<T>;
|
||||
striped?: boolean;
|
||||
};
|
||||
|
||||
// Экспортируем тип рефа, чтобы он мог быть использован в других компонентах
|
||||
export type BaseTableRef<T extends MRT_RowData> = {
|
||||
@@ -25,33 +24,42 @@ export type BaseTableRef<T extends MRT_RowData> = {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
export const BaseTable = forwardRef<BaseTableRef<never>, Props<never>>((props, ref) => {
|
||||
const {data, columns, restProps, striped = true, onSelectionChange} = props;
|
||||
export const BaseTable = forwardRef<BaseTableRef<never>, Props<never>>(
|
||||
(props, ref) => {
|
||||
const {
|
||||
data,
|
||||
columns,
|
||||
restProps,
|
||||
striped = true,
|
||||
onSelectionChange,
|
||||
} = props;
|
||||
|
||||
const table = useMantineReactTable({
|
||||
localization: MRT_Localization_RU,
|
||||
enablePagination: false,
|
||||
data,
|
||||
columns,
|
||||
mantineTableProps: {
|
||||
striped: striped
|
||||
},
|
||||
enableTopToolbar: false,
|
||||
enableBottomToolbar: false,
|
||||
enableRowSelection: onSelectionChange !== undefined,
|
||||
...restProps,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!onSelectionChange) return;
|
||||
onSelectionChange(table.getSelectedRowModel().rows.map(row => row.original))
|
||||
}, [table.getState().rowSelection]);
|
||||
const table = useMantineReactTable({
|
||||
localization: MRT_Localization_RU,
|
||||
enablePagination: false,
|
||||
data,
|
||||
columns,
|
||||
mantineTableProps: {
|
||||
striped: striped,
|
||||
},
|
||||
enableTopToolbar: false,
|
||||
enableBottomToolbar: false,
|
||||
enableRowSelection: onSelectionChange !== undefined,
|
||||
...restProps,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!onSelectionChange) return;
|
||||
onSelectionChange(
|
||||
table.getSelectedRowModel().rows.map(row => row.original)
|
||||
);
|
||||
}, [table.getState().rowSelection]);
|
||||
|
||||
// Используем useImperativeHandle для определения, что будет доступно через ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
// @ts-ignore
|
||||
getTable: () => table
|
||||
}));
|
||||
// Используем useImperativeHandle для определения, что будет доступно через ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
// @ts-ignore
|
||||
getTable: () => table,
|
||||
}));
|
||||
|
||||
|
||||
return <MantineReactTable table={table}/>;
|
||||
});
|
||||
return <MantineReactTable table={table} />;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import {Button, rem, Tooltip} from '@mantine/core';
|
||||
import {IconCheck, IconCopy} from '@tabler/icons-react';
|
||||
import {FC} from "react";
|
||||
import {useClipboard} from "@mantine/hooks";
|
||||
import { Button, rem, Tooltip } from "@mantine/core";
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||
import { FC } from "react";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
|
||||
type Props = {
|
||||
children: string;
|
||||
value: string
|
||||
value: string;
|
||||
onCopiedLabel: string;
|
||||
}
|
||||
};
|
||||
|
||||
export const ButtonCopy: FC<Props> = ({children, onCopiedLabel, value}) => {
|
||||
export const ButtonCopy: FC<Props> = ({ children, onCopiedLabel, value }) => {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
return (
|
||||
@@ -18,20 +18,19 @@ export const ButtonCopy: FC<Props> = ({children, onCopiedLabel, value}) => {
|
||||
offset={5}
|
||||
position="bottom"
|
||||
radius="xl"
|
||||
transitionProps={{duration: 100, transition: 'slide-down'}}
|
||||
opened={clipboard.copied}
|
||||
>
|
||||
transitionProps={{ duration: 100, transition: "slide-down" }}
|
||||
opened={clipboard.copied}>
|
||||
<Button
|
||||
variant="light"
|
||||
rightSection={
|
||||
clipboard.copied ? (
|
||||
<IconCheck
|
||||
style={{width: rem(20), height: rem(20)}}
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
) : (
|
||||
<IconCopy
|
||||
style={{width: rem(20), height: rem(20)}}
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
)
|
||||
@@ -42,13 +41,12 @@ export const ButtonCopy: FC<Props> = ({children, onCopiedLabel, value}) => {
|
||||
root: {
|
||||
paddingRight: rem(14),
|
||||
},
|
||||
section: {marginLeft: rem(22)},
|
||||
section: { marginLeft: rem(22) },
|
||||
}}
|
||||
onClick={() => clipboard.copy(value)}
|
||||
>
|
||||
onClick={() => clipboard.copy(value)}>
|
||||
{children}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
export default ButtonCopy;
|
||||
};
|
||||
export default ButtonCopy;
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
import {Button, rem, Tooltip} from '@mantine/core';
|
||||
import {IconCheck, IconCopy} from '@tabler/icons-react';
|
||||
import {FC} from "react";
|
||||
import { Button, rem, Tooltip } from "@mantine/core";
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react";
|
||||
import { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
children: string;
|
||||
onCopyClick: () => void;
|
||||
onCopiedLabel: string;
|
||||
copied: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export const ButtonCopyControlled: FC<Props> = ({children, onCopiedLabel, onCopyClick, copied}) => {
|
||||
export const ButtonCopyControlled: FC<Props> = ({
|
||||
children,
|
||||
onCopiedLabel,
|
||||
onCopyClick,
|
||||
copied,
|
||||
}) => {
|
||||
return (
|
||||
<Tooltip
|
||||
label={onCopiedLabel}
|
||||
offset={5}
|
||||
position="bottom"
|
||||
radius="xl"
|
||||
transitionProps={{duration: 100, transition: 'slide-down'}}
|
||||
opened={copied}
|
||||
>
|
||||
transitionProps={{ duration: 100, transition: "slide-down" }}
|
||||
opened={copied}>
|
||||
<Button
|
||||
variant="light"
|
||||
rightSection={
|
||||
copied ? (
|
||||
<IconCheck
|
||||
style={{width: rem(20), height: rem(20)}}
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
) : (
|
||||
<IconCopy
|
||||
style={{width: rem(20), height: rem(20)}}
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
)
|
||||
@@ -40,12 +44,11 @@ export const ButtonCopyControlled: FC<Props> = ({children, onCopiedLabel, onCopy
|
||||
root: {
|
||||
paddingRight: rem(14),
|
||||
},
|
||||
section: {marginLeft: rem(22)},
|
||||
section: { marginLeft: rem(22) },
|
||||
}}
|
||||
onClick={onCopyClick}
|
||||
>
|
||||
onClick={onCopyClick}>
|
||||
{children}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import {NumberInput, NumberInputProps} from "@mantine/core";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useDebouncedValue} from "@mantine/hooks";
|
||||
import {isNumber, omit} from "lodash";
|
||||
|
||||
import { NumberInput, NumberInputProps } from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { isNumber, omit } from "lodash";
|
||||
|
||||
type Props = NumberInputProps;
|
||||
|
||||
const DebouncedNumberInput = (props: Props) => {
|
||||
const [value, setValue] = useState<number | string>(props.defaultValue || props.value || '');
|
||||
const [value, setValue] = useState<number | string>(
|
||||
props.defaultValue || props.value || ""
|
||||
);
|
||||
const [debounced] = useDebouncedValue(value, 200);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.onChange) return;
|
||||
props.onChange(debounced);
|
||||
}, [debounced])
|
||||
}, [debounced]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNumber(props.value)) return;
|
||||
setValue(props.value);
|
||||
}, [props.value])
|
||||
}, [props.value]);
|
||||
|
||||
const restProps = omit(props, ["onChange", "value"])
|
||||
const restProps = omit(props, ["onChange", "value"]);
|
||||
return (
|
||||
<NumberInput
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
{...restProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default DebouncedNumberInput;
|
||||
export default DebouncedNumberInput;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/*background-color: green;*/
|
||||
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -34,7 +33,6 @@
|
||||
}
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-5);
|
||||
|
||||
}
|
||||
border-radius: var(--item-border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {FC} from "react";
|
||||
import styles from './Board.module.css';
|
||||
import {Divider, Text, Title} from '@mantine/core';
|
||||
import {Draggable, Droppable} from "@hello-pangea/dnd";
|
||||
import { FC } from "react";
|
||||
import styles from "./Board.module.css";
|
||||
import { Divider, Text, Title } from "@mantine/core";
|
||||
import { Draggable, Droppable } from "@hello-pangea/dnd";
|
||||
import CreateDealButton from "../CreateDealButton/CreateDealButton.tsx";
|
||||
import {DealSummary} from "../../../client";
|
||||
import { DealSummary } from "../../../client";
|
||||
import DealSummaryCard from "../DealSummaryCard/DealSummaryCard.tsx";
|
||||
import classNames from "classnames";
|
||||
import {getPluralForm} from "../../../shared/lib/utils.ts";
|
||||
import {sum} from "lodash";
|
||||
import { getPluralForm } from "../../../shared/lib/utils.ts";
|
||||
import { sum } from "lodash";
|
||||
|
||||
type Props = {
|
||||
droppableId: string;
|
||||
@@ -15,66 +15,74 @@ type Props = {
|
||||
withCreateButton?: boolean;
|
||||
summaries: DealSummary[];
|
||||
color: string;
|
||||
}
|
||||
};
|
||||
|
||||
export const Board: FC<Props> = ({droppableId, title, summaries, color, withCreateButton = false}) => {
|
||||
export const Board: FC<Props> = ({
|
||||
droppableId,
|
||||
title,
|
||||
summaries,
|
||||
color,
|
||||
withCreateButton = false,
|
||||
}) => {
|
||||
const getDealsText = () => {
|
||||
const pluralForm = getPluralForm(summaries.length, 'сделка', 'сделки', 'сделок');
|
||||
const pluralForm = getPluralForm(
|
||||
summaries.length,
|
||||
"сделка",
|
||||
"сделки",
|
||||
"сделок"
|
||||
);
|
||||
return `${summaries.length} ${pluralForm}: ${sum(summaries.map(summary => summary.totalPrice)).toLocaleString("ru-RU")}₽`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles["container"]}>
|
||||
<div className={styles["header"]}>
|
||||
<Title size={"h4"}>{title}</Title>
|
||||
<Text>{getDealsText()}</Text>
|
||||
<Divider size={"xl"} my={10} color={color}/>
|
||||
<Divider
|
||||
size={"xl"}
|
||||
my={10}
|
||||
color={color}
|
||||
/>
|
||||
</div>
|
||||
<Droppable
|
||||
droppableId={droppableId}
|
||||
>
|
||||
<Droppable droppableId={droppableId}>
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef}
|
||||
className={classNames(
|
||||
styles["items-list"],
|
||||
(snapshot.isDraggingOver && !snapshot.draggingFromThisWith)
|
||||
&& styles["items-list-drag-over"]
|
||||
)}
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{withCreateButton &&
|
||||
<CreateDealButton
|
||||
|
||||
onClick={() => {
|
||||
}}
|
||||
/>}
|
||||
{summaries.map(summary =>
|
||||
(
|
||||
<Draggable
|
||||
draggableId={summary.id.toString()}
|
||||
index={summary.rank}
|
||||
key={summary.id}
|
||||
>
|
||||
{(provided) => (
|
||||
<div {...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
|
||||
>
|
||||
<DealSummaryCard dealSummary={summary}/>
|
||||
</div>
|
||||
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
className={classNames(
|
||||
styles["items-list"],
|
||||
snapshot.isDraggingOver &&
|
||||
!snapshot.draggingFromThisWith &&
|
||||
styles["items-list-drag-over"]
|
||||
)}
|
||||
{...provided.droppableProps}>
|
||||
{withCreateButton && (
|
||||
<CreateDealButton onClick={() => {}} />
|
||||
)}
|
||||
{summaries.map(summary => (
|
||||
<Draggable
|
||||
draggableId={summary.id.toString()}
|
||||
index={summary.rank}
|
||||
key={summary.id}>
|
||||
{provided => (
|
||||
<div
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}>
|
||||
<DealSummaryCard
|
||||
dealSummary={summary}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Board;
|
||||
export default Board;
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.container:hover {
|
||||
background-color: light-dark(var(--mantine-color-default-hover), var(--mantine-color-gray-filled-hover));
|
||||
|
||||
}
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-default-hover),
|
||||
var(--mantine-color-gray-filled-hover)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,60 +1,61 @@
|
||||
import {FC, useState} from "react";
|
||||
import { FC, useState } from "react";
|
||||
|
||||
import styles from './CreateDealButton.module.css';
|
||||
import {Text, Transition} from '@mantine/core';
|
||||
import styles from "./CreateDealButton.module.css";
|
||||
import { Text, Transition } from "@mantine/core";
|
||||
import CreateDealFrom from "../CreateDealForm/CreateDealFrom.tsx";
|
||||
import {DealService} from "../../../client";
|
||||
import {useQueryClient} from "@tanstack/react-query";
|
||||
import {dateWithoutTimezone} from "../../../shared/lib/date.ts";
|
||||
import { DealService } from "../../../client";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { dateWithoutTimezone } from "../../../shared/lib/date.ts";
|
||||
|
||||
type Props = {
|
||||
onClick: () => void;
|
||||
}
|
||||
};
|
||||
const CreateDealButton: FC<Props> = () => {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isTransitionEnded, setIsTransitionEnded] = useState(true);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return (
|
||||
<div className={styles['container']}
|
||||
onClick={() => {
|
||||
if (isCreating) return;
|
||||
setIsCreating(prevState => !prevState)
|
||||
setIsTransitionEnded(false);
|
||||
}}
|
||||
>
|
||||
{(!isCreating && isTransitionEnded) &&
|
||||
<div
|
||||
className={styles["container"]}
|
||||
onClick={() => {
|
||||
if (isCreating) return;
|
||||
setIsCreating(prevState => !prevState);
|
||||
setIsTransitionEnded(false);
|
||||
}}>
|
||||
{!isCreating && isTransitionEnded && (
|
||||
<Text>Быстрое добавление</Text>
|
||||
}
|
||||
)}
|
||||
<Transition
|
||||
mounted={isCreating}
|
||||
transition={"scale-y"}
|
||||
onExited={() => setIsTransitionEnded(true)}
|
||||
|
||||
>
|
||||
{(styles) => (
|
||||
onExited={() => setIsTransitionEnded(true)}>
|
||||
{styles => (
|
||||
<div style={styles}>
|
||||
<CreateDealFrom
|
||||
onCancel={() => {
|
||||
setIsCreating(false)
|
||||
setIsCreating(false);
|
||||
}}
|
||||
onSubmit={(quickDeal) => {
|
||||
onSubmit={quickDeal => {
|
||||
DealService.quickCreateDealQuickCreatePost({
|
||||
requestBody: {
|
||||
...quickDeal,
|
||||
acceptanceDate: dateWithoutTimezone(quickDeal.acceptanceDate)
|
||||
}
|
||||
acceptanceDate: dateWithoutTimezone(
|
||||
quickDeal.acceptanceDate
|
||||
),
|
||||
},
|
||||
}).then(async () => {
|
||||
await queryClient.invalidateQueries({queryKey: ['getDealSummaries']});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["getDealSummaries"],
|
||||
});
|
||||
setIsCreating(false);
|
||||
})
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Transition>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default CreateDealButton;
|
||||
);
|
||||
};
|
||||
export default CreateDealButton;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputs * {
|
||||
@@ -12,4 +12,3 @@
|
||||
display: flex;
|
||||
gap: rem(10);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,15 +5,14 @@ import { useForm } from "@mantine/form";
|
||||
import styles from "./CreateDealForm.module.css";
|
||||
import ClientAutocomplete from "../../Selects/ClientAutocomplete/ClientAutocomplete.tsx";
|
||||
import { DateTimePicker } from "@mantine/dates";
|
||||
import ShippingWarehouseAutocomplete
|
||||
from "../../Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
|
||||
import ShippingWarehouseAutocomplete from "../../Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
|
||||
import BaseMarketplaceSelect from "../../Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
|
||||
import ServicePriceCategorySelect from "../../Selects/ServicePriceCategorySelect/ServicePriceCategorySelect.tsx";
|
||||
|
||||
type Props = {
|
||||
onSubmit: (quickDeal: QuickDeal) => void
|
||||
onSubmit: (quickDeal: QuickDeal) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
};
|
||||
const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
|
||||
const form = useForm<QuickDeal>({
|
||||
initialValues: {
|
||||
@@ -33,15 +32,15 @@ const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
|
||||
return (
|
||||
<form
|
||||
style={{ width: "100%" }}
|
||||
onSubmit={form.onSubmit((values) => onSubmit(values))}
|
||||
>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
gap: rem(10),
|
||||
padding: rem(10),
|
||||
}}>
|
||||
onSubmit={form.onSubmit(values => onSubmit(values))}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
gap: rem(10),
|
||||
padding: rem(10),
|
||||
}}>
|
||||
<div className={styles["inputs"]}>
|
||||
<TextInput
|
||||
placeholder={"Название сделки"}
|
||||
@@ -63,7 +62,6 @@ const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
|
||||
{...form.getInputProps("shippingWarehouse")}
|
||||
placeholder={"Склад отгрузки"}
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className={styles["inputs"]}>
|
||||
<ServicePriceCategorySelect
|
||||
@@ -89,20 +87,17 @@ const CreateDealFrom: FC<Props> = ({ onSubmit, onCancel }) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div className={styles["buttons"]}>
|
||||
<Button
|
||||
type={"submit"}
|
||||
>Добавить</Button>
|
||||
<Button type={"submit"}>Добавить</Button>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => onCancel()}
|
||||
>Отменить</Button>
|
||||
onClick={() => onCancel()}>
|
||||
Отменить
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateDealFrom;
|
||||
export default CreateDealFrom;
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.flex-row-left {
|
||||
@@ -27,13 +26,11 @@
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-row-right {
|
||||
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,109 +1,147 @@
|
||||
import {FC} from "react";
|
||||
import {DealService, DealSummary} from "../../../client";
|
||||
import styles from './DealSummaryCard.module.css';
|
||||
import { FC } from "react";
|
||||
import { DealService, DealSummary } from "../../../client";
|
||||
import styles from "./DealSummaryCard.module.css";
|
||||
|
||||
import {ActionIcon, Badge, CopyButton, Flex, Image, Popover, rem, Text} from '@mantine/core';
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
CopyButton,
|
||||
Flex,
|
||||
Image,
|
||||
Popover,
|
||||
rem,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import classNames from "classnames";
|
||||
import {useDealPageContext} from "../../../pages/LeadsPage/contexts/DealPageContext.tsx";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faCheck} from "@fortawesome/free-solid-svg-icons";
|
||||
import { useDealPageContext } from "../../../pages/LeadsPage/contexts/DealPageContext.tsx";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
type Props = {
|
||||
dealSummary: DealSummary
|
||||
}
|
||||
dealSummary: DealSummary;
|
||||
};
|
||||
|
||||
const DealSummaryCard: FC<Props> = ({dealSummary}) => {
|
||||
const {setSelectedDeal} = useDealPageContext();
|
||||
const DealSummaryCard: FC<Props> = ({ dealSummary }) => {
|
||||
const { setSelectedDeal } = useDealPageContext();
|
||||
const onDealSummaryClick = () => {
|
||||
DealService.getDealById({dealId: dealSummary.id})
|
||||
.then((deal) => {
|
||||
setSelectedDeal(deal);
|
||||
})
|
||||
}
|
||||
DealService.getDealById({ dealId: dealSummary.id }).then(deal => {
|
||||
setSelectedDeal(deal);
|
||||
});
|
||||
};
|
||||
const getDeadlineTextColor = (deadline: string) => {
|
||||
// generate three colors, yellow for 1 day, red for 0 days, green for more than 1 day
|
||||
const deadlineDate = new Date(deadline);
|
||||
const currentDate = new Date();
|
||||
const diff = deadlineDate.getTime() - currentDate.getTime();
|
||||
const diffDays = Math.ceil(diff / (1000 * 3600 * 24));
|
||||
if (diffDays < 0)
|
||||
return 'red.8'; // for past deadlines
|
||||
if (diffDays < 0) return "red.8"; // for past deadlines
|
||||
if (diffDays === 1) {
|
||||
return 'yellow.8';
|
||||
return "yellow.8";
|
||||
}
|
||||
if (diffDays === 0) {
|
||||
return 'orange.8';
|
||||
return "orange.8";
|
||||
}
|
||||
return 'green.8';
|
||||
}
|
||||
return "green.8";
|
||||
};
|
||||
return (
|
||||
<div onClick={() => onDealSummaryClick()} className={styles['container']}>
|
||||
<div className={classNames(styles['flex-row'], styles["flex-row-left"])}>
|
||||
|
||||
<div className={styles['flex-item']}>
|
||||
<Text size={"sm"} c={"gray.6"}>
|
||||
<div
|
||||
onClick={() => onDealSummaryClick()}
|
||||
className={styles["container"]}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles["flex-row"],
|
||||
styles["flex-row-left"]
|
||||
)}>
|
||||
<div className={styles["flex-item"]}>
|
||||
<Text
|
||||
size={"sm"}
|
||||
c={"gray.6"}>
|
||||
Клиент: {dealSummary.clientName}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles['flex-item']}>
|
||||
<Text size={"md"} c={"blue.5"}>{dealSummary.name}</Text>
|
||||
{dealSummary.shipmentWarehouseName &&
|
||||
<Text size={"sm"} c={"gray.6"}>{dealSummary.shipmentWarehouseName}</Text>
|
||||
}
|
||||
<div className={styles["flex-item"]}>
|
||||
<Text
|
||||
size={"md"}
|
||||
c={"blue.5"}>
|
||||
{dealSummary.name}
|
||||
</Text>
|
||||
{dealSummary.shipmentWarehouseName && (
|
||||
<Text
|
||||
size={"sm"}
|
||||
c={"gray.6"}>
|
||||
{dealSummary.shipmentWarehouseName}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles['flex-item']}>
|
||||
<Text size={"sm"} c={"gray.6"}>
|
||||
{dealSummary.totalPrice.toLocaleString('ru-RU')} руб, {dealSummary.totalProducts.toLocaleString("ru-RU")} тов.
|
||||
<div className={styles["flex-item"]}>
|
||||
<Text
|
||||
size={"sm"}
|
||||
c={"gray.6"}>
|
||||
{dealSummary.totalPrice.toLocaleString("ru-RU")} руб,{" "}
|
||||
{dealSummary.totalProducts.toLocaleString("ru-RU")} тов.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
className={classNames(styles['flex-row'], styles['flex-row-right'])}>
|
||||
<div className={styles['flex-item']}>
|
||||
className={classNames(
|
||||
styles["flex-row"],
|
||||
styles["flex-row-right"]
|
||||
)}>
|
||||
<div className={styles["flex-item"]}>
|
||||
<ActionIcon variant={"transparent"}>
|
||||
<Image src={dealSummary.baseMarketplace?.iconUrl || ""}/>
|
||||
<Image
|
||||
src={dealSummary.baseMarketplace?.iconUrl || ""}
|
||||
/>
|
||||
</ActionIcon>
|
||||
</div>
|
||||
<div className={styles['flex-item']}>
|
||||
<Text size={"sm"} c={getDeadlineTextColor(dealSummary.deadline)}>
|
||||
{new Date(dealSummary.deadline).toLocaleString('ru-RU').slice(0, -3)}
|
||||
<div className={styles["flex-item"]}>
|
||||
<Text
|
||||
size={"sm"}
|
||||
c={getDeadlineTextColor(dealSummary.deadline)}>
|
||||
{new Date(dealSummary.deadline)
|
||||
.toLocaleString("ru-RU")
|
||||
.slice(0, -3)}
|
||||
</Text>
|
||||
</div>
|
||||
<CopyButton value={"https://google.com"}>
|
||||
{({copy, copied}) => (
|
||||
<Popover opened={copied} withArrow>
|
||||
{({ copy, copied }) => (
|
||||
<Popover
|
||||
opened={copied}
|
||||
withArrow>
|
||||
<Popover.Target>
|
||||
<div
|
||||
|
||||
onClick={(e) => {
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
copy();
|
||||
}}
|
||||
className={styles['flex-item']}>
|
||||
<Badge variant={"light"} radius={"sm"}>
|
||||
className={styles["flex-item"]}>
|
||||
<Badge
|
||||
variant={"light"}
|
||||
radius={"sm"}>
|
||||
ID: {dealSummary.id}
|
||||
</Badge>
|
||||
</div>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Flex justify={"center"} align={"center"} gap={rem(5)}>
|
||||
<Flex
|
||||
justify={"center"}
|
||||
align={"center"}
|
||||
gap={rem(5)}>
|
||||
<FontAwesomeIcon
|
||||
bounce
|
||||
style={{animationIterationCount: 1}}
|
||||
style={{ animationIterationCount: 1 }}
|
||||
icon={faCheck}
|
||||
/>
|
||||
<Text size={"xs"}>ID сделки скопирован</Text>
|
||||
<Text size={"xs"}>
|
||||
ID сделки скопирован
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
)}
|
||||
</CopyButton>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
export default DealSummaryCard;
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: var(--mantine-radius-md);
|
||||
border: rem(1px) solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
border: rem(1px) solid
|
||||
light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
|
||||
padding: var(--mantine-spacing-sm) var(--mantine-spacing-xl);
|
||||
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-5));
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-white),
|
||||
var(--mantine-color-dark-5)
|
||||
);
|
||||
margin-bottom: var(--mantine-spacing-sm);
|
||||
}
|
||||
|
||||
@@ -16,4 +20,4 @@
|
||||
font-size: rem(30px);
|
||||
font-weight: 700;
|
||||
width: rem(60px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
import styles from './Header.module.css';
|
||||
import {Button, TextInput} from "@mantine/core";
|
||||
import {FC} from "react";
|
||||
|
||||
const Header: FC = ()=>{
|
||||
import styles from "./Header.module.css";
|
||||
import { Button, TextInput } from "@mantine/core";
|
||||
import { FC } from "react";
|
||||
|
||||
const Header: FC = () => {
|
||||
return (
|
||||
<div className={styles['header']}>
|
||||
|
||||
<div className={styles["header"]}>
|
||||
<TextInput
|
||||
radius={0}
|
||||
placeholder={"Поиск и фильтры"}
|
||||
size={"xl"}
|
||||
className={styles['header-input']}
|
||||
className={styles["header-input"]}
|
||||
/>
|
||||
<Button
|
||||
radius={0}
|
||||
color={"gray"}
|
||||
variant={'default'}
|
||||
className={styles['header-button']}
|
||||
>Поиск</Button>
|
||||
variant={"default"}
|
||||
className={styles["header-button"]}>
|
||||
Поиск
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
export default Header;
|
||||
|
||||
@@ -1,31 +1,45 @@
|
||||
import {Dropzone, DropzoneProps, FileWithPath} from "@mantine/dropzone";
|
||||
import {FC, useState} from "react";
|
||||
import {Button, Fieldset, Flex, Group, Image, Loader, rem, Text} from "@mantine/core";
|
||||
import {IconPhoto, IconUpload, IconX} from "@tabler/icons-react";
|
||||
import {omit} from "lodash";
|
||||
import {BaseFormInputProps} from "../../types/utils.ts";
|
||||
import {notifications} from "../../shared/lib/notifications.ts";
|
||||
import {ProductService} from "../../client";
|
||||
import { Dropzone, DropzoneProps, FileWithPath } from "@mantine/dropzone";
|
||||
import { FC, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Fieldset,
|
||||
Flex,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
rem,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
|
||||
import { omit } from "lodash";
|
||||
import { BaseFormInputProps } from "../../types/utils.ts";
|
||||
import { notifications } from "../../shared/lib/notifications.ts";
|
||||
import { ProductService } from "../../client";
|
||||
|
||||
interface RestProps {
|
||||
imageUrlInputProps?: BaseFormInputProps<string>;
|
||||
productId?: number;
|
||||
}
|
||||
|
||||
|
||||
type Props = Omit<DropzoneProps, 'onDrop'> & RestProps;
|
||||
type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
|
||||
|
||||
const ImageDropzone: FC<Props> = (props: Props) => {
|
||||
const [showDropzone, setShowDropzone] = useState(
|
||||
!(typeof props.imageUrlInputProps?.value === 'string' &&
|
||||
props.imageUrlInputProps.value.trim() !== '')
|
||||
!(
|
||||
typeof props.imageUrlInputProps?.value === "string" &&
|
||||
props.imageUrlInputProps.value.trim() !== ""
|
||||
)
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const restProps = omit(props, ['imageUrl', 'productId', 'imageUrlInputProps']);
|
||||
const restProps = omit(props, [
|
||||
"imageUrl",
|
||||
"productId",
|
||||
"imageUrlInputProps",
|
||||
]);
|
||||
const onDrop = (files: FileWithPath[]) => {
|
||||
if (!props.productId || !props.imageUrlInputProps) return;
|
||||
if (files.length > 1) {
|
||||
notifications.error({message: "Прикрепите одно изображение"});
|
||||
notifications.error({ message: "Прикрепите одно изображение" });
|
||||
return;
|
||||
}
|
||||
const file = files[0];
|
||||
@@ -34,33 +48,35 @@ const ImageDropzone: FC<Props> = (props: Props) => {
|
||||
ProductService.uploadProductImage({
|
||||
productId: props.productId,
|
||||
formData: {
|
||||
upload_file: file
|
||||
}
|
||||
}).then(({ok, message, imageUrl}) => {
|
||||
notifications.guess(ok, {message});
|
||||
setIsLoading(false);
|
||||
upload_file: file,
|
||||
},
|
||||
})
|
||||
.then(({ ok, message, imageUrl }) => {
|
||||
notifications.guess(ok, { message });
|
||||
setIsLoading(false);
|
||||
|
||||
if (!ok || !imageUrl) {
|
||||
if (!ok || !imageUrl) {
|
||||
setShowDropzone(true);
|
||||
|
||||
return;
|
||||
}
|
||||
props.imageUrlInputProps?.onChange(imageUrl);
|
||||
setShowDropzone(false);
|
||||
})
|
||||
.catch(error => {
|
||||
notifications.error({ message: error.toString() });
|
||||
setShowDropzone(true);
|
||||
|
||||
return;
|
||||
}
|
||||
props.imageUrlInputProps?.onChange(imageUrl);
|
||||
setShowDropzone(false);
|
||||
}).catch(error => {
|
||||
notifications.error({message: error.toString()});
|
||||
setShowDropzone(true);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
const getBody = () => {
|
||||
return props.imageUrlInputProps?.value && !showDropzone ? (
|
||||
<Image src={props.imageUrlInputProps.value}/>
|
||||
<Image src={props.imageUrlInputProps.value} />
|
||||
) : (
|
||||
|
||||
<Dropzone
|
||||
{...restProps}
|
||||
accept={["image/png",
|
||||
accept={[
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/bmp",
|
||||
@@ -68,58 +84,74 @@ const ImageDropzone: FC<Props> = (props: Props) => {
|
||||
"image/x-icon",
|
||||
"image/webp",
|
||||
"image/svg+xml",
|
||||
"image/heic"]}
|
||||
"image/heic",
|
||||
]}
|
||||
multiple={false}
|
||||
onDrop={onDrop}
|
||||
|
||||
>
|
||||
<Group justify="center" gap="xl" style={{pointerEvents: 'none'}}>
|
||||
onDrop={onDrop}>
|
||||
<Group
|
||||
justify="center"
|
||||
gap="xl"
|
||||
style={{ pointerEvents: "none" }}>
|
||||
<Dropzone.Accept>
|
||||
<IconUpload
|
||||
style={{width: rem(52), height: rem(52), color: 'var(--mantine-color-blue-6)'}}
|
||||
style={{
|
||||
width: rem(52),
|
||||
height: rem(52),
|
||||
color: "var(--mantine-color-blue-6)",
|
||||
}}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<IconX
|
||||
style={{width: rem(52), height: rem(52), color: 'var(--mantine-color-red-6)'}}
|
||||
style={{
|
||||
width: rem(52),
|
||||
height: rem(52),
|
||||
color: "var(--mantine-color-red-6)",
|
||||
}}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</Dropzone.Reject>
|
||||
<Dropzone.Idle>
|
||||
<IconPhoto
|
||||
style={{width: rem(52), height: rem(52), color: 'var(--mantine-color-dimmed)'}}
|
||||
style={{
|
||||
width: rem(52),
|
||||
height: rem(52),
|
||||
color: "var(--mantine-color-dimmed)",
|
||||
}}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</Dropzone.Idle>
|
||||
|
||||
<div style={{textAlign: "center"}}>
|
||||
<Text size="xl" inline>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Text
|
||||
size="xl"
|
||||
inline>
|
||||
Перенесите изображение или нажмите чтоб выбрать файл
|
||||
</Text>
|
||||
|
||||
</div>
|
||||
</Group>
|
||||
</Dropzone>
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Flex
|
||||
gap={rem(10)}
|
||||
direction={"column"}
|
||||
|
||||
>
|
||||
<Fieldset
|
||||
legend={'Изображение'}>
|
||||
direction={"column"}>
|
||||
<Fieldset legend={"Изображение"}>
|
||||
<Flex justify={"center"}>
|
||||
{isLoading ? <Loader/> : getBody()}
|
||||
{isLoading ? <Loader /> : getBody()}
|
||||
</Flex>
|
||||
</Fieldset>
|
||||
{!showDropzone &&
|
||||
<Button onClick={() => setShowDropzone(true)} variant={"default"}>Заменить изображение {}</Button>
|
||||
}
|
||||
{!showDropzone && (
|
||||
<Button
|
||||
onClick={() => setShowDropzone(true)}
|
||||
variant={"default"}>
|
||||
Заменить изображение {}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageDropzone;
|
||||
export default ImageDropzone;
|
||||
|
||||
@@ -1,34 +1,47 @@
|
||||
.control {
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||
color: var(--mantine-color-text);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||
color: var(--mantine-color-text);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
}
|
||||
@mixin hover {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-0),
|
||||
var(--mantine-color-dark-7)
|
||||
);
|
||||
color: light-dark(
|
||||
var(--mantine-color-black),
|
||||
var(--mantine-color-dark-0)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||
padding-left: var(--mantine-spacing-md);
|
||||
margin-left: var(--mantine-spacing-xl);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
border-left: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
|
||||
padding-left: var(--mantine-spacing-md);
|
||||
margin-left: var(--mantine-spacing-xl);
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
border-left: 1px solid
|
||||
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7));
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
}
|
||||
@mixin hover {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-0),
|
||||
var(--mantine-color-dark-7)
|
||||
);
|
||||
color: light-dark(
|
||||
var(--mantine-color-black),
|
||||
var(--mantine-color-dark-0)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 200ms ease;
|
||||
}
|
||||
transition: transform 200ms ease;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import {useState} from 'react';
|
||||
import {Box, Collapse, Group, rem, ThemeIcon, UnstyledButton} from '@mantine/core';
|
||||
import {IconCalendarStats, IconChevronRight} from '@tabler/icons-react';
|
||||
import classes from './LinksGroup.module.css';
|
||||
import {Link} from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Collapse,
|
||||
Group,
|
||||
rem,
|
||||
ThemeIcon,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import { IconCalendarStats, IconChevronRight } from "@tabler/icons-react";
|
||||
import classes from "./LinksGroup.module.css";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
interface LinksGroupProps {
|
||||
icon: React.FC<any>;
|
||||
@@ -11,28 +18,36 @@ interface LinksGroupProps {
|
||||
links?: { label: string; link: string }[];
|
||||
}
|
||||
|
||||
export function LinksGroup({icon: Icon, label, initiallyOpened, links}: LinksGroupProps) {
|
||||
export function LinksGroup({
|
||||
icon: Icon,
|
||||
label,
|
||||
initiallyOpened,
|
||||
links,
|
||||
}: LinksGroupProps) {
|
||||
const hasLinks = Array.isArray(links);
|
||||
const [opened, setOpened] = useState(initiallyOpened || false);
|
||||
const items = (hasLinks ? links : []).map((link) => (
|
||||
<Link to={link.link}
|
||||
className={classes.link}
|
||||
key={link.label}
|
||||
|
||||
>
|
||||
|
||||
const items = (hasLinks ? links : []).map(link => (
|
||||
<Link
|
||||
to={link.link}
|
||||
className={classes.link}
|
||||
key={link.label}>
|
||||
{link.label}
|
||||
</Link>
|
||||
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<UnstyledButton onClick={() => setOpened((o) => !o)} className={classes.control}>
|
||||
<Group justify="space-between" gap={0}>
|
||||
<Box style={{display: 'flex', alignItems: 'center'}}>
|
||||
<ThemeIcon variant="light" size={30}>
|
||||
<Icon style={{width: rem(18), height: rem(18)}}/>
|
||||
<UnstyledButton
|
||||
onClick={() => setOpened(o => !o)}
|
||||
className={classes.control}>
|
||||
<Group
|
||||
justify="space-between"
|
||||
gap={0}>
|
||||
<Box style={{ display: "flex", alignItems: "center" }}>
|
||||
<ThemeIcon
|
||||
variant="light"
|
||||
size={30}>
|
||||
<Icon style={{ width: rem(18), height: rem(18) }} />
|
||||
</ThemeIcon>
|
||||
<Box ml="md">{label}</Box>
|
||||
</Box>
|
||||
@@ -43,7 +58,7 @@ export function LinksGroup({icon: Icon, label, initiallyOpened, links}: LinksGro
|
||||
style={{
|
||||
width: rem(16),
|
||||
height: rem(16),
|
||||
transform: opened ? 'rotate(-90deg)' : 'none',
|
||||
transform: opened ? "rotate(-90deg)" : "none",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -55,19 +70,21 @@ export function LinksGroup({icon: Icon, label, initiallyOpened, links}: LinksGro
|
||||
}
|
||||
|
||||
const mockdata = {
|
||||
label: 'Releases',
|
||||
label: "Releases",
|
||||
icon: IconCalendarStats,
|
||||
links: [
|
||||
{label: 'Upcoming releases', link: '/'},
|
||||
{label: 'Previous releases', link: '/'},
|
||||
{label: 'Releases schedule', link: '/'},
|
||||
{ label: "Upcoming releases", link: "/" },
|
||||
{ label: "Previous releases", link: "/" },
|
||||
{ label: "Releases schedule", link: "/" },
|
||||
],
|
||||
};
|
||||
|
||||
export function NavbarLinksGroup() {
|
||||
return (
|
||||
<Box mih={220} p="md">
|
||||
<Box
|
||||
mih={220}
|
||||
p="md">
|
||||
<LinksGroup {...mockdata} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
export function Logo(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg {...props} version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1024.000000 1024.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
|
||||
fill="#2a75ec" stroke="none">
|
||||
<path d="M2600 7290 l0 -760 690 0 690 0 0 196 0 195 728 -4 c613 -3 743 -7
|
||||
<svg
|
||||
{...props}
|
||||
version="1.0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1024.000000 1024.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<g
|
||||
transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
|
||||
fill="#2a75ec"
|
||||
stroke="none">
|
||||
<path
|
||||
d="M2600 7290 l0 -760 690 0 690 0 0 196 0 195 728 -4 c613 -3 743 -7
|
||||
831 -20 57 -10 106 -15 108 -13 7 7 341 -80 358 -93 8 -7 79 -34 95 -36 28 -4
|
||||
293 -151 298 -166 2 -5 8 -9 13 -9 9 0 75 -51 177 -138 44 -38 120 -123 218
|
||||
-245 73 -91 215 -390 230 -484 3 -21 8 -42 11 -46 20 -33 46 -190 58 -355 12
|
||||
@@ -27,11 +32,13 @@ export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
|
||||
-40 57 -103 155 -5 9 -15 20 -21 26 -6 5 -40 48 -76 95 -127 165 -291 325
|
||||
-473 459 -38 29 -83 62 -98 74 -16 12 -40 29 -55 37 -15 8 -67 39 -115 69 -99
|
||||
59 -339 177 -443 216 -245 93 -524 167 -774 205 -335 51 -306 50 -1877 50
|
||||
l-1473 0 0 -760z"/>
|
||||
<path d="M1730 5175 l0 -1095 1580 0 1580 0 0 1095 0 1095 -1580 0 -1580 0 0
|
||||
-1095z"/>
|
||||
l-1473 0 0 -760z"
|
||||
/>
|
||||
<path
|
||||
d="M1730 5175 l0 -1095 1580 0 1580 0 0 1095 0 1095 -1580 0 -1580 0 0
|
||||
-1095z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,15 @@
|
||||
align-items: center;
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-body);
|
||||
box-shadow: 0 2px 4px var(--mantine-color-dark-7), 0 4px 24px var(--mantine-color-dark-7);
|
||||
box-shadow:
|
||||
0 2px 4px var(--mantine-color-dark-7),
|
||||
0 4px 24px var(--mantine-color-dark-7);
|
||||
}
|
||||
@mixin light {
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, .08), 0 4px 24px rgba(0, 0, 0, .08);
|
||||
box-shadow:
|
||||
0 2px 4px rgba(0, 0, 0, 0.08),
|
||||
0 4px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
border-radius: rem(20);
|
||||
width: 100%;
|
||||
@@ -32,7 +36,10 @@
|
||||
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0));
|
||||
|
||||
&:hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5));
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-0),
|
||||
var(--mantine-color-dark-5)
|
||||
);
|
||||
}
|
||||
|
||||
&[data-active] {
|
||||
@@ -43,4 +50,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
import {Center, Flex, Image, rem, Stack, Tooltip, UnstyledButton, useMantineColorScheme} from '@mantine/core';
|
||||
import {
|
||||
Center,
|
||||
Flex,
|
||||
Image,
|
||||
rem,
|
||||
Stack,
|
||||
Tooltip,
|
||||
UnstyledButton,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconBarcode,
|
||||
IconBox, IconBuildingWarehouse,
|
||||
IconBox,
|
||||
IconBuildingWarehouse,
|
||||
IconCash,
|
||||
IconDashboard,
|
||||
IconFileBarcode,
|
||||
IconHome2,
|
||||
IconLogout,
|
||||
IconMan,
|
||||
IconMoon, IconShoppingCart,
|
||||
IconMoon,
|
||||
IconShoppingCart,
|
||||
IconSun,
|
||||
} from '@tabler/icons-react';
|
||||
import classes from './Navbar.module.css';
|
||||
import {useAppDispatch} from "../../redux/store.ts";
|
||||
import {logout} from "../../features/authSlice.ts";
|
||||
import {useNavigate, useRouterState} from "@tanstack/react-router";
|
||||
} from "@tabler/icons-react";
|
||||
import classes from "./Navbar.module.css";
|
||||
import { useAppDispatch } from "../../redux/store.ts";
|
||||
import { logout } from "../../features/authSlice.ts";
|
||||
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
||||
|
||||
interface NavbarLinkProps {
|
||||
icon: typeof IconHome2;
|
||||
@@ -28,24 +39,31 @@ interface NavbarLinkProps {
|
||||
}
|
||||
|
||||
function NavbarLink(props: NavbarLinkProps) {
|
||||
const {icon: Icon, label, active, onClick} = props;
|
||||
const { icon: Icon, label, active, onClick } = props;
|
||||
return (
|
||||
<Tooltip display={!label ? "none" : "flex"} label={label} position="right" transitionProps={{duration: 0}}>
|
||||
<UnstyledButton onClick={() => onClick && onClick(props)}
|
||||
className={classes.link}
|
||||
data-active={active || undefined}>
|
||||
<Icon style={{width: rem(20), height: rem(20)}} stroke={1.5}/>
|
||||
<Tooltip
|
||||
display={!label ? "none" : "flex"}
|
||||
label={label}
|
||||
position="right"
|
||||
transitionProps={{ duration: 0 }}>
|
||||
<UnstyledButton
|
||||
onClick={() => onClick && onClick(props)}
|
||||
className={classes.link}
|
||||
data-active={active || undefined}>
|
||||
<Icon
|
||||
style={{ width: rem(20), height: rem(20) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</UnstyledButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const mockdata = [
|
||||
|
||||
{
|
||||
icon: IconCash,
|
||||
label: 'Сделки',
|
||||
href: '/leads'
|
||||
label: "Сделки",
|
||||
href: "/leads",
|
||||
},
|
||||
// {
|
||||
// icon: IconTable,
|
||||
@@ -54,48 +72,50 @@ const mockdata = [
|
||||
// },
|
||||
{
|
||||
icon: IconMan,
|
||||
label: 'Клиенты',
|
||||
href: '/clients'
|
||||
label: "Клиенты",
|
||||
href: "/clients",
|
||||
},
|
||||
{
|
||||
icon: IconBox,
|
||||
label: 'Услуги',
|
||||
href: '/services'
|
||||
label: "Услуги",
|
||||
href: "/services",
|
||||
},
|
||||
{
|
||||
icon: IconBarcode,
|
||||
label: 'Товары',
|
||||
href: '/products'
|
||||
label: "Товары",
|
||||
href: "/products",
|
||||
},
|
||||
{
|
||||
icon: IconFileBarcode,
|
||||
label: 'Штрихкоды',
|
||||
href: '/barcode'
|
||||
label: "Штрихкоды",
|
||||
href: "/barcode",
|
||||
},
|
||||
{
|
||||
icon: IconBuildingWarehouse,
|
||||
label: 'Склады отгрузки',
|
||||
href: '/shipping_warehouses'
|
||||
label: "Склады отгрузки",
|
||||
href: "/shipping_warehouses",
|
||||
},
|
||||
{
|
||||
icon:IconShoppingCart,
|
||||
label: 'Маркетплейсы',
|
||||
href: '/marketplaces'
|
||||
}
|
||||
icon: IconShoppingCart,
|
||||
label: "Маркетплейсы",
|
||||
href: "/marketplaces",
|
||||
},
|
||||
];
|
||||
|
||||
export function Navbar() {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const router = useRouterState();
|
||||
const {colorScheme, toggleColorScheme} = useMantineColorScheme({keepTransitions: true});
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme({
|
||||
keepTransitions: true,
|
||||
});
|
||||
const onLogoutClick = () => {
|
||||
dispatch(logout());
|
||||
navigate({to: '/login'});
|
||||
}
|
||||
navigate({ to: "/login" });
|
||||
};
|
||||
const onNavlinkClick = (props: NavbarLinkProps) => {
|
||||
navigate({to: props.href});
|
||||
}
|
||||
navigate({ to: props.href });
|
||||
};
|
||||
const links = mockdata.map((link, index) => (
|
||||
<NavbarLink
|
||||
{...link}
|
||||
@@ -108,35 +128,62 @@ export function Navbar() {
|
||||
|
||||
return (
|
||||
<nav className={classes.navbar}>
|
||||
<Flex direction={"column"} gap={rem(30)}>
|
||||
<Center
|
||||
p={rem(5)}
|
||||
>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(30)}>
|
||||
<Center p={rem(5)}>
|
||||
<Image
|
||||
flex={1}
|
||||
// style={{filter: "drop-shadow(0 0 30px #fff)"}}
|
||||
src={colorScheme == "dark" ? "/icons/logo-light.png" : "/icons/logo.png"}
|
||||
src={
|
||||
colorScheme == "dark"
|
||||
? "/icons/logo-light.png"
|
||||
: "/icons/logo.png"
|
||||
}
|
||||
/>
|
||||
</Center>
|
||||
|
||||
<div className={classes.navbarMain}>
|
||||
<Stack justify="center" gap={rem(10)}>
|
||||
<Stack
|
||||
justify="center"
|
||||
gap={rem(10)}>
|
||||
{links}
|
||||
</Stack>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
<Stack w={"100%"} justify="center" gap={0}>
|
||||
<NavbarLink icon={IconDashboard}
|
||||
href={"/admin"}
|
||||
index={-1}
|
||||
label={"Панель администратора"}
|
||||
onClick={() => onNavlinkClick({href: "/admin", index: -1, icon: IconDashboard})}
|
||||
<Stack
|
||||
w={"100%"}
|
||||
justify="center"
|
||||
gap={0}>
|
||||
<NavbarLink
|
||||
icon={IconDashboard}
|
||||
href={"/admin"}
|
||||
index={-1}
|
||||
label={"Панель администратора"}
|
||||
onClick={() =>
|
||||
onNavlinkClick({
|
||||
href: "/admin",
|
||||
index: -1,
|
||||
icon: IconDashboard,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<NavbarLink
|
||||
label={"Сменить тему"}
|
||||
onClick={toggleColorScheme}
|
||||
icon={colorScheme == "dark" ? IconSun : IconMoon}
|
||||
href={"#"}
|
||||
index={-1}
|
||||
/>
|
||||
<NavbarLink
|
||||
index={-1}
|
||||
href={"#"}
|
||||
onClick={onLogoutClick}
|
||||
icon={IconLogout}
|
||||
label="Выйти"
|
||||
/>
|
||||
<NavbarLink label={"Сменить тему"} onClick={toggleColorScheme}
|
||||
icon={colorScheme == "dark" ? IconSun : IconMoon} href={"#"} index={-1}/>
|
||||
<NavbarLink index={-1} href={"#"} onClick={onLogoutClick} icon={IconLogout} label="Выйти"/>
|
||||
</Stack>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
.user {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-md);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-md);
|
||||
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
|
||||
|
||||
@mixin hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
|
||||
}
|
||||
}
|
||||
@mixin hover {
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-0),
|
||||
var(--mantine-color-dark-8)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { UnstyledButton, Group, Avatar, Text, rem } from '@mantine/core';
|
||||
import { IconChevronRight } from '@tabler/icons-react';
|
||||
import classes from './UserButton.module.css';
|
||||
import { UnstyledButton, Group, Avatar, Text, rem } from "@mantine/core";
|
||||
import { IconChevronRight } from "@tabler/icons-react";
|
||||
import classes from "./UserButton.module.css";
|
||||
|
||||
export function UserButton() {
|
||||
return (
|
||||
@@ -12,17 +12,24 @@ export function UserButton() {
|
||||
/>
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size="sm" fw={500}>
|
||||
<Text
|
||||
size="sm"
|
||||
fw={500}>
|
||||
Harriette Spoonlicker
|
||||
</Text>
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
<Text
|
||||
c="dimmed"
|
||||
size="xs">
|
||||
hspoonlicker@outlook.com
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<IconChevronRight style={{ width: rem(14), height: rem(14) }} stroke={1.5} />
|
||||
<IconChevronRight
|
||||
style={{ width: rem(14), height: rem(14) }}
|
||||
stroke={1.5}
|
||||
/>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import {IconAdjustments, IconGauge} from "@tabler/icons-react";
|
||||
import { IconAdjustments, IconGauge } from "@tabler/icons-react";
|
||||
|
||||
export const NavbarLinks = [
|
||||
{
|
||||
label: 'Главная',
|
||||
label: "Главная",
|
||||
icon: IconGauge,
|
||||
links: [
|
||||
{
|
||||
label: "123",
|
||||
link: "/login"
|
||||
link: "/login",
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Настройки',
|
||||
label: "Настройки",
|
||||
icon: IconAdjustments,
|
||||
links:[
|
||||
links: [
|
||||
{
|
||||
label: "Профиль"
|
||||
}
|
||||
]
|
||||
label: "Профиль",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
@@ -1,40 +1,44 @@
|
||||
import {Autocomplete, AutocompleteProps} from "@mantine/core";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {ObjectWithNameAndId} from "../../types/utils.ts";
|
||||
import {omit} from "lodash";
|
||||
|
||||
import { Autocomplete, AutocompleteProps } from "@mantine/core";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ObjectWithNameAndId } from "../../types/utils.ts";
|
||||
import { omit } from "lodash";
|
||||
|
||||
export type AutocompleteObjectType<T extends ObjectWithNameAndId> = T;
|
||||
|
||||
type ControlledValueProps<T extends ObjectWithNameAndId> = {
|
||||
value: AutocompleteObjectType<T>,
|
||||
value: AutocompleteObjectType<T>;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
};
|
||||
|
||||
type RestProps<T extends ObjectWithNameAndId> = {
|
||||
defaultValue?: AutocompleteObjectType<T>
|
||||
defaultValue?: AutocompleteObjectType<T>;
|
||||
onChange: (value: string) => void;
|
||||
data: AutocompleteObjectType<T>[];
|
||||
filterBy?: (item: AutocompleteObjectType<T>) => boolean;
|
||||
}
|
||||
};
|
||||
|
||||
export type ObjectAutocompleteProps<T extends ObjectWithNameAndId> =
|
||||
(RestProps<T> & Partial<ControlledValueProps<T>>)
|
||||
& Omit<AutocompleteProps, 'value' | 'onChange' | 'data'>;
|
||||
(RestProps<T> & Partial<ControlledValueProps<T>>) &
|
||||
Omit<AutocompleteProps, "value" | "onChange" | "data">;
|
||||
|
||||
const ObjectAutocomplete = <T extends ObjectWithNameAndId, >(props: ObjectAutocompleteProps<T>) => {
|
||||
|
||||
const isControlled = 'value' in props;
|
||||
const [internalValue, setInternalValue] = useState<undefined | string>(props.defaultValue);
|
||||
const ObjectAutocomplete = <T extends ObjectWithNameAndId>(
|
||||
props: ObjectAutocompleteProps<T>
|
||||
) => {
|
||||
const isControlled = "value" in props;
|
||||
const [internalValue, setInternalValue] = useState<undefined | string>(
|
||||
props.defaultValue
|
||||
);
|
||||
|
||||
const value = isControlled ? props.value?.name : internalValue;
|
||||
|
||||
const data = useMemo(() => {
|
||||
const propsData = props.filterBy ? props.data.filter(props.filterBy) : props.data;
|
||||
const propsData = props.filterBy
|
||||
? props.data.filter(props.filterBy)
|
||||
: props.data;
|
||||
|
||||
return propsData.map(item => ({
|
||||
label: item.name,
|
||||
value: item.id.toString()
|
||||
value: item.id.toString(),
|
||||
}));
|
||||
}, [props.data]);
|
||||
|
||||
@@ -45,13 +49,13 @@ const ObjectAutocomplete = <T extends ObjectWithNameAndId, >(props: ObjectAutoco
|
||||
return;
|
||||
}
|
||||
setInternalValue(event);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isControlled || !internalValue) return;
|
||||
props.onChange(internalValue);
|
||||
}, [internalValue]);
|
||||
const restProps = omit(props, ['filterBy', 'groupBy']);
|
||||
const restProps = omit(props, ["filterBy", "groupBy"]);
|
||||
return (
|
||||
<Autocomplete
|
||||
{...restProps}
|
||||
@@ -59,7 +63,7 @@ const ObjectAutocomplete = <T extends ObjectWithNameAndId, >(props: ObjectAutoco
|
||||
onChange={handleOnChange}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ObjectAutocomplete;
|
||||
export default ObjectAutocomplete;
|
||||
|
||||
@@ -1,83 +1,91 @@
|
||||
import {MultiSelect, MultiSelectProps} from "@mantine/core";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {groupBy} from "lodash";
|
||||
import { MultiSelect, MultiSelectProps } from "@mantine/core";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { groupBy } from "lodash";
|
||||
|
||||
interface ObjectWithIdAndName {
|
||||
id: number,
|
||||
name: string
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type MultiselectObjectType<T> = T;
|
||||
|
||||
type ControlledValueProps<T> = {
|
||||
value: MultiselectObjectType<T>[],
|
||||
value: MultiselectObjectType<T>[];
|
||||
onChange: (value: MultiselectObjectType<T>[]) => void;
|
||||
}
|
||||
};
|
||||
|
||||
type CustomLabelAndKeyProps<T> = {
|
||||
getLabelFn: (item: MultiselectObjectType<T>) => string;
|
||||
getValueFn: (item: MultiselectObjectType<T>) => string;
|
||||
}
|
||||
};
|
||||
type RestProps<T> = {
|
||||
defaultValue?: MultiselectObjectType<T>[]
|
||||
defaultValue?: MultiselectObjectType<T>[];
|
||||
onChange: (value: MultiselectObjectType<T>[]) => void;
|
||||
data: MultiselectObjectType<T>[];
|
||||
groupBy?: (item: MultiselectObjectType<T>) => string;
|
||||
filterBy?: (item: MultiselectObjectType<T>) => boolean;
|
||||
}
|
||||
};
|
||||
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
|
||||
return item.name;
|
||||
}
|
||||
};
|
||||
|
||||
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
|
||||
return item.id.toString();
|
||||
}
|
||||
export type ObjectMultiSelectProps<T> =
|
||||
(RestProps<T> & Partial<ControlledValueProps<T>>)
|
||||
& Omit<MultiSelectProps, 'value' | 'onChange' | 'data'>
|
||||
& (T extends ObjectWithIdAndName ? Partial<CustomLabelAndKeyProps<T>> : CustomLabelAndKeyProps<T>);
|
||||
};
|
||||
export type ObjectMultiSelectProps<T> = (RestProps<T> &
|
||||
Partial<ControlledValueProps<T>>) &
|
||||
Omit<MultiSelectProps, "value" | "onChange" | "data"> &
|
||||
(T extends ObjectWithIdAndName
|
||||
? Partial<CustomLabelAndKeyProps<T>>
|
||||
: CustomLabelAndKeyProps<T>);
|
||||
|
||||
const ObjectMultiSelect = <T, >(props: ObjectMultiSelectProps<T>) => {
|
||||
const ObjectMultiSelect = <T,>(props: ObjectMultiSelectProps<T>) => {
|
||||
const isControlled = "value" in props;
|
||||
const haveGetValueFn = "getValueFn" in props;
|
||||
const haveGetLabelFn = "getLabelFn" in props;
|
||||
|
||||
const isControlled = 'value' in props;
|
||||
const haveGetValueFn = 'getValueFn' in props;
|
||||
const haveGetLabelFn = 'getLabelFn' in props;
|
||||
|
||||
const [internalValue, setInternalValue] = useState<MultiselectObjectType<T>[] | undefined>(props.defaultValue);
|
||||
const [internalValue, setInternalValue] = useState<
|
||||
MultiselectObjectType<T>[] | undefined
|
||||
>(props.defaultValue);
|
||||
|
||||
const value = (isControlled ? props.value : internalValue) || [];
|
||||
|
||||
const getValueFn = (haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
||||
const getLabelFn = (haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
||||
const getValueFn =
|
||||
(haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
||||
const getLabelFn =
|
||||
(haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
||||
|
||||
const data = useMemo(() => {
|
||||
const propsData = props.filterBy ? props.data.filter(props.filterBy) : props.data;
|
||||
const propsData = props.filterBy
|
||||
? props.data.filter(props.filterBy)
|
||||
: props.data;
|
||||
if (props.groupBy) {
|
||||
|
||||
const groupedData = groupBy(propsData, props.groupBy);
|
||||
return Object.entries(groupedData).map(([group, items]) => ({
|
||||
group,
|
||||
items: items.map(item => ({
|
||||
label: getLabelFn(item),
|
||||
value: getValueFn(item)
|
||||
}))
|
||||
value: getValueFn(item),
|
||||
})),
|
||||
}));
|
||||
} else {
|
||||
return propsData.map(item => ({
|
||||
label: getLabelFn(item),
|
||||
value: getValueFn(item)
|
||||
value: getValueFn(item),
|
||||
}));
|
||||
}
|
||||
}, [props.data, props.groupBy]);
|
||||
|
||||
const handleOnChange = (event: string[]) => {
|
||||
const objects = props.data.filter(item => event.includes(getValueFn(item)));
|
||||
const objects = props.data.filter(item =>
|
||||
event.includes(getValueFn(item))
|
||||
);
|
||||
if (isControlled) {
|
||||
props.onChange(objects);
|
||||
return;
|
||||
}
|
||||
setInternalValue(objects);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (isControlled || !internalValue) return;
|
||||
props.onChange(internalValue);
|
||||
@@ -90,7 +98,7 @@ const ObjectMultiSelect = <T, >(props: ObjectMultiSelectProps<T>) => {
|
||||
onChange={handleOnChange}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ObjectMultiSelect;
|
||||
export default ObjectMultiSelect;
|
||||
|
||||
@@ -1,73 +1,77 @@
|
||||
import {Select, SelectProps} from "@mantine/core";
|
||||
import {useEffect, useMemo, useState} from "react";
|
||||
import {groupBy, omit} from "lodash";
|
||||
import { Select, SelectProps } from "@mantine/core";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { groupBy, omit } from "lodash";
|
||||
|
||||
interface ObjectWithIdAndName {
|
||||
id: number,
|
||||
name: string
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type SelectObjectType<T> = T;
|
||||
|
||||
type ControlledValueProps<T> = {
|
||||
value: SelectObjectType<T>,
|
||||
value: SelectObjectType<T>;
|
||||
onChange: (value: SelectObjectType<T>) => void;
|
||||
}
|
||||
};
|
||||
type CustomLabelAndKeyProps<T> = {
|
||||
getLabelFn: (item: SelectObjectType<T>) => string;
|
||||
getValueFn: (item: SelectObjectType<T>) => string;
|
||||
}
|
||||
};
|
||||
|
||||
type RestProps<T> = {
|
||||
defaultValue?: SelectObjectType<T>
|
||||
defaultValue?: SelectObjectType<T>;
|
||||
onChange: (value: SelectObjectType<T>) => void;
|
||||
data: SelectObjectType<T>[];
|
||||
groupBy?: (item: SelectObjectType<T>) => string;
|
||||
filterBy?: (item: SelectObjectType<T>) => boolean;
|
||||
};
|
||||
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
|
||||
|
||||
return item.name;
|
||||
}
|
||||
};
|
||||
|
||||
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
|
||||
if (!item) return item;
|
||||
return item.id.toString();
|
||||
}
|
||||
export type ObjectSelectProps<T> =
|
||||
(RestProps<T> & Partial<ControlledValueProps<T>>)
|
||||
& Omit<SelectProps, 'value' | 'onChange' | 'data'>
|
||||
& (T extends ObjectWithIdAndName ? Partial<CustomLabelAndKeyProps<T>> : CustomLabelAndKeyProps<T>)
|
||||
};
|
||||
export type ObjectSelectProps<T> = (RestProps<T> &
|
||||
Partial<ControlledValueProps<T>>) &
|
||||
Omit<SelectProps, "value" | "onChange" | "data"> &
|
||||
(T extends ObjectWithIdAndName
|
||||
? Partial<CustomLabelAndKeyProps<T>>
|
||||
: CustomLabelAndKeyProps<T>);
|
||||
|
||||
const ObjectSelect = <T, >(props: ObjectSelectProps<T>) => {
|
||||
|
||||
const isControlled = 'value' in props;
|
||||
const haveGetValueFn = 'getValueFn' in props;
|
||||
const haveGetLabelFn = 'getLabelFn' in props;
|
||||
const [internalValue, setInternalValue] = useState<SelectObjectType<T> | undefined>(props.defaultValue);
|
||||
const ObjectSelect = <T,>(props: ObjectSelectProps<T>) => {
|
||||
const isControlled = "value" in props;
|
||||
const haveGetValueFn = "getValueFn" in props;
|
||||
const haveGetLabelFn = "getLabelFn" in props;
|
||||
const [internalValue, setInternalValue] = useState<
|
||||
SelectObjectType<T> | undefined
|
||||
>(props.defaultValue);
|
||||
|
||||
const value = isControlled ? props.value : internalValue;
|
||||
|
||||
|
||||
const getValueFn = (haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
||||
const getLabelFn = (haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
||||
const getValueFn =
|
||||
(haveGetValueFn && props.getValueFn) || defaultGetValueFn;
|
||||
const getLabelFn =
|
||||
(haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
|
||||
|
||||
const data = useMemo(() => {
|
||||
const propsData = props.filterBy ? props.data.filter(props.filterBy) : props.data;
|
||||
const propsData = props.filterBy
|
||||
? props.data.filter(props.filterBy)
|
||||
: props.data;
|
||||
if (props.groupBy) {
|
||||
|
||||
const groupedData = groupBy(propsData, props.groupBy);
|
||||
return Object.entries(groupedData).map(([group, items]) => ({
|
||||
group,
|
||||
items: items.map(item => ({
|
||||
label: getLabelFn(item),
|
||||
value: getValueFn(item)
|
||||
}))
|
||||
value: getValueFn(item),
|
||||
})),
|
||||
}));
|
||||
} else {
|
||||
return propsData.map(item => ({
|
||||
label: getLabelFn(item),
|
||||
value: getValueFn(item)
|
||||
value: getValueFn(item),
|
||||
}));
|
||||
}
|
||||
}, [props.data, props.groupBy]);
|
||||
@@ -81,14 +85,19 @@ const ObjectSelect = <T, >(props: ObjectSelectProps<T>) => {
|
||||
return;
|
||||
}
|
||||
setInternalValue(object);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isControlled || !internalValue) return;
|
||||
props.onChange(internalValue);
|
||||
}, [internalValue]);
|
||||
|
||||
const restProps = omit(props, ['filterBy', 'groupBy', 'getValueFn', 'getLabelFn']);
|
||||
const restProps = omit(props, [
|
||||
"filterBy",
|
||||
"groupBy",
|
||||
"getValueFn",
|
||||
"getLabelFn",
|
||||
]);
|
||||
return (
|
||||
<Select
|
||||
{...restProps}
|
||||
@@ -96,7 +105,7 @@ const ObjectSelect = <T, >(props: ObjectSelectProps<T>) => {
|
||||
onChange={handleOnChange}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ObjectSelect;
|
||||
export default ObjectSelect;
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
background-color: #f9f9f9;
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-body);
|
||||
box-shadow: 0 2px 4px var(--mantine-color-dark-7), 0 4px 24px var(--mantine-color-dark-7);
|
||||
box-shadow:
|
||||
0 2px 4px var(--mantine-color-dark-7),
|
||||
0 4px 24px var(--mantine-color-dark-7);
|
||||
}
|
||||
@mixin light {
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, .08), 0 4px 24px rgba(0, 0, 0, .08);
|
||||
box-shadow:
|
||||
0 2px 4px rgba(0, 0, 0, 0.08),
|
||||
0 4px 24px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
padding: rem(15);
|
||||
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
@@ -23,5 +26,4 @@
|
||||
|
||||
.container-full-height-fixed {
|
||||
height: calc(100vh - (rem(20) * 2));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import {CSSProperties, FC, ReactNode} from "react";
|
||||
import styles from './PageBlock.module.css';
|
||||
import { CSSProperties, FC, ReactNode } from "react";
|
||||
import styles from "./PageBlock.module.css";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
children: ReactNode;
|
||||
fluid?: boolean;
|
||||
style?: CSSProperties;
|
||||
fullHeight?: boolean;
|
||||
fullHeightFixed?: boolean;
|
||||
}
|
||||
export const PageBlock: FC<Props> = ({children, style, fluid = true, fullHeight = false, fullHeightFixed = false}) => {
|
||||
};
|
||||
export const PageBlock: FC<Props> = ({
|
||||
children,
|
||||
style,
|
||||
fluid = true,
|
||||
fullHeight = false,
|
||||
fullHeightFixed = false,
|
||||
}) => {
|
||||
return (
|
||||
<div style={style}
|
||||
className={
|
||||
classNames(
|
||||
styles['container'],
|
||||
fluid && styles['container-fluid'],
|
||||
fullHeight && styles['container-full-height'],
|
||||
fullHeightFixed && styles['container-full-height-fixed']
|
||||
)
|
||||
}>
|
||||
<div
|
||||
style={style}
|
||||
className={classNames(
|
||||
styles["container"],
|
||||
fluid && styles["container-fluid"],
|
||||
fullHeight && styles["container-full-height"],
|
||||
fullHeightFixed && styles["container-full-height-fixed"]
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default PageBlock;
|
||||
);
|
||||
};
|
||||
export default PageBlock;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.number-input {
|
||||
width: rem(50);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import {ActionIcon, Flex, NumberInput, rem} from "@mantine/core";
|
||||
import {IconMinus, IconPlus} from "@tabler/icons-react";
|
||||
import styles from './PlusMinusInput.module.css';
|
||||
import {FC, useEffect, useState} from "react";
|
||||
import { ActionIcon, Flex, NumberInput, rem } from "@mantine/core";
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react";
|
||||
import styles from "./PlusMinusInput.module.css";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
|
||||
type ControlledValueProps = {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
};
|
||||
|
||||
type RestProps = {
|
||||
defaultValue?: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
};
|
||||
|
||||
type Props = RestProps & Partial<ControlledValueProps>;
|
||||
|
||||
@@ -22,7 +22,6 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
||||
const value = isControlled ? props.value : internalValue;
|
||||
|
||||
const onMinusClick = () => {
|
||||
|
||||
const newValue = (value || 0) - 1;
|
||||
if (newValue < 0) {
|
||||
return;
|
||||
@@ -32,7 +31,7 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
||||
} else {
|
||||
setInternalValue(newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onPlusClick = () => {
|
||||
const newValue = (value || 0) + 1;
|
||||
@@ -41,7 +40,7 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
||||
} else {
|
||||
setInternalValue(newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (event: number | string) => {
|
||||
let newValue = typeof event === "string" ? 0 : event;
|
||||
@@ -53,7 +52,7 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
||||
} else {
|
||||
setInternalValue(newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isControlled) {
|
||||
@@ -64,34 +63,33 @@ const PlusMinusInput: FC<Props> = (props: Props) => {
|
||||
return (
|
||||
<Flex
|
||||
align={"center"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
gap={rem(10)}>
|
||||
<ActionIcon
|
||||
disabled={value === 0}
|
||||
onClick={onMinusClick}
|
||||
variant={"default"}>
|
||||
<IconMinus/>
|
||||
<IconMinus />
|
||||
</ActionIcon>
|
||||
<NumberInput
|
||||
min={0}
|
||||
styles={{
|
||||
input: {
|
||||
textAlign: "center"
|
||||
}
|
||||
textAlign: "center",
|
||||
},
|
||||
}}
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
value={value}
|
||||
className={styles['number-input']}
|
||||
onChange={(event) => handleInputChange(event)}
|
||||
className={styles["number-input"]}
|
||||
onChange={event => handleInputChange(event)}
|
||||
/>
|
||||
<ActionIcon
|
||||
onClick={onPlusClick}
|
||||
variant={"default"}>
|
||||
<IconPlus/>
|
||||
<IconPlus />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PlusMinusInput;
|
||||
export default PlusMinusInput;
|
||||
|
||||
@@ -1,81 +1,113 @@
|
||||
import {ProductSchema} from "../../client";
|
||||
import {FC, useState} from "react";
|
||||
import { ProductSchema } from "../../client";
|
||||
import { FC, useState } from "react";
|
||||
import useProductsList from "../../pages/ProductsPage/hooks/useProductsList.tsx";
|
||||
import {omit} from "lodash";
|
||||
import ObjectSelect, {ObjectSelectProps} from "../ObjectSelect/ObjectSelect.tsx";
|
||||
import {ComboboxItem, Image, Loader, OptionsFilter, rem, SelectProps, Text, Tooltip} from "@mantine/core";
|
||||
import {getProductFields} from "../../types/utils.ts";
|
||||
import {useDebouncedValue} from "@mantine/hooks";
|
||||
import { omit } from "lodash";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../ObjectSelect/ObjectSelect.tsx";
|
||||
import {
|
||||
ComboboxItem,
|
||||
Image,
|
||||
Loader,
|
||||
OptionsFilter,
|
||||
rem,
|
||||
SelectProps,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { getProductFields } from "../../types/utils.ts";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
|
||||
type RestProps = {
|
||||
clientId: number;
|
||||
}
|
||||
};
|
||||
const MAX_PRODUCTS = 200;
|
||||
type Props = Omit<ObjectSelectProps<ProductSchema>, 'data'> & RestProps;
|
||||
type Props = Omit<ObjectSelectProps<ProductSchema>, "data"> & RestProps;
|
||||
const ProductSelect: FC<Props> = (props: Props) => {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [debounced] = useDebouncedValue(searchValue, 500);
|
||||
const {products, isLoading} = useProductsList({
|
||||
const { products, isLoading } = useProductsList({
|
||||
clientId: props.clientId,
|
||||
searchInput: debounced,
|
||||
page: 0,
|
||||
itemsPerPage: MAX_PRODUCTS
|
||||
itemsPerPage: MAX_PRODUCTS,
|
||||
});
|
||||
const restProps = omit(props, ['clientId']);
|
||||
const renderOption: SelectProps['renderOption'] = (item) => {
|
||||
const product = products.find(product => product.id == parseInt(item.option.value));
|
||||
const restProps = omit(props, ["clientId"]);
|
||||
const renderOption: SelectProps["renderOption"] = item => {
|
||||
const product = products.find(
|
||||
product => product.id == parseInt(item.option.value)
|
||||
);
|
||||
if (!product) return item.option.label;
|
||||
const productFields = getProductFields(product);
|
||||
const imageUrl = product.images && product.images[0] ? product.images[0].imageUrl : undefined;
|
||||
const imageUrl =
|
||||
product.images && product.images[0]
|
||||
? product.images[0].imageUrl
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
style={{whiteSpace: "pre-line"}}
|
||||
style={{ whiteSpace: "pre-line" }}
|
||||
multiline
|
||||
disabled={productFields.length === 0}
|
||||
label={
|
||||
<>
|
||||
{productFields.map(([key, value]) => {
|
||||
return `${key.toString()}: ${value.toString()}`;
|
||||
}).join('\n')}
|
||||
{imageUrl && <Image
|
||||
src={imageUrl}
|
||||
alt={product.name}
|
||||
maw={rem(250)}
|
||||
/>}
|
||||
{productFields
|
||||
.map(([key, value]) => {
|
||||
return `${key.toString()}: ${value.toString()}`;
|
||||
})
|
||||
.join("\n")}
|
||||
{imageUrl && (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={product.name}
|
||||
maw={rem(250)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}>
|
||||
<div>
|
||||
{product.name}<br/>
|
||||
{product.barcodes && <Text size={"xs"}>{product.barcodes[0]}</Text>}
|
||||
{product.name}
|
||||
<br />
|
||||
{product.barcodes && (
|
||||
<Text size={"xs"}>{product.barcodes[0]}</Text>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>)
|
||||
}
|
||||
const optionsFilter: OptionsFilter = ({options, search}) => {
|
||||
return options;
|
||||
const filtered = (options as ComboboxItem[]).filter((option) => {
|
||||
const product = products.find(product => product.id == parseInt(option.value));
|
||||
if (!product) return true;
|
||||
return product.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
product.barcodes.some((value) => value.toLowerCase().includes(search.toLowerCase())) ||
|
||||
product.article?.toLowerCase() === search.toLowerCase();
|
||||
}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
const optionsFilter: OptionsFilter = ({ options, search }) => {
|
||||
return options;
|
||||
const filtered = (options as ComboboxItem[]).filter(option => {
|
||||
const product = products.find(
|
||||
product => product.id == parseInt(option.value)
|
||||
);
|
||||
if (!product) return true;
|
||||
return (
|
||||
product.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
product.barcodes.some(value =>
|
||||
value.toLowerCase().includes(search.toLowerCase())
|
||||
) ||
|
||||
product.article?.toLowerCase() === search.toLowerCase()
|
||||
);
|
||||
});
|
||||
filtered.sort((a, b) => a.label.localeCompare(b.label));
|
||||
return filtered.length > MAX_PRODUCTS ? filtered.slice(0, MAX_PRODUCTS) : filtered;
|
||||
return filtered.length > MAX_PRODUCTS
|
||||
? filtered.slice(0, MAX_PRODUCTS)
|
||||
: filtered;
|
||||
};
|
||||
const setSearchValueImpl = (value: string) => {
|
||||
const names = products.map(product => product.name);
|
||||
if (names.includes(value)) return;
|
||||
setSearchValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ObjectSelect
|
||||
// disabled={isLoading}
|
||||
rightSection={
|
||||
(isLoading || searchValue !== debounced) ?
|
||||
<Loader size={"sm"}/> : null
|
||||
isLoading || searchValue !== debounced ? (
|
||||
<Loader size={"sm"} />
|
||||
) : null
|
||||
}
|
||||
onSearchChange={setSearchValueImpl}
|
||||
renderOption={renderOption}
|
||||
@@ -84,8 +116,8 @@ const ProductSelect: FC<Props> = (props: Props) => {
|
||||
data={products}
|
||||
filter={optionsFilter}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
export default ProductSelect;
|
||||
// type ControlledValueProps = {
|
||||
// value: ProductSchema;
|
||||
@@ -140,4 +172,4 @@ export default ProductSelect;
|
||||
// />
|
||||
// )
|
||||
// }
|
||||
// export default ProductSelect;
|
||||
// export default ProductSelect;
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {BarcodeTemplateSchema} from "../../../client";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { BarcodeTemplateSchema } from "../../../client";
|
||||
import useGetAllBarcodeTemplates from "../../../api/barcode/useGetAllBarcodeTemplates.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<BarcodeTemplateSchema>, 'data'>;
|
||||
type Props = Omit<ObjectSelectProps<BarcodeTemplateSchema>, "data">;
|
||||
|
||||
const BarcodeTemplateSelect = (props: Props) => {
|
||||
const {barcodeTemplates} = useGetAllBarcodeTemplates();
|
||||
const { barcodeTemplates } = useGetAllBarcodeTemplates();
|
||||
|
||||
return (
|
||||
<ObjectSelect
|
||||
data={barcodeTemplates}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default BarcodeTemplateSelect;
|
||||
export default BarcodeTemplateSelect;
|
||||
|
||||
@@ -1,45 +1,54 @@
|
||||
import {BaseEnumListSchema, type CancelablePromise} from "../../../client";
|
||||
import {FC, useEffect, useMemo, useState} from "react";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {Select, SelectProps} from "@mantine/core";
|
||||
import {omit} from "lodash";
|
||||
import { BaseEnumListSchema, type CancelablePromise } from "../../../client";
|
||||
import { FC, useEffect, useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Select, SelectProps } from "@mantine/core";
|
||||
import { omit } from "lodash";
|
||||
|
||||
type ControlledValueProps = {
|
||||
value: number,
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
type RestProps = {
|
||||
defaultValue?: number;
|
||||
onChange: (value: number) => void;
|
||||
fetchFn: () => CancelablePromise<BaseEnumListSchema>;
|
||||
queryKey: string;
|
||||
}
|
||||
export type BaseEnumSelectProps =
|
||||
(RestProps & Partial<ControlledValueProps>)
|
||||
& Omit<SelectProps, 'value' | 'onChange' | 'data' | 'defaultValue'>;
|
||||
};
|
||||
export type BaseEnumSelectProps = (RestProps & Partial<ControlledValueProps>) &
|
||||
Omit<SelectProps, "value" | "onChange" | "data" | "defaultValue">;
|
||||
|
||||
export type EnumSelectProps = Omit<BaseEnumSelectProps, 'fetchFn' | 'queryKey'>;
|
||||
export type EnumSelectProps = Omit<BaseEnumSelectProps, "fetchFn" | "queryKey">;
|
||||
|
||||
const BaseEnumSelect: FC<BaseEnumSelectProps> = (props: BaseEnumSelectProps) => {
|
||||
const {data: queryData = []} = useQuery({
|
||||
const BaseEnumSelect: FC<BaseEnumSelectProps> = (
|
||||
props: BaseEnumSelectProps
|
||||
) => {
|
||||
const { data: queryData = [] } = useQuery({
|
||||
queryKey: [props.queryKey],
|
||||
queryFn: props.fetchFn,
|
||||
select: data => data.items || []
|
||||
})
|
||||
const isControlled = 'value' in props;
|
||||
const [internalValue, setInternalValue] = useState<number | undefined>(props.defaultValue);
|
||||
select: data => data.items || [],
|
||||
});
|
||||
const isControlled = "value" in props;
|
||||
const [internalValue, setInternalValue] = useState<number | undefined>(
|
||||
props.defaultValue
|
||||
);
|
||||
const value = isControlled ? props.value : internalValue;
|
||||
const selectData = useMemo(() => queryData.reduce((acc, item) => {
|
||||
acc.push({
|
||||
label: item.name,
|
||||
value: item.id.toString()
|
||||
});
|
||||
return acc;
|
||||
}, [] as { label: string, value: string }[]), [queryData]);
|
||||
const selectData = useMemo(
|
||||
() =>
|
||||
queryData.reduce(
|
||||
(acc, item) => {
|
||||
acc.push({
|
||||
label: item.name,
|
||||
value: item.id.toString(),
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
[] as { label: string; value: string }[]
|
||||
),
|
||||
[queryData]
|
||||
);
|
||||
const handleOnChange = (event: string | null) => {
|
||||
if (typeof event === 'undefined' || event === null) return;
|
||||
if (typeof event === "undefined" || event === null) return;
|
||||
const object = queryData.find(item => event == item.id.toString());
|
||||
if (!object) return;
|
||||
if (isControlled) {
|
||||
@@ -47,22 +56,23 @@ const BaseEnumSelect: FC<BaseEnumSelectProps> = (props: BaseEnumSelectProps) =>
|
||||
return;
|
||||
}
|
||||
setInternalValue(parseInt(event));
|
||||
}
|
||||
const restProps = omit(props, ['fetchFn', 'queryKey'])
|
||||
};
|
||||
const restProps = omit(props, ["fetchFn", "queryKey"]);
|
||||
useEffect(() => {
|
||||
if (isControlled || typeof internalValue === 'undefined') return;
|
||||
if (isControlled || typeof internalValue === "undefined") return;
|
||||
props.onChange(internalValue);
|
||||
}, [internalValue]);
|
||||
return (
|
||||
<Select
|
||||
|
||||
{...restProps}
|
||||
defaultValue={props.defaultValue ? props.defaultValue.toString() : undefined}
|
||||
defaultValue={
|
||||
props.defaultValue ? props.defaultValue.toString() : undefined
|
||||
}
|
||||
value={value?.toString()}
|
||||
onChange={handleOnChange}
|
||||
data={selectData}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseEnumSelect;
|
||||
export default BaseEnumSelect;
|
||||
|
||||
@@ -1,30 +1,42 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {BaseMarketplaceSchema} from "../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { BaseMarketplaceSchema } from "../../../client";
|
||||
import { FC } from "react";
|
||||
import useBaseMarketplacesList from "../../../hooks/useBaseMarketplacesList.tsx";
|
||||
import {ActionIcon, Image} from "@mantine/core";
|
||||
import { ActionIcon, Image } from "@mantine/core";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<BaseMarketplaceSchema>, 'data' | 'getValueFn' | 'getLabelFn'>
|
||||
type Props = Omit<
|
||||
ObjectSelectProps<BaseMarketplaceSchema>,
|
||||
"data" | "getValueFn" | "getLabelFn"
|
||||
>;
|
||||
|
||||
const BaseMarketplaceSelect: FC<Props> = (props) => {
|
||||
const {objects: baseMarketplaces} = useBaseMarketplacesList();
|
||||
const BaseMarketplaceSelect: FC<Props> = props => {
|
||||
const { objects: baseMarketplaces } = useBaseMarketplacesList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
renderOption={(baseMarketplace) =>
|
||||
renderOption={baseMarketplace => (
|
||||
<>
|
||||
<ActionIcon radius={"md"} variant={"transparent"}>
|
||||
<ActionIcon
|
||||
radius={"md"}
|
||||
variant={"transparent"}>
|
||||
<Image
|
||||
src={baseMarketplaces.find(el => baseMarketplace.option.value === el.key)?.iconUrl || ""}/>
|
||||
src={
|
||||
baseMarketplaces.find(
|
||||
el =>
|
||||
baseMarketplace.option.value === el.key
|
||||
)?.iconUrl || ""
|
||||
}
|
||||
/>
|
||||
</ActionIcon>
|
||||
{baseMarketplace.option.label}
|
||||
</>
|
||||
|
||||
}
|
||||
getValueFn={(baseMarketplace) => baseMarketplace.key}
|
||||
getLabelFn={(baseMarketplace) => baseMarketplace.name}
|
||||
)}
|
||||
getValueFn={baseMarketplace => baseMarketplace.key}
|
||||
getLabelFn={baseMarketplace => baseMarketplace.name}
|
||||
data={baseMarketplaces}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default BaseMarketplaceSelect
|
||||
);
|
||||
};
|
||||
export default BaseMarketplaceSelect;
|
||||
|
||||
@@ -1,54 +1,63 @@
|
||||
import {useDebouncedValue} from "@mantine/hooks";
|
||||
import {Autocomplete, AutocompleteProps, TextInputProps} from "@mantine/core";
|
||||
import {FC, useEffect, useState} from "react";
|
||||
import {Client} from "../../../types/Client.ts";
|
||||
import {ClientService} from "../../../client";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { Autocomplete, AutocompleteProps, TextInputProps } from "@mantine/core";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { Client } from "../../../types/Client.ts";
|
||||
import { ClientService } from "../../../client";
|
||||
|
||||
type Props = {
|
||||
onSelect?: (client: Client) => void;
|
||||
withAddress?: boolean;
|
||||
nameRestProps?: AutocompleteProps;
|
||||
addressRestProps?: TextInputProps;
|
||||
}
|
||||
const ClientAutocomplete: FC<Props> = ({onSelect, addressRestProps, nameRestProps, withAddress = false}) => {
|
||||
const [value, setValue] = useState('');
|
||||
};
|
||||
const ClientAutocomplete: FC<Props> = ({
|
||||
onSelect,
|
||||
addressRestProps,
|
||||
nameRestProps,
|
||||
withAddress = false,
|
||||
}) => {
|
||||
const [value, setValue] = useState("");
|
||||
const [debouncedValue] = useDebouncedValue(value, 200);
|
||||
|
||||
// const [isLoading, setIsLoading] = useState(false);
|
||||
const [clients, setClients] = useState<Client[]>([])
|
||||
const [clients, setClients] = useState<Client[]>([]);
|
||||
const [selectedClient, selectClient] = useState<Client>();
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setClients([]);
|
||||
setValue(value);
|
||||
}
|
||||
};
|
||||
const handleDebouncedChange = () => {
|
||||
if (!value.trim()) return;
|
||||
// setIsLoading(true);
|
||||
ClientService.searchClients({name: value}).then(({clients}) => {
|
||||
ClientService.searchClients({ name: value }).then(({ clients }) => {
|
||||
setClients(clients);
|
||||
// setIsLoading(false);
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleDebouncedChange();
|
||||
}, [debouncedValue]);
|
||||
|
||||
useEffect(() => {
|
||||
selectClient(clients.find(client =>
|
||||
client.name.toLowerCase().trim() == value.toLowerCase().trim())
|
||||
||
|
||||
{
|
||||
selectClient(
|
||||
clients.find(
|
||||
client =>
|
||||
client.name.toLowerCase().trim() ==
|
||||
value.toLowerCase().trim()
|
||||
) || {
|
||||
name: value,
|
||||
id: -1,
|
||||
address: ""
|
||||
});
|
||||
address: "",
|
||||
}
|
||||
);
|
||||
}, [value]);
|
||||
useEffect(() => {
|
||||
if (!selectedClient) return;
|
||||
if (onSelect) onSelect(selectedClient);
|
||||
if (nameRestProps?.onChange) nameRestProps.onChange(selectedClient.name);
|
||||
if (nameRestProps?.onChange)
|
||||
nameRestProps.onChange(selectedClient.name);
|
||||
if (addressRestProps?.onChange) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
@@ -59,25 +68,22 @@ const ClientAutocomplete: FC<Props> = ({onSelect, addressRestProps, nameRestProp
|
||||
<>
|
||||
<Autocomplete
|
||||
{...nameRestProps}
|
||||
placeholder={'Клиент'}
|
||||
placeholder={"Клиент"}
|
||||
onChange={handleChange}
|
||||
value={value}
|
||||
data={clients.map(client => client.name)}
|
||||
styles={withAddress ? {
|
||||
input: {
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0
|
||||
}
|
||||
} : {}}
|
||||
styles={
|
||||
withAddress
|
||||
? {
|
||||
input: {
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
export default ClientAutocomplete;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
import {FC} from "react";
|
||||
import {Select} from "@mantine/core";
|
||||
import {ClientSchema} from "../../../client";
|
||||
import { FC } from "react";
|
||||
import { Select } from "@mantine/core";
|
||||
import { ClientSchema } from "../../../client";
|
||||
import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx";
|
||||
|
||||
|
||||
type Props = {
|
||||
value?: ClientSchema;
|
||||
onChange: (client: ClientSchema) => void;
|
||||
withLabel?: boolean;
|
||||
}
|
||||
const ClientSelect: FC<Props> = ({value, onChange, withLabel = false}) => {
|
||||
const {clients} = useClientsList();
|
||||
const options = clients.map(client => ({label: client.name, value: client.id.toString()}))
|
||||
};
|
||||
const ClientSelect: FC<Props> = ({ value, onChange, withLabel = false }) => {
|
||||
const { clients } = useClientsList();
|
||||
const options = clients.map(client => ({
|
||||
label: client.name,
|
||||
value: client.id.toString(),
|
||||
}));
|
||||
return (
|
||||
<Select
|
||||
searchable
|
||||
placeholder={"Выберите клиента"}
|
||||
value={value && options.find(client => client.value == value.id.toString())?.value}
|
||||
value={
|
||||
value &&
|
||||
options.find(client => client.value == value.id.toString())
|
||||
?.value
|
||||
}
|
||||
onChange={event => {
|
||||
if (!event) return;
|
||||
const client = clients.find(client => client.id == parseInt(event));
|
||||
const client = clients.find(
|
||||
client => client.id == parseInt(event)
|
||||
);
|
||||
if (!client) return;
|
||||
onChange(client);
|
||||
}}
|
||||
data={options}
|
||||
label={withLabel && "Клиент"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default ClientSelect;
|
||||
);
|
||||
};
|
||||
export default ClientSelect;
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {ClientSchema} from "../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { ClientSchema } from "../../../client";
|
||||
import { FC } from "react";
|
||||
import useClientsList from "../../../pages/ClientsPage/hooks/useClientsList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<ClientSchema>, 'data'>
|
||||
type Props = Omit<ObjectSelectProps<ClientSchema>, "data">;
|
||||
|
||||
const ClientSelectNew: FC<Props> = (props) => {
|
||||
const {clients} = useClientsList();
|
||||
const ClientSelectNew: FC<Props> = props => {
|
||||
const { clients } = useClientsList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
searchable
|
||||
data={clients}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default ClientSelectNew;
|
||||
);
|
||||
};
|
||||
export default ClientSelectNew;
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {PayRateSchema} from "../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { PayRateSchema } from "../../../client";
|
||||
import { FC } from "react";
|
||||
import usePayRatesList from "../../../pages/AdminPage/hooks/usePayRatesList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<PayRateSchema>, 'data' | 'getValueFn' | 'getLabelFn'>
|
||||
type Props = Omit<
|
||||
ObjectSelectProps<PayRateSchema>,
|
||||
"data" | "getValueFn" | "getLabelFn"
|
||||
>;
|
||||
|
||||
const PayRateSelect: FC<Props> = (props) => {
|
||||
const {objects: payRates} = usePayRatesList();
|
||||
const PayRateSelect: FC<Props> = props => {
|
||||
const { objects: payRates } = usePayRatesList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
getValueFn={(baseMarketplace) => baseMarketplace.id.toLocaleString()}
|
||||
getLabelFn={(baseMarketplace) => baseMarketplace.name}
|
||||
getValueFn={baseMarketplace => baseMarketplace.id.toLocaleString()}
|
||||
getLabelFn={baseMarketplace => baseMarketplace.name}
|
||||
data={payRates}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default PayRateSelect;
|
||||
);
|
||||
};
|
||||
export default PayRateSelect;
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {PayrollSchemeSchema} from "../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { PayrollSchemeSchema } from "../../../client";
|
||||
import { FC } from "react";
|
||||
import usePayrollSchemasList from "../../../hooks/usePayrollSchemasList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<PayrollSchemeSchema>, 'data' | 'getValueFn' | 'getLabelFn'>
|
||||
type Props = Omit<
|
||||
ObjectSelectProps<PayrollSchemeSchema>,
|
||||
"data" | "getValueFn" | "getLabelFn"
|
||||
>;
|
||||
|
||||
const PayrollSchemeSelect: FC<Props> = (props) => {
|
||||
const {objects: payrollSchemeSchemas} = usePayrollSchemasList();
|
||||
const PayrollSchemeSelect: FC<Props> = props => {
|
||||
const { objects: payrollSchemeSchemas } = usePayrollSchemasList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
getValueFn={(baseMarketplace) => baseMarketplace.key}
|
||||
getLabelFn={(baseMarketplace) => baseMarketplace.name}
|
||||
getValueFn={baseMarketplace => baseMarketplace.key}
|
||||
getLabelFn={baseMarketplace => baseMarketplace.name}
|
||||
data={payrollSchemeSchemas}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default PayrollSchemeSelect
|
||||
);
|
||||
};
|
||||
export default PayrollSchemeSelect;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import ObjectSelect, { ObjectSelectProps } from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { ServicePriceCategorySchema } from "../../../client";
|
||||
import useServicePriceCategoriesList from "../../../pages/ServicesPage/hooks/useServicePriceCategoriesList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<ServicePriceCategorySchema>, "data">
|
||||
type Props = Omit<ObjectSelectProps<ServicePriceCategorySchema>, "data">;
|
||||
|
||||
const ServicePriceCategorySelect = (props: Props) => {
|
||||
const { objects } = useServicePriceCategoriesList();
|
||||
@@ -14,4 +16,4 @@ const ServicePriceCategorySelect = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ServicePriceCategorySelect;
|
||||
export default ServicePriceCategorySelect;
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {ServiceSchema} from "../../../client";
|
||||
import { FC } from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { ServiceSchema } from "../../../client";
|
||||
import useServicesList from "../../../pages/ServicesPage/hooks/useServicesList.tsx";
|
||||
import {omit} from "lodash";
|
||||
import {ServiceType} from "../../../shared/enums/ServiceType.ts";
|
||||
import {ComboboxItem, OptionsFilter} from "@mantine/core";
|
||||
import { omit } from "lodash";
|
||||
import { ServiceType } from "../../../shared/enums/ServiceType.ts";
|
||||
import { ComboboxItem, OptionsFilter } from "@mantine/core";
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
import {ComboboxParsedItemGroup} from "@mantine/core/lib/components/Combobox/Combobox.types";
|
||||
|
||||
import { ComboboxParsedItemGroup } from "@mantine/core/lib/components/Combobox/Combobox.types";
|
||||
|
||||
type RestProps = {
|
||||
filterType?: ServiceType;
|
||||
}
|
||||
type Props = Omit<ObjectSelectProps<ServiceSchema>, 'data'> & RestProps;
|
||||
};
|
||||
type Props = Omit<ObjectSelectProps<ServiceSchema>, "data"> & RestProps;
|
||||
const ServiceSelectNew: FC<Props> = (props: Props) => {
|
||||
const {services} = useServicesList();
|
||||
const data = props.filterType ? services.filter(service => service.serviceType === props.filterType) : services;
|
||||
const { services } = useServicesList();
|
||||
const data = props.filterType
|
||||
? services.filter(service => service.serviceType === props.filterType)
|
||||
: services;
|
||||
|
||||
const restProps = omit(props, ['filterType']);
|
||||
const optionsFilter: OptionsFilter = ({options, search}) => {
|
||||
return (options as ComboboxParsedItemGroup<ComboboxItem>[]).map((option) => {
|
||||
const restProps = omit(props, ["filterType"]);
|
||||
const optionsFilter: OptionsFilter = ({ options, search }) => {
|
||||
return (options as ComboboxParsedItemGroup<ComboboxItem>[]).map(
|
||||
option => {
|
||||
return {
|
||||
...option,
|
||||
items:
|
||||
option.items.filter((item: ComboboxItem) => item.label.toLowerCase().includes(search.toLowerCase()))
|
||||
}
|
||||
items: option.items.filter((item: ComboboxItem) =>
|
||||
item.label.toLowerCase().includes(search.toLowerCase())
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -37,6 +42,6 @@ const ServiceSelectNew: FC<Props> = (props: Props) => {
|
||||
groupBy={item => item.category.name}
|
||||
filter={optionsFilter}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default ServiceSelectNew;
|
||||
);
|
||||
};
|
||||
export default ServiceSelectNew;
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {GetServiceKitSchema} from "../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { GetServiceKitSchema } from "../../../client";
|
||||
import { FC } from "react";
|
||||
import useServicesKitsList from "../../../pages/ServicesPage/hooks/useServicesKitsList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<GetServiceKitSchema>, 'data'>
|
||||
const ServicesKitSelect: FC<Props> = (props) => {
|
||||
const {objects} = useServicesKitsList();
|
||||
type Props = Omit<ObjectSelectProps<GetServiceKitSchema>, "data">;
|
||||
const ServicesKitSelect: FC<Props> = props => {
|
||||
const { objects } = useServicesKitsList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
data={objects}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ServicesKitSelect;
|
||||
export default ServicesKitSelect;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import ObjectMultiSelect, {ObjectMultiSelectProps} from "../../ObjectMultiSelect/ObjectMultiSelect.tsx";
|
||||
import {ServiceSchema} from "../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectMultiSelect, {
|
||||
ObjectMultiSelectProps,
|
||||
} from "../../ObjectMultiSelect/ObjectMultiSelect.tsx";
|
||||
import { ServiceSchema } from "../../../client";
|
||||
import { FC } from "react";
|
||||
import useServicesList from "../../../pages/ServicesPage/hooks/useServicesList.tsx";
|
||||
|
||||
type Props = Omit<ObjectMultiSelectProps<ServiceSchema>, 'data'>
|
||||
type Props = Omit<ObjectMultiSelectProps<ServiceSchema>, "data">;
|
||||
const ServicesMultiselect: FC<Props> = (props: Props) => {
|
||||
const {services} = useServicesList();
|
||||
const { services } = useServicesList();
|
||||
return (
|
||||
<ObjectMultiSelect
|
||||
data={services}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default ServicesMultiselect;
|
||||
);
|
||||
};
|
||||
export default ServicesMultiselect;
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import ObjectAutocomplete, {ObjectAutocompleteProps} from "../../ObjectAutocomplete/ObjectAutocomplete.tsx";
|
||||
import ObjectAutocomplete, {
|
||||
ObjectAutocompleteProps,
|
||||
} from "../../ObjectAutocomplete/ObjectAutocomplete.tsx";
|
||||
import useShippingWarehousesList from "./hooks/useShippingWarehousesList.tsx";
|
||||
import {FC} from "react";
|
||||
import {ShippingWarehouseSchema} from "../../../client";
|
||||
import { FC } from "react";
|
||||
import { ShippingWarehouseSchema } from "../../../client";
|
||||
|
||||
type Props = Omit<ObjectAutocompleteProps<ShippingWarehouseSchema>, 'data'>;
|
||||
const ShippingWarehouseAutocomplete: FC<Props> = (props) => {
|
||||
const {shippingWarehouses} = useShippingWarehousesList();
|
||||
type Props = Omit<ObjectAutocompleteProps<ShippingWarehouseSchema>, "data">;
|
||||
const ShippingWarehouseAutocomplete: FC<Props> = props => {
|
||||
const { shippingWarehouses } = useShippingWarehousesList();
|
||||
return (
|
||||
<ObjectAutocomplete
|
||||
{...props}
|
||||
data={shippingWarehouses}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ShippingWarehouseAutocomplete;
|
||||
export default ShippingWarehouseAutocomplete;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {ShippingWarehouseService} from "../../../../client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ShippingWarehouseService } from "../../../../client";
|
||||
|
||||
const useShippingWarehousesList = () => {
|
||||
const {isPending, error, data, refetch} = useQuery({
|
||||
queryKey: ['getAllShippingWarehouses'],
|
||||
queryFn: ShippingWarehouseService.getAllShippingWarehouses
|
||||
const { isPending, error, data, refetch } = useQuery({
|
||||
queryKey: ["getAllShippingWarehouses"],
|
||||
queryFn: ShippingWarehouseService.getAllShippingWarehouses,
|
||||
});
|
||||
const shippingWarehouses = isPending || error || !data ? [] : data.shippingWarehouses;
|
||||
const shippingWarehouses =
|
||||
isPending || error || !data ? [] : data.shippingWarehouses;
|
||||
|
||||
return {shippingWarehouses, refetch}
|
||||
}
|
||||
export default useShippingWarehousesList;
|
||||
return { shippingWarehouses, refetch };
|
||||
};
|
||||
export default useShippingWarehousesList;
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import {UserSchema} from "../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../ObjectSelect/ObjectSelect.tsx";
|
||||
import { UserSchema } from "../../../client";
|
||||
import { FC } from "react";
|
||||
import useUsersList from "../../../pages/AdminPage/hooks/useUsersList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<UserSchema>, 'data' | 'getValueFn' | 'getLabelFn'>
|
||||
const UserSelect: FC<Props> = (props) => {
|
||||
const {objects: users} = useUsersList();
|
||||
return (<ObjectSelect
|
||||
data={users}
|
||||
getLabelFn={(user) => `${user.firstName} ${user.secondName}`}
|
||||
getValueFn={(user) => user.id.toString()}
|
||||
{...props}
|
||||
/>)
|
||||
}
|
||||
export default UserSelect;
|
||||
type Props = Omit<
|
||||
ObjectSelectProps<UserSchema>,
|
||||
"data" | "getValueFn" | "getLabelFn"
|
||||
>;
|
||||
const UserSelect: FC<Props> = props => {
|
||||
const { objects: users } = useUsersList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
data={users}
|
||||
getLabelFn={user => `${user.firstName} ${user.secondName}`}
|
||||
getValueFn={user => user.id.toString()}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default UserSelect;
|
||||
|
||||
@@ -1,48 +1,66 @@
|
||||
import {ServiceSchema} from "../../client";
|
||||
import {FC, useEffect, useMemo, useState} from "react";
|
||||
import {Select, SelectProps} from "@mantine/core";
|
||||
import { ServiceSchema } from "../../client";
|
||||
import { FC, useEffect, useMemo, useState } from "react";
|
||||
import { Select, SelectProps } from "@mantine/core";
|
||||
import useServicesList from "../../pages/ServicesPage/hooks/useServicesList.tsx";
|
||||
|
||||
type ControlledValueProps = {
|
||||
value: ServiceSchema;
|
||||
onChange: (value: ServiceSchema) => void;
|
||||
}
|
||||
};
|
||||
type RestProps = {
|
||||
defaultValue?: ServiceSchema;
|
||||
onChange: (value: ServiceSchema) => void;
|
||||
}
|
||||
type Props = (RestProps & Partial<ControlledValueProps>) & Omit<SelectProps, 'value' | 'onChange'>;
|
||||
};
|
||||
type Props = (RestProps & Partial<ControlledValueProps>) &
|
||||
Omit<SelectProps, "value" | "onChange">;
|
||||
|
||||
|
||||
const ServiceSelect: FC<Props> = (props) => {
|
||||
const ServiceSelect: FC<Props> = props => {
|
||||
const isControlled = props.value !== undefined;
|
||||
const [internalValue, setInternalValue] = useState<ServiceSchema | undefined>(props.defaultValue);
|
||||
const [internalValue, setInternalValue] = useState<
|
||||
ServiceSchema | undefined
|
||||
>(props.defaultValue);
|
||||
const value = isControlled ? props.value : internalValue;
|
||||
const {services} = useServicesList();
|
||||
const categories = useMemo(() => services.reduce((acc, service) => {
|
||||
if (!acc.includes(service.category.name)) {
|
||||
acc.push(service.category.name);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]), [services]);
|
||||
|
||||
const data = useMemo(() => categories.map(category => ({
|
||||
group: category,
|
||||
items: services.filter(service => service.category.name === category)
|
||||
.map(service => ({
|
||||
value: service.id.toString(),
|
||||
label: service.name
|
||||
}))
|
||||
})), [services, categories]);
|
||||
const { services } = useServicesList();
|
||||
const categories = useMemo(
|
||||
() =>
|
||||
services.reduce((acc, service) => {
|
||||
if (!acc.includes(service.category.name)) {
|
||||
acc.push(service.category.name);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]),
|
||||
[services]
|
||||
);
|
||||
|
||||
const data = useMemo(
|
||||
() =>
|
||||
categories.map(category => ({
|
||||
group: category,
|
||||
items: services
|
||||
.filter(service => service.category.name === category)
|
||||
.map(service => ({
|
||||
value: service.id.toString(),
|
||||
label: service.name,
|
||||
})),
|
||||
})),
|
||||
[services, categories]
|
||||
);
|
||||
|
||||
const handleOnChange = (value: string) => {
|
||||
if (isControlled) {
|
||||
props.onChange(services.find(service => service.id.toString() === value) as ServiceSchema);
|
||||
props.onChange(
|
||||
services.find(
|
||||
service => service.id.toString() === value
|
||||
) as ServiceSchema
|
||||
);
|
||||
return;
|
||||
}
|
||||
setInternalValue(services.find(service => service.id.toString() === value) as ServiceSchema);
|
||||
}
|
||||
setInternalValue(
|
||||
services.find(
|
||||
service => service.id.toString() === value
|
||||
) as ServiceSchema
|
||||
);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!isControlled) {
|
||||
props.onChange(internalValue as ServiceSchema);
|
||||
@@ -57,7 +75,7 @@ const ServiceSelect: FC<Props> = (props) => {
|
||||
onChange={event => event && handleOnChange(event)}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceSelect;
|
||||
export default ServiceSelect;
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { ObjectSelectProps } from "../ObjectSelect/ObjectSelect.tsx";
|
||||
import { ServicePriceCategorySchema, ServiceSchema } from "../../client";
|
||||
import { Flex, FlexProps, NumberInput, NumberInputProps, rem } from "@mantine/core";
|
||||
import {
|
||||
Flex,
|
||||
FlexProps,
|
||||
NumberInput,
|
||||
NumberInputProps,
|
||||
rem,
|
||||
} from "@mantine/core";
|
||||
import { FC, useEffect, useRef, useState } from "react";
|
||||
import ServiceSelectNew from "../Selects/ServiceSelectNew/ServiceSelectNew.tsx";
|
||||
import { ServiceType } from "../../shared/enums/ServiceType.ts";
|
||||
@@ -9,38 +15,47 @@ type ServiceProps = Omit<ObjectSelectProps<ServiceSchema>, "data">;
|
||||
type PriceProps = NumberInputProps;
|
||||
|
||||
type Props = {
|
||||
serviceProps: ServiceProps,
|
||||
priceProps: PriceProps,
|
||||
quantity: number,
|
||||
containerProps: FlexProps,
|
||||
filterType?: ServiceType,
|
||||
lockOnEdit?: boolean
|
||||
category?: ServicePriceCategorySchema
|
||||
}
|
||||
serviceProps: ServiceProps;
|
||||
priceProps: PriceProps;
|
||||
quantity: number;
|
||||
containerProps: FlexProps;
|
||||
filterType?: ServiceType;
|
||||
lockOnEdit?: boolean;
|
||||
category?: ServicePriceCategorySchema;
|
||||
};
|
||||
const ServiceWithPriceInput: FC<Props> = ({
|
||||
serviceProps,
|
||||
priceProps,
|
||||
quantity,
|
||||
containerProps,
|
||||
filterType = ServiceType.PRODUCT_SERVICE,
|
||||
lockOnEdit = true,
|
||||
category,
|
||||
}) => {
|
||||
serviceProps,
|
||||
priceProps,
|
||||
quantity,
|
||||
containerProps,
|
||||
filterType = ServiceType.PRODUCT_SERVICE,
|
||||
lockOnEdit = true,
|
||||
category,
|
||||
}) => {
|
||||
const [price, setPrice] = useState<number | undefined>(
|
||||
typeof priceProps.value === "number" ? priceProps.value : undefined);
|
||||
const [service, setService] = useState<ServiceSchema | undefined>(serviceProps.value);
|
||||
typeof priceProps.value === "number" ? priceProps.value : undefined
|
||||
);
|
||||
const [service, setService] = useState<ServiceSchema | undefined>(
|
||||
serviceProps.value
|
||||
);
|
||||
const isFirstRender = useRef(true);
|
||||
const setPriceBasedOnQuantity = (): boolean => {
|
||||
if (!service || !service.priceRanges.length) return false;
|
||||
const range = service.priceRanges.find(priceRange =>
|
||||
quantity >= priceRange.fromQuantity && quantity <= priceRange.toQuantity) || service.priceRanges[0];
|
||||
const range =
|
||||
service.priceRanges.find(
|
||||
priceRange =>
|
||||
quantity >= priceRange.fromQuantity &&
|
||||
quantity <= priceRange.toQuantity
|
||||
) || service.priceRanges[0];
|
||||
|
||||
setPrice(range.price);
|
||||
return true;
|
||||
};
|
||||
const setPriceBasedOnCategory = () => {
|
||||
if (!category || !service) return false;
|
||||
const categoryPrice = service.categoryPrices.find(categoryPrice => categoryPrice.category.id === category.id);
|
||||
const categoryPrice = service.categoryPrices.find(
|
||||
categoryPrice => categoryPrice.category.id === category.id
|
||||
);
|
||||
if (!categoryPrice) return false;
|
||||
setPrice(categoryPrice.price);
|
||||
return true;
|
||||
@@ -84,14 +99,11 @@ const ServiceWithPriceInput: FC<Props> = ({
|
||||
isFirstRender.current = false;
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<Flex
|
||||
align={"center"}
|
||||
gap={rem(10)}
|
||||
{...containerProps}
|
||||
>
|
||||
|
||||
{...containerProps}>
|
||||
{/*<ActionIcon variant={"default"}>*/}
|
||||
{/*<IconReload onClick={() => onReload()}/>*/}
|
||||
{/*</ActionIcon>*/}
|
||||
@@ -108,9 +120,8 @@ const ServiceWithPriceInput: FC<Props> = ({
|
||||
// value={price}
|
||||
defaultValue={priceProps.value}
|
||||
/>
|
||||
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceWithPriceInput;
|
||||
export default ServiceWithPriceInput;
|
||||
|
||||
@@ -1,90 +1,93 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import PropTypes from "prop-types";
|
||||
import React, { useRef, useEffect } from "react";
|
||||
|
||||
export interface TelegramUser {
|
||||
id: number
|
||||
first_name: string
|
||||
username: string
|
||||
photo_url: string
|
||||
auth_date: number
|
||||
hash: string
|
||||
id: number;
|
||||
first_name: string;
|
||||
username: string;
|
||||
photo_url: string;
|
||||
auth_date: number;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
botName: string
|
||||
usePic?: boolean
|
||||
className?: string
|
||||
cornerRadius?: number
|
||||
requestAccess?: boolean
|
||||
dataAuthUrl?: string
|
||||
dataOnauth?: (user: TelegramUser) => void
|
||||
buttonSize?: 'large' | 'medium' | 'small'
|
||||
wrapperProps?: React.HTMLProps<HTMLDivElement>
|
||||
botName: string;
|
||||
usePic?: boolean;
|
||||
className?: string;
|
||||
cornerRadius?: number;
|
||||
requestAccess?: boolean;
|
||||
dataAuthUrl?: string;
|
||||
dataOnauth?: (user: TelegramUser) => void;
|
||||
buttonSize?: "large" | "medium" | "small";
|
||||
wrapperProps?: React.HTMLProps<HTMLDivElement>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
TelegramLoginWidget: {
|
||||
dataOnauth: (user: TelegramUser) => void
|
||||
}
|
||||
dataOnauth: (user: TelegramUser) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const TelegramLoginButton: React.FC<Props> = ({
|
||||
wrapperProps,
|
||||
dataAuthUrl,
|
||||
usePic = false,
|
||||
botName,
|
||||
className,
|
||||
buttonSize = 'large',
|
||||
dataOnauth,
|
||||
cornerRadius,
|
||||
requestAccess = true
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
wrapperProps,
|
||||
dataAuthUrl,
|
||||
usePic = false,
|
||||
botName,
|
||||
className,
|
||||
buttonSize = "large",
|
||||
dataOnauth,
|
||||
cornerRadius,
|
||||
requestAccess = true,
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current === null) return
|
||||
if (ref.current === null) return;
|
||||
|
||||
if (
|
||||
typeof dataOnauth === 'undefined' &&
|
||||
typeof dataAuthUrl === 'undefined'
|
||||
typeof dataOnauth === "undefined" &&
|
||||
typeof dataAuthUrl === "undefined"
|
||||
) {
|
||||
throw new Error(
|
||||
'One of this props should be defined: dataAuthUrl (redirect URL), dataOnauth (callback fn) should be defined.'
|
||||
)
|
||||
"One of this props should be defined: dataAuthUrl (redirect URL), dataOnauth (callback fn) should be defined."
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof dataOnauth === 'function') {
|
||||
if (typeof dataOnauth === "function") {
|
||||
window.TelegramLoginWidget = {
|
||||
dataOnauth: (user: TelegramUser) => dataOnauth(user)
|
||||
}
|
||||
dataOnauth: (user: TelegramUser) => dataOnauth(user),
|
||||
};
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://telegram.org/js/telegram-widget.js?22'
|
||||
script.setAttribute('data-telegram-login', botName)
|
||||
script.setAttribute('data-size', buttonSize)
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://telegram.org/js/telegram-widget.js?22";
|
||||
script.setAttribute("data-telegram-login", botName);
|
||||
script.setAttribute("data-size", buttonSize);
|
||||
|
||||
if (cornerRadius !== undefined) {
|
||||
script.setAttribute('data-radius', cornerRadius.toString())
|
||||
script.setAttribute("data-radius", cornerRadius.toString());
|
||||
}
|
||||
|
||||
if (requestAccess) {
|
||||
script.setAttribute('data-request-access', 'write')
|
||||
script.setAttribute("data-request-access", "write");
|
||||
}
|
||||
|
||||
script.setAttribute('data-userpic', usePic.toString())
|
||||
script.setAttribute("data-userpic", usePic.toString());
|
||||
|
||||
if (typeof dataAuthUrl === 'string') {
|
||||
script.setAttribute('data-auth-url', dataAuthUrl)
|
||||
if (typeof dataAuthUrl === "string") {
|
||||
script.setAttribute("data-auth-url", dataAuthUrl);
|
||||
} else {
|
||||
script.setAttribute('data-onauth', 'TelegramLoginWidget.dataOnauth(user)')
|
||||
script.setAttribute(
|
||||
"data-onauth",
|
||||
"TelegramLoginWidget.dataOnauth(user)"
|
||||
);
|
||||
}
|
||||
|
||||
script.async = true
|
||||
script.async = true;
|
||||
|
||||
ref.current.appendChild(script)
|
||||
ref.current.appendChild(script);
|
||||
}, [
|
||||
botName,
|
||||
buttonSize,
|
||||
@@ -93,11 +96,17 @@ const TelegramLoginButton: React.FC<Props> = ({
|
||||
requestAccess,
|
||||
usePic,
|
||||
ref,
|
||||
dataAuthUrl
|
||||
])
|
||||
dataAuthUrl,
|
||||
]);
|
||||
|
||||
return <div ref={ref} className={className} {...wrapperProps} />
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={className}
|
||||
{...wrapperProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
TelegramLoginButton.propTypes = {
|
||||
botName: PropTypes.string.isRequired,
|
||||
@@ -108,7 +117,7 @@ TelegramLoginButton.propTypes = {
|
||||
wrapperProps: PropTypes.object,
|
||||
dataOnauth: PropTypes.func,
|
||||
dataAuthUrl: PropTypes.string,
|
||||
buttonSize: PropTypes.oneOf(['large', 'medium', 'small'])
|
||||
}
|
||||
buttonSize: PropTypes.oneOf(["large", "medium", "small"]),
|
||||
};
|
||||
|
||||
export default TelegramLoginButton
|
||||
export default TelegramLoginButton;
|
||||
|
||||
@@ -1,59 +1,75 @@
|
||||
import cx from 'clsx';
|
||||
import {Text} from '@mantine/core';
|
||||
import {useListState} from '@mantine/hooks';
|
||||
import {DragDropContext, Droppable, Draggable} from '@hello-pangea/dnd';
|
||||
import classes from './DndList.module.css';
|
||||
|
||||
import cx from "clsx";
|
||||
import { Text } from "@mantine/core";
|
||||
import { useListState } from "@mantine/hooks";
|
||||
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
|
||||
import classes from "./DndList.module.css";
|
||||
|
||||
const data = [
|
||||
{position: 6, mass: 12.011, symbol: 'C', name: 'Carbon'},
|
||||
{position: 7, mass: 14.007, symbol: 'N', name: 'Nitrogen'},
|
||||
{position: 39, mass: 88.906, symbol: 'Y', name: 'Yttrium'},
|
||||
{position: 56, mass: 137.33, symbol: 'Ba', name: 'Barium'},
|
||||
{position: 58, mass: 140.12, symbol: 'Ce', name: 'Cerium'},
|
||||
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
|
||||
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
|
||||
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
|
||||
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
|
||||
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
|
||||
];
|
||||
|
||||
export function DndList() {
|
||||
const [state, handlers] = useListState(data);
|
||||
|
||||
const items = (listIndex: number) => state.map((item, index) => (
|
||||
<Draggable key={item.symbol + `${listIndex}`} index={index} draggableId={item.symbol + `${listIndex}`}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={cx(classes.item, {[classes.itemDragging]: snapshot.isDragging})}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<Text className={classes.symbol}>{item.symbol}</Text>
|
||||
<div>
|
||||
<Text>{item.name}</Text>
|
||||
<Text c="dimmed" size="sm">
|
||||
Position: {item.position} • Mass: {item.mass}
|
||||
</Text>
|
||||
const items = (listIndex: number) =>
|
||||
state.map((item, index) => (
|
||||
<Draggable
|
||||
key={item.symbol + `${listIndex}`}
|
||||
index={index}
|
||||
draggableId={item.symbol + `${listIndex}`}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
className={cx(classes.item, {
|
||||
[classes.itemDragging]: snapshot.isDragging,
|
||||
})}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
ref={provided.innerRef}>
|
||||
<Text className={classes.symbol}>{item.symbol}</Text>
|
||||
<div>
|
||||
<Text>{item.name}</Text>
|
||||
<Text
|
||||
c="dimmed"
|
||||
size="sm">
|
||||
Position: {item.position} • Mass: {item.mass}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
));
|
||||
)}
|
||||
</Draggable>
|
||||
));
|
||||
|
||||
return (
|
||||
<DragDropContext
|
||||
onDragEnd={({destination, source}) =>
|
||||
handlers.reorder({from: source.index, to: destination?.index || 0})
|
||||
}
|
||||
>
|
||||
<Droppable droppableId="dnd-list" direction="vertical">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
onDragEnd={({ destination, source }) =>
|
||||
handlers.reorder({
|
||||
from: source.index,
|
||||
to: destination?.index || 0,
|
||||
})
|
||||
}>
|
||||
<Droppable
|
||||
droppableId="dnd-list"
|
||||
direction="vertical">
|
||||
{provided => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}>
|
||||
{items(1)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
<Droppable droppableId="dnd-list-2" direction="vertical">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
<Droppable
|
||||
droppableId="dnd-list-2"
|
||||
direction="vertical">
|
||||
{provided => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}>
|
||||
{items(2)}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
@@ -61,4 +77,4 @@ export function DndList() {
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user