This commit is contained in:
2024-07-21 10:58:51 +03:00
parent 54c9ca8908
commit af05b51d1c
188 changed files with 1155 additions and 555 deletions

View File

@@ -23,6 +23,8 @@ const DealServicesTable: FC<Props> = (
tableRef
}) => {
const serviceIds = items.map(item => item.service.id);
const columns = useDealServicesTableColumns({
data: items
});
@@ -50,59 +52,67 @@ const DealServicesTable: FC<Props> = (
}
})
}
return (
<BaseTable
ref={tableRef}
data={items}
columns={columns}
onSelectionChange={onSelectionChange}
restProps={{
enableGrouping: true,
initialState: {grouping: ["service.category"]},
enableColumnActions: false,
enableSorting: false,
enableBottomToolbar: true,
enableRowActions: true,
enableRowSelection: true,
renderBottomToolbar: ({table}) => (
<Flex justify={"flex-end"} gap={rem(10)} p={rem(10)}>
{(onMultipleDelete && table.getSelectedRowModel().rows.length > 0) && (
<Button
onClick={() => {
onMultipleDelete(table.getSelectedRowModel().rows.map(row => row.original))
}}
variant={"filled"}
color={"red"}
>
Удалить выбранные
</Button>
)}
<Button onClick={onCreateClick} variant={"default"}>
Добавить услугу
</Button>
</Flex>
),
renderRowActions: ({row}) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon onClick={() => {
if (onDelete) onDelete(row.original);
}} variant={"default"}>
<IconTrash/>
</ActionIcon>
</Tooltip>
<Tooltip label="Редактировать">
<ActionIcon onClick={() => {
onEditClick(row.original);
}} variant={"default"}>
<IconEdit/>
</ActionIcon>
</Tooltip>
</Flex>
)
} as MRT_TableOptions<DealServiceSchema>}
/>
return (
<>
<BaseTable
ref={tableRef}
data={items}
columns={columns}
onSelectionChange={onSelectionChange}
restProps={{
enableGrouping: true,
initialState: {grouping: ["service.category"]},
enableColumnActions: false,
enableSorting: false,
enableBottomToolbar: true,
enableRowActions: true,
enableRowSelection: true,
renderBottomToolbar: ({table}) => (
<Flex justify={"flex-end"} gap={rem(10)} p={rem(10)}>
{(onMultipleDelete && table.getSelectedRowModel().rows.length > 0) && (
<Button
onClick={() => {
onMultipleDelete(table.getSelectedRowModel().rows.map(row => row.original))
}}
variant={"filled"}
color={"red"}
>
Удалить выбранные
</Button>
)}
<Button onClick={onCreateClick} variant={"default"}>
Добавить услугу
</Button>
</Flex>
),
renderRowActions: ({row}) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon onClick={() => {
if (onDelete) onDelete(row.original);
}} variant={"default"}>
<IconTrash/>
</ActionIcon>
</Tooltip>
<Tooltip label="Редактировать">
<ActionIcon onClick={() => {
onEditClick(row.original);
}} variant={"default"}>
<IconEdit/>
</ActionIcon>
</Tooltip>
</Flex>
)
} as MRT_TableOptions<DealServiceSchema>}
/>
</>
)
}

View File

@@ -12,8 +12,8 @@ export const useDealStatusChangeTableColumns = () => {
accessorFn: (row) => new Date(row.changedAt).toLocaleString('ru-RU'),
},
{
accessorKey: "user.id",
header: "ID пользователя",
header: "Пользователь",
accessorFn: (row) => `${row.user.firstName} ${row.user.secondName}`
},
{
accessorKey: "fromStatus",
@@ -29,11 +29,11 @@ export const useDealStatusChangeTableColumns = () => {
accessorKey: "comment",
header: "Комментарий",
Cell: ({row}) =>
<Spoiler maxHeight={80} showLabel={"Показать весь"} hideLabel={"Скрыть"}>
<Text style={{wordWrap: "break-word", wordBreak: "break-all", whiteSpace: "normal"}} span>
{row.original.comment}<br/>
</Text>
</Spoiler>
<Spoiler maxHeight={80} showLabel={"Показать весь"} hideLabel={"Скрыть"}>
<Text style={{wordWrap: "break-word", wordBreak: "break-all", whiteSpace: "normal"}} span>
{row.original.comment}<br/>
</Text>
</Spoiler>
,
}

View File

@@ -0,0 +1,62 @@
import {UserSchema} from "../../../../client";
import {FC} from "react";
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
import {useSimpleUsersTableColumns} from "./columns.tsx";
import {MRT_TableOptions} from "mantine-react-table";
import {ActionIcon, Button, Flex, rem, Tooltip} from "@mantine/core";
import {modals} from "@mantine/modals";
import {IconTrash} from "@tabler/icons-react";
export type SimpleUsersTableProps = {
items: UserSchema[],
onChange: (items: UserSchema[]) => void
}
const SimpleUsersTable: FC<SimpleUsersTableProps> = ({items, onChange}) => {
const columns = useSimpleUsersTableColumns();
const onAddClick = () => {
const userIds = items.map(user => user.id);
modals.openContextModal({
title: "Выберите сотрудника",
modal: "employeeSelect",
withCloseButton: false,
innerProps: {
selectProps: {
filterBy: (user) => !userIds.includes(user.id) //&& user.roleKey === UserRoleEnum.EMPLOYEE
},
onSelect: (user) => onChange([...items, user])
}
})
}
return (
<BaseTable
data={items}
columns={columns}
restProps={{
enableColumnActions: false,
enableSorting: false,
enableBottomToolbar: true,
renderBottomToolbar: (
<Flex justify={"flex-end"} gap={rem(10)} p={rem(10)}>
<Button onClick={() => onAddClick()} variant={"default"}>
Добавить сотрудника к услуге
</Button>
</Flex>
),
enableRowActions: true,
renderRowActions: ({row}) => (
<Tooltip label="Удалить">
<ActionIcon onClick={() => {
onChange(items.filter(item => item.id !== row.original.id));
}} variant={"default"}>
<IconTrash/>
</ActionIcon>
</Tooltip>),
} as MRT_TableOptions<UserSchema>}
/>
)
}
export default SimpleUsersTable;

View File

@@ -0,0 +1,20 @@
import {useMemo} from "react";
import {MRT_ColumnDef} from "mantine-react-table";
import {UserSchema} from "../../../../client";
export const useSimpleUsersTableColumns = () => {
return useMemo<MRT_ColumnDef<UserSchema>[]>(() => [
{
accessorKey: "firstName",
header: "Имя"
},
{
accessorKey: "secondName",
header: "Фамилия"
},
{
accessorKey: "position.name",
header: "Должность"
},
], []);
}

View File

@@ -12,6 +12,7 @@ import DealStatusChangeTable from "../../components/DealStatusChangeTable/DealSt
import DealEditDrawerGeneralTab from "./tabs/DealEditDrawerGeneralTab.tsx";
import {useQueryClient} from "@tanstack/react-query";
import ProductAndServiceTab from "../../tabs/ProductAndServiceTab/ProductAndServiceTab.tsx";
import {motion} from "framer-motion";
// import styles from './DealEditDrawer.module.css';
const useDealServicesTableState = () => {
@@ -317,7 +318,7 @@ const DealEditDrawer: FC = () => {
}, [isVisible]);
return (
<Drawer
size={"95%"}
size={"calc(100vw - 150px)"}
position={"right"}
onClose={onClose}
removeScrollProps={{allowPinchZoom: true}}
@@ -329,7 +330,10 @@ const DealEditDrawer: FC = () => {
defaultValue={"general"}
h={'100%'}
variant={"outline"}
orientation={"vertical"}>
orientation={"vertical"}
keepMounted={false}
>
<Tabs.List
>
<Tabs.Tab value={"general"} leftSection={<IconSettings/>}>
@@ -343,19 +347,43 @@ const DealEditDrawer: FC = () => {
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value={"general"}>
<Box h={"100%"} w={"100%"} p={rem(10)}>
<DealEditDrawerGeneralTab/>
</Box>
<motion.div
initial={{opacity: 0}}
animate={{opacity: 1}}
transition={{duration: 0.2}}
>
<Box h={"100%"} w={"100%"} p={rem(10)}>
<DealEditDrawerGeneralTab/>
</Box>
</motion.div>
</Tabs.Panel>
<Tabs.Panel value={"history"}>
<Box p={rem(10)}>
<DealEditDrawerStatusChangeTable/>
</Box>
<motion.div
initial={{opacity: 0}}
animate={{opacity: 1}}
transition={{duration: 0.2}}
>
<Box p={rem(10)}>
<DealEditDrawerStatusChangeTable/>
</Box>
</motion.div>
</Tabs.Panel>
<Tabs.Panel value={"servicesAndProducts"}>
<Box p={rem(10)}>
<ProductAndServiceTab/>
</Box>
<motion.div
initial={{opacity: 0}}
animate={{opacity: 1}}
transition={{duration: 0.2}}
>
<Box p={rem(10)}>
<ProductAndServiceTab/>
</Box>
</motion.div>
</Tabs.Panel>
<Tabs.Panel value={"services"}>
<Box p={rem(10)}>

View File

@@ -20,6 +20,7 @@ const AddDealServiceModal = ({
initialValues: isEditing ? innerProps.element : {
service: undefined,
quantity: 1,
employees: []
},
validate: {
service: (service?: DealServiceSchema['service']) => service !== undefined ? null : "Необходимо выбрать услугу",

View File

@@ -20,7 +20,8 @@ const ProductServiceFormModal = ({
const isEditing = 'onChange' in innerProps;
const initialValues: Partial<DealProductServiceSchema> = isEditing ? innerProps.element : {
service: undefined,
price: undefined
price: undefined,
employees: []
}
const form = useForm<Partial<DealProductServiceSchema>>({
initialValues,

View File

@@ -1,13 +1,18 @@
import {CRUDTableProps} from "../../../../../../types/CRUDTable.tsx";
import {DealServiceSchema} from "../../../../../../client";
import {FC} from "react";
import {ActionIcon, Button, Flex, NumberInput, rem, Text, Title, Tooltip} from "@mantine/core";
import {IconTrash} from "@tabler/icons-react";
import {DealServiceSchema, UserSchema} from "../../../../../../client";
import {FC, useState} from "react";
import {ActionIcon, Button, Flex, Modal, NumberInput, rem, Text, Title, Tooltip} from "@mantine/core";
import {IconTrash, IconUsersGroup} from "@tabler/icons-react";
import {modals} from "@mantine/modals";
import {isNumber} from "lodash";
import SimpleUsersTable from "../../../../components/SimpleUsersTable/SimpleUsersTable.tsx";
type Props = CRUDTableProps<DealServiceSchema>;
const DealServicesTable: FC<Props> = ({items, onDelete, onCreate, onChange}) => {
const [currentService, setCurrentService] = useState<DealServiceSchema | undefined>();
const [employeesModalVisible, setEmployeesModalVisible] = useState(false);
const onDeleteClick = (item: DealServiceSchema) => {
if (!onDelete) return;
onDelete(item);
@@ -38,73 +43,119 @@ const DealServicesTable: FC<Props> = ({items, onDelete, onCreate, onChange}) =>
price
})
}
const onEmployeeClick = (item: DealServiceSchema) => {
if (!onChange) return;
setCurrentService(item);
setEmployeesModalVisible(true);
}
const onEmployeeModalClose = () => {
setEmployeesModalVisible(false);
setCurrentService(undefined);
}
const getCurrentEmployees = (): UserSchema[] => {
if (!currentService) return [];
const item = items.find(i => i.service.id === currentService.service.id)
if (!item) return [];
return item.employees;
}
const onEmployeesChange = (items: UserSchema[]) => {
if (!currentService || !onChange) return;
onChange({
...currentService,
employees: items
});
}
return (
<Flex
direction={"column"}
gap={rem(10)}
h={"100%"}
>
<>
<Flex
direction={"column"}
gap={rem(10)}
h={"100%"}
direction={"column"}>
<Title
order={3}
w={"100%"}
style={{textAlign: "center"}}
mb={rem(10)}
>Общие услуги</Title>
>
<Flex
direction={"column"}
gap={rem(10)}
>
h={"100%"}
direction={"column"}>
<Title
order={3}
w={"100%"}
style={{textAlign: "center"}}
mb={rem(10)}
>Общие услуги</Title>
<Flex
direction={"column"}
gap={rem(10)}
>
{items.map(service => (
<Flex
key={service.service.id}
w={"100%"}
gap={rem(10)}
align={"center"}
>
<Tooltip
onClick={() => onDeleteClick(service)}
label="Удалить услугу">
<ActionIcon
variant={"default"}>
<IconTrash/>
</ActionIcon>
</Tooltip>
<Text
flex={1}
>{service.service.name}</Text>
<NumberInput
suffix={" шт."}
onChange={event => isNumber(event) && onQuantityChange(service, event)}
value={service.quantity}
/>
<NumberInput
onChange={event => isNumber(event) && onPriceChange(service, event)}
suffix={"₽"}
value={service.price}
/>
</Flex>
))}
{items.map(service => (
<Flex
key={service.service.id}
w={"100%"}
gap={rem(10)}
align={"center"}
>
<Tooltip
onClick={() => onDeleteClick(service)}
label="Удалить услугу">
<ActionIcon
variant={"default"}>
<IconTrash/>
</ActionIcon>
</Tooltip>
<Tooltip label="Сотрудники">
<ActionIcon onClick={() => onEmployeeClick(service)} variant={"default"}>
<IconUsersGroup/>
</ActionIcon>
</Tooltip>
<Text
flex={1}
>{service.service.name}</Text>
<NumberInput
suffix={" шт."}
onChange={event => isNumber(event) && onQuantityChange(service, event)}
value={service.quantity}
/>
<NumberInput
onChange={event => isNumber(event) && onPriceChange(service, event)}
suffix={"₽"}
value={service.price}
/>
</Flex>
))}
</Flex>
<Title
style={{textAlign: "end"}}
mt={rem(10)}
order={3}
>Итог: {items.reduce((acc, item) => acc + (item.price * item.quantity), 0)}</Title>
</Flex>
<Flex pb={rem(10)} mt={"auto"}>
<Button
onClick={onCreateClick}
fullWidth
variant={"default"}
>Добавить услугу</Button>
</Flex>
</Flex>
<Modal
title={"Добавление сотрудника к услуге"}
opened={employeesModalVisible}
onClose={onEmployeeModalClose}
size={"xl"}
>
<Flex direction={"column"} gap={rem(10)}>
<SimpleUsersTable
items={getCurrentEmployees()}
onChange={onEmployeesChange}
/>
</Flex>
</Modal>
</>
<Title
style={{textAlign: "end"}}
mt={rem(10)}
order={3}
>Итог: {items.reduce((acc, item) => acc + (item.price * item.quantity), 0)}</Title>
</Flex>
<Flex pb={rem(10)} mt={"auto"}>
<Button
onClick={onCreateClick}
fullWidth
variant={"default"}
>Добавить услугу</Button>
</Flex>
</Flex>
)
}
export default DealServicesTable;

View File

@@ -1,21 +1,26 @@
import {CRUDTableProps} from "../../../../../../types/CRUDTable.tsx";
import {DealProductServiceSchema} from "../../../../../../client";
import {FC} from "react";
import {DealProductServiceSchema, UserSchema} from "../../../../../../client";
import {FC, useState} from "react";
import useProductServicesTableColumns from "./columns.tsx";
import {BaseTable} from "../../../../../../components/BaseTable/BaseTable.tsx";
import {MRT_TableOptions} from "mantine-react-table";
import {ActionIcon, Button, Flex, rem, Tooltip} from "@mantine/core";
import {IconEdit, IconTrash} from "@tabler/icons-react";
import {ActionIcon, Button, Flex, Modal, rem, Tooltip} from "@mantine/core";
import {IconEdit, IconTrash, IconUsersGroup} from "@tabler/icons-react";
import {modals} from "@mantine/modals";
import SimpleUsersTable from "../../../../components/SimpleUsersTable/SimpleUsersTable.tsx";
type RestProps = {
quantity: number;
}
type Props = CRUDTableProps<DealProductServiceSchema> & RestProps;
const ProductServicesTable: FC<Props> = ({items, quantity, onCreate, onDelete, onChange}) => {
const columns = useProductServicesTableColumns({data: items, quantity});
const serviceIds = items.map(service => service.service.id);
const [currentService, setCurrentService] = useState<DealProductServiceSchema | undefined>();
const [employeesModalVisible, setEmployeesModalVisible] = useState(false);
const onCreateClick = () => {
if (!onCreate) return;
modals.openContextModal({
@@ -30,10 +35,7 @@ const ProductServicesTable: FC<Props> = ({items, quantity, onCreate, onDelete, o
}
const onChangeClick = (item: DealProductServiceSchema) => {
console.log('change click')
if (!onChange) return;
console.log('change click1')
modals.openContextModal({
modal: "productServiceForm",
innerProps: {
@@ -45,52 +47,93 @@ const ProductServicesTable: FC<Props> = ({items, quantity, onCreate, onDelete, o
withCloseButton: false
})
}
const onEmployeeClick = (item: DealProductServiceSchema) => {
if (!onChange) return;
setCurrentService(item);
setEmployeesModalVisible(true);
}
const onEmployeeModalClose = () => {
setEmployeesModalVisible(false);
setCurrentService(undefined);
}
const getCurrentEmployees = (): UserSchema[] => {
if (!currentService) return [];
const item = items.find(i => i.service.id === currentService.service.id)
if (!item) return [];
return item.employees;
}
const onEmployeesChange = (items: UserSchema[]) => {
if (!currentService || !onChange) return;
onChange({
...currentService,
employees: items
});
}
return (
<Flex
direction={"column"}
gap={rem(10)}
>
<BaseTable
data={items}
columns={columns}
restProps={{
enableColumnActions: false,
enableSorting: false,
enableRowActions: true,
enableBottomToolbar: true,
renderBottomToolbar: (
<Flex justify={"flex-end"} gap={rem(10)} p={rem(10)}>
<>
<Flex
direction={"column"}
gap={rem(10)}
>
<BaseTable
data={items}
columns={columns}
restProps={{
enableColumnActions: false,
enableSorting: false,
enableRowActions: true,
enableBottomToolbar: true,
renderBottomToolbar: (
<Flex justify={"flex-end"} gap={rem(10)} p={rem(10)}>
<Button onClick={onCreateClick} variant={"default"}>
Добавить услугу
</Button>
<Button onClick={onCreateClick} variant={"default"}>
Добавить услугу
</Button>
</Flex>
),
renderRowActions: ({row}) => (
<Flex gap="md">
<Tooltip label="Редактировать">
<ActionIcon
onClick={() => onChangeClick(row.original)}
variant={"default"}>
<IconEdit/>
</ActionIcon>
</Tooltip>
<Tooltip label="Удалить">
<ActionIcon onClick={() => {
if (onDelete) onDelete(row.original);
// console.log(row);
}} variant={"default"}>
<IconTrash/>
</ActionIcon>
</Tooltip>
</Flex>
)
} as MRT_TableOptions<DealProductServiceSchema>}
/>
{/*<Button variant={"default"}>Добавить услугу</Button>*/}
</Flex>
</Flex>
),
renderRowActions: ({row}) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon onClick={() => {
if (onDelete) onDelete(row.original);
}} variant={"default"}>
<IconTrash/>
</ActionIcon>
</Tooltip>
<Tooltip label="Редактировать">
<ActionIcon
onClick={() => onChangeClick(row.original)}
variant={"default"}>
<IconEdit/>
</ActionIcon>
</Tooltip>
<Tooltip label="Сотрудники">
<ActionIcon onClick={() => onEmployeeClick(row.original)} variant={"default"}>
<IconUsersGroup/>
</ActionIcon>
</Tooltip>
</Flex>
)
} as MRT_TableOptions<DealProductServiceSchema>}
/>
</Flex>
<Modal
title={"Добавление сотрудника к услуге"}
opened={employeesModalVisible}
onClose={onEmployeeModalClose}
size={"xl"}
>
<Flex direction={"column"} gap={rem(10)}>
<SimpleUsersTable
items={getCurrentEmployees()}
onChange={onEmployeesChange}
/>
</Flex>
</Modal>
</>
)
}

View File

@@ -90,7 +90,7 @@ const ProductView: FC<Props> = ({product, onDelete, onChange}) => {
{Object.entries(product.product).map(([key, value]) => {
const fieldName = ProductFieldNames[key as keyof ProductSchema];
if (!fieldName || isNil(value) || value === '') return;
return (<Text>{fieldName}: {value.toString()} </Text>)
return (<Text key={fieldName}>{fieldName}: {value.toString()} </Text>)
})}
<Text>Штрихкоды: {product.product.barcodes.join(', ')}</Text>
</Spoiler>

View File

@@ -96,88 +96,89 @@ export const LeadsPage: FC = () => {
<>
<DealPageContextProvider>
<PageBlock fullHeight>
<div className={styles['container']}>
<DragDropContext
onDragStart={() => {
setIsDragEnded(false);
}}
onDragEnd={onDragEnd}>
<div className={styles['boards']}>
<Board
withCreateButton
summaries={summaries
.filter(summary => summary.status == DealStatus.AWAITING_ACCEPTANCE)}
title={"Ожидает приемки"}
droppableId={"AWAITING_ACCEPTANCE"}
color={'#4A90E2'}
/>
<Board
summaries={summaries
.filter(summary => summary.status == DealStatus.PACKAGING)}
title={"Упаковка"}
droppableId={"PACKAGING"}
color={'#F5A623'}
<PageBlock fullHeight style={{
display: "flex",
flexDirection: "column"
}}>
<DragDropContext
onDragStart={() => {
setIsDragEnded(false);
}}
onDragEnd={onDragEnd}>
<div className={styles['boards']}>
<Board
withCreateButton
summaries={summaries
.filter(summary => summary.status == DealStatus.AWAITING_ACCEPTANCE)}
title={"Ожидает приемки"}
droppableId={"AWAITING_ACCEPTANCE"}
color={'#4A90E2'}
/>
<Board
summaries={summaries
.filter(summary => summary.status == DealStatus.PACKAGING)}
title={"Упаковка"}
droppableId={"PACKAGING"}
color={'#F5A623'}
/>
<Board
summaries={summaries
.filter(summary => summary.status == DealStatus.AWAITING_SHIPMENT)}
title={"Ожидает отгрузки"}
droppableId={"AWAITING_SHIPMENT"}
color={'#7ED321'}
/>
<Board
summaries={summaries
.filter(summary => summary.status == DealStatus.AWAITING_SHIPMENT)}
title={"Ожидает отгрузки"}
droppableId={"AWAITING_SHIPMENT"}
color={'#7ED321'}
/>
<Board
summaries={summaries
.filter(summary => summary.status == DealStatus.AWAITING_PAYMENT)}
title={"Ожидает оплаты"}
droppableId={"AWAITING_PAYMENT"}
color={'#D0021B'}
/>
<Board
summaries={summaries
.filter(summary => summary.status == DealStatus.AWAITING_PAYMENT)}
title={"Ожидает оплаты"}
droppableId={"AWAITING_PAYMENT"}
color={'#D0021B'}
/>
<Board
summaries={summaries
.filter(summary => summary.status == DealStatus.COMPLETED)}
title={"Завершена"}
droppableId={"COMPLETED"}
color={'#417505'}
/>
</div>
<Flex justify={"flex-end"}>
<div
className={
classNames(
styles['delete'],
isDragEnded && styles['delete-hidden']
)
}
>
<Droppable droppableId={"DELETE"}>
{(provided) => (
<>
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
{!isDragEnded &&
<span>
/>
<Board
summaries={summaries
.filter(summary => summary.status == DealStatus.COMPLETED)}
title={"Завершена"}
droppableId={"COMPLETED"}
color={'#417505'}
/>
</div>
<Flex justify={"flex-end"}>
<div
className={
classNames(
styles['delete'],
isDragEnded && styles['delete-hidden']
)
}
>
<Droppable droppableId={"DELETE"}>
{(provided) => (
<>
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
{!isDragEnded &&
<span>
Удалить
</span>
}
}
</div>
{provided.placeholder}
</>
</div>
{provided.placeholder}
</>
)}
</Droppable>
</div>
</Flex>
</DragDropContext>
)}
</Droppable>
</div>
</Flex>
</DragDropContext>
</div>
</PageBlock>
<DealEditDrawer
/>