feat: prettier
This commit is contained in:
@@ -1,75 +1,84 @@
|
||||
import styles from './AdminPage.module.css';
|
||||
import {Tabs} from "@mantine/core";
|
||||
import styles from "./AdminPage.module.css";
|
||||
import { Tabs } from "@mantine/core";
|
||||
import PageBlock from "../../components/PageBlock/PageBlock.tsx";
|
||||
import {IconBriefcase, IconCalendarUser, IconCurrencyDollar, IconUser} from "@tabler/icons-react";
|
||||
import {
|
||||
IconBriefcase,
|
||||
IconCalendarUser,
|
||||
IconCurrencyDollar,
|
||||
IconUser,
|
||||
} from "@tabler/icons-react";
|
||||
import RolesAndPositionsTab from "./tabs/RolesAndPositions/RolesAndPositionsTab.tsx";
|
||||
import UsersTab from "./tabs/Users/UsersTab.tsx";
|
||||
import {motion} from "framer-motion";
|
||||
import { motion } from "framer-motion";
|
||||
import FinancesTab from "./tabs/Finances/FinancesTab.tsx";
|
||||
import WorkTimeTable from "./tabs/WorkTimeTable/ui/WorkTimeTable.tsx";
|
||||
|
||||
const AdminPage = () => {
|
||||
|
||||
return (
|
||||
<div className={styles['container']}>
|
||||
<div className={styles["container"]}>
|
||||
<PageBlock fullHeight>
|
||||
<Tabs variant={"outline"} keepMounted={false} defaultValue={"users"}>
|
||||
<Tabs
|
||||
variant={"outline"}
|
||||
keepMounted={false}
|
||||
defaultValue={"users"}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value={"users"} leftSection={<IconUser/>}>
|
||||
<Tabs.Tab
|
||||
value={"users"}
|
||||
leftSection={<IconUser />}>
|
||||
Пользователи
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value={"finances"} leftSection={<IconCurrencyDollar/>}>
|
||||
<Tabs.Tab
|
||||
value={"finances"}
|
||||
leftSection={<IconCurrencyDollar />}>
|
||||
Финансы
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value={"rolesAndPositions"} leftSection={<IconBriefcase/>}>
|
||||
<Tabs.Tab
|
||||
value={"rolesAndPositions"}
|
||||
leftSection={<IconBriefcase />}>
|
||||
Должности
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value={"workTimeTable"} leftSection={<IconCalendarUser/>}>
|
||||
<Tabs.Tab
|
||||
value={"workTimeTable"}
|
||||
leftSection={<IconCalendarUser />}>
|
||||
Рабочее время
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value={"users"}>
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.2}}
|
||||
>
|
||||
<UsersTab/>
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
<UsersTab />
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value={"rolesAndPositions"}>
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.2}}
|
||||
>
|
||||
<RolesAndPositionsTab/>
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
<RolesAndPositionsTab />
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value={"finances"}>
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.2}}
|
||||
>
|
||||
<FinancesTab/>
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
<FinancesTab />
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value={"workTimeTable"}>
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.2}}
|
||||
>
|
||||
<WorkTimeTable/>
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
<WorkTimeTable />
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
|
||||
</Tabs>
|
||||
</PageBlock>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPage;
|
||||
export default AdminPage;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
||||
import {PayRateSchema} from "../../../../client";
|
||||
import {FC} from "react";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {usePayRatesTableColumns} from "./columns.tsx";
|
||||
import {ActionIcon, Button, Flex, rem, Text, Tooltip} from "@mantine/core";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {IconEdit, IconTrash} from "@tabler/icons-react";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { PayRateSchema } from "../../../../client";
|
||||
import { FC } from "react";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { usePayRatesTableColumns } from "./columns.tsx";
|
||||
import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
|
||||
type Props = CRUDTableProps<PayRateSchema>;
|
||||
|
||||
const PayRateTable: FC<Props> = ({items, onCreate, onChange, onDelete}) => {
|
||||
const PayRateTable: FC<Props> = ({ items, onCreate, onChange, onDelete }) => {
|
||||
const columns = usePayRatesTableColumns();
|
||||
|
||||
const onCreateClick = () => {
|
||||
@@ -19,76 +19,77 @@ const PayRateTable: FC<Props> = ({items, onCreate, onChange, onDelete}) => {
|
||||
modal: "payRateForm",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onCreate: onCreate
|
||||
}
|
||||
})
|
||||
}
|
||||
onCreate: onCreate,
|
||||
},
|
||||
});
|
||||
};
|
||||
const onEditClick = (payRate: PayRateSchema) => {
|
||||
if (!onChange) return;
|
||||
modals.openContextModal({
|
||||
modal: "payRateForm",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onChange: (event) => onChange({...event, id: payRate.id}),
|
||||
element: payRate
|
||||
}
|
||||
})
|
||||
}
|
||||
onChange: event => onChange({ ...event, id: payRate.id }),
|
||||
element: payRate,
|
||||
},
|
||||
});
|
||||
};
|
||||
const onDeleteClick = (payRate: PayRateSchema) => {
|
||||
if (!onDelete) return;
|
||||
modals.openConfirmModal({
|
||||
title: 'Удаление тарифа',
|
||||
title: "Удаление тарифа",
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Вы уверены что хотите удалить тариф {payRate.name}
|
||||
</Text>
|
||||
),
|
||||
labels: {confirm: 'Да', cancel: "Нет"},
|
||||
confirmProps: {color: 'red'},
|
||||
onConfirm: () => onDelete(payRate)
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onDelete(payRate),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseTable
|
||||
data={items}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableTopToolbar: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}
|
||||
>
|
||||
Создать тариф
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
),
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon onClick={() => onDeleteClick(row.original)} variant={"default"}>
|
||||
<IconTrash/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)
|
||||
} as MRT_TableOptions<PayRateSchema>}
|
||||
restProps={
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableTopToolbar: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}>
|
||||
Создать тариф
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => onDeleteClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<PayRateSchema>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PayRateTable;
|
||||
export default PayRateTable;
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {PayRateSchema} from "../../../../client";
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { PayRateSchema } from "../../../../client";
|
||||
|
||||
export const usePayRatesTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<PayRateSchema>[]>(() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название тарифа"
|
||||
},
|
||||
{
|
||||
accessorKey: "payrollScheme.name",
|
||||
header: "Система оплаты"
|
||||
},
|
||||
{
|
||||
accessorKey: "baseRate",
|
||||
header: "Базовая ставка",
|
||||
Cell: ({row}) => `${row.original.baseRate.toLocaleString("ru")}₽`
|
||||
|
||||
},
|
||||
{
|
||||
accessorKey: "overtimeThreshold",
|
||||
header: "Порог сверхурочных"
|
||||
},
|
||||
{
|
||||
accessorKey: "overtimeRate",
|
||||
header: "Сверхурочная ставка",
|
||||
Cell: ({row}) => row.original.overtimeRate && `${row.original.overtimeRate.toLocaleString("ru")}₽`
|
||||
|
||||
}
|
||||
], []);
|
||||
}
|
||||
return useMemo<MRT_ColumnDef<PayRateSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название тарифа",
|
||||
},
|
||||
{
|
||||
accessorKey: "payrollScheme.name",
|
||||
header: "Система оплаты",
|
||||
},
|
||||
{
|
||||
accessorKey: "baseRate",
|
||||
header: "Базовая ставка",
|
||||
Cell: ({ row }) =>
|
||||
`${row.original.baseRate.toLocaleString("ru")}₽`,
|
||||
},
|
||||
{
|
||||
accessorKey: "overtimeThreshold",
|
||||
header: "Порог сверхурочных",
|
||||
},
|
||||
{
|
||||
accessorKey: "overtimeRate",
|
||||
header: "Сверхурочная ставка",
|
||||
Cell: ({ row }) =>
|
||||
row.original.overtimeRate &&
|
||||
`${row.original.overtimeRate.toLocaleString("ru")}₽`,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import {FC, useEffect, useState} from "react";
|
||||
import {ActionIcon, Button, Flex, Pagination, rem, Text, Tooltip} from "@mantine/core";
|
||||
import {usePaymentRecordsList} from "../../../../hooks/usePaymentRecordsList.tsx";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {usePaymentRecordsTableColumns} from "./columns.tsx";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {PaymentRecordCreateSchema, PaymentRecordGetSchema, PayrollService} from "../../../../client";
|
||||
import {notifications} from "../../../../shared/lib/notifications.ts";
|
||||
import {IconTrash} from "@tabler/icons-react";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import {formatDate} from "../../../../types/utils.ts";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Flex,
|
||||
Pagination,
|
||||
rem,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { usePaymentRecordsList } from "../../../../hooks/usePaymentRecordsList.tsx";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { usePaymentRecordsTableColumns } from "./columns.tsx";
|
||||
import { modals } from "@mantine/modals";
|
||||
import {
|
||||
PaymentRecordCreateSchema,
|
||||
PaymentRecordGetSchema,
|
||||
PayrollService,
|
||||
} from "../../../../client";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { formatDate } from "../../../../types/utils.ts";
|
||||
|
||||
const PaymentRecordsTable: FC = () => {
|
||||
const [totalPages, setTotalPages] = useState(10);
|
||||
@@ -16,8 +28,8 @@ const PaymentRecordsTable: FC = () => {
|
||||
const {
|
||||
pagination: paginationInfo,
|
||||
objects: paymentRecords,
|
||||
refetch
|
||||
} = usePaymentRecordsList({page: page, itemsPerPage: 10});
|
||||
refetch,
|
||||
} = usePaymentRecordsList({ page: page, itemsPerPage: 10 });
|
||||
useEffect(() => {
|
||||
if (!paginationInfo) return;
|
||||
setTotalPages(paginationInfo.totalPages);
|
||||
@@ -26,98 +38,99 @@ const PaymentRecordsTable: FC = () => {
|
||||
const onCreate = (request: PaymentRecordCreateSchema) => {
|
||||
PayrollService.createPaymentRecord({
|
||||
requestBody: {
|
||||
data: request
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
data: request,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
const onCreateClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "createPaymentRecord",
|
||||
title: "Создание начисления",
|
||||
innerProps: {
|
||||
onCreate: onCreate
|
||||
onCreate: onCreate,
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
const onDelete = (record: PaymentRecordGetSchema) => {
|
||||
PayrollService.deletePaymentRecord({
|
||||
requestBody: {
|
||||
paymentRecordId: record.id
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
paymentRecordId: record.id,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
const onDeleteClick = (record: PaymentRecordGetSchema) => {
|
||||
modals.openConfirmModal({
|
||||
title: 'Удаление начисления',
|
||||
title: "Удаление начисления",
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Вы уверены что хотите удалить начисление
|
||||
пользователю {record.user.firstName} {record.user.secondName} от {formatDate(record.createdAt)}
|
||||
Вы уверены что хотите удалить начисление пользователю{" "}
|
||||
{record.user.firstName} {record.user.secondName} от{" "}
|
||||
{formatDate(record.createdAt)}
|
||||
</Text>
|
||||
),
|
||||
labels: {confirm: 'Да', cancel: "Нет"},
|
||||
confirmProps: {color: 'red'},
|
||||
onConfirm: () => onDelete(record)
|
||||
})
|
||||
}
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onDelete(record),
|
||||
});
|
||||
};
|
||||
const columns = usePaymentRecordsTableColumns();
|
||||
return (
|
||||
<Flex
|
||||
direction={"column"}
|
||||
h={"100%"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
gap={rem(10)}>
|
||||
<BaseTable
|
||||
data={paymentRecords}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableTopToolbar: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}
|
||||
>
|
||||
Создать начисление
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon onClick={() => onDeleteClick(row.original)} variant={"default"}>
|
||||
<IconTrash/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)
|
||||
} as MRT_TableOptions<PaymentRecordGetSchema>}
|
||||
restProps={
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableTopToolbar: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}>
|
||||
Создать начисление
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
onDeleteClick(row.original)
|
||||
}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<PaymentRecordGetSchema>
|
||||
}
|
||||
/>
|
||||
{totalPages > 1 &&
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
style={{alignSelf: "flex-end"}}
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
withEdges
|
||||
onChange={event => setPage(event)}
|
||||
value={page}
|
||||
total={totalPages}
|
||||
/>
|
||||
}
|
||||
|
||||
)}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
export default PaymentRecordsTable;
|
||||
);
|
||||
};
|
||||
export default PaymentRecordsTable;
|
||||
|
||||
@@ -1,58 +1,79 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {PaymentRecordGetSchema} from "../../../../client";
|
||||
import {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts";
|
||||
import {getPluralForm} from "../../../../shared/lib/utils.ts";
|
||||
import {formatDate} from "../../../../types/utils.ts";
|
||||
import {isEqual} from "lodash";
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { PaymentRecordGetSchema } from "../../../../client";
|
||||
import { PaySchemeType } from "../../../../shared/enums/PaySchemeType.ts";
|
||||
import { getPluralForm } from "../../../../shared/lib/utils.ts";
|
||||
import { formatDate } from "../../../../types/utils.ts";
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
export const usePaymentRecordsTableColumns = () => {
|
||||
const getWorkUnitsText = (paymentRecord: PaymentRecordGetSchema) => {
|
||||
const payrollScheme = paymentRecord.payrollScheme;
|
||||
if (payrollScheme.key === PaySchemeType.HOURLY) {
|
||||
return getPluralForm(paymentRecord.workUnits, "час", "часа", "часов")
|
||||
} else if (
|
||||
payrollScheme.key === PaySchemeType.DAILY
|
||||
) {
|
||||
return getPluralForm(paymentRecord.workUnits, "день", "дня", "дней")
|
||||
} else if (
|
||||
payrollScheme.key === PaySchemeType.MONTHLY
|
||||
) {
|
||||
return getPluralForm(paymentRecord.workUnits, "месяц", "месяца", "месяцев");
|
||||
return getPluralForm(
|
||||
paymentRecord.workUnits,
|
||||
"час",
|
||||
"часа",
|
||||
"часов"
|
||||
);
|
||||
} else if (payrollScheme.key === PaySchemeType.DAILY) {
|
||||
return getPluralForm(
|
||||
paymentRecord.workUnits,
|
||||
"день",
|
||||
"дня",
|
||||
"дней"
|
||||
);
|
||||
} else if (payrollScheme.key === PaySchemeType.MONTHLY) {
|
||||
return getPluralForm(
|
||||
paymentRecord.workUnits,
|
||||
"месяц",
|
||||
"месяца",
|
||||
"месяцев"
|
||||
);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
};
|
||||
const getDateRangesText = (paymentRecord: PaymentRecordGetSchema) => {
|
||||
if (paymentRecord.endDate && !isEqual(paymentRecord.startDate, paymentRecord.endDate)) {
|
||||
return `${formatDate(paymentRecord.startDate)} - ${formatDate(paymentRecord.endDate)}`
|
||||
if (
|
||||
paymentRecord.endDate &&
|
||||
!isEqual(paymentRecord.startDate, paymentRecord.endDate)
|
||||
) {
|
||||
return `${formatDate(paymentRecord.startDate)} - ${formatDate(paymentRecord.endDate)}`;
|
||||
}
|
||||
return `${formatDate(paymentRecord.startDate)}`;
|
||||
}
|
||||
};
|
||||
|
||||
return useMemo<MRT_ColumnDef<PaymentRecordGetSchema>[]>(() => [
|
||||
{
|
||||
header: "Дата начисления",
|
||||
Cell: ({row}) => new Date(row.original.createdAt).toLocaleString('ru-RU')
|
||||
},
|
||||
{
|
||||
header: "Получил начисление",
|
||||
Cell: ({row}) => `${row.original.user.firstName} ${row.original.user.secondName}`
|
||||
},
|
||||
{
|
||||
header: "Создал начисление",
|
||||
Cell: ({row}) => `${row.original.createdByUser.firstName} ${row.original.createdByUser.secondName}`
|
||||
},
|
||||
{
|
||||
header: "Количество",
|
||||
Cell: ({row}) => `${row.original.workUnits} ${getWorkUnitsText(row.original)}`
|
||||
},
|
||||
{
|
||||
header: "Сумма начисления",
|
||||
Cell: ({row}) => row.original.amount.toLocaleString("ru-RU")
|
||||
},
|
||||
{
|
||||
header: "Временной промежуток",
|
||||
Cell: ({row}) => getDateRangesText(row.original)
|
||||
}
|
||||
], [])
|
||||
}
|
||||
return useMemo<MRT_ColumnDef<PaymentRecordGetSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "Дата начисления",
|
||||
Cell: ({ row }) =>
|
||||
new Date(row.original.createdAt).toLocaleString("ru-RU"),
|
||||
},
|
||||
{
|
||||
header: "Получил начисление",
|
||||
Cell: ({ row }) =>
|
||||
`${row.original.user.firstName} ${row.original.user.secondName}`,
|
||||
},
|
||||
{
|
||||
header: "Создал начисление",
|
||||
Cell: ({ row }) =>
|
||||
`${row.original.createdByUser.firstName} ${row.original.createdByUser.secondName}`,
|
||||
},
|
||||
{
|
||||
header: "Количество",
|
||||
Cell: ({ row }) =>
|
||||
`${row.original.workUnits} ${getWorkUnitsText(row.original)}`,
|
||||
},
|
||||
{
|
||||
header: "Сумма начисления",
|
||||
Cell: ({ row }) => row.original.amount.toLocaleString("ru-RU"),
|
||||
},
|
||||
{
|
||||
header: "Временной промежуток",
|
||||
Cell: ({ row }) => getDateRangesText(row.original),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import {PositionSchema} from "../../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import { PositionSchema } from "../../../../client";
|
||||
import { FC } from "react";
|
||||
import usePositionsList from "../../hooks/usePositionsList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<PositionSchema>, 'data' | 'getLabelFn' | 'getValueFn'>;
|
||||
type Props = Omit<
|
||||
ObjectSelectProps<PositionSchema>,
|
||||
"data" | "getLabelFn" | "getValueFn"
|
||||
>;
|
||||
|
||||
const PositionSelect: FC<Props> = (props) => {
|
||||
const {objects: positions} = usePositionsList();
|
||||
const PositionSelect: FC<Props> = props => {
|
||||
const { objects: positions } = usePositionsList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
getLabelFn={(position) => position.name}
|
||||
getValueFn={(position) => position.key}
|
||||
getLabelFn={position => position.name}
|
||||
getValueFn={position => position.key}
|
||||
data={positions}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default PositionSelect;
|
||||
);
|
||||
};
|
||||
export default PositionSelect;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
||||
import {PositionSchema} from "../../../../client";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {usePositionsTableColumns} from "./columns.tsx";
|
||||
import {FC} from "react";
|
||||
import {ActionIcon, Button, Flex, rem, Text, Tooltip} from "@mantine/core";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {IconTrash} from "@tabler/icons-react";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { PositionSchema } from "../../../../client";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { usePositionsTableColumns } from "./columns.tsx";
|
||||
import { FC } from "react";
|
||||
import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
|
||||
type Props = CRUDTableProps<PositionSchema>;
|
||||
|
||||
const PositionsTable: FC<Props> = ({items, onCreate, onDelete}) => {
|
||||
const PositionsTable: FC<Props> = ({ items, onCreate, onDelete }) => {
|
||||
const columns = usePositionsTableColumns();
|
||||
|
||||
const onCreateClick = () => {
|
||||
@@ -19,63 +19,60 @@ const PositionsTable: FC<Props> = ({items, onCreate, onDelete}) => {
|
||||
modal: "positionForm",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onCreate: onCreate
|
||||
}
|
||||
})
|
||||
}
|
||||
onCreate: onCreate,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onDeleteClick = (position: PositionSchema) => {
|
||||
if (!onDelete) return;
|
||||
modals.openConfirmModal({
|
||||
title: 'Удаление должности',
|
||||
title: "Удаление должности",
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Вы уверены что хотите удалить должность {position.name}
|
||||
</Text>
|
||||
),
|
||||
labels: {confirm: 'Да', cancel: "Нет"},
|
||||
confirmProps: {color: 'red'},
|
||||
onConfirm: () => onDelete(position)
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onDelete(position),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<BaseTable
|
||||
data={items}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableTopToolbar: true,
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}
|
||||
>
|
||||
Создать должность
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
),
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon onClick={() => onDeleteClick(row.original)} variant={"default"}>
|
||||
<IconTrash/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)
|
||||
} as MRT_TableOptions<PositionSchema>}
|
||||
|
||||
restProps={
|
||||
{
|
||||
enableTopToolbar: true,
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}>
|
||||
Создать должность
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => onDeleteClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<PositionSchema>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default PositionsTable;
|
||||
export default PositionsTable;
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {PositionSchema} from "../../../../client";
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { PositionSchema } from "../../../../client";
|
||||
|
||||
export const usePositionsTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<PositionSchema>[]>(() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название должности"
|
||||
},
|
||||
{
|
||||
accessorKey: "key",
|
||||
header: "Ключ"
|
||||
},
|
||||
|
||||
], []);
|
||||
}
|
||||
return useMemo<MRT_ColumnDef<PositionSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название должности",
|
||||
},
|
||||
{
|
||||
accessorKey: "key",
|
||||
header: "Ключ",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import {RoleSchema} from "../../../../client";
|
||||
import {FC} from "react";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import { RoleSchema } from "../../../../client";
|
||||
import { FC } from "react";
|
||||
import useRolesList from "../../hooks/useRolesList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<RoleSchema>, 'data' | 'getLabelFn' | 'getValueFn'>;
|
||||
type Props = Omit<
|
||||
ObjectSelectProps<RoleSchema>,
|
||||
"data" | "getLabelFn" | "getValueFn"
|
||||
>;
|
||||
|
||||
const RolesSelect: FC<Props> = (props) => {
|
||||
const {objects: roles} = useRolesList();
|
||||
const RolesSelect: FC<Props> = props => {
|
||||
const { objects: roles } = useRolesList();
|
||||
return (
|
||||
<ObjectSelect
|
||||
getLabelFn={(position) => position.name}
|
||||
getValueFn={(position) => position.key}
|
||||
getLabelFn={position => position.name}
|
||||
getValueFn={position => position.key}
|
||||
data={roles}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default RolesSelect;
|
||||
);
|
||||
};
|
||||
export default RolesSelect;
|
||||
|
||||
@@ -1,102 +1,103 @@
|
||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
||||
import {UserSchema} from "../../../../client";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {FC} from "react";
|
||||
import {ActionIcon, Button, Flex, rem, Text, Tooltip} from "@mantine/core";
|
||||
import {useUsersTableColumns} from "./columns.tsx";
|
||||
import {IconEdit, IconTrash} from "@tabler/icons-react";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { UserSchema } from "../../../../client";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { FC } from "react";
|
||||
import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
|
||||
import { useUsersTableColumns } from "./columns.tsx";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
|
||||
type Props = CRUDTableProps<UserSchema>;
|
||||
|
||||
const UsersTable: FC<Props> = ({items, onChange, onDelete, onCreate}) => {
|
||||
const UsersTable: FC<Props> = ({ items, onChange, onDelete, onCreate }) => {
|
||||
const columns = useUsersTableColumns();
|
||||
const onEditClick = (user: UserSchema) => {
|
||||
if (!onChange) return;
|
||||
modals.openContextModal({
|
||||
modal: "userFormModal",
|
||||
title: 'Редактирование пользователя',
|
||||
title: "Редактирование пользователя",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onChange: onChange,
|
||||
element: user,
|
||||
},
|
||||
size: "md"
|
||||
})
|
||||
}
|
||||
size: "md",
|
||||
});
|
||||
};
|
||||
const onDeleteClick = (user: UserSchema) => {
|
||||
if (!onDelete) return;
|
||||
modals.openConfirmModal({
|
||||
title: 'Удаление пользователя',
|
||||
title: "Удаление пользователя",
|
||||
// centered: true,
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Вы уверены что хотите удалить пользователя {user.firstName} {user.secondName}
|
||||
Вы уверены что хотите удалить пользователя {user.firstName}{" "}
|
||||
{user.secondName}
|
||||
</Text>
|
||||
),
|
||||
labels: {confirm: 'Да', cancel: "Нет"},
|
||||
confirmProps: {color: 'red'},
|
||||
onConfirm: () => onDelete(user)
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onDelete(user),
|
||||
});
|
||||
}
|
||||
};
|
||||
const onCreateClick = () => {
|
||||
if (!onCreate) return;
|
||||
modals.openContextModal({
|
||||
modal: "userFormModal",
|
||||
title: 'Редактирование пользователя',
|
||||
title: "Редактирование пользователя",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onCreate: onCreate
|
||||
onCreate: onCreate,
|
||||
},
|
||||
size: "md"
|
||||
})
|
||||
}
|
||||
size: "md",
|
||||
});
|
||||
};
|
||||
return (
|
||||
<BaseTable
|
||||
data={items}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableTopToolbar: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}
|
||||
>
|
||||
Создать пользователя
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip
|
||||
onClick={() => {
|
||||
onEditClick(row.original)
|
||||
}}
|
||||
label="Редактировать">
|
||||
<ActionIcon
|
||||
variant={"default"}>
|
||||
<IconEdit/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip onClick={() => {
|
||||
onDeleteClick(row.original);
|
||||
}} label="Удалить">
|
||||
<ActionIcon variant={"default"}>
|
||||
<IconTrash/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<UserSchema>}
|
||||
|
||||
restProps={
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableTopToolbar: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)}>
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}>
|
||||
Создать пользователя
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip
|
||||
onClick={() => {
|
||||
onEditClick(row.original);
|
||||
}}
|
||||
label="Редактировать">
|
||||
<ActionIcon variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
onClick={() => {
|
||||
onDeleteClick(row.original);
|
||||
}}
|
||||
label="Удалить">
|
||||
<ActionIcon variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<UserSchema>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default UsersTable;
|
||||
export default UsersTable;
|
||||
|
||||
@@ -1,45 +1,49 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {UserSchema} from "../../../../client";
|
||||
import {IconCheck, IconX} from "@tabler/icons-react";
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { UserSchema } from "../../../../client";
|
||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
|
||||
export const useUsersTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<UserSchema>[]>(() => [
|
||||
|
||||
{
|
||||
header: "ФИО",
|
||||
Cell: ({row}) => `${row.original.firstName} ${row.original.secondName}`
|
||||
},
|
||||
{
|
||||
accessorKey: "phoneNumber",
|
||||
header: "Номер телефона"
|
||||
},
|
||||
{
|
||||
accessorKey: "role.name",
|
||||
header: "Роль"
|
||||
},
|
||||
{
|
||||
accessorKey: "position.name",
|
||||
header: "Должность"
|
||||
},
|
||||
{
|
||||
accessorKey: "payRate.name",
|
||||
header: "Тариф"
|
||||
},
|
||||
{
|
||||
accessorKey: "comment",
|
||||
header: "Дополнительная информация"
|
||||
},
|
||||
{
|
||||
accessorKey: "isAdmin",
|
||||
header: "Администратор",
|
||||
Cell: ({row}) => row.original.isAdmin ? <IconCheck/> : <IconX/>
|
||||
},
|
||||
{
|
||||
accessorKey: "isBlocked",
|
||||
header: "Заблокирован",
|
||||
Cell: ({row}) => row.original.isBlocked ? <IconCheck/> : <IconX/>
|
||||
},
|
||||
], []);
|
||||
}
|
||||
|
||||
return useMemo<MRT_ColumnDef<UserSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "ФИО",
|
||||
Cell: ({ row }) =>
|
||||
`${row.original.firstName} ${row.original.secondName}`,
|
||||
},
|
||||
{
|
||||
accessorKey: "phoneNumber",
|
||||
header: "Номер телефона",
|
||||
},
|
||||
{
|
||||
accessorKey: "role.name",
|
||||
header: "Роль",
|
||||
},
|
||||
{
|
||||
accessorKey: "position.name",
|
||||
header: "Должность",
|
||||
},
|
||||
{
|
||||
accessorKey: "payRate.name",
|
||||
header: "Тариф",
|
||||
},
|
||||
{
|
||||
accessorKey: "comment",
|
||||
header: "Дополнительная информация",
|
||||
},
|
||||
{
|
||||
accessorKey: "isAdmin",
|
||||
header: "Администратор",
|
||||
Cell: ({ row }) =>
|
||||
row.original.isAdmin ? <IconCheck /> : <IconX />,
|
||||
},
|
||||
{
|
||||
accessorKey: "isBlocked",
|
||||
header: "Заблокирован",
|
||||
Cell: ({ row }) =>
|
||||
row.original.isBlocked ? <IconCheck /> : <IconX />,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {PayrollService} from "../../../client";
|
||||
import { PayrollService } from "../../../client";
|
||||
import ObjectList from "../../../hooks/objectList.tsx";
|
||||
|
||||
const usePayRatesList = () => ObjectList({
|
||||
queryFn: PayrollService.getAllPayRates,
|
||||
getObjectsFn: response => response.payRates,
|
||||
queryKey: "getAllPayRates"
|
||||
})
|
||||
export default usePayRatesList;
|
||||
const usePayRatesList = () =>
|
||||
ObjectList({
|
||||
queryFn: PayrollService.getAllPayRates,
|
||||
getObjectsFn: response => response.payRates,
|
||||
queryKey: "getAllPayRates",
|
||||
});
|
||||
export default usePayRatesList;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {PositionService} from "../../../client";
|
||||
import { PositionService } from "../../../client";
|
||||
import ObjectList from "../../../hooks/objectList.tsx";
|
||||
|
||||
const usePositionsList = () => ObjectList({
|
||||
queryFn: PositionService.getAllPositions,
|
||||
getObjectsFn: response => response.positions,
|
||||
queryKey: "getAllPositions"
|
||||
})
|
||||
export default usePositionsList;
|
||||
const usePositionsList = () =>
|
||||
ObjectList({
|
||||
queryFn: PositionService.getAllPositions,
|
||||
getObjectsFn: response => response.positions,
|
||||
queryKey: "getAllPositions",
|
||||
});
|
||||
export default usePositionsList;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import ObjectList from "../../../hooks/objectList.tsx";
|
||||
import {RoleService} from "../../../client";
|
||||
import { RoleService } from "../../../client";
|
||||
|
||||
const useRolesList = () => ObjectList({
|
||||
queryFn: RoleService.getAllRoles,
|
||||
getObjectsFn: response => response.roles,
|
||||
queryKey: "getAllRoles"
|
||||
})
|
||||
export default useRolesList;
|
||||
const useRolesList = () =>
|
||||
ObjectList({
|
||||
queryFn: RoleService.getAllRoles,
|
||||
getObjectsFn: response => response.roles,
|
||||
queryKey: "getAllRoles",
|
||||
});
|
||||
export default useRolesList;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import ObjectList from "../../../hooks/objectList.tsx";
|
||||
import {UserService} from "../../../client";
|
||||
import { UserService } from "../../../client";
|
||||
|
||||
const useUsersList = () =>
|
||||
ObjectList({
|
||||
queryFn: UserService.getAllUsers,
|
||||
getObjectsFn: response => response.users,
|
||||
queryKey: "getAllUsers",
|
||||
});
|
||||
|
||||
const useUsersList = () => ObjectList({
|
||||
queryFn: UserService.getAllUsers,
|
||||
getObjectsFn: (response) => response.users,
|
||||
queryKey: "getAllUsers"
|
||||
});
|
||||
|
||||
export default useUsersList;
|
||||
export default useUsersList;
|
||||
|
||||
@@ -1,41 +1,51 @@
|
||||
import BaseFormModal, {CreateProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import {PaymentRecordCreateSchema} from "../../../../client";
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {Flex, NumberInput, rem} from "@mantine/core";
|
||||
import {DatePickerInput, MonthPickerInput} from "@mantine/dates";
|
||||
import {useEffect, useState} from "react";
|
||||
import BaseFormModal, {
|
||||
CreateProps,
|
||||
} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { PaymentRecordCreateSchema } from "../../../../client";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { Flex, NumberInput, rem } from "@mantine/core";
|
||||
import { DatePickerInput, MonthPickerInput } from "@mantine/dates";
|
||||
import { useEffect, useState } from "react";
|
||||
import UserSelect from "../../../../components/Selects/UserSelect/UserSelect.tsx";
|
||||
import {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts";
|
||||
import {motion} from "framer-motion";
|
||||
import {dateWithoutTimezone} from "../../../../shared/lib/date.ts";
|
||||
import { PaySchemeType } from "../../../../shared/enums/PaySchemeType.ts";
|
||||
import { motion } from "framer-motion";
|
||||
import { dateWithoutTimezone } from "../../../../shared/lib/date.ts";
|
||||
|
||||
type Props = CreateProps<PaymentRecordCreateSchema>;
|
||||
const CreatePaymentRecordModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps
|
||||
}: ContextModalProps<Props>) => {
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const form = useForm<Partial<PaymentRecordCreateSchema>>({
|
||||
validate: {
|
||||
user: (user) => !user && "Необходимо выбрать сотрудника",
|
||||
startDate: (startDate) => !startDate && "Необходимо указать временной промежуток",
|
||||
workUnits: (workUnits) => !workUnits && "Укажите количество"
|
||||
}
|
||||
})
|
||||
const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([null, null]);
|
||||
user: user => !user && "Необходимо выбрать сотрудника",
|
||||
startDate: startDate =>
|
||||
!startDate && "Необходимо указать временной промежуток",
|
||||
workUnits: workUnits => !workUnits && "Укажите количество",
|
||||
},
|
||||
});
|
||||
const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([
|
||||
null,
|
||||
null,
|
||||
]);
|
||||
useEffect(() => {
|
||||
|
||||
const setDates = (start: string | undefined, end: string | undefined) => {
|
||||
const setDates = (
|
||||
start: string | undefined,
|
||||
end: string | undefined
|
||||
) => {
|
||||
form.setFieldValue("startDate", start);
|
||||
form.setFieldValue("endDate", end);
|
||||
};
|
||||
|
||||
if (dateRange.every(dr => dr == null)) {
|
||||
setDates(undefined, undefined);
|
||||
return
|
||||
return;
|
||||
} else {
|
||||
const notNullValues = dateRange.filter((dr): dr is Date => dr !== null).map(dateWithoutTimezone);
|
||||
const notNullValues = dateRange
|
||||
.filter((dr): dr is Date => dr !== null)
|
||||
.map(dateWithoutTimezone);
|
||||
const startDate = notNullValues[0];
|
||||
const endDate = notNullValues[1] || startDate;
|
||||
setDates(startDate, endDate);
|
||||
@@ -43,14 +53,13 @@ const CreatePaymentRecordModal = ({
|
||||
}, [dateRange]);
|
||||
|
||||
const getDateRangeInput = () => {
|
||||
if (!form.values.user) return <></>
|
||||
if (!form.values.user) return <></>;
|
||||
const payRate = form.values.user.payRate;
|
||||
if (!payRate) return <></>;
|
||||
if (payRate.payrollScheme.key == PaySchemeType.MONTHLY)
|
||||
return (
|
||||
<MonthPickerInput
|
||||
error={form.getInputProps("startDate").error}
|
||||
|
||||
label={"Временной промежуток"}
|
||||
placeholder={"Выберите временной промежуток"}
|
||||
type={"range"}
|
||||
@@ -58,73 +67,78 @@ const CreatePaymentRecordModal = ({
|
||||
onChange={setDateRange}
|
||||
allowSingleDateInRange
|
||||
/>
|
||||
)
|
||||
return (<DatePickerInput
|
||||
error={form.getInputProps("startDate").error}
|
||||
label={"Временной промежуток"}
|
||||
placeholder={"Выберите временной промежуток"}
|
||||
type={"range"}
|
||||
allowSingleDateInRange
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
/>);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<DatePickerInput
|
||||
error={form.getInputProps("startDate").error}
|
||||
label={"Временной промежуток"}
|
||||
placeholder={"Выберите временной промежуток"}
|
||||
type={"range"}
|
||||
allowSingleDateInRange
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const getAmountLabel = () => {
|
||||
const user = form.values.user;
|
||||
if (!user) return "";
|
||||
const payRate = user?.payRate;
|
||||
if (!payRate) return "";
|
||||
if (payRate.payrollScheme.key == PaySchemeType.HOURLY) return "Количество часов";
|
||||
if (payRate.payrollScheme.key == PaySchemeType.MONTHLY) return "Количество месяцев";
|
||||
if (payRate.payrollScheme.key == PaySchemeType.DAILY) return "Количество дней";
|
||||
if (payRate.payrollScheme.key == PaySchemeType.HOURLY)
|
||||
return "Количество часов";
|
||||
if (payRate.payrollScheme.key == PaySchemeType.MONTHLY)
|
||||
return "Количество месяцев";
|
||||
if (payRate.payrollScheme.key == PaySchemeType.DAILY)
|
||||
return "Количество дней";
|
||||
return "";
|
||||
}
|
||||
};
|
||||
const getAmountPlaceholder = () => {
|
||||
return "Укажите " + getAmountLabel().toLowerCase();
|
||||
}
|
||||
return (<BaseFormModal
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
{...innerProps}
|
||||
>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
<UserSelect
|
||||
label={"Сотрудник"}
|
||||
placeholder={"Выберите сотрудника"}
|
||||
searchable
|
||||
filterBy={(user) => !!user.payRate}
|
||||
{...form.getInputProps("user")}
|
||||
/>
|
||||
};
|
||||
return (
|
||||
<BaseFormModal
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
{...innerProps}>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<UserSelect
|
||||
label={"Сотрудник"}
|
||||
placeholder={"Выберите сотрудника"}
|
||||
searchable
|
||||
filterBy={user => !!user.payRate}
|
||||
{...form.getInputProps("user")}
|
||||
/>
|
||||
|
||||
{form.values.user &&
|
||||
<>
|
||||
<motion.div
|
||||
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.3}}
|
||||
>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
|
||||
{getDateRangeInput()}
|
||||
<NumberInput
|
||||
label={getAmountLabel()}
|
||||
placeholder={getAmountPlaceholder()}
|
||||
hideControls
|
||||
{...form.getInputProps("workUnits")}
|
||||
/>
|
||||
</Flex>
|
||||
</motion.div>
|
||||
</>
|
||||
}
|
||||
</Flex>
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>)
|
||||
}
|
||||
export default CreatePaymentRecordModal;
|
||||
{form.values.user && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
{getDateRangeInput()}
|
||||
<NumberInput
|
||||
label={getAmountLabel()}
|
||||
placeholder={getAmountPlaceholder()}
|
||||
hideControls
|
||||
{...form.getInputProps("workUnits")}
|
||||
/>
|
||||
</Flex>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
);
|
||||
};
|
||||
export default CreatePaymentRecordModal;
|
||||
|
||||
@@ -1,41 +1,47 @@
|
||||
import BaseFormModal, {CreateEditFormProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import {PayRateSchemaBase} from "../../../../client";
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {Fieldset, Flex, NumberInput, rem, TextInput} from "@mantine/core";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { PayRateSchemaBase } from "../../../../client";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { Fieldset, Flex, NumberInput, rem, TextInput } from "@mantine/core";
|
||||
import PayrollSchemeSelect from "../../../../components/Selects/PayrollSchemeSelect/PayrollSchemeSelect.tsx";
|
||||
import {PaySchemeType} from "../../../../shared/enums/PaySchemeType.ts";
|
||||
import { PaySchemeType } from "../../../../shared/enums/PaySchemeType.ts";
|
||||
|
||||
type Props = CreateEditFormProps<PayRateSchemaBase>
|
||||
type Props = CreateEditFormProps<PayRateSchemaBase>;
|
||||
|
||||
const PayRateFormModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = 'element' in innerProps;
|
||||
const initialValue: Partial<PayRateSchemaBase> = isEditing ? innerProps.element : {};
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "element" in innerProps;
|
||||
const initialValue: Partial<PayRateSchemaBase> = isEditing
|
||||
? innerProps.element
|
||||
: {};
|
||||
|
||||
const form = useForm<Partial<PayRateSchemaBase>>({
|
||||
initialValues: initialValue,
|
||||
validate: {
|
||||
name: (name) => !name && "Необходимо указать название тарифа",
|
||||
payrollScheme: (scheme) => !scheme && "Необходимо выбрать систему оплаты",
|
||||
baseRate: (baseRate) => !baseRate && "Небходимо указать базовую ставку"
|
||||
}
|
||||
name: name => !name && "Необходимо указать название тарифа",
|
||||
payrollScheme: scheme =>
|
||||
!scheme && "Необходимо выбрать систему оплаты",
|
||||
baseRate: baseRate =>
|
||||
!baseRate && "Небходимо указать базовую ставку",
|
||||
},
|
||||
});
|
||||
return (
|
||||
<BaseFormModal
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
{...innerProps}
|
||||
>
|
||||
{...innerProps}>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Fieldset legend={"Общие параметры"}>
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<TextInput
|
||||
label={"Название"}
|
||||
placeholder={"Введите название тарифа"}
|
||||
@@ -47,11 +53,11 @@ const PayRateFormModal = ({
|
||||
{...form.getInputProps("payrollScheme")}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
</Fieldset>
|
||||
<Fieldset>
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<NumberInput
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
@@ -61,38 +67,42 @@ const PayRateFormModal = ({
|
||||
thousandSeparator={" "}
|
||||
suffix={"₽"}
|
||||
{...form.getInputProps("baseRate")}
|
||||
|
||||
/>
|
||||
{form.values.payrollScheme?.key === PaySchemeType.HOURLY &&
|
||||
{form.values.payrollScheme?.key ===
|
||||
PaySchemeType.HOURLY && (
|
||||
<>
|
||||
<NumberInput
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
allowDecimal={false}
|
||||
label={"Порог сверхурочных"}
|
||||
placeholder={"Введите порог сверхурочных"}
|
||||
{...form.getInputProps("overtimeThreshold")}
|
||||
placeholder={
|
||||
"Введите порог сверхурочных"
|
||||
}
|
||||
{...form.getInputProps(
|
||||
"overtimeThreshold"
|
||||
)}
|
||||
/>
|
||||
<NumberInput
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
decimalScale={2}
|
||||
label={"Сверхурочная ставка"}
|
||||
placeholder={"Выберите сверхурочную ставку"}
|
||||
placeholder={
|
||||
"Выберите сверхурочную ставку"
|
||||
}
|
||||
thousandSeparator={" "}
|
||||
suffix={"₽"}
|
||||
{...form.getInputProps("overtimeRate")}
|
||||
/>
|
||||
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
</Fieldset>
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default PayRateFormModal;
|
||||
export default PayRateFormModal;
|
||||
|
||||
@@ -1,46 +1,65 @@
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import BaseFormModal, {CreateEditFormProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import {UserSchema} from "../../../../client";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {Checkbox, Fieldset, Input, Stack, Textarea, TextInput} from "@mantine/core";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { UserSchema } from "../../../../client";
|
||||
import { useForm } from "@mantine/form";
|
||||
import {
|
||||
Checkbox,
|
||||
Fieldset,
|
||||
Input,
|
||||
Stack,
|
||||
Textarea,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import RoleSelect from "../../components/RoleSelect/RoleSelect.tsx";
|
||||
import PositionSelect from "../../components/PositionSelect/PositionSelect.tsx";
|
||||
import {UserRoleEnum} from "../../../../shared/enums/UserRole.ts";
|
||||
import {capitalize} from "lodash";
|
||||
import {IMaskInput} from "react-imask";
|
||||
import { UserRoleEnum } from "../../../../shared/enums/UserRole.ts";
|
||||
import { capitalize } from "lodash";
|
||||
import { IMaskInput } from "react-imask";
|
||||
import phone from "phone";
|
||||
import PayRateSelect from "../../../../components/Selects/PayRateSelect/PayRateSelect.tsx";
|
||||
|
||||
type Props = CreateEditFormProps<UserSchema>;
|
||||
const UserFormModal = ({context, id, innerProps}: ContextModalProps<Props>) => {
|
||||
const isEditing = 'element' in innerProps;
|
||||
const initialValues = isEditing ? innerProps.element : {
|
||||
isAdmin: false,
|
||||
isBlocked: false,
|
||||
isDeleted: false,
|
||||
comment: "",
|
||||
roleKey: UserRoleEnum.USER
|
||||
};
|
||||
const UserFormModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "element" in innerProps;
|
||||
const initialValues = isEditing
|
||||
? innerProps.element
|
||||
: {
|
||||
isAdmin: false,
|
||||
isBlocked: false,
|
||||
isDeleted: false,
|
||||
comment: "",
|
||||
roleKey: UserRoleEnum.USER,
|
||||
};
|
||||
|
||||
const form = useForm<Partial<UserSchema>>({
|
||||
initialValues: initialValues,
|
||||
validate: {
|
||||
firstName: value => !value?.trim() && "Укажите имя пользователя",
|
||||
secondName: value => !value?.trim() && "Укажите фамилию",
|
||||
position: (value, values) => ((values.role?.key === UserRoleEnum.EMPLOYEE) && (!value)) && 'Необходимо указать должность сотрудника',
|
||||
phoneNumber: value => !phone(value || '', {
|
||||
country: "",
|
||||
strictDetection: false,
|
||||
validateMobilePrefix: false
|
||||
}).isValid && 'Неверно указан номер телефона',
|
||||
}
|
||||
position: (value, values) =>
|
||||
values.role?.key === UserRoleEnum.EMPLOYEE &&
|
||||
!value &&
|
||||
"Необходимо указать должность сотрудника",
|
||||
phoneNumber: value =>
|
||||
!phone(value || "", {
|
||||
country: "",
|
||||
strictDetection: false,
|
||||
validateMobilePrefix: false,
|
||||
}).isValid && "Неверно указан номер телефона",
|
||||
},
|
||||
});
|
||||
return (<BaseFormModal
|
||||
return (
|
||||
<BaseFormModal
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
{...innerProps}
|
||||
>
|
||||
{...innerProps}>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Fieldset legend={"Общая информация"}>
|
||||
@@ -49,19 +68,33 @@ const UserFormModal = ({context, id, innerProps}: ContextModalProps<Props>) => {
|
||||
label={"Имя"}
|
||||
placeholder={"Введите имя пользователя"}
|
||||
{...form.getInputProps("firstName")}
|
||||
onChange={event => form.getInputProps('firstName').onChange(capitalize(event.target.value).trim())}
|
||||
|
||||
onChange={event =>
|
||||
form
|
||||
.getInputProps("firstName")
|
||||
.onChange(
|
||||
capitalize(
|
||||
event.target.value
|
||||
).trim()
|
||||
)
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
{...form.getInputProps("secondName")}
|
||||
label={"Фамилия"}
|
||||
placeholder={"Введите фамилию пользователя"}
|
||||
onChange={event => form.getInputProps('secondName').onChange(capitalize(event.target.value).trim())}
|
||||
onChange={event =>
|
||||
form
|
||||
.getInputProps("secondName")
|
||||
.onChange(
|
||||
capitalize(
|
||||
event.target.value
|
||||
).trim()
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Input.Wrapper
|
||||
label={"Номер телефона"}
|
||||
error={form.getInputProps("phoneNumber").error}
|
||||
>
|
||||
error={form.getInputProps("phoneNumber").error}>
|
||||
<Input
|
||||
component={IMaskInput}
|
||||
mask="+7 000 000-00-00"
|
||||
@@ -76,46 +109,53 @@ const UserFormModal = ({context, id, innerProps}: ContextModalProps<Props>) => {
|
||||
<RoleSelect
|
||||
label={"Роль пользователя"}
|
||||
placeholder={"Выберите роль пользователя"}
|
||||
{...form.getInputProps('role')}
|
||||
{...form.getInputProps("role")}
|
||||
/>
|
||||
{form.values.role?.key === UserRoleEnum.EMPLOYEE &&
|
||||
{form.values.role?.key ===
|
||||
UserRoleEnum.EMPLOYEE && (
|
||||
<>
|
||||
<PositionSelect
|
||||
label={"Должность сотрудника"}
|
||||
placeholder={"Выберите должность сотрудника"}
|
||||
{...form.getInputProps('position')}
|
||||
placeholder={
|
||||
"Выберите должность сотрудника"
|
||||
}
|
||||
{...form.getInputProps("position")}
|
||||
/>
|
||||
<PayRateSelect
|
||||
label={"Тариф"}
|
||||
placeholder={"Выберите тариф сотрудника"}
|
||||
placeholder={
|
||||
"Выберите тариф сотрудника"
|
||||
}
|
||||
{...form.getInputProps("payRate")}
|
||||
/>
|
||||
</>
|
||||
|
||||
}
|
||||
)}
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
<Fieldset legend={"Дополнительные параметры"}>
|
||||
<Stack>
|
||||
<Checkbox
|
||||
label={"Права администратора"}
|
||||
|
||||
{...form.getInputProps('isAdmin', {type: "checkbox"})}
|
||||
{...form.getInputProps("isAdmin", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Заблокирован"}
|
||||
{...form.getInputProps('isBlocked', {type: "checkbox"})}
|
||||
{...form.getInputProps("isBlocked", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Textarea
|
||||
label={"Дополнительная информация"}
|
||||
{...form.getInputProps('comment')}
|
||||
{...form.getInputProps("comment")}
|
||||
/>
|
||||
</Stack>
|
||||
</Fieldset>
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default UserFormModal;
|
||||
export default UserFormModal;
|
||||
|
||||
@@ -1,50 +1,54 @@
|
||||
import {Tabs} from "@mantine/core";
|
||||
import {IconBusinessplan, IconHistory} from "@tabler/icons-react";
|
||||
import {motion} from "framer-motion";
|
||||
import { Tabs } from "@mantine/core";
|
||||
import { IconBusinessplan, IconHistory } from "@tabler/icons-react";
|
||||
import { motion } from "framer-motion";
|
||||
import PayRateTable from "../../components/PayRateTable/PayRateTable.tsx";
|
||||
import usePayRatesList from "../../hooks/usePayRatesList.tsx";
|
||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
||||
import {PayRateSchema, PayRateSchemaBase, PayrollService} from "../../../../client";
|
||||
import {notifications} from "../../../../shared/lib/notifications.ts";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import {
|
||||
PayRateSchema,
|
||||
PayRateSchemaBase,
|
||||
PayrollService,
|
||||
} from "../../../../client";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import PaymentRecordsTable from "../../components/PaymentRecordsTable/PaymentRecordsTable.tsx";
|
||||
|
||||
const payRateTableState = (): CRUDTableProps<PayRateSchema> => {
|
||||
const {objects: items, refetch} = usePayRatesList();
|
||||
const { objects: items, refetch } = usePayRatesList();
|
||||
const onCreate = (item: PayRateSchemaBase) => {
|
||||
PayrollService.createPayRate({
|
||||
requestBody: {
|
||||
data: item
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
data: item,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
});
|
||||
}
|
||||
};
|
||||
const onChange = (item: PayRateSchema) => {
|
||||
PayrollService.updatePayRate({
|
||||
requestBody: {
|
||||
data: item
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
data: item,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
});
|
||||
}
|
||||
};
|
||||
const onDelete = (item: PayRateSchema) => {
|
||||
PayrollService.deletePayRate({
|
||||
requestBody: {
|
||||
payRateId: item.id
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
payRateId: item.id,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
});
|
||||
}
|
||||
return {items, onCreate, onChange, onDelete};
|
||||
}
|
||||
};
|
||||
return { items, onCreate, onChange, onDelete };
|
||||
};
|
||||
|
||||
const FinancesTab = () => {
|
||||
const payRateState = payRateTableState();
|
||||
@@ -53,45 +57,40 @@ const FinancesTab = () => {
|
||||
<Tabs
|
||||
keepMounted={false}
|
||||
defaultValue={"paymentRecords"}
|
||||
color={"gray.6"}
|
||||
>
|
||||
color={"gray.6"}>
|
||||
<Tabs.List
|
||||
justify={"center"}
|
||||
grow
|
||||
>
|
||||
<Tabs.Tab value={"paymentRecords"} leftSection={<IconHistory/>}>
|
||||
grow>
|
||||
<Tabs.Tab
|
||||
value={"paymentRecords"}
|
||||
leftSection={<IconHistory />}>
|
||||
Начисления
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value={"tariffs"} leftSection={<IconBusinessplan/>}>
|
||||
<Tabs.Tab
|
||||
value={"tariffs"}
|
||||
leftSection={<IconBusinessplan />}>
|
||||
Тарифы
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value={"tariffs"}>
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.2}}
|
||||
|
||||
>
|
||||
<PayRateTable
|
||||
{...payRateState}
|
||||
/>
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
<PayRateTable {...payRateState} />
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value={"paymentRecords"}>
|
||||
<motion.div
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: 1}}
|
||||
transition={{duration: 0.2}}
|
||||
>
|
||||
<PaymentRecordsTable/>
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}>
|
||||
<PaymentRecordsTable />
|
||||
</motion.div>
|
||||
</Tabs.Panel>
|
||||
|
||||
|
||||
</Tabs>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FinancesTab
|
||||
export default FinancesTab;
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import PositionsTable from "../../components/PositionsTable/PositionsTable.tsx";
|
||||
import usePositionsList from "../../hooks/usePositionsList.tsx";
|
||||
import {PositionSchema, PositionService} from "../../../../client";
|
||||
import {notifications} from "../../../../shared/lib/notifications.ts";
|
||||
import { PositionSchema, PositionService } from "../../../../client";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
|
||||
const RolesAndPositionsTab = () => {
|
||||
const {objects: positions, refetch} = usePositionsList();
|
||||
const { objects: positions, refetch } = usePositionsList();
|
||||
const onCreate = (position: PositionSchema) => {
|
||||
PositionService.createPosition({
|
||||
requestBody: {
|
||||
data: position
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
data: position,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
const onDelete = (position: PositionSchema) => {
|
||||
PositionService.deletePosition({
|
||||
requestBody: {
|
||||
positionKey: position.key
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
positionKey: position.key,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PositionsTable
|
||||
@@ -34,6 +34,6 @@ const RolesAndPositionsTab = () => {
|
||||
onCreate={onCreate}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default RolesAndPositionsTab;
|
||||
);
|
||||
};
|
||||
export default RolesAndPositionsTab;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import useUsersList from "../../hooks/useUsersList.tsx";
|
||||
import UsersTable from "../../components/UsersTable/UsersTable.tsx";
|
||||
import {UserSchema, UserService} from "../../../../client";
|
||||
import {notifications} from "../../../../shared/lib/notifications.ts";
|
||||
import { UserSchema, UserService } from "../../../../client";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
|
||||
const UsersTab = () => {
|
||||
const {objects: users, refetch} = useUsersList();
|
||||
const { objects: users, refetch } = useUsersList();
|
||||
|
||||
const onChange = (user: UserSchema) => {
|
||||
UserService.updateUser({
|
||||
@@ -12,32 +12,32 @@ const UsersTab = () => {
|
||||
data: {
|
||||
...user,
|
||||
positionKey: user.position?.key,
|
||||
roleKey: user.role.key
|
||||
}
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
roleKey: user.role.key,
|
||||
},
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
const onDelete = async (user: UserSchema) => {
|
||||
onChange({...user, isDeleted: true});
|
||||
}
|
||||
onChange({ ...user, isDeleted: true });
|
||||
};
|
||||
const onCreate = (user: UserSchema) => {
|
||||
UserService.createUser({
|
||||
requestBody: {
|
||||
data: {
|
||||
...user,
|
||||
telegramId: -1
|
||||
}
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
telegramId: -1,
|
||||
},
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<UsersTable
|
||||
items={users}
|
||||
@@ -45,7 +45,7 @@ const UsersTab = () => {
|
||||
onDelete={onDelete}
|
||||
onCreate={onCreate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersTab;
|
||||
export default UsersTab;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {getDayOfWeek} from "../../../../../shared/lib/date.ts";
|
||||
import {Box, Flex, NumberInput, rem, Text} from "@mantine/core";
|
||||
import {isNumber, last} from "lodash";
|
||||
import {processSelectedCells} from "../../../../../shared/lib/interpolateCells.ts";
|
||||
import {TimeTrackingData} from "../../../../../client";
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { getDayOfWeek } from "../../../../../shared/lib/date.ts";
|
||||
import { Box, Flex, NumberInput, rem, Text } from "@mantine/core";
|
||||
import { isNumber, last } from "lodash";
|
||||
import { processSelectedCells } from "../../../../../shared/lib/interpolateCells.ts";
|
||||
import { TimeTrackingData } from "../../../../../client";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export type EmployeeData = {
|
||||
@@ -14,143 +14,153 @@ export type EmployeeData = {
|
||||
comment: string;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
data?: TimeTrackingData[],
|
||||
data?: TimeTrackingData[];
|
||||
[key: string]: number | string;
|
||||
};
|
||||
type Props = {
|
||||
month: Date;
|
||||
data: EmployeeData[];
|
||||
onUpdate: (date: Date, userId: number, value: number) => void,
|
||||
onUpdate: (date: Date, userId: number, value: number) => void;
|
||||
selectedCells: string[];
|
||||
setSelectedCells: (cells: string[]) => void
|
||||
setSelectedCells: (cells: string[]) => void;
|
||||
selectedBoundaries: [Date | null, Date | null];
|
||||
range: dayjs.Dayjs[];
|
||||
}
|
||||
};
|
||||
const useWorkTableColumns = ({
|
||||
month,
|
||||
onUpdate,
|
||||
data,
|
||||
selectedCells,
|
||||
setSelectedCells,
|
||||
selectedBoundaries,
|
||||
range
|
||||
}: Props) => {
|
||||
month,
|
||||
onUpdate,
|
||||
data,
|
||||
selectedCells,
|
||||
setSelectedCells,
|
||||
selectedBoundaries,
|
||||
range,
|
||||
}: Props) => {
|
||||
const totalAmount = useMemo(() => {
|
||||
return data.reduce((acc, value) => {
|
||||
if (value.data) {
|
||||
const sum = value.data.reduce((innerAcc, item) => innerAcc + item.amount, 0);
|
||||
const sum = value.data.reduce(
|
||||
(innerAcc, item) => innerAcc + item.amount,
|
||||
0
|
||||
);
|
||||
return acc + sum;
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
}, [data, month, selectedCells, selectedBoundaries]);
|
||||
const getBorderStyles = (cellId: string) => {
|
||||
if (selectedCells.length <= 1) return {}
|
||||
if (selectedCells.length <= 1) return {};
|
||||
if (selectedCells[0] === cellId)
|
||||
return {
|
||||
borderTopLeftRadius: rem(20),
|
||||
borderBottomLeftRadius: rem(20),
|
||||
}
|
||||
};
|
||||
else if (last(selectedCells) === cellId)
|
||||
return {
|
||||
borderTopRightRadius: rem(20),
|
||||
borderBottomRightRadius: rem(20),
|
||||
}
|
||||
return {}
|
||||
}
|
||||
};
|
||||
return {};
|
||||
};
|
||||
|
||||
return useMemo<MRT_ColumnDef<EmployeeData>[]>(() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "ФИО",
|
||||
Cell: ({row}) => (
|
||||
<Flex direction={"column"}>
|
||||
<Text size={"sm"}>
|
||||
{row.original.name}
|
||||
</Text>
|
||||
<Text size={"xs"}>
|
||||
{row.original.comment}
|
||||
</Text>
|
||||
</Flex>
|
||||
)
|
||||
},
|
||||
|
||||
...range.map(date => ({
|
||||
size: 80,
|
||||
accessorKey: date.date().toString(),
|
||||
header: date.date().toString(),
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
Header: (
|
||||
<Flex
|
||||
align={"center"}
|
||||
direction={"column"}>
|
||||
<Box>
|
||||
{date.date()}
|
||||
</Box>
|
||||
<Box>
|
||||
{getDayOfWeek(date.day())}
|
||||
</Box>
|
||||
</Flex>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
mantineTableBodyCellProps: ({cell}) => ({
|
||||
style: selectedCells.includes(cell.id) ? {
|
||||
backgroundColor: "var(--mantine-primary-color-filled)",
|
||||
...getBorderStyles(cell.id)
|
||||
} : {},
|
||||
onClick: () => {
|
||||
const result = processSelectedCells(selectedCells, cell.id);
|
||||
setSelectedCells(result);
|
||||
},
|
||||
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
Cell: ({cell, row}) => {
|
||||
return (
|
||||
return useMemo<MRT_ColumnDef<EmployeeData>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "ФИО",
|
||||
Cell: ({ row }) => (
|
||||
<Flex direction={"column"}>
|
||||
<NumberInput
|
||||
// key={row.original.name + date.date().toString()}
|
||||
onChange={event => isNumber(event) && onUpdate(date.toDate(), row.original.userId, event)}
|
||||
styles={{input: {textAlign: "center"}}}
|
||||
hideControls
|
||||
value={cell.renderValue()}
|
||||
/>
|
||||
<Text size={"sm"}>{row.original.name}</Text>
|
||||
<Text size={"xs"}>{row.original.comment}</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
})),
|
||||
{
|
||||
header: "Всего часов",
|
||||
Cell: ({row}) => {
|
||||
return Object.entries(row.original).reduce((acc, [key, value]) => {
|
||||
if (isNaN(parseInt(key)) || !isNumber(value)) return acc;
|
||||
acc += value;
|
||||
return acc;
|
||||
}, 0);
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "totalAmount",
|
||||
header: "Итоговая сумма заработка",
|
||||
|
||||
Cell: ({row}) => {
|
||||
return (row.original.data || []).reduce((acc, value) => {
|
||||
acc += value.amount;
|
||||
return acc;
|
||||
}, 0);
|
||||
...range.map(date => ({
|
||||
size: 80,
|
||||
accessorKey: date.date().toString(),
|
||||
header: date.date().toString(),
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
Header: (
|
||||
<Flex
|
||||
align={"center"}
|
||||
direction={"column"}>
|
||||
<Box>{date.date()}</Box>
|
||||
<Box>{getDayOfWeek(date.day())}</Box>
|
||||
</Flex>
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
mantineTableBodyCellProps: ({ cell }) => ({
|
||||
style: selectedCells.includes(cell.id)
|
||||
? {
|
||||
backgroundColor:
|
||||
"var(--mantine-primary-color-filled)",
|
||||
...getBorderStyles(cell.id),
|
||||
}
|
||||
: {},
|
||||
onClick: () => {
|
||||
const result = processSelectedCells(
|
||||
selectedCells,
|
||||
cell.id
|
||||
);
|
||||
setSelectedCells(result);
|
||||
},
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
Cell: ({ cell, row }) => {
|
||||
return (
|
||||
<Flex direction={"column"}>
|
||||
<NumberInput
|
||||
// key={row.original.name + date.date().toString()}
|
||||
onChange={event =>
|
||||
isNumber(event) &&
|
||||
onUpdate(
|
||||
date.toDate(),
|
||||
row.original.userId,
|
||||
event
|
||||
)
|
||||
}
|
||||
styles={{ input: { textAlign: "center" } }}
|
||||
hideControls
|
||||
value={cell.renderValue()}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
})),
|
||||
{
|
||||
header: "Всего часов",
|
||||
Cell: ({ row }) => {
|
||||
return Object.entries(row.original).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (isNaN(parseInt(key)) || !isNumber(value))
|
||||
return acc;
|
||||
acc += value;
|
||||
return acc;
|
||||
},
|
||||
0
|
||||
);
|
||||
},
|
||||
},
|
||||
Footer: (
|
||||
<Flex>
|
||||
Всего: {totalAmount.toLocaleString("ru-RU")}
|
||||
</Flex>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "totalAmount",
|
||||
header: "Итоговая сумма заработка",
|
||||
|
||||
Cell: ({ row }) => {
|
||||
return (row.original.data || []).reduce((acc, value) => {
|
||||
acc += value.amount;
|
||||
return acc;
|
||||
}, 0);
|
||||
},
|
||||
Footer: (
|
||||
<Flex>Всего: {totalAmount.toLocaleString("ru-RU")}</Flex>
|
||||
),
|
||||
},
|
||||
],
|
||||
[month, selectedCells, selectedBoundaries, totalAmount]
|
||||
);
|
||||
};
|
||||
|
||||
], [month, selectedCells, selectedBoundaries, totalAmount]);
|
||||
}
|
||||
|
||||
export default useWorkTableColumns;
|
||||
export default useWorkTableColumns;
|
||||
|
||||
@@ -1,39 +1,50 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {TimeTrackingRecord, TimeTrackingService} from "../../../../../client";
|
||||
import {dateWithoutTimezone, getDatesInMonth} from "../../../../../shared/lib/date.ts";
|
||||
import {last} from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TimeTrackingRecord, TimeTrackingService } from "../../../../../client";
|
||||
import {
|
||||
dateWithoutTimezone,
|
||||
getDatesInMonth,
|
||||
} from "../../../../../shared/lib/date.ts";
|
||||
import { last } from "lodash";
|
||||
|
||||
const getDateBoundaries = (month: Date) => {
|
||||
return [getDatesInMonth(month)[0].toDate(), last(getDatesInMonth(month))?.toDate()]
|
||||
}
|
||||
return [
|
||||
getDatesInMonth(month)[0].toDate(),
|
||||
last(getDatesInMonth(month))?.toDate(),
|
||||
];
|
||||
};
|
||||
const useWorkTableState = () => {
|
||||
const [month, setMonth] = useState<Date>(new Date(new Date().getFullYear(), new Date().getMonth(), 1));
|
||||
const [trackingRecords, setTrackingRecords] = useState<TimeTrackingRecord[]>([]);
|
||||
const [dateBoundaries, setDateBoundaries] = useState(getDateBoundaries(month));
|
||||
const [month, setMonth] = useState<Date>(
|
||||
new Date(new Date().getFullYear(), new Date().getMonth(), 1)
|
||||
);
|
||||
const [trackingRecords, setTrackingRecords] = useState<
|
||||
TimeTrackingRecord[]
|
||||
>([]);
|
||||
const [dateBoundaries, setDateBoundaries] = useState(
|
||||
getDateBoundaries(month)
|
||||
);
|
||||
|
||||
const refetch = async () => {
|
||||
return TimeTrackingService.getTimeTrackingRecords({
|
||||
requestBody: {
|
||||
date: dateWithoutTimezone(month),
|
||||
userIds: []
|
||||
}
|
||||
}).then((response) => setTrackingRecords(response.records));
|
||||
}
|
||||
userIds: [],
|
||||
},
|
||||
}).then(response => setTrackingRecords(response.records));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
refetch().then(_ => {
|
||||
setDateBoundaries(getDateBoundaries(month));
|
||||
});
|
||||
|
||||
}, [month])
|
||||
}, [month]);
|
||||
return {
|
||||
month,
|
||||
setMonth,
|
||||
refetch,
|
||||
trackingRecords,
|
||||
dateBoundaries
|
||||
}
|
||||
}
|
||||
dateBoundaries,
|
||||
};
|
||||
};
|
||||
|
||||
export default useWorkTableState;
|
||||
export default useWorkTableState;
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
import {ActionIcon, Flex, MultiSelect, rem, Tooltip} from "@mantine/core";
|
||||
import {DatePickerInput, MonthPickerInput} from "@mantine/dates";
|
||||
import { ActionIcon, Flex, MultiSelect, rem, Tooltip } from "@mantine/core";
|
||||
import { DatePickerInput, MonthPickerInput } from "@mantine/dates";
|
||||
import useWorkTableState from "../hooks/useWorkTableState.tsx";
|
||||
import {BaseTable} from "../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {useEffect, useState} from "react";
|
||||
import useWorkTableColumns, {EmployeeData} from "../hooks/useWorkTableColumns.tsx";
|
||||
import {TimeTrackingRecord, TimeTrackingService, UserSchema} from "../../../../../client";
|
||||
import {dateWithoutTimezone, getDatesBetween, getDatesInMonth} from "../../../../../shared/lib/date.ts";
|
||||
import { BaseTable } from "../../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { useEffect, useState } from "react";
|
||||
import useWorkTableColumns, {
|
||||
EmployeeData,
|
||||
} from "../hooks/useWorkTableColumns.tsx";
|
||||
import {
|
||||
TimeTrackingRecord,
|
||||
TimeTrackingService,
|
||||
UserSchema,
|
||||
} from "../../../../../client";
|
||||
import {
|
||||
dateWithoutTimezone,
|
||||
getDatesBetween,
|
||||
getDatesInMonth,
|
||||
} from "../../../../../shared/lib/date.ts";
|
||||
import useUsersList from "../../../hooks/useUsersList.tsx";
|
||||
import {notifications} from "../../../../../shared/lib/notifications.ts";
|
||||
import {PaySchemeType} from "../../../../../shared/enums/PaySchemeType.ts";
|
||||
import {IconEyeOff} from "@tabler/icons-react";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import {difference, omit} from "lodash";
|
||||
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
import { PaySchemeType } from "../../../../../shared/enums/PaySchemeType.ts";
|
||||
import { IconEyeOff } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { difference, omit } from "lodash";
|
||||
|
||||
const WorkTimeTable = () => {
|
||||
const [data, setData] = useState<EmployeeData[]>([]);
|
||||
const {
|
||||
dateBoundaries,
|
||||
month,
|
||||
setMonth,
|
||||
trackingRecords,
|
||||
refetch
|
||||
} = useWorkTableState();
|
||||
const { dateBoundaries, month, setMonth, trackingRecords, refetch } =
|
||||
useWorkTableState();
|
||||
const [hiddenUsers, setHiddenUsers] = useState<UserSchema[]>([]);
|
||||
const [selectedBoundaries, setSelectedBoundaries] = useState<[Date | null, Date | null]>([null, null]);
|
||||
const [selectedBoundaries, setSelectedBoundaries] = useState<
|
||||
[Date | null, Date | null]
|
||||
>([null, null]);
|
||||
|
||||
const users = useUsersList().objects.filter(user => user.payRate?.payrollScheme.key === PaySchemeType.HOURLY);
|
||||
const users = useUsersList().objects.filter(
|
||||
user => user.payRate?.payrollScheme.key === PaySchemeType.HOURLY
|
||||
);
|
||||
|
||||
const getRange = () => {
|
||||
const startDate = selectedBoundaries[0];
|
||||
@@ -36,128 +44,153 @@ const WorkTimeTable = () => {
|
||||
} else {
|
||||
return getDatesInMonth(month);
|
||||
}
|
||||
}
|
||||
};
|
||||
const range = getRange();
|
||||
|
||||
const transformTrackingRecordsToData = (trackingRecords: TimeTrackingRecord[]): EmployeeData[] => {
|
||||
const transformTrackingRecordsToData = (
|
||||
trackingRecords: TimeTrackingRecord[]
|
||||
): EmployeeData[] => {
|
||||
if (!month) return [];
|
||||
const rangeDays = range.map(r => r.date());
|
||||
|
||||
trackingRecords = trackingRecords.map(tr => ({
|
||||
...tr,
|
||||
data: tr.data.filter(d => rangeDays.includes((new Date(d.date)).getDate()))
|
||||
data: tr.data.filter(d =>
|
||||
rangeDays.includes(new Date(d.date).getDate())
|
||||
),
|
||||
}));
|
||||
const existingUserIds = trackingRecords.map(tr => tr.user.id);
|
||||
const firstResult = trackingRecords.map((record) => ({
|
||||
const firstResult = trackingRecords.map(record => ({
|
||||
name: `${record.user.firstName} ${record.user.secondName}`,
|
||||
comment: record.user.comment,
|
||||
userId: record.user.id,
|
||||
totalAmount: record.totalAmount,
|
||||
data: record.data,
|
||||
...Object.fromEntries(getDatesInMonth(month).reduce((acc, day) => {
|
||||
return acc.set(day.date().toString(), 0);
|
||||
}, new Map<string, number>)),
|
||||
...Object.fromEntries(record.data.reduce((acc, recordData) => {
|
||||
return acc.set((new Date(recordData.date).getDate().toString()), recordData.hours);
|
||||
}, new Map<string, number>))
|
||||
...Object.fromEntries(
|
||||
getDatesInMonth(month).reduce((acc, day) => {
|
||||
return acc.set(day.date().toString(), 0);
|
||||
}, new Map<string, number>())
|
||||
),
|
||||
...Object.fromEntries(
|
||||
record.data.reduce((acc, recordData) => {
|
||||
return acc.set(
|
||||
new Date(recordData.date).getDate().toString(),
|
||||
recordData.hours
|
||||
);
|
||||
}, new Map<string, number>())
|
||||
),
|
||||
}));
|
||||
const restUsersResult = users.filter(user => !existingUserIds.includes(user.id)).map(user => ({
|
||||
name: `${user.firstName} ${user.secondName}`,
|
||||
userId: user.id,
|
||||
comment: user.comment,
|
||||
totalAmount: 0,
|
||||
...Object.fromEntries(getDatesInMonth(month).reduce((acc, day) => {
|
||||
return acc.set(day.date().toString(), 0);
|
||||
}, new Map<string, number>)),
|
||||
}))
|
||||
const restUsersResult = users
|
||||
.filter(user => !existingUserIds.includes(user.id))
|
||||
.map(user => ({
|
||||
name: `${user.firstName} ${user.secondName}`,
|
||||
userId: user.id,
|
||||
comment: user.comment,
|
||||
totalAmount: 0,
|
||||
...Object.fromEntries(
|
||||
getDatesInMonth(month).reduce((acc, day) => {
|
||||
return acc.set(day.date().toString(), 0);
|
||||
}, new Map<string, number>())
|
||||
),
|
||||
}));
|
||||
const hiddenUserIds = hiddenUsers.map(user => user.id);
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
const result = (firstResult.concat(restUsersResult)).filter(r => !hiddenUserIds.includes(r.userId));
|
||||
const result = firstResult
|
||||
.concat(restUsersResult)
|
||||
.filter(r => !hiddenUserIds.includes(r.userId));
|
||||
const firstDate = selectedBoundaries[0];
|
||||
const lastDate = selectedBoundaries[1];
|
||||
if (firstDate && lastDate) {
|
||||
const allDays = getDatesInMonth(month).map(d => d.date().toString());
|
||||
const allowedDays = getDatesBetween(firstDate, lastDate).map(d => d.date().toString());
|
||||
const allDays = getDatesInMonth(month).map(d =>
|
||||
d.date().toString()
|
||||
);
|
||||
const allowedDays = getDatesBetween(firstDate, lastDate).map(d =>
|
||||
d.date().toString()
|
||||
);
|
||||
const omitDays = difference(allDays, allowedDays);
|
||||
|
||||
return result.map(r => {
|
||||
return omit(r, omitDays);
|
||||
}) as unknown as EmployeeData[];
|
||||
}
|
||||
return (result as unknown as EmployeeData[]).sort((a, b) => a.userId - b.userId);
|
||||
}
|
||||
return (result as unknown as EmployeeData[]).sort(
|
||||
(a, b) => a.userId - b.userId
|
||||
);
|
||||
};
|
||||
|
||||
const optimisticUpdate = (date: Date, userId: number, value: number) => {
|
||||
const user = users.find(user => user.id === userId);
|
||||
if (!user) return;
|
||||
setData(prevState => prevState.map(record => {
|
||||
if (record.userId !== userId) return record;
|
||||
record[date.getDate()] = value;
|
||||
return record;
|
||||
}))
|
||||
|
||||
setData(prevState =>
|
||||
prevState.map(record => {
|
||||
if (record.userId !== userId) return record;
|
||||
record[date.getDate()] = value;
|
||||
return record;
|
||||
})
|
||||
);
|
||||
|
||||
TimeTrackingService.updateTimeTrackingRecord({
|
||||
requestBody: {
|
||||
date: dateWithoutTimezone(date),
|
||||
hours: value,
|
||||
userId: user.id
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
userId: user.id,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
if (!ok) {
|
||||
notifications.guess(ok, {message});
|
||||
notifications.guess(ok, { message });
|
||||
}
|
||||
await refetch();
|
||||
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
const columns = useWorkTableColumns({
|
||||
month,
|
||||
selectedBoundaries,
|
||||
data,
|
||||
onUpdate: optimisticUpdate,
|
||||
selectedCells: [],
|
||||
setSelectedCells: () => {
|
||||
},
|
||||
range
|
||||
setSelectedCells: () => {},
|
||||
range,
|
||||
});
|
||||
useEffect(() => {
|
||||
setData(transformTrackingRecordsToData(trackingRecords));
|
||||
}, [trackingRecords, hiddenUsers, selectedBoundaries]);
|
||||
useEffect(() => {
|
||||
setSelectedBoundaries([null, null]);
|
||||
}, [month])
|
||||
}, [month]);
|
||||
return (
|
||||
<Flex
|
||||
direction={"column"}
|
||||
p={rem(10)}
|
||||
gap={rem(10)}
|
||||
>
|
||||
gap={rem(10)}>
|
||||
<Flex
|
||||
align={"center"}
|
||||
justify={"space-between"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
|
||||
gap={rem(10)}>
|
||||
<MultiSelect
|
||||
data={users.map(user => ({
|
||||
label: `${user.firstName} ${user.secondName}`,
|
||||
value: user.id.toString()
|
||||
value: user.id.toString(),
|
||||
}))}
|
||||
onChange={(event) => setHiddenUsers(users.filter(user => event.includes(user.id.toString())))}
|
||||
onChange={event =>
|
||||
setHiddenUsers(
|
||||
users.filter(user =>
|
||||
event.includes(user.id.toString())
|
||||
)
|
||||
)
|
||||
}
|
||||
value={hiddenUsers.map(user => user.id.toString())}
|
||||
placeholder={hiddenUsers.length > 0 ? "" : "Скрытые пользователи"}
|
||||
placeholder={
|
||||
hiddenUsers.length > 0 ? "" : "Скрытые пользователи"
|
||||
}
|
||||
/>
|
||||
<Flex gap={rem(10)}>
|
||||
|
||||
<DatePickerInput
|
||||
styles={{
|
||||
input: {
|
||||
textAlign: "center"
|
||||
}
|
||||
textAlign: "center",
|
||||
},
|
||||
}}
|
||||
miw={rem(80)}
|
||||
valueFormat={"DD"}
|
||||
@@ -170,42 +203,49 @@ const WorkTimeTable = () => {
|
||||
/>
|
||||
<MonthPickerInput
|
||||
allowDeselect={false}
|
||||
onChange={(event) => event && setMonth(event)}
|
||||
onChange={event => event && setMonth(event)}
|
||||
value={month}
|
||||
placeholder={"Выберите месяц"}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
</Flex>
|
||||
<Flex>
|
||||
<BaseTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableColumnActions: false,
|
||||
enableSorting: false,
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Скрыть">
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
const user = users.find(user => user.id === row.original.userId);
|
||||
if (!user) return;
|
||||
setHiddenUsers(prevState => [...prevState, user]);
|
||||
}}
|
||||
variant={"default"}>
|
||||
<IconEyeOff/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<EmployeeData>}
|
||||
restProps={
|
||||
{
|
||||
enableColumnActions: false,
|
||||
enableSorting: false,
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Скрыть">
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
const user = users.find(
|
||||
user =>
|
||||
user.id ===
|
||||
row.original.userId
|
||||
);
|
||||
if (!user) return;
|
||||
setHiddenUsers(prevState => [
|
||||
...prevState,
|
||||
user,
|
||||
]);
|
||||
}}
|
||||
variant={"default"}>
|
||||
<IconEyeOff />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<EmployeeData>
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkTimeTable;
|
||||
export default WorkTimeTable;
|
||||
|
||||
@@ -9,4 +9,4 @@
|
||||
padding: rem(5);
|
||||
gap: rem(10);
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,74 @@
|
||||
import styles from './BarcodePage.module.css';
|
||||
import styles from "./BarcodePage.module.css";
|
||||
import PageBlock from "../../components/PageBlock/PageBlock.tsx";
|
||||
import {Button} from "@mantine/core";
|
||||
import { Button } from "@mantine/core";
|
||||
import BarcodeTemplatesTable from "./components/BarcodeTemplatesTable/BarcodeTemplatesTable.tsx";
|
||||
import useGetAllBarcodeTemplates from "../../api/barcode/useGetAllBarcodeTemplates.tsx";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {BarcodeService, BarcodeTemplateSchema} from "../../client";
|
||||
import {notifications} from "../../shared/lib/notifications.ts";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { BarcodeService, BarcodeTemplateSchema } from "../../client";
|
||||
import { notifications } from "../../shared/lib/notifications.ts";
|
||||
|
||||
const BarcodePage = () => {
|
||||
const {barcodeTemplates, refetch} = useGetAllBarcodeTemplates();
|
||||
const { barcodeTemplates, refetch } = useGetAllBarcodeTemplates();
|
||||
|
||||
const onCreate = (template: BarcodeTemplateSchema) => {
|
||||
BarcodeService.createBarcodeTemplate(({
|
||||
BarcodeService.createBarcodeTemplate({
|
||||
requestBody: {
|
||||
...template,
|
||||
attributeIds: template.attributes.map(attr => attr.id)
|
||||
}
|
||||
})).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
attributeIds: template.attributes.map(attr => attr.id),
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch()
|
||||
})
|
||||
}
|
||||
await refetch();
|
||||
});
|
||||
};
|
||||
|
||||
const onChange = (template: BarcodeTemplateSchema) => {
|
||||
BarcodeService.updateBarcodeTemplate({
|
||||
requestBody: {
|
||||
...template,
|
||||
attributeIds: template.attributes.map(attr => attr.id)
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
attributeIds: template.attributes.map(attr => attr.id),
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch()
|
||||
})
|
||||
}
|
||||
await refetch();
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = (template: BarcodeTemplateSchema) => {
|
||||
BarcodeService.deleteBarcodeTemplate({
|
||||
requestBody: {
|
||||
id: template.id
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
id: template.id,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch()
|
||||
})
|
||||
}
|
||||
await refetch();
|
||||
});
|
||||
};
|
||||
|
||||
const onCreateClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "barcodeTemplateFormModal",
|
||||
title: 'Создание шаблона',
|
||||
title: "Создание шаблона",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onCreate: onCreate
|
||||
}
|
||||
onCreate: onCreate,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles['container']}>
|
||||
<div className={styles["container"]}>
|
||||
<PageBlock>
|
||||
<div className={styles['top-panel']}>
|
||||
<div className={styles["top-panel"]}>
|
||||
<Button
|
||||
onClick={() => onCreateClick()}
|
||||
variant={"default"}
|
||||
>
|
||||
variant={"default"}>
|
||||
Создать шаблон
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</PageBlock>
|
||||
<PageBlock>
|
||||
<BarcodeTemplatesTable
|
||||
@@ -80,7 +78,7 @@ const BarcodePage = () => {
|
||||
/>
|
||||
</PageBlock>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default BarcodePage;
|
||||
export default BarcodePage;
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
import {FC, useEffect, useState} from "react";
|
||||
import {ActionIcon, Button, Flex, rem, TextInput} from "@mantine/core";
|
||||
import {BarcodeTemplateAdditionalAttributeSchema} from "../../../../client";
|
||||
import {IconX} from "@tabler/icons-react";
|
||||
import {isEqual} from "lodash";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { ActionIcon, Button, Flex, rem, TextInput } from "@mantine/core";
|
||||
import { BarcodeTemplateAdditionalAttributeSchema } from "../../../../client";
|
||||
import { IconX } from "@tabler/icons-react";
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
type FieldType = BarcodeTemplateAdditionalAttributeSchema;
|
||||
type Props = {
|
||||
onChange: (value: FieldType[]) => void;
|
||||
value?: FieldType[];
|
||||
error?: string | null;
|
||||
}
|
||||
};
|
||||
const BarcodeTemplateAdditionalFieldTable: FC<Props> = (props: Props) => {
|
||||
const {value, onChange} = props;
|
||||
const [innerValue, setInnerValue] = useState<FieldType[]>(props.value || []);
|
||||
const { value, onChange } = props;
|
||||
const [innerValue, setInnerValue] = useState<FieldType[]>(
|
||||
props.value || []
|
||||
);
|
||||
const onNameChange = (field: FieldType, newName: string) => {
|
||||
const newField = {...field, name: newName};
|
||||
const newFields = innerValue.map(f => f === field ? newField : f);
|
||||
const newField = { ...field, name: newName };
|
||||
const newFields = innerValue.map(f => (f === field ? newField : f));
|
||||
setInnerValue(newFields);
|
||||
onChange(newFields);
|
||||
}
|
||||
};
|
||||
const onValueChange = (field: FieldType, newValue: string) => {
|
||||
const newField = {...field, value: newValue};
|
||||
const newFields = innerValue.map(f => f === field ? newField : f);
|
||||
const newField = { ...field, value: newValue };
|
||||
const newFields = innerValue.map(f => (f === field ? newField : f));
|
||||
setInnerValue(newFields);
|
||||
onChange(newFields);
|
||||
}
|
||||
};
|
||||
const onCreateField = () => {
|
||||
const newField = {name: "", value: ""};
|
||||
const newField = { name: "", value: "" };
|
||||
setInnerValue([...innerValue, newField]);
|
||||
onChange([...innerValue, newField]);
|
||||
}
|
||||
};
|
||||
const onDeleteField = (field: FieldType) => {
|
||||
const newFields = innerValue.filter(f => f !== field);
|
||||
setInnerValue(newFields);
|
||||
onChange(newFields);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (isEqual(value, innerValue)) return;
|
||||
setInnerValue(value || []);
|
||||
@@ -43,32 +45,43 @@ const BarcodeTemplateAdditionalFieldTable: FC<Props> = (props: Props) => {
|
||||
<>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
gap={rem(10)}>
|
||||
{/*{innerValue.length === 0 && <Text>Дополнительных полей нет</Text>}*/}
|
||||
{innerValue.map((field, idx) => (
|
||||
<Flex align={"center"} gap={rem(10)} key={idx}>
|
||||
<ActionIcon onClick={() => onDeleteField(field)} variant={"default"}>
|
||||
<IconX/>
|
||||
<Flex
|
||||
align={"center"}
|
||||
gap={rem(10)}
|
||||
key={idx}>
|
||||
<ActionIcon
|
||||
onClick={() => onDeleteField(field)}
|
||||
variant={"default"}>
|
||||
<IconX />
|
||||
</ActionIcon>
|
||||
<TextInput
|
||||
placeholder={"Название атрибута"}
|
||||
value={field.name}
|
||||
onChange={event => event &&
|
||||
onNameChange(field, event.target.value)}/>
|
||||
onChange={event =>
|
||||
event && onNameChange(field, event.target.value)
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Значение атрибута"}
|
||||
value={field.value}
|
||||
onChange={event => event &&
|
||||
onValueChange(field, event.target.value)}
|
||||
onChange={event =>
|
||||
event &&
|
||||
onValueChange(field, event.target.value)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
))
|
||||
}
|
||||
<Button variant={"default"} onClick={onCreateField}>Создать дополительный атрибут</Button>
|
||||
))}
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={onCreateField}>
|
||||
Создать дополительный атрибут
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default BarcodeTemplateAdditionalFieldTable;
|
||||
export default BarcodeTemplateAdditionalFieldTable;
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import ObjectMultiSelect, {
|
||||
ObjectMultiSelectProps
|
||||
ObjectMultiSelectProps,
|
||||
} from "../../../../components/ObjectMultiSelect/ObjectMultiSelect.tsx";
|
||||
import {BarcodeTemplateAttributeSchema} from "../../../../client";
|
||||
import {FC} from "react";
|
||||
import { BarcodeTemplateAttributeSchema } from "../../../../client";
|
||||
import { FC } from "react";
|
||||
import useBarcodeTemplateAttributesList from "../../hooks/useBarcodeTemplateAttributesList.tsx";
|
||||
|
||||
type Props = Omit<ObjectMultiSelectProps<BarcodeTemplateAttributeSchema>, 'data'>
|
||||
type Props = Omit<
|
||||
ObjectMultiSelectProps<BarcodeTemplateAttributeSchema>,
|
||||
"data"
|
||||
>;
|
||||
|
||||
const BarcodeTemplateAttributeMultiselect: FC<Props> = (props: Props) => {
|
||||
const {barcodeTemplateAttributes} = useBarcodeTemplateAttributesList();
|
||||
const { barcodeTemplateAttributes } = useBarcodeTemplateAttributesList();
|
||||
return (
|
||||
<ObjectMultiSelect
|
||||
data={barcodeTemplateAttributes}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default BarcodeTemplateAttributeMultiselect;
|
||||
export default BarcodeTemplateAttributeMultiselect;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import {BarcodeTemplateSizeSchema} from "../../../../client";
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import { BarcodeTemplateSizeSchema } from "../../../../client";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import useBarcodeTemplateSizesList from "../../hooks/useBarcodeTemplateSizesList.tsx";
|
||||
|
||||
type Props = Omit<ObjectSelectProps<BarcodeTemplateSizeSchema>, 'data'>
|
||||
type Props = Omit<ObjectSelectProps<BarcodeTemplateSizeSchema>, "data">;
|
||||
const BarcodeTemplateSizeSelect = (props: Props) => {
|
||||
const {barcodeTemplateSizes} = useBarcodeTemplateSizesList();
|
||||
const { barcodeTemplateSizes } = useBarcodeTemplateSizesList();
|
||||
|
||||
return (
|
||||
<ObjectSelect
|
||||
data={barcodeTemplateSizes}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default BarcodeTemplateSizeSelect;
|
||||
);
|
||||
};
|
||||
export default BarcodeTemplateSizeSelect;
|
||||
|
||||
@@ -1,62 +1,69 @@
|
||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
||||
import {BarcodeTemplateSchema} from "../../../../client";
|
||||
import {FC} from "react";
|
||||
import {useBarcodeTemplatesTableColumns} from "./columns.tsx";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {ActionIcon, Flex, Tooltip} from "@mantine/core";
|
||||
import {IconEdit, IconTrash} from "@tabler/icons-react";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { BarcodeTemplateSchema } from "../../../../client";
|
||||
import { FC } from "react";
|
||||
import { useBarcodeTemplatesTableColumns } from "./columns.tsx";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
|
||||
const BarcodeTemplatesTable: FC<CRUDTableProps<BarcodeTemplateSchema>> = ({
|
||||
items,
|
||||
onDelete,
|
||||
onChange
|
||||
}) => {
|
||||
items,
|
||||
onDelete,
|
||||
onChange,
|
||||
}) => {
|
||||
const columns = useBarcodeTemplatesTableColumns();
|
||||
const onEditClick = (template: BarcodeTemplateSchema) => {
|
||||
if (!onChange) return;
|
||||
modals.openContextModal({
|
||||
modal: "barcodeTemplateFormModal",
|
||||
title: 'Создание шаблона',
|
||||
title: "Создание шаблона",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onChange: (newTemplate) => onChange(newTemplate),
|
||||
onChange: newTemplate => onChange(newTemplate),
|
||||
element: template,
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<BaseTable
|
||||
striped
|
||||
data={items}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon onClick={() => {
|
||||
if (onDelete) onDelete(row.original);
|
||||
}} variant={"default"}>
|
||||
<IconTrash/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)
|
||||
} as MRT_TableOptions<BarcodeTemplateSchema>}
|
||||
restProps={
|
||||
{
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
onEditClick(row.original)
|
||||
}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
if (onDelete)
|
||||
onDelete(row.original);
|
||||
}}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<BarcodeTemplateSchema>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default BarcodeTemplatesTable;
|
||||
export default BarcodeTemplatesTable;
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
import {BarcodeTemplateSchema} from "../../../../client";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {useMemo} from "react";
|
||||
import {IconCheck, IconX} from "@tabler/icons-react";
|
||||
import { BarcodeTemplateSchema } from "../../../../client";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { useMemo } from "react";
|
||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
|
||||
export const useBarcodeTemplatesTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<BarcodeTemplateSchema>[]>(() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название",
|
||||
},
|
||||
{
|
||||
accessorKey: "attributes",
|
||||
header: "Атрибуты",
|
||||
Cell: ({row}) => <>{row.original.attributes.map(attr => attr.name).join(', ')}</>
|
||||
},
|
||||
{
|
||||
accessorKey: "size.name",
|
||||
header: "Размер",
|
||||
},
|
||||
{
|
||||
accessorKey: "isDefault",
|
||||
header: "По умолчанию",
|
||||
Cell: ({row}) => row.original.isDefault ? <IconCheck/> : <IconX/>
|
||||
}
|
||||
], []);
|
||||
}
|
||||
return useMemo<MRT_ColumnDef<BarcodeTemplateSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название",
|
||||
},
|
||||
{
|
||||
accessorKey: "attributes",
|
||||
header: "Атрибуты",
|
||||
Cell: ({ row }) => (
|
||||
<>
|
||||
{row.original.attributes
|
||||
.map(attr => attr.name)
|
||||
.join(", ")}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "size.name",
|
||||
header: "Размер",
|
||||
},
|
||||
{
|
||||
accessorKey: "isDefault",
|
||||
header: "По умолчанию",
|
||||
Cell: ({ row }) =>
|
||||
row.original.isDefault ? <IconCheck /> : <IconX />,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import {BarcodeService} from "../../../client";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import { BarcodeService } from "../../../client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
const useBarcodeTemplateAttributesList = () => {
|
||||
const {isLoading, data, error, refetch} = useQuery({
|
||||
queryKey: ['getAllBarcodeTemplateAttributes'],
|
||||
queryFn: BarcodeService.getAllBarcodeTemplateAttributes
|
||||
const { isLoading, data, error, refetch } = useQuery({
|
||||
queryKey: ["getAllBarcodeTemplateAttributes"],
|
||||
queryFn: BarcodeService.getAllBarcodeTemplateAttributes,
|
||||
});
|
||||
|
||||
const barcodeTemplateAttributes = isLoading || error || !data ? [] : data.attributes;
|
||||
const barcodeTemplateAttributes =
|
||||
isLoading || error || !data ? [] : data.attributes;
|
||||
|
||||
return {barcodeTemplateAttributes, refetch}
|
||||
}
|
||||
export default useBarcodeTemplateAttributesList;
|
||||
return { barcodeTemplateAttributes, refetch };
|
||||
};
|
||||
export default useBarcodeTemplateAttributesList;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {BarcodeService} from "../../../client";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import { BarcodeService } from "../../../client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
const useBarcodeTemplateSizesList = () => {
|
||||
const {isLoading, data, error, refetch} = useQuery({
|
||||
queryKey: ['getAllBarcodeTemplateSizes'],
|
||||
queryFn: BarcodeService.getAllBarcodeTemplateSizes
|
||||
const { isLoading, data, error, refetch } = useQuery({
|
||||
queryKey: ["getAllBarcodeTemplateSizes"],
|
||||
queryFn: BarcodeService.getAllBarcodeTemplateSizes,
|
||||
});
|
||||
|
||||
const barcodeTemplateSizes = isLoading || error || !data ? [] : data.sizes;
|
||||
|
||||
return {barcodeTemplateSizes, refetch}
|
||||
}
|
||||
export default useBarcodeTemplateSizesList;
|
||||
return { barcodeTemplateSizes, refetch };
|
||||
};
|
||||
export default useBarcodeTemplateSizesList;
|
||||
|
||||
@@ -1,85 +1,108 @@
|
||||
import BaseFormModal, {CreateEditFormProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import {
|
||||
BarcodeTemplateAdditionalAttributeSchema,
|
||||
BarcodeTemplateAttributeSchema,
|
||||
BarcodeTemplateSchema
|
||||
BarcodeTemplateSchema,
|
||||
} from "../../../../client";
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {Checkbox, Fieldset, Flex, rem, Textarea, TextInput} from "@mantine/core";
|
||||
import BarcodeTemplateAttributeMultiselect
|
||||
from "../../components/BarcodeTemplateAttributeMultiselect/BarcodeTemplateAttributeMultiselect.tsx";
|
||||
import BarcodeTemplateAdditionalFieldTable
|
||||
from "../../components/BarcodeTemplateAdditionalFieldTable/BarcodeTemplateAdditionalFieldTable.tsx";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { useForm } from "@mantine/form";
|
||||
import {
|
||||
Checkbox,
|
||||
Fieldset,
|
||||
Flex,
|
||||
rem,
|
||||
Textarea,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import BarcodeTemplateAttributeMultiselect from "../../components/BarcodeTemplateAttributeMultiselect/BarcodeTemplateAttributeMultiselect.tsx";
|
||||
import BarcodeTemplateAdditionalFieldTable from "../../components/BarcodeTemplateAdditionalFieldTable/BarcodeTemplateAdditionalFieldTable.tsx";
|
||||
import BarcodeTemplateSizeSelect from "../../components/BarcodeTemplateSizeSelect/BarcodeTemplateSizeSelect.tsx";
|
||||
|
||||
type Props = CreateEditFormProps<BarcodeTemplateSchema>
|
||||
type Props = CreateEditFormProps<BarcodeTemplateSchema>;
|
||||
const BarcodeTemplateFormModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = 'onChange' in innerProps;
|
||||
const initialValues = isEditing ? innerProps.element : {
|
||||
name: "",
|
||||
isDefault: false,
|
||||
attributes: [] as Array<BarcodeTemplateAttributeSchema>,
|
||||
additionalAttributes: [] as Array<BarcodeTemplateAdditionalAttributeSchema>
|
||||
} as Partial<BarcodeTemplateSchema>;
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "onChange" in innerProps;
|
||||
const initialValues = isEditing
|
||||
? innerProps.element
|
||||
: ({
|
||||
name: "",
|
||||
isDefault: false,
|
||||
attributes: [] as Array<BarcodeTemplateAttributeSchema>,
|
||||
additionalAttributes:
|
||||
[] as Array<BarcodeTemplateAdditionalAttributeSchema>,
|
||||
} as Partial<BarcodeTemplateSchema>);
|
||||
const form = useForm<Partial<BarcodeTemplateSchema>>({
|
||||
initialValues: initialValues,
|
||||
validate: {
|
||||
attributes: (attributes: Array<BarcodeTemplateAttributeSchema> | undefined) => attributes ? null : "Необходимо добавить хотя бы один атрибут",
|
||||
name: (name: string | undefined) => name && name.trim() !== '' ? null : "Необходимо ввести название шаблона",
|
||||
}
|
||||
})
|
||||
attributes: (
|
||||
attributes: Array<BarcodeTemplateAttributeSchema> | undefined
|
||||
) =>
|
||||
attributes ? null : "Необходимо добавить хотя бы один атрибут",
|
||||
name: (name: string | undefined) =>
|
||||
name && name.trim() !== ""
|
||||
? null
|
||||
: "Необходимо ввести название шаблона",
|
||||
},
|
||||
});
|
||||
return (
|
||||
<BaseFormModal
|
||||
{...innerProps}
|
||||
closeOnSubmit
|
||||
form={form}
|
||||
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
>
|
||||
onClose={() => context.closeContextModal(id)}>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Fieldset legend={"Шаблон штрихкода"}>
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<TextInput
|
||||
label={'Название'}
|
||||
label={"Название"}
|
||||
placeholder={"Введите название шаблона"}
|
||||
{...form.getInputProps('name')}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<BarcodeTemplateSizeSelect
|
||||
label={"Размер"}
|
||||
placeholder={"Выберите размер шаблона"}
|
||||
{...form.getInputProps('size')}
|
||||
{...form.getInputProps("size")}
|
||||
/>
|
||||
|
||||
<BarcodeTemplateAttributeMultiselect
|
||||
label={"Стандартные атрибуты"}
|
||||
placeholder={!form.values.attributes?.length ? "Выберите атрибуты" : undefined}
|
||||
{...form.getInputProps('attributes')}
|
||||
placeholder={
|
||||
!form.values.attributes?.length
|
||||
? "Выберите атрибуты"
|
||||
: undefined
|
||||
}
|
||||
{...form.getInputProps("attributes")}
|
||||
/>
|
||||
<BarcodeTemplateAdditionalFieldTable
|
||||
{...form.getInputProps('additionalAttributes')}
|
||||
{...form.getInputProps("additionalAttributes")}
|
||||
/>
|
||||
<Checkbox
|
||||
label={"Использовать как стандартный шаблон"}
|
||||
checked={form.getInputProps('isDefault').value as boolean}
|
||||
{...form.getInputProps('isDefault')}/>
|
||||
checked={
|
||||
form.getInputProps("isDefault")
|
||||
.value as boolean
|
||||
}
|
||||
{...form.getInputProps("isDefault")}
|
||||
/>
|
||||
<Textarea
|
||||
label={"Дополнительное поле"}
|
||||
placeholder={"Введите дополнительное поле"}
|
||||
{...form.getInputProps('additionalField')}
|
||||
{...form.getInputProps("additionalField")}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
</Fieldset>
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default BarcodeTemplateFormModal;
|
||||
export default BarcodeTemplateFormModal;
|
||||
|
||||
@@ -9,4 +9,4 @@
|
||||
padding: rem(5);
|
||||
gap: rem(10);
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,65 @@
|
||||
import {FC} from "react";
|
||||
import { FC } from "react";
|
||||
import ClientsTable from "./components/ClientsTable/ClientsTable.tsx";
|
||||
import useClientsList from "./hooks/useClientsList.tsx";
|
||||
import PageBlock from "../../components/PageBlock/PageBlock.tsx";
|
||||
import styles from './ClientsPage.module.css';
|
||||
import {Button} from "@mantine/core";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {ClientSchema, ClientService} from "../../client";
|
||||
import {notifications} from "../../shared/lib/notifications.ts";
|
||||
import styles from "./ClientsPage.module.css";
|
||||
import { Button } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { ClientSchema, ClientService } from "../../client";
|
||||
import { notifications } from "../../shared/lib/notifications.ts";
|
||||
|
||||
const ClientsPage: FC = () => {
|
||||
const {clients, refetch} = useClientsList();
|
||||
const { clients, refetch } = useClientsList();
|
||||
const onCreate = (client: ClientSchema) => {
|
||||
ClientService
|
||||
.createClientApi({
|
||||
requestBody: {
|
||||
data: client
|
||||
}
|
||||
})
|
||||
.then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
if (ok)
|
||||
await refetch()
|
||||
})
|
||||
}
|
||||
ClientService.createClientApi({
|
||||
requestBody: {
|
||||
data: client,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (ok) await refetch();
|
||||
});
|
||||
};
|
||||
const onChange = (client: ClientSchema) => {
|
||||
ClientService
|
||||
.updateClient({
|
||||
requestBody: {
|
||||
data: client
|
||||
}
|
||||
})
|
||||
.then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
if (ok)
|
||||
await refetch()
|
||||
})
|
||||
}
|
||||
ClientService.updateClient({
|
||||
requestBody: {
|
||||
data: client,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (ok) await refetch();
|
||||
});
|
||||
};
|
||||
const onDelete = (client: ClientSchema) => {
|
||||
ClientService
|
||||
.deleteClient({
|
||||
requestBody: {
|
||||
clientId: client.id
|
||||
}
|
||||
})
|
||||
.then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
if (ok)
|
||||
await refetch()
|
||||
})
|
||||
}
|
||||
ClientService.deleteClient({
|
||||
requestBody: {
|
||||
clientId: client.id,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (ok) await refetch();
|
||||
});
|
||||
};
|
||||
const onCreateClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: 'productFormModal',
|
||||
modal: "productFormModal",
|
||||
title: "Создание клиента",
|
||||
innerProps: {
|
||||
onCreate
|
||||
}
|
||||
})
|
||||
}
|
||||
onCreate,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles['container']}>
|
||||
<div className={styles["container"]}>
|
||||
<PageBlock>
|
||||
<div className={styles['top-panel']}>
|
||||
<div className={styles["top-panel"]}>
|
||||
<Button
|
||||
onClick={onCreateClick}
|
||||
variant={"default"}
|
||||
>
|
||||
variant={"default"}>
|
||||
Создать клиента
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</PageBlock>
|
||||
<PageBlock>
|
||||
<ClientsTable
|
||||
@@ -80,6 +69,6 @@ const ClientsPage: FC = () => {
|
||||
/>
|
||||
</PageBlock>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ClientsPage;
|
||||
);
|
||||
};
|
||||
export default ClientsPage;
|
||||
|
||||
@@ -1,64 +1,69 @@
|
||||
import {FC} from "react";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {useClientsTableColumns} from "./columns.tsx";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import {ClientSchema} from "../../../../client";
|
||||
import {ActionIcon, Flex, Tooltip} from "@mantine/core";
|
||||
import {IconEdit, IconTrash} from "@tabler/icons-react";
|
||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
||||
import {modals} from "@mantine/modals";
|
||||
|
||||
import { FC } from "react";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { useClientsTableColumns } from "./columns.tsx";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { ClientSchema } from "../../../../client";
|
||||
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { modals } from "@mantine/modals";
|
||||
|
||||
const ClientsTable: FC<CRUDTableProps<ClientSchema>> = ({
|
||||
items,
|
||||
onDelete,
|
||||
onChange
|
||||
}) => {
|
||||
items,
|
||||
onDelete,
|
||||
onChange,
|
||||
}) => {
|
||||
const columns = useClientsTableColumns();
|
||||
const onEditClick = (client: ClientSchema) => {
|
||||
if (!onChange) return;
|
||||
modals.openContextModal({
|
||||
modal: "productFormModal",
|
||||
title: 'Создание клиента',
|
||||
title: "Создание клиента",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onChange: (newClient) => onChange(newClient),
|
||||
onChange: newClient => onChange(newClient),
|
||||
element: client,
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<BaseTable
|
||||
striped
|
||||
data={items}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon onClick={() => {
|
||||
if (onDelete) onDelete(row.original);
|
||||
}} variant={"default"}>
|
||||
<IconTrash/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)
|
||||
} as MRT_TableOptions<ClientSchema>}
|
||||
restProps={
|
||||
{
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
onEditClick(row.original)
|
||||
}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
if (onDelete)
|
||||
onDelete(row.original);
|
||||
}}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<ClientSchema>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default ClientsTable;
|
||||
export default ClientsTable;
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {ClientSchema} from "../../../../client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { ClientSchema } from "../../../../client";
|
||||
|
||||
export const useClientsTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<ClientSchema>[]>(() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Имя",
|
||||
},
|
||||
{
|
||||
accessorKey: "details.telegram",
|
||||
header: "Телеграм"
|
||||
},
|
||||
{
|
||||
accessorKey: "details.email",
|
||||
header: "EMAIL"
|
||||
},
|
||||
{
|
||||
accessorKey: "details.phoneNumber",
|
||||
header: "Телефон"
|
||||
},
|
||||
{
|
||||
accessorKey: "details.inn",
|
||||
header: "ИНН"
|
||||
},
|
||||
{
|
||||
accessorKey: "companyName",
|
||||
header: "Название компании"
|
||||
},
|
||||
{
|
||||
accessorKey: "barcodeTemplate.name",
|
||||
header: "Шаблон штрихкодов"
|
||||
}
|
||||
], []);
|
||||
}
|
||||
return useMemo<MRT_ColumnDef<ClientSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Имя",
|
||||
},
|
||||
{
|
||||
accessorKey: "details.telegram",
|
||||
header: "Телеграм",
|
||||
},
|
||||
{
|
||||
accessorKey: "details.email",
|
||||
header: "EMAIL",
|
||||
},
|
||||
{
|
||||
accessorKey: "details.phoneNumber",
|
||||
header: "Телефон",
|
||||
},
|
||||
{
|
||||
accessorKey: "details.inn",
|
||||
header: "ИНН",
|
||||
},
|
||||
{
|
||||
accessorKey: "companyName",
|
||||
header: "Название компании",
|
||||
},
|
||||
{
|
||||
accessorKey: "barcodeTemplate.name",
|
||||
header: "Шаблон штрихкодов",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {ClientService} from "../../../client";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import { ClientService } from "../../../client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
const useClientsList = () => {
|
||||
const {isPending, error, data, refetch} = useQuery({
|
||||
queryKey: ['getAllClients'],
|
||||
queryFn: ClientService.getAllClients
|
||||
const { isPending, error, data, refetch } = useQuery({
|
||||
queryKey: ["getAllClients"],
|
||||
queryFn: ClientService.getAllClients,
|
||||
});
|
||||
const clients = isPending || error || !data ? [] : data.clients;
|
||||
|
||||
return {clients, refetch}
|
||||
}
|
||||
export default useClientsList;
|
||||
return { clients, refetch };
|
||||
};
|
||||
export default useClientsList;
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import {UseFormReturnType} from "@mantine/form";
|
||||
import {Button, Flex, rem} from "@mantine/core";
|
||||
import {FC} from "react";
|
||||
import { UseFormReturnType } from "@mantine/form";
|
||||
import { Button, Flex, rem } from "@mantine/core";
|
||||
import { FC } from "react";
|
||||
|
||||
export type CreateProps<T> = {
|
||||
onCreate(values: T): void;
|
||||
}
|
||||
};
|
||||
export type EditProps<T> = {
|
||||
onChange(values: T): void;
|
||||
element: T;
|
||||
}
|
||||
};
|
||||
|
||||
export type CreateEditFormProps<T> = CreateProps<T> | EditProps<T>;
|
||||
export type BaseFormProps<T> = {
|
||||
form: UseFormReturnType<T>
|
||||
form: UseFormReturnType<T>;
|
||||
onClose: () => void;
|
||||
closeOnSubmit?: boolean;
|
||||
children: React.JSX.Element;
|
||||
}
|
||||
};
|
||||
type Props<T> = BaseFormProps<T> & (CreateProps<T> | EditProps<T>);
|
||||
|
||||
const BaseFormModal = <T, >(props: Props<T>) => {
|
||||
const {closeOnSubmit = false} = props;
|
||||
const isEditing = 'onChange' in props;
|
||||
const BaseFormModal = <T,>(props: Props<T>) => {
|
||||
const { closeOnSubmit = false } = props;
|
||||
const isEditing = "onChange" in props;
|
||||
|
||||
const onSubmit = (values: T) => {
|
||||
if (isEditing) {
|
||||
@@ -31,38 +31,42 @@ const BaseFormModal = <T, >(props: Props<T>) => {
|
||||
}
|
||||
|
||||
if (closeOnSubmit) props.onClose();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<form onSubmit={props.form.onSubmit((values) => onSubmit(values))}>
|
||||
<Flex gap={rem(10)} direction={"column"}>
|
||||
<form onSubmit={props.form.onSubmit(values => onSubmit(values))}>
|
||||
<Flex
|
||||
gap={rem(10)}
|
||||
direction={"column"}>
|
||||
{props.children}
|
||||
<Flex justify={"flex-end"} gap={rem(10)}>
|
||||
<Flex
|
||||
justify={"flex-end"}
|
||||
gap={rem(10)}>
|
||||
<Button
|
||||
variant={"subtle"}
|
||||
onClick={() => props.onClose()}
|
||||
>
|
||||
onClick={() => props.onClose()}>
|
||||
Отменить
|
||||
</Button>
|
||||
<Button
|
||||
type={"submit"}
|
||||
variant={"default"}
|
||||
>
|
||||
variant={"default"}>
|
||||
Сохранить
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
type BodyProps = {
|
||||
children: React.JSX.Element;
|
||||
}
|
||||
const Body: FC<BodyProps> = ({children}) => {
|
||||
};
|
||||
const Body: FC<BodyProps> = ({ children }) => {
|
||||
return (
|
||||
<Flex gap={rem(10)} direction={"column"}>
|
||||
<Flex
|
||||
gap={rem(10)}
|
||||
direction={"column"}>
|
||||
{children}
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
BaseFormModal.Body = Body;
|
||||
export default BaseFormModal;
|
||||
export default BaseFormModal;
|
||||
|
||||
@@ -1,55 +1,59 @@
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {Fieldset, TextInput} from "@mantine/core";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {ClientSchema} from "../../../../client";
|
||||
import BaseFormModal, {CreateEditFormProps} from "../BaseFormModal/BaseFormModal.tsx";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { Fieldset, TextInput } from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { ClientSchema } from "../../../../client";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../BaseFormModal/BaseFormModal.tsx";
|
||||
import BarcodeTemplateSelect from "../../../../components/Selects/BarcodeTemplateSelect/BarcodeTemplateSelect.tsx";
|
||||
|
||||
|
||||
type Props = CreateEditFormProps<ClientSchema>;
|
||||
|
||||
const ClientFormModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = 'onChange' in innerProps;
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "onChange" in innerProps;
|
||||
|
||||
const initialValues: ClientSchema = isEditing ? innerProps.element : {
|
||||
id: -1,
|
||||
name: '',
|
||||
companyName: '',
|
||||
details: {
|
||||
telegram: '',
|
||||
phoneNumber: '',
|
||||
email: '',
|
||||
inn: undefined,
|
||||
}
|
||||
}
|
||||
const initialValues: ClientSchema = isEditing
|
||||
? innerProps.element
|
||||
: {
|
||||
id: -1,
|
||||
name: "",
|
||||
companyName: "",
|
||||
details: {
|
||||
telegram: "",
|
||||
phoneNumber: "",
|
||||
email: "",
|
||||
inn: undefined,
|
||||
},
|
||||
};
|
||||
const form = useForm<ClientSchema>({
|
||||
initialValues: initialValues,
|
||||
validate: {
|
||||
name: (name: string) => name.trim() !== '' ? null : "Необходимо ввести название клиента",
|
||||
name: (name: string) =>
|
||||
name.trim() !== ""
|
||||
? null
|
||||
: "Необходимо ввести название клиента",
|
||||
// details: {
|
||||
// telegram: (address: string | undefined | null) => (address && address.trim() !== '') ? null : "Необходимо ввести телеграм",
|
||||
// phoneNumber: (phoneNumber: string | undefined | null) => (phoneNumber && phoneNumber.trim() !== '') ? null : "Необходимо ввести номер телефона",
|
||||
// email: (email: string | undefined | null) => (email && email.trim() !== '') ? null : "Необходимо ввести почту",
|
||||
// inn: (inn: string | undefined | null) => (inn && getDigitsCount(parseInt(inn)) >= 10) ? null : "ИНН должен содержать не менее 10 цифр",
|
||||
// }
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
context.closeContextModal(id);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<BaseFormModal
|
||||
{...innerProps}
|
||||
closeOnSubmit
|
||||
form={form}
|
||||
onClose={onClose}
|
||||
>
|
||||
onClose={onClose}>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Fieldset legend={"Основная информация"}>
|
||||
@@ -57,47 +61,47 @@ const ClientFormModal = ({
|
||||
required
|
||||
label={"Название клиента"}
|
||||
placeholder={"Введите название клиента"}
|
||||
{...form.getInputProps('name')}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
</Fieldset>
|
||||
<Fieldset legend={"Дополнительная информация"}>
|
||||
<TextInput
|
||||
label={"Телеграм"}
|
||||
placeholder={"Введите телеграм"}
|
||||
{...form.getInputProps('details.telegram')}
|
||||
{...form.getInputProps("details.telegram")}
|
||||
/>
|
||||
<TextInput
|
||||
label={"Номер телефона"}
|
||||
placeholder={"Введите номер телефона"}
|
||||
{...form.getInputProps('details.phoneNumber')}
|
||||
{...form.getInputProps("details.phoneNumber")}
|
||||
/>
|
||||
<TextInput
|
||||
label={"Почта"}
|
||||
placeholder={"Введите почту"}
|
||||
{...form.getInputProps('details.email')}
|
||||
{...form.getInputProps("details.email")}
|
||||
/>
|
||||
<TextInput
|
||||
label={"ИНН"}
|
||||
placeholder={"Введите ИНН"}
|
||||
{...form.getInputProps('details.inn')}
|
||||
{...form.getInputProps("details.inn")}
|
||||
/>
|
||||
<TextInput
|
||||
label={"Название компании"}
|
||||
placeholder={"Введите название компании"}
|
||||
{...form.getInputProps('companyName')}/>
|
||||
{...form.getInputProps("companyName")}
|
||||
/>
|
||||
</Fieldset>
|
||||
<Fieldset legend={'Настройки'}>
|
||||
<Fieldset legend={"Настройки"}>
|
||||
<BarcodeTemplateSelect
|
||||
label={'Шаблон штрихкодов'}
|
||||
placeholder={'Выберите шаблон штрихкодов'}
|
||||
{...form.getInputProps('barcodeTemplate')}
|
||||
label={"Шаблон штрихкодов"}
|
||||
placeholder={"Выберите шаблон штрихкодов"}
|
||||
{...form.getInputProps("barcodeTemplate")}
|
||||
/>
|
||||
</Fieldset>
|
||||
</>
|
||||
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientFormModal;
|
||||
export default ClientFormModal;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export {DealPage} from './ui/DealPage'
|
||||
export { DealPage } from "./ui/DealPage";
|
||||
|
||||
@@ -1,38 +1,32 @@
|
||||
import {useParams} from "@tanstack/react-router";
|
||||
import {DealPageContextProvider, useDealPageContext} from "../../LeadsPage/contexts/DealPageContext.tsx";
|
||||
import { useParams } from "@tanstack/react-router";
|
||||
import {
|
||||
DealPageContextProvider,
|
||||
useDealPageContext,
|
||||
} from "../../LeadsPage/contexts/DealPageContext.tsx";
|
||||
import ProductAndServiceTab from "../../LeadsPage/tabs/ProductAndServiceTab/ProductAndServiceTab.tsx";
|
||||
import {FC, useEffect} from "react";
|
||||
import {DealService} from "../../../client";
|
||||
import { FC, useEffect } from "react";
|
||||
import { DealService } from "../../../client";
|
||||
|
||||
export type Props = {
|
||||
dealId: number;
|
||||
}
|
||||
const DealPageContent: FC<Props> = ({dealId}) => {
|
||||
const {setSelectedDeal} = useDealPageContext();
|
||||
};
|
||||
const DealPageContent: FC<Props> = ({ dealId }) => {
|
||||
const { setSelectedDeal } = useDealPageContext();
|
||||
useEffect(() => {
|
||||
DealService.getDealById({dealId}).then(deal => {
|
||||
DealService.getDealById({ dealId }).then(deal => {
|
||||
setSelectedDeal(deal);
|
||||
})
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<ProductAndServiceTab/>
|
||||
)
|
||||
}
|
||||
const DealPageWrapper: FC<{ children: React.ReactNode }> = ({children}) => {
|
||||
return (
|
||||
<DealPageContextProvider>
|
||||
{children}
|
||||
</DealPageContextProvider>
|
||||
)
|
||||
}
|
||||
return <ProductAndServiceTab />;
|
||||
};
|
||||
const DealPageWrapper: FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return <DealPageContextProvider>{children}</DealPageContextProvider>;
|
||||
};
|
||||
export const DealPage = () => {
|
||||
const {dealId} = useParams({strict: false});
|
||||
const { dealId } = useParams({ strict: false });
|
||||
return (
|
||||
|
||||
|
||||
<DealPageWrapper>
|
||||
<DealPageContent dealId={parseInt(dealId || "-1")}/>
|
||||
<DealPageContent dealId={parseInt(dealId || "-1")} />
|
||||
</DealPageWrapper>
|
||||
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import ObjectSelect, {ObjectSelectProps} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import {FC} from "react";
|
||||
import {DealStatuses} from "../../../../shared/enums/DealStatus.ts";
|
||||
import ObjectSelect, {
|
||||
ObjectSelectProps,
|
||||
} from "../../../../components/ObjectSelect/ObjectSelect.tsx";
|
||||
import { FC } from "react";
|
||||
import { DealStatuses } from "../../../../shared/enums/DealStatus.ts";
|
||||
|
||||
type DealStatus = {
|
||||
name: string;
|
||||
id: number
|
||||
}
|
||||
type Props = Omit<ObjectSelectProps<DealStatus>, 'data'>;
|
||||
id: number;
|
||||
};
|
||||
type Props = Omit<ObjectSelectProps<DealStatus>, "data">;
|
||||
|
||||
const DealStatusSelect: FC<Props> = (props) => {
|
||||
const DealStatusSelect: FC<Props> = props => {
|
||||
const data: DealStatus[] = DealStatuses;
|
||||
return (
|
||||
<ObjectSelect
|
||||
data={data}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default DealStatusSelect;
|
||||
);
|
||||
};
|
||||
export default DealStatusSelect;
|
||||
|
||||
@@ -1,53 +1,54 @@
|
||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
||||
import {DealService, DealSummary} from "../../../../client";
|
||||
import {FC} from "react";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { DealService, DealSummary } from "../../../../client";
|
||||
import { FC } from "react";
|
||||
import useDealsTableColumns from "./columns.tsx";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {ActionIcon, Flex, Tooltip} from "@mantine/core";
|
||||
import {IconEdit} from "@tabler/icons-react";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import {useDealPageContext} from "../../../LeadsPage/contexts/DealPageContext.tsx";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { useDealPageContext } from "../../../LeadsPage/contexts/DealPageContext.tsx";
|
||||
|
||||
type RestProps = {
|
||||
viewOnly?: boolean;
|
||||
}
|
||||
};
|
||||
type Props = CRUDTableProps<DealSummary> & RestProps;
|
||||
|
||||
const DealsTable: FC<Props> = ({items, viewOnly = false}) => {
|
||||
const DealsTable: FC<Props> = ({ items, viewOnly = false }) => {
|
||||
const columns = useDealsTableColumns();
|
||||
const {setSelectedDeal} = useDealPageContext();
|
||||
const { setSelectedDeal } = useDealPageContext();
|
||||
const onEditClick = (dealSummary: DealSummary) => {
|
||||
DealService.getDealById({dealId: dealSummary.id})
|
||||
.then((deal) => {
|
||||
setSelectedDeal(deal);
|
||||
})
|
||||
}
|
||||
DealService.getDealById({ dealId: dealSummary.id }).then(deal => {
|
||||
setSelectedDeal(deal);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<BaseTable
|
||||
data={items}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableSorting: true,
|
||||
enableColumnActions: false,
|
||||
enablePagination: !viewOnly,
|
||||
enableBottomToolbar: !viewOnly,
|
||||
paginationDisplayMode: "pages",
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
disabled={viewOnly}
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)
|
||||
} as MRT_TableOptions<DealSummary>}
|
||||
restProps={
|
||||
{
|
||||
enableSorting: true,
|
||||
enableColumnActions: false,
|
||||
enablePagination: !viewOnly,
|
||||
enableBottomToolbar: !viewOnly,
|
||||
paginationDisplayMode: "pages",
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
disabled={viewOnly}
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<DealSummary>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default DealsTable;
|
||||
export default DealsTable;
|
||||
|
||||
@@ -1,50 +1,60 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {DealSummary} from "../../../../client";
|
||||
import {ActionIcon, Image} from "@mantine/core";
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { DealSummary } from "../../../../client";
|
||||
import { ActionIcon, Image } from "@mantine/core";
|
||||
|
||||
const useDealsTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<DealSummary>[]>(() => [
|
||||
return useMemo<MRT_ColumnDef<DealSummary>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "Маркетплейс",
|
||||
size: 10,
|
||||
Cell: ({ row }) => (
|
||||
<ActionIcon variant={"transparent"}>
|
||||
<Image
|
||||
src={row.original.baseMarketplace?.iconUrl || ""}
|
||||
/>
|
||||
</ActionIcon>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Дата создания",
|
||||
accessorKey: "createdAt",
|
||||
Cell: ({ row }) =>
|
||||
new Date(row.original.createdAt).toLocaleString("ru-RU"),
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) =>
|
||||
new Date(rowB.original.createdAt).getTime() -
|
||||
new Date(rowA.original.createdAt).getTime(),
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "clientName",
|
||||
header: "Клиент",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
Cell: ({ row }) =>
|
||||
new Date(row.original.deadline).toLocaleString("ru-RU"),
|
||||
accessorKey: "deadline",
|
||||
header: "Дедлайн",
|
||||
sortingFn: (rowA, rowB) =>
|
||||
new Date(rowB.original.deadline).getTime() -
|
||||
new Date(rowA.original.deadline).getTime(),
|
||||
},
|
||||
{
|
||||
header: "Общая стоимость",
|
||||
Cell: ({ row }) =>
|
||||
row.original.totalPrice.toLocaleString("ru-RU") + "₽",
|
||||
accessorKey: "totalPrice",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
{
|
||||
header: "Маркетплейс",
|
||||
size: 10,
|
||||
Cell: ({row}) => (
|
||||
<ActionIcon variant={"transparent"}>
|
||||
<Image src={row.original.baseMarketplace?.iconUrl || ""}/>
|
||||
</ActionIcon>
|
||||
)
|
||||
},
|
||||
{
|
||||
header: "Дата создания",
|
||||
accessorKey: "createdAt",
|
||||
Cell: ({row}) => (new Date(row.original.createdAt)).toLocaleString("ru-RU"),
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => ((new Date(rowB.original.createdAt)).getTime() - (new Date(rowA.original.createdAt)).getTime())
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название",
|
||||
enableSorting: false
|
||||
},
|
||||
{
|
||||
accessorKey: "clientName",
|
||||
header: "Клиент",
|
||||
enableSorting: false
|
||||
},
|
||||
{
|
||||
Cell: ({row}) => (new Date(row.original.deadline)).toLocaleString("ru-RU"),
|
||||
accessorKey: "deadline",
|
||||
header: "Дедлайн",
|
||||
sortingFn: (rowA, rowB) => ((new Date(rowB.original.deadline)).getTime() - (new Date(rowA.original.deadline)).getTime())
|
||||
|
||||
},
|
||||
{
|
||||
header: "Общая стоимость",
|
||||
Cell: ({row}) => (row.original.totalPrice).toLocaleString("ru-RU") + '₽',
|
||||
accessorKey:"totalPrice"
|
||||
}
|
||||
], [])
|
||||
}
|
||||
|
||||
export default useDealsTableColumns;
|
||||
export default useDealsTableColumns;
|
||||
|
||||
@@ -1,42 +1,47 @@
|
||||
import {useDealSummariesFull} from "../../LeadsPage/hooks/useDealSummaries.tsx";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {useEffect, useState} from "react";
|
||||
import {BaseMarketplaceSchema, ClientSchema} from "../../../client";
|
||||
import {DealStatusType} from "../../../shared/enums/DealStatus.ts";
|
||||
import { useDealSummariesFull } from "../../LeadsPage/hooks/useDealSummaries.tsx";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { useEffect, useState } from "react";
|
||||
import { BaseMarketplaceSchema, ClientSchema } from "../../../client";
|
||||
import { DealStatusType } from "../../../shared/enums/DealStatus.ts";
|
||||
|
||||
type State = {
|
||||
marketplace: BaseMarketplaceSchema | null;
|
||||
dealStatus: DealStatusType | null;
|
||||
client: ClientSchema | null;
|
||||
}
|
||||
};
|
||||
const useDealsPageState = () => {
|
||||
const {objects} = useDealSummariesFull();
|
||||
const { objects } = useDealSummariesFull();
|
||||
const form = useForm<State>({
|
||||
initialValues: {
|
||||
marketplace: null,
|
||||
dealStatus: null,
|
||||
client: null
|
||||
}
|
||||
client: null,
|
||||
},
|
||||
});
|
||||
const [data, setData] = useState(objects);
|
||||
const applyFilters = () => {
|
||||
let result = objects;
|
||||
if (form.values.marketplace) {
|
||||
result = result.filter(obj => obj.baseMarketplace?.key === form.values.marketplace?.key);
|
||||
result = result.filter(
|
||||
obj => obj.baseMarketplace?.key === form.values.marketplace?.key
|
||||
);
|
||||
}
|
||||
if (form.values.dealStatus) {
|
||||
result = result.filter(obj => obj.status === form.values.dealStatus?.id);
|
||||
result = result.filter(
|
||||
obj => obj.status === form.values.dealStatus?.id
|
||||
);
|
||||
}
|
||||
if (form.values.client) {
|
||||
result = result.filter(obj => obj.clientName === form.values.client?.name);
|
||||
|
||||
result = result.filter(
|
||||
obj => obj.clientName === form.values.client?.name
|
||||
);
|
||||
}
|
||||
setData(result);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
applyFilters();
|
||||
}, [form.values, objects])
|
||||
return {data, form}
|
||||
}
|
||||
}, [form.values, objects]);
|
||||
return { data, form };
|
||||
};
|
||||
|
||||
export default useDealsPageState;
|
||||
export default useDealsPageState;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export {DealsPage} from './ui/DealsPage.tsx';
|
||||
export { DealsPage } from "./ui/DealsPage.tsx";
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
gap: rem(10);
|
||||
}
|
||||
|
||||
|
||||
.body-container {
|
||||
}
|
||||
|
||||
@@ -23,5 +22,4 @@
|
||||
|
||||
.table-pagination {
|
||||
align-self: flex-end;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
import {FC} from "react";
|
||||
import { FC } from "react";
|
||||
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
|
||||
import styles from './DealsPage.module.css';
|
||||
import styles from "./DealsPage.module.css";
|
||||
import DealStatusSelect from "../components/DealStatusSelect/DealStatusSelect.tsx";
|
||||
import DealsTable from "../components/DealsTable/DealsTable.tsx";
|
||||
import BaseMarketplaceSelect from "../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
|
||||
import ClientSelectNew from "../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
|
||||
import {DealPageContextProvider} from "../../LeadsPage/contexts/DealPageContext.tsx";
|
||||
import { DealPageContextProvider } from "../../LeadsPage/contexts/DealPageContext.tsx";
|
||||
import DealEditDrawer from "../../LeadsPage/drawers/DealEditDrawer/DealEditDrawer.tsx";
|
||||
import useDealsPageState from "../hooks/useDealsPageState.tsx";
|
||||
|
||||
export const DealsPage: FC = () => {
|
||||
const {data, form} = useDealsPageState();
|
||||
const { data, form } = useDealsPageState();
|
||||
return (
|
||||
<>
|
||||
<DealPageContextProvider>
|
||||
<div className={styles['container']}>
|
||||
<div className={styles["container"]}>
|
||||
<PageBlock>
|
||||
<div className={styles['top-panel']}>
|
||||
<div className={styles["top-panel"]}>
|
||||
<DealStatusSelect
|
||||
onClear={() => form.setFieldValue("dealStatus", null)}
|
||||
onClear={() =>
|
||||
form.setFieldValue("dealStatus", null)
|
||||
}
|
||||
clearable
|
||||
placeholder={"Выберите статус "}
|
||||
{...form.getInputProps("dealStatus")}
|
||||
/>
|
||||
<BaseMarketplaceSelect
|
||||
onClear={() => form.setFieldValue("marketplace", null)}
|
||||
onClear={() =>
|
||||
form.setFieldValue("marketplace", null)
|
||||
}
|
||||
clearable
|
||||
|
||||
placeholder={"Выберите маркетплейс"}
|
||||
{...form.getInputProps("marketplace")}
|
||||
/>
|
||||
<ClientSelectNew
|
||||
onClear={() => form.setFieldValue("client", null)}
|
||||
onClear={() =>
|
||||
form.setFieldValue("client", null)
|
||||
}
|
||||
clearable
|
||||
searchable
|
||||
placeholder={"Выберите клиента"}
|
||||
@@ -40,16 +45,15 @@ export const DealsPage: FC = () => {
|
||||
</div>
|
||||
</PageBlock>
|
||||
<PageBlock>
|
||||
<div className={styles['body-container']}>
|
||||
<div className={styles['table-container']}>
|
||||
<DealsTable items={data}/>
|
||||
<div className={styles["body-container"]}>
|
||||
<div className={styles["table-container"]}>
|
||||
<DealsTable items={data} />
|
||||
</div>
|
||||
</div>
|
||||
</PageBlock>
|
||||
</div>
|
||||
<DealEditDrawer
|
||||
/>
|
||||
<DealEditDrawer />
|
||||
</DealPageContextProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,89 +1,132 @@
|
||||
import {ActionIcon, Button, ComboboxItem, ComboboxItemGroup, Flex, Input, OptionsFilter, rem} from "@mantine/core";
|
||||
import {BaseFormInputProps} from "../../../../types/utils.ts";
|
||||
import {DealProductServiceSchema, ServiceSchema} from "../../../../client";
|
||||
import {FC, useEffect, useState} from "react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
ComboboxItem,
|
||||
ComboboxItemGroup,
|
||||
Flex,
|
||||
Input,
|
||||
OptionsFilter,
|
||||
rem,
|
||||
} from "@mantine/core";
|
||||
import { BaseFormInputProps } from "../../../../types/utils.ts";
|
||||
import { DealProductServiceSchema, ServiceSchema } from "../../../../client";
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import ServiceWithPriceInput from "../../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx";
|
||||
import {isNumber} from "lodash";
|
||||
import {notifications} from "../../../../shared/lib/notifications.ts";
|
||||
import {IconTrash} from "@tabler/icons-react";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState} from "../../../../redux/store.ts";
|
||||
import { isNumber } from "lodash";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { IconTrash } from "@tabler/icons-react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../../redux/store.ts";
|
||||
|
||||
type RestProps = {
|
||||
quantity: number;
|
||||
}
|
||||
};
|
||||
type Props = BaseFormInputProps<DealProductServiceSchema[]> & RestProps;
|
||||
const DealProductServiceTable: FC<Props> = (props: Props) => {
|
||||
const {value, onChange, quantity, error} = props;
|
||||
const { value, onChange, quantity, error } = props;
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const [innerValue, setInnerValue] = useState<Partial<DealProductServiceSchema>[]>(value || []);
|
||||
const [innerValue, setInnerValue] = useState<
|
||||
Partial<DealProductServiceSchema>[]
|
||||
>(value || []);
|
||||
const onServiceChange = (idx: number, value: ServiceSchema) => {
|
||||
setInnerValue(oldValue => oldValue.map((item, i) => i === idx ? {...item, service: value} : item));
|
||||
}
|
||||
setInnerValue(oldValue =>
|
||||
oldValue.map((item, i) =>
|
||||
i === idx ? { ...item, service: value } : item
|
||||
)
|
||||
);
|
||||
};
|
||||
const onQuantityChange = (idx: number, value: string | number) => {
|
||||
if (!isNumber(value)) return;
|
||||
setInnerValue(oldValue => oldValue.map((item, i) => i === idx ? {...item, price: value} : item));
|
||||
}
|
||||
setInnerValue(oldValue =>
|
||||
oldValue.map((item, i) =>
|
||||
i === idx ? { ...item, price: value } : item
|
||||
)
|
||||
);
|
||||
};
|
||||
const onCreate = () => {
|
||||
if (innerValue.length > 0 && !innerValue.at(-1)?.service) {
|
||||
notifications.error({message: "Заполните последнюю услугу"})
|
||||
notifications.error({ message: "Заполните последнюю услугу" });
|
||||
return;
|
||||
}
|
||||
setInnerValue(prevState => [...prevState, {service: undefined, quantity: 1}])
|
||||
}
|
||||
setInnerValue(prevState => [
|
||||
...prevState,
|
||||
{ service: undefined, quantity: 1 },
|
||||
]);
|
||||
};
|
||||
const onDelete = (idx: number) => {
|
||||
setInnerValue(oldValue => oldValue.filter((_, i) => i !== idx));
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
const serviceOptionsFilter = ({options}: { options: ComboboxItemGroup[] }) => {
|
||||
const productServiceIds = innerValue.map(service => service.service?.id);
|
||||
return (options as ComboboxItemGroup[]).map(({items, group}) => {
|
||||
const serviceOptionsFilter = ({
|
||||
options,
|
||||
}: {
|
||||
options: ComboboxItemGroup[];
|
||||
}) => {
|
||||
const productServiceIds = innerValue.map(
|
||||
service => service.service?.id
|
||||
);
|
||||
return (options as ComboboxItemGroup[]).map(({ items, group }) => {
|
||||
return {
|
||||
group,
|
||||
items: items.filter(item => !productServiceIds.includes(parseInt((item as ComboboxItem).value)))
|
||||
}
|
||||
})
|
||||
items: items.filter(
|
||||
item =>
|
||||
!productServiceIds.includes(
|
||||
parseInt((item as ComboboxItem).value)
|
||||
)
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
onChange(innerValue as DealProductServiceSchema[]);
|
||||
}, [innerValue]);
|
||||
return (
|
||||
<Input.Wrapper error={error}>
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
{innerValue.map((service, idx) => (
|
||||
<Flex key={service.service?.name || idx} direction={"row"} gap={rem(10)} align={"center"}
|
||||
justify={"stretch"}>
|
||||
<ActionIcon onClick={() => onDelete(idx)} variant={"default"}>
|
||||
<IconTrash/>
|
||||
<Flex
|
||||
key={service.service?.name || idx}
|
||||
direction={"row"}
|
||||
gap={rem(10)}
|
||||
align={"center"}
|
||||
justify={"stretch"}>
|
||||
<ActionIcon
|
||||
onClick={() => onDelete(idx)}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
<ServiceWithPriceInput
|
||||
serviceProps={{
|
||||
onChange: (event) => onServiceChange(idx, event),
|
||||
onChange: event => onServiceChange(idx, event),
|
||||
value: service.service,
|
||||
placeholder: "Выберите услугу",
|
||||
style: {width: "100%"},
|
||||
filter: serviceOptionsFilter as OptionsFilter
|
||||
style: { width: "100%" },
|
||||
filter: serviceOptionsFilter as OptionsFilter,
|
||||
}}
|
||||
priceProps={{
|
||||
onChange: (event) => onQuantityChange(idx, event),
|
||||
onChange: event => onQuantityChange(idx, event),
|
||||
value: service.price,
|
||||
placeholder: "Введите стоимость",
|
||||
hideControls: true,
|
||||
style: {width: "100%"},
|
||||
style: { width: "100%" },
|
||||
suffix: "₽",
|
||||
disabled: authState.isGuest
|
||||
disabled: authState.isGuest,
|
||||
}}
|
||||
containerProps={{w: "100%"}}
|
||||
containerProps={{ w: "100%" }}
|
||||
quantity={quantity}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
))}
|
||||
<Button onClick={onCreate} variant={"default"}>Добавить услугу</Button>
|
||||
<Button
|
||||
onClick={onCreate}
|
||||
variant={"default"}>
|
||||
Добавить услугу
|
||||
</Button>
|
||||
</Flex>
|
||||
</Input.Wrapper>
|
||||
)
|
||||
}
|
||||
export default DealProductServiceTable;
|
||||
);
|
||||
};
|
||||
export default DealProductServiceTable;
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import useDealProductsTableColumns from "./columns.tsx";
|
||||
import {FC} from "react";
|
||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
||||
import {DealProductSchema, ProductService} from "../../../../client";
|
||||
import {ActionIcon, Button, Flex, rem, Tooltip} from "@mantine/core";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {IconBarcode, IconEdit, IconTrash} from "@tabler/icons-react";
|
||||
import {notifications} from "../../../../shared/lib/notifications.ts";
|
||||
import {CreateProductRequest} from "../../../ProductsPage/types.ts";
|
||||
import { FC } from "react";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { DealProductSchema, ProductService } from "../../../../client";
|
||||
import { ActionIcon, Button, Flex, rem, Tooltip } from "@mantine/core";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { IconBarcode, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { CreateProductRequest } from "../../../ProductsPage/types.ts";
|
||||
|
||||
type RestProps = {
|
||||
clientId: number;
|
||||
onMultipleDelete?: (items: DealProductSchema[]) => void;
|
||||
}
|
||||
};
|
||||
type Props = CRUDTableProps<DealProductSchema> & RestProps;
|
||||
const DealProductsTable: FC<Props> = (props: Props) => {
|
||||
const {items, clientId, onChange, onCreate, onDelete, onMultipleDelete} = props;
|
||||
const { items, clientId, onChange, onCreate, onDelete, onMultipleDelete } =
|
||||
props;
|
||||
|
||||
const columns = useDealProductsTableColumns({
|
||||
onChange: (product, quantity) => {
|
||||
@@ -25,10 +26,9 @@ const DealProductsTable: FC<Props> = (props: Props) => {
|
||||
onDelete(product);
|
||||
return;
|
||||
}
|
||||
onChange({...product, quantity})
|
||||
onChange({ ...product, quantity });
|
||||
},
|
||||
data: items
|
||||
|
||||
data: items,
|
||||
});
|
||||
|
||||
const onCreateClick = () => {
|
||||
@@ -37,112 +37,132 @@ const DealProductsTable: FC<Props> = (props: Props) => {
|
||||
modal: "addDealProduct",
|
||||
title: "Добавление товара",
|
||||
innerProps: {
|
||||
onCreate: (product) => onCreate(product as DealProductSchema),
|
||||
clientId
|
||||
onCreate: product => onCreate(product as DealProductSchema),
|
||||
clientId,
|
||||
},
|
||||
size: "lg"
|
||||
})
|
||||
}
|
||||
size: "lg",
|
||||
});
|
||||
};
|
||||
const onPrintBarcodeClick = (product: DealProductSchema) => {
|
||||
modals.openContextModal({
|
||||
modal: "printBarcode",
|
||||
title: 'Печать штрихкода',
|
||||
title: "Печать штрихкода",
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
productId: product.product.id,
|
||||
defaultQuantity: product.quantity
|
||||
}
|
||||
})
|
||||
}
|
||||
defaultQuantity: product.quantity,
|
||||
},
|
||||
});
|
||||
};
|
||||
const onCreateProduct = (newProduct: CreateProductRequest) => {
|
||||
ProductService.createProduct({
|
||||
requestBody: newProduct
|
||||
}).then(({ok, message}) => {
|
||||
notifications.guess(ok, {message: message});
|
||||
})
|
||||
}
|
||||
requestBody: newProduct,
|
||||
}).then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message: message });
|
||||
});
|
||||
};
|
||||
const onCreateProductClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "createProduct",
|
||||
title: 'Создание товара',
|
||||
title: "Создание товара",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
clientId: clientId,
|
||||
onCreate: onCreateProduct
|
||||
}
|
||||
})
|
||||
}
|
||||
onCreate: onCreateProduct,
|
||||
},
|
||||
});
|
||||
};
|
||||
const onEditClick = (product: DealProductSchema) => {
|
||||
if (!onChange) return;
|
||||
modals.openContextModal({
|
||||
modal: "addDealProduct",
|
||||
title: 'Создание товара',
|
||||
title: "Создание товара",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
clientId: clientId,
|
||||
element: product,
|
||||
onChange: onChange
|
||||
onChange: onChange,
|
||||
},
|
||||
size: "lg"
|
||||
|
||||
})
|
||||
}
|
||||
size: "lg",
|
||||
});
|
||||
};
|
||||
return (
|
||||
<BaseTable
|
||||
data={items}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableBottomToolbar: true,
|
||||
enableRowActions: true,
|
||||
enableRowSelection: true,
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon onClick={() => onDelete && onDelete(row.original)} variant={"default"}>
|
||||
<IconTrash/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Печать штрихкода">
|
||||
<ActionIcon onClick={() => onPrintBarcodeClick(row.original)} variant={"default"}>
|
||||
<IconBarcode/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon onClick={() => onEditClick(row.original)} variant={"default"}>
|
||||
<IconEdit/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
renderBottomToolbar: ({table}) => (
|
||||
<Flex justify={"flex-end"} gap={rem(10)} p={rem(10)}>
|
||||
{(onMultipleDelete && table.getSelectedRowModel().rows.length > 0) && (
|
||||
restProps={
|
||||
{
|
||||
enableBottomToolbar: true,
|
||||
enableRowActions: true,
|
||||
enableRowSelection: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
onDelete && onDelete(row.original)
|
||||
}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Печать штрихкода">
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
onPrintBarcodeClick(row.original)
|
||||
}
|
||||
variant={"default"}>
|
||||
<IconBarcode />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
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={() => {
|
||||
onMultipleDelete(table.getSelectedRowModel().rows.map(row => row.original))
|
||||
}}
|
||||
variant={"filled"}
|
||||
color={"red"}
|
||||
>
|
||||
Удалить выбранные
|
||||
variant={"default"}
|
||||
onClick={onCreateProductClick}>
|
||||
Создать товар
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant={"default"}
|
||||
onClick={onCreateProductClick}>
|
||||
Создать товар
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={onCreateClick}
|
||||
variant={"default"}>
|
||||
Добавить товар в сделку
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<DealProductSchema>}
|
||||
<Button
|
||||
onClick={onCreateClick}
|
||||
variant={"default"}>
|
||||
Добавить товар в сделку
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<DealProductSchema>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default DealProductsTable;
|
||||
export default DealProductsTable;
|
||||
|
||||
@@ -1,74 +1,107 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {DealProductSchema} from "../../../../client";
|
||||
import {List} from "@mantine/core";
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { DealProductSchema } from "../../../../client";
|
||||
import { List } from "@mantine/core";
|
||||
|
||||
type Props = {
|
||||
onChange: (product: DealProductSchema, quantity: number) => void;
|
||||
data: DealProductSchema[];
|
||||
}
|
||||
};
|
||||
const useDealProductsTableColumns = (props: Props) => {
|
||||
const {onChange, data} = props;
|
||||
const totalQuantity = useMemo(() => data.reduce((acc, row) => acc + row.quantity, 0), [data]);
|
||||
const totalPrice = useMemo(() => data.reduce((totalAcc, row) => totalAcc + row.services.reduce((singleAcc, service) => singleAcc + service.price * row.quantity, 0), 0), [data]);
|
||||
return useMemo<MRT_ColumnDef<DealProductSchema>[]>(() => [
|
||||
{
|
||||
accessorKey: "product.article",
|
||||
header: "Артикул",
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "product.name",
|
||||
header: "Название",
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "product.barcodes",
|
||||
header: "Штрихкоды",
|
||||
Cell: ({cell}) => {
|
||||
return (
|
||||
<List size={"sm"}>
|
||||
|
||||
{cell.getValue<string[]>()?.map(barcode => (
|
||||
<List.Item key={barcode}>
|
||||
{barcode}
|
||||
</List.Item>
|
||||
))}
|
||||
|
||||
</List>
|
||||
)
|
||||
const { onChange, data } = props;
|
||||
const totalQuantity = useMemo(
|
||||
() => data.reduce((acc, row) => acc + row.quantity, 0),
|
||||
[data]
|
||||
);
|
||||
const totalPrice = useMemo(
|
||||
() =>
|
||||
data.reduce(
|
||||
(totalAcc, row) =>
|
||||
totalAcc +
|
||||
row.services.reduce(
|
||||
(singleAcc, service) =>
|
||||
singleAcc + service.price * row.quantity,
|
||||
0
|
||||
),
|
||||
0
|
||||
),
|
||||
[data]
|
||||
);
|
||||
return useMemo<MRT_ColumnDef<DealProductSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "product.article",
|
||||
header: "Артикул",
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
},
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "quantity",
|
||||
header: "Количество",
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
Footer: <>Всего товаров: {totalQuantity} </>,
|
||||
},
|
||||
{
|
||||
header: "Услуги",
|
||||
Cell: ({row}) => <List size={"sm"}>{
|
||||
row.original.services.map(service => `${service.service.name} (${service.price}₽ за шт)`)
|
||||
.map(serviceText => <List.Item key={serviceText}>
|
||||
{serviceText}
|
||||
</List.Item>
|
||||
)}
|
||||
</List>,
|
||||
enableColumnActions: false,
|
||||
},
|
||||
{
|
||||
header: "Итоговая стоимость услуг",
|
||||
Cell: ({row}) => <>
|
||||
{row.original.services.reduce((acc, service) => acc + row.original.quantity * service.price, 0)}</>,
|
||||
enableColumnActions: false,
|
||||
Footer: <>Всего стоимость услуг: {totalPrice}</>
|
||||
}
|
||||
], [onChange, data])
|
||||
}
|
||||
{
|
||||
accessorKey: "product.name",
|
||||
header: "Название",
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "product.barcodes",
|
||||
header: "Штрихкоды",
|
||||
Cell: ({ cell }) => {
|
||||
return (
|
||||
<List size={"sm"}>
|
||||
{cell
|
||||
.getValue<string[]>()
|
||||
?.map(barcode => (
|
||||
<List.Item key={barcode}>
|
||||
{barcode}
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "quantity",
|
||||
header: "Количество",
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
Footer: <>Всего товаров: {totalQuantity} </>,
|
||||
},
|
||||
{
|
||||
header: "Услуги",
|
||||
Cell: ({ row }) => (
|
||||
<List size={"sm"}>
|
||||
{row.original.services
|
||||
.map(
|
||||
service =>
|
||||
`${service.service.name} (${service.price}₽ за шт)`
|
||||
)
|
||||
.map(serviceText => (
|
||||
<List.Item key={serviceText}>
|
||||
{serviceText}
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
),
|
||||
enableColumnActions: false,
|
||||
},
|
||||
{
|
||||
header: "Итоговая стоимость услуг",
|
||||
Cell: ({ row }) => (
|
||||
<>
|
||||
{row.original.services.reduce(
|
||||
(acc, service) =>
|
||||
acc + row.original.quantity * service.price,
|
||||
0
|
||||
)}
|
||||
</>
|
||||
),
|
||||
enableColumnActions: false,
|
||||
Footer: <>Всего стоимость услуг: {totalPrice}</>,
|
||||
},
|
||||
],
|
||||
[onChange, data]
|
||||
);
|
||||
};
|
||||
|
||||
export default useDealProductsTableColumns;
|
||||
export default useDealProductsTableColumns;
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
import {FC} from "react";
|
||||
import {useDealServicesTableColumns} from "./columns.tsx";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {DealServiceSchema} from "../../../../client";
|
||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import {ActionIcon, Button, Flex, rem, Tooltip} from "@mantine/core";
|
||||
import {openContextModal} from "@mantine/modals";
|
||||
import {IconEdit, IconTrash} from "@tabler/icons-react";
|
||||
import { FC } from "react";
|
||||
import { useDealServicesTableColumns } from "./columns.tsx";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { DealServiceSchema } from "../../../../client";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { ActionIcon, Button, Flex, rem, Tooltip } from "@mantine/core";
|
||||
import { openContextModal } from "@mantine/modals";
|
||||
import { IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
|
||||
type RestProps = {
|
||||
onMultipleDelete?: (items: DealServiceSchema[]) => void;
|
||||
}
|
||||
};
|
||||
type Props = CRUDTableProps<DealServiceSchema> & RestProps;
|
||||
const DealServicesTable: FC<Props> = (
|
||||
{
|
||||
items,
|
||||
onChange,
|
||||
onDelete,
|
||||
onCreate,
|
||||
onSelectionChange,
|
||||
onMultipleDelete,
|
||||
tableRef
|
||||
}) => {
|
||||
const DealServicesTable: FC<Props> = ({
|
||||
items,
|
||||
onChange,
|
||||
onDelete,
|
||||
onCreate,
|
||||
onSelectionChange,
|
||||
onMultipleDelete,
|
||||
tableRef,
|
||||
}) => {
|
||||
const serviceIds = items.map(item => item.service.id);
|
||||
|
||||
|
||||
const columns = useDealServicesTableColumns({
|
||||
data: items
|
||||
data: items,
|
||||
});
|
||||
const onCreateClick = () => {
|
||||
if (!onCreate) return;
|
||||
@@ -34,12 +32,11 @@ const DealServicesTable: FC<Props> = (
|
||||
title: "Добавление услуги",
|
||||
modal: "addDealService",
|
||||
innerProps: {
|
||||
onCreate: (event) => onCreate(event as DealServiceSchema),
|
||||
serviceIds
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
onCreate: event => onCreate(event as DealServiceSchema),
|
||||
serviceIds,
|
||||
},
|
||||
});
|
||||
};
|
||||
const onEditClick = (service: DealServiceSchema) => {
|
||||
if (!onChange) return;
|
||||
openContextModal({
|
||||
@@ -48,12 +45,10 @@ const DealServicesTable: FC<Props> = (
|
||||
innerProps: {
|
||||
element: service,
|
||||
onChange,
|
||||
serviceIds
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
serviceIds,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -62,58 +57,73 @@ const DealServicesTable: FC<Props> = (
|
||||
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) && (
|
||||
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={() => {
|
||||
onMultipleDelete(table.getSelectedRowModel().rows.map(row => row.original))
|
||||
}}
|
||||
variant={"filled"}
|
||||
color={"red"}
|
||||
>
|
||||
Удалить выбранные
|
||||
onClick={onCreateClick}
|
||||
variant={"default"}>
|
||||
Добавить услугу
|
||||
</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>}
|
||||
</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>
|
||||
}
|
||||
/>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default DealServicesTable;
|
||||
export default DealServicesTable;
|
||||
|
||||
@@ -1,62 +1,69 @@
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {useMemo} from "react";
|
||||
import {DealServiceSchema} from "../../../../client";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { useMemo } from "react";
|
||||
import { DealServiceSchema } from "../../../../client";
|
||||
|
||||
type Props = {
|
||||
// onChange: (service: DealServiceSchema, quantity: number) => void;
|
||||
data: DealServiceSchema[];
|
||||
}
|
||||
};
|
||||
|
||||
export const useDealServicesTableColumns = (props: Props) => {
|
||||
const {data} = props;
|
||||
const totalPrice = useMemo(() =>
|
||||
data.reduce((acc, row) => acc + row.quantity * row.price, 0)
|
||||
,
|
||||
[data]);
|
||||
const { data } = props;
|
||||
const totalPrice = useMemo(
|
||||
() => data.reduce((acc, row) => acc + row.quantity * row.price, 0),
|
||||
[data]
|
||||
);
|
||||
|
||||
return useMemo<MRT_ColumnDef<DealServiceSchema>[]>(() => [
|
||||
{
|
||||
accessorKey: "service.category",
|
||||
header: "Категория",
|
||||
accessorFn: (row) => row.service.category.name,
|
||||
},
|
||||
{
|
||||
enableGrouping: false,
|
||||
accessorKey: "service.name",
|
||||
header: "Услуга",
|
||||
},
|
||||
{
|
||||
enableGrouping: false,
|
||||
accessorKey: "price",
|
||||
header: "Цена",
|
||||
},
|
||||
{
|
||||
enableGrouping: false,
|
||||
accessorKey: "service.cost",
|
||||
header: "Себестоимость"
|
||||
},
|
||||
{
|
||||
enableGrouping: false,
|
||||
accessorKey: "quantity",
|
||||
header: "Количество",
|
||||
},
|
||||
{
|
||||
enableGrouping: false,
|
||||
header: "Сумма",
|
||||
Cell: ({row}) => {
|
||||
return row.original.quantity * row.original.price;
|
||||
return useMemo<MRT_ColumnDef<DealServiceSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "service.category",
|
||||
header: "Категория",
|
||||
accessorFn: row => row.service.category.name,
|
||||
},
|
||||
aggregationFn: "sum",
|
||||
AggregatedCell: ({cell}) => {
|
||||
return <>Итоговая сумма по категории: {" "}
|
||||
{
|
||||
cell.row.subRows?.reduce((acc, row) =>
|
||||
acc + row.original.quantity * row.original.price, 0)
|
||||
}
|
||||
</>;
|
||||
{
|
||||
enableGrouping: false,
|
||||
accessorKey: "service.name",
|
||||
header: "Услуга",
|
||||
},
|
||||
Footer: <>Итоговая сумма по услугам: {totalPrice}</>
|
||||
|
||||
}
|
||||
], []);
|
||||
}
|
||||
{
|
||||
enableGrouping: false,
|
||||
accessorKey: "price",
|
||||
header: "Цена",
|
||||
},
|
||||
{
|
||||
enableGrouping: false,
|
||||
accessorKey: "service.cost",
|
||||
header: "Себестоимость",
|
||||
},
|
||||
{
|
||||
enableGrouping: false,
|
||||
accessorKey: "quantity",
|
||||
header: "Количество",
|
||||
},
|
||||
{
|
||||
enableGrouping: false,
|
||||
header: "Сумма",
|
||||
Cell: ({ row }) => {
|
||||
return row.original.quantity * row.original.price;
|
||||
},
|
||||
aggregationFn: "sum",
|
||||
AggregatedCell: ({ cell }) => {
|
||||
return (
|
||||
<>
|
||||
Итоговая сумма по категории:{" "}
|
||||
{cell.row.subRows?.reduce(
|
||||
(acc, row) =>
|
||||
acc +
|
||||
row.original.quantity * row.original.price,
|
||||
0
|
||||
)}
|
||||
</>
|
||||
);
|
||||
},
|
||||
Footer: <>Итоговая сумма по услугам: {totalPrice}</>,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {DealStatusHistorySchema} from "../../../../client";
|
||||
import {useDealStatusChangeTableColumns} from "./columns.tsx";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {FC} from "react";
|
||||
import { DealStatusHistorySchema } from "../../../../client";
|
||||
import { useDealStatusChangeTableColumns } from "./columns.tsx";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { FC } from "react";
|
||||
|
||||
type Props = {
|
||||
items: DealStatusHistorySchema[];
|
||||
}
|
||||
};
|
||||
|
||||
const DealStatusChangeTable: FC<Props> = (props: Props) => {
|
||||
const {items} = props;
|
||||
const { items } = props;
|
||||
|
||||
return (
|
||||
<BaseTable
|
||||
@@ -21,9 +21,9 @@ const DealStatusChangeTable: FC<Props> = (props: Props) => {
|
||||
enableBottomToolbar: false,
|
||||
enableColumnFilters: false,
|
||||
enableColumnVisibilityToggle: false,
|
||||
layoutMode:"grid"
|
||||
layoutMode: "grid",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default DealStatusChangeTable;
|
||||
};
|
||||
export default DealStatusChangeTable;
|
||||
|
||||
@@ -1,41 +1,60 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {DealStatusHistorySchema} from "../../../../client";
|
||||
import {DealStatus, DealStatusDictionary} from "../../../../shared/enums/DealStatus.ts";
|
||||
import {Spoiler, Text} from "@mantine/core";
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { DealStatusHistorySchema } from "../../../../client";
|
||||
import {
|
||||
DealStatus,
|
||||
DealStatusDictionary,
|
||||
} from "../../../../shared/enums/DealStatus.ts";
|
||||
import { Spoiler, Text } from "@mantine/core";
|
||||
|
||||
export const useDealStatusChangeTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<DealStatusHistorySchema>[]>(() => [
|
||||
{
|
||||
accessorKey: "changedAt",
|
||||
header: "Дата",
|
||||
accessorFn: (row) => new Date(row.changedAt).toLocaleString('ru-RU'),
|
||||
},
|
||||
{
|
||||
header: "Пользователь",
|
||||
accessorFn: (row) => `${row.user.firstName} ${row.user.secondName}`
|
||||
},
|
||||
{
|
||||
accessorKey: "fromStatus",
|
||||
header: "Из статуса",
|
||||
accessorFn: (row) => DealStatusDictionary[row.fromStatus as DealStatus],
|
||||
},
|
||||
{
|
||||
accessorKey: "toStatus",
|
||||
header: "В статус",
|
||||
accessorFn: (row) => DealStatusDictionary[row.toStatus as DealStatus],
|
||||
},
|
||||
{
|
||||
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>
|
||||
,
|
||||
|
||||
}
|
||||
], []);
|
||||
}
|
||||
return useMemo<MRT_ColumnDef<DealStatusHistorySchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "changedAt",
|
||||
header: "Дата",
|
||||
accessorFn: row =>
|
||||
new Date(row.changedAt).toLocaleString("ru-RU"),
|
||||
},
|
||||
{
|
||||
header: "Пользователь",
|
||||
accessorFn: row =>
|
||||
`${row.user.firstName} ${row.user.secondName}`,
|
||||
},
|
||||
{
|
||||
accessorKey: "fromStatus",
|
||||
header: "Из статуса",
|
||||
accessorFn: row =>
|
||||
DealStatusDictionary[row.fromStatus as DealStatus],
|
||||
},
|
||||
{
|
||||
accessorKey: "toStatus",
|
||||
header: "В статус",
|
||||
accessorFn: row =>
|
||||
DealStatusDictionary[row.toStatus as DealStatus],
|
||||
},
|
||||
{
|
||||
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>
|
||||
),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
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";
|
||||
import {UserRoleEnum} from "../../../../shared/enums/UserRole.ts";
|
||||
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";
|
||||
import { UserRoleEnum } from "../../../../shared/enums/UserRole.ts";
|
||||
|
||||
export type SimpleUsersTableProps = {
|
||||
items: UserSchema[],
|
||||
onChange: (items: UserSchema[]) => void
|
||||
}
|
||||
items: UserSchema[];
|
||||
onChange: (items: UserSchema[]) => void;
|
||||
};
|
||||
|
||||
const SimpleUsersTable: FC<SimpleUsersTableProps> = ({items, onChange}) => {
|
||||
const SimpleUsersTable: FC<SimpleUsersTableProps> = ({ items, onChange }) => {
|
||||
const columns = useSimpleUsersTableColumns();
|
||||
|
||||
const onAddClick = () => {
|
||||
@@ -24,40 +24,56 @@ const SimpleUsersTable: FC<SimpleUsersTableProps> = ({items, onChange}) => {
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
selectProps: {
|
||||
filterBy: (user) => !userIds.includes(user.id) && user.roleKey === UserRoleEnum.EMPLOYEE
|
||||
filterBy: user =>
|
||||
!userIds.includes(user.id) &&
|
||||
user.roleKey === UserRoleEnum.EMPLOYEE,
|
||||
},
|
||||
onSelect: (user) => onChange([...items, user])
|
||||
}
|
||||
})
|
||||
}
|
||||
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>}
|
||||
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;
|
||||
export default SimpleUsersTable;
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {UserSchema} from "../../../../client";
|
||||
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: "Должность"
|
||||
},
|
||||
], []);
|
||||
}
|
||||
return useMemo<MRT_ColumnDef<UserSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "firstName",
|
||||
header: "Имя",
|
||||
},
|
||||
{
|
||||
accessorKey: "secondName",
|
||||
header: "Фамилия",
|
||||
},
|
||||
{
|
||||
accessorKey: "position.name",
|
||||
header: "Должность",
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,34 +1,42 @@
|
||||
import {createContext, FC, useContext, useState} from "react";
|
||||
import {DealSchema} from "../../../client";
|
||||
import { createContext, FC, useContext, useState } from "react";
|
||||
import { DealSchema } from "../../../client";
|
||||
|
||||
type DealPageContextState = {
|
||||
selectedDeal?: DealSchema;
|
||||
setSelectedDeal: (deal: DealSchema | undefined) => void;
|
||||
}
|
||||
};
|
||||
|
||||
const DealPageContext = createContext<DealPageContextState | undefined>(undefined);
|
||||
const DealPageContext = createContext<DealPageContextState | undefined>(
|
||||
undefined
|
||||
);
|
||||
const useDealPageContextState = () => {
|
||||
const [selectedDeal, setSelectedDeal] = useState<DealSchema | undefined>(undefined);
|
||||
return {selectedDeal, setSelectedDeal};
|
||||
}
|
||||
const [selectedDeal, setSelectedDeal] = useState<DealSchema | undefined>(
|
||||
undefined
|
||||
);
|
||||
return { selectedDeal, setSelectedDeal };
|
||||
};
|
||||
|
||||
type DealPageContextProviderProps = {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
};
|
||||
|
||||
export const DealPageContextProvider: FC<DealPageContextProviderProps> = ({children}) => {
|
||||
export const DealPageContextProvider: FC<DealPageContextProviderProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const state = useDealPageContextState();
|
||||
return (
|
||||
<DealPageContext.Provider value={state}>
|
||||
{children}
|
||||
</DealPageContext.Provider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const useDealPageContext = () => {
|
||||
const context = useContext(DealPageContext);
|
||||
if (!context) {
|
||||
throw new Error('useDealPageContext must be used within a DealPageContextProvider');
|
||||
throw new Error(
|
||||
"useDealPageContext must be used within a DealPageContextProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,4 +7,4 @@
|
||||
@mixin dark {
|
||||
background-color: var(--mantine-color-dark-5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ import { Box, Drawer, rem, Tabs, Text } from "@mantine/core";
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
import DealServicesTable from "../../components/DealServicesTable/DealServicesTable.tsx";
|
||||
import { useDealPageContext } from "../../contexts/DealPageContext.tsx";
|
||||
import { DealProductSchema, DealService, DealServiceSchema } from "../../../../client";
|
||||
import {
|
||||
DealProductSchema,
|
||||
DealService,
|
||||
DealServiceSchema,
|
||||
} from "../../../../client";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { BaseTableRef } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
@@ -27,13 +31,13 @@ const useDealServicesTableState = () => {
|
||||
service,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
|
||||
if (!ok) {
|
||||
notifications.guess(ok, { message });
|
||||
return;
|
||||
}
|
||||
await DealService.getDealById({ dealId: selectedDeal.id })
|
||||
.then(setSelectedDeal);
|
||||
await DealService.getDealById({ dealId: selectedDeal.id }).then(
|
||||
setSelectedDeal
|
||||
);
|
||||
});
|
||||
};
|
||||
const onServiceDelete = (service: DealServiceSchema) => {
|
||||
@@ -42,15 +46,9 @@ const useDealServicesTableState = () => {
|
||||
title: "Удаление услуги",
|
||||
children: (
|
||||
<>
|
||||
<Text>
|
||||
Вы уверены, что хотите удалить услугу:
|
||||
</Text>
|
||||
<Text>
|
||||
{service.service.name}?
|
||||
</Text>
|
||||
|
||||
<Text>Вы уверены, что хотите удалить услугу:</Text>
|
||||
<Text>{service.service.name}?</Text>
|
||||
</>
|
||||
|
||||
),
|
||||
onConfirm: () => {
|
||||
DealService.deleteDealService({
|
||||
@@ -63,8 +61,9 @@ const useDealServicesTableState = () => {
|
||||
notifications.guess(ok, { message });
|
||||
return;
|
||||
}
|
||||
await DealService.getDealById({ dealId: selectedDeal.id })
|
||||
.then(setSelectedDeal);
|
||||
await DealService.getDealById({
|
||||
dealId: selectedDeal.id,
|
||||
}).then(setSelectedDeal);
|
||||
});
|
||||
},
|
||||
labels: {
|
||||
@@ -87,8 +86,9 @@ const useDealServicesTableState = () => {
|
||||
notifications.guess(ok, { message });
|
||||
return;
|
||||
}
|
||||
await DealService.getDealById({ dealId: selectedDeal.id })
|
||||
.then(setSelectedDeal);
|
||||
await DealService.getDealById({ dealId: selectedDeal.id }).then(
|
||||
setSelectedDeal
|
||||
);
|
||||
});
|
||||
};
|
||||
const onsServiceMultipleDelete = (items: DealServiceSchema[]) => {
|
||||
@@ -113,8 +113,9 @@ const useDealServicesTableState = () => {
|
||||
notifications.guess(ok, { message });
|
||||
return;
|
||||
}
|
||||
await DealService.getDealById({ dealId: selectedDeal.id })
|
||||
.then(setSelectedDeal);
|
||||
await DealService.getDealById({
|
||||
dealId: selectedDeal.id,
|
||||
}).then(setSelectedDeal);
|
||||
});
|
||||
},
|
||||
labels: {
|
||||
@@ -143,14 +144,16 @@ const DealEditDrawerServicesTable = () => {
|
||||
onsServiceMultipleDelete,
|
||||
} = useDealServicesTableState();
|
||||
|
||||
return (<DealServicesTable
|
||||
tableRef={tableRef}
|
||||
items={services}
|
||||
onChange={onServiceUpdate}
|
||||
onDelete={onServiceDelete}
|
||||
onCreate={onServiceCreate}
|
||||
onMultipleDelete={onsServiceMultipleDelete}
|
||||
/>);
|
||||
return (
|
||||
<DealServicesTable
|
||||
tableRef={tableRef}
|
||||
items={services}
|
||||
onChange={onServiceUpdate}
|
||||
onDelete={onServiceDelete}
|
||||
onCreate={onServiceCreate}
|
||||
onMultipleDelete={onsServiceMultipleDelete}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const useDealProductTableState = () => {
|
||||
@@ -166,8 +169,9 @@ const useDealProductTableState = () => {
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await DealService.getDealById({ dealId: selectedDeal.id })
|
||||
.then(setSelectedDeal);
|
||||
await DealService.getDealById({ dealId: selectedDeal.id }).then(
|
||||
setSelectedDeal
|
||||
);
|
||||
});
|
||||
};
|
||||
const onProductDelete = (product: DealProductSchema) => {
|
||||
@@ -176,15 +180,9 @@ const useDealProductTableState = () => {
|
||||
title: "Удаление товара",
|
||||
children: (
|
||||
<>
|
||||
<Text>
|
||||
Вы уверены, что хотите удалить товар:
|
||||
</Text>
|
||||
<Text>
|
||||
{product.product.name}?
|
||||
</Text>
|
||||
|
||||
<Text>Вы уверены, что хотите удалить товар:</Text>
|
||||
<Text>{product.product.name}?</Text>
|
||||
</>
|
||||
|
||||
),
|
||||
onConfirm: () => {
|
||||
DealService.deleteDealProduct({
|
||||
@@ -197,8 +195,9 @@ const useDealProductTableState = () => {
|
||||
notifications.guess(ok, { message });
|
||||
return;
|
||||
}
|
||||
await DealService.getDealById({ dealId: selectedDeal.id })
|
||||
.then(setSelectedDeal);
|
||||
await DealService.getDealById({
|
||||
dealId: selectedDeal.id,
|
||||
}).then(setSelectedDeal);
|
||||
});
|
||||
},
|
||||
labels: {
|
||||
@@ -219,8 +218,9 @@ const useDealProductTableState = () => {
|
||||
notifications.guess(ok, { message });
|
||||
return;
|
||||
}
|
||||
await DealService.getDealById({ dealId: selectedDeal.id })
|
||||
.then(setSelectedDeal);
|
||||
await DealService.getDealById({ dealId: selectedDeal.id }).then(
|
||||
setSelectedDeal
|
||||
);
|
||||
});
|
||||
};
|
||||
const onProductMultipleDelete = (items: DealProductSchema[]) => {
|
||||
@@ -245,8 +245,9 @@ const useDealProductTableState = () => {
|
||||
notifications.guess(ok, { message });
|
||||
return;
|
||||
}
|
||||
await DealService.getDealById({ dealId: selectedDeal.id })
|
||||
.then(setSelectedDeal);
|
||||
await DealService.getDealById({
|
||||
dealId: selectedDeal.id,
|
||||
}).then(setSelectedDeal);
|
||||
});
|
||||
},
|
||||
labels: {
|
||||
@@ -281,7 +282,6 @@ const DealEditDrawerProductsTable = () => {
|
||||
onMultipleDelete={onProductMultipleDelete}
|
||||
onDelete={onProductDelete}
|
||||
onCreate={onProductCreate}
|
||||
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -295,10 +295,7 @@ const useDealStatusChangeState = () => {
|
||||
const DealEditDrawerStatusChangeTable = () => {
|
||||
const { statusHistory } = useDealStatusChangeState();
|
||||
|
||||
return (
|
||||
<DealStatusChangeTable
|
||||
items={statusHistory}
|
||||
/>);
|
||||
return <DealStatusChangeTable items={statusHistory} />;
|
||||
};
|
||||
|
||||
const useDealEditDrawerState = () => {
|
||||
@@ -328,53 +325,51 @@ const DealEditDrawer: FC = () => {
|
||||
body: {
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column", gap: rem(10),
|
||||
flexDirection: "column",
|
||||
gap: rem(10),
|
||||
},
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Tabs
|
||||
defaultValue={"general"}
|
||||
flex={1}
|
||||
variant={"outline"}
|
||||
orientation={"vertical"}
|
||||
keepMounted={false}
|
||||
|
||||
>
|
||||
|
||||
<Tabs.List
|
||||
>
|
||||
<Tabs.Tab value={"general"} leftSection={<IconSettings />}>
|
||||
keepMounted={false}>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab
|
||||
value={"general"}
|
||||
leftSection={<IconSettings />}>
|
||||
Общее
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value={"history"} leftSection={<IconCalendarUser />}>
|
||||
<Tabs.Tab
|
||||
value={"history"}
|
||||
leftSection={<IconCalendarUser />}>
|
||||
История
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value={"servicesAndProducts"} leftSection={<IconBox />}>
|
||||
<Tabs.Tab
|
||||
value={"servicesAndProducts"}
|
||||
leftSection={<IconBox />}>
|
||||
Товары и услуги
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value={"general"}>
|
||||
<motion.div
|
||||
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
|
||||
<Box h={"100%"} w={"100%"} p={rem(10)}>
|
||||
transition={{ duration: 0.2 }}>
|
||||
<Box
|
||||
h={"100%"}
|
||||
w={"100%"}
|
||||
p={rem(10)}>
|
||||
<DealEditDrawerGeneralTab />
|
||||
|
||||
</Box>
|
||||
</motion.div>
|
||||
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value={"history"}>
|
||||
<motion.div
|
||||
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
transition={{ duration: 0.2 }}>
|
||||
<Box p={rem(10)}>
|
||||
<DealEditDrawerStatusChangeTable />
|
||||
</Box>
|
||||
@@ -382,11 +377,9 @@ const DealEditDrawer: FC = () => {
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value={"servicesAndProducts"}>
|
||||
<motion.div
|
||||
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
transition={{ duration: 0.2 }}>
|
||||
<Box p={rem(10)}>
|
||||
<ProductAndServiceTab />
|
||||
</Box>
|
||||
@@ -407,4 +400,4 @@ const DealEditDrawer: FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DealEditDrawer;
|
||||
export default DealEditDrawer;
|
||||
|
||||
@@ -14,13 +14,20 @@ import {
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { ClientService, DealSchema, DealService, ShippingWarehouseSchema } from "../../../../../client";
|
||||
import { DealStatus, DealStatusDictionary } from "../../../../../shared/enums/DealStatus.ts";
|
||||
import {
|
||||
ClientService,
|
||||
DealSchema,
|
||||
DealService,
|
||||
ShippingWarehouseSchema,
|
||||
} from "../../../../../client";
|
||||
import {
|
||||
DealStatus,
|
||||
DealStatusDictionary,
|
||||
} from "../../../../../shared/enums/DealStatus.ts";
|
||||
import { isEqual } from "lodash";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import ShippingWarehouseAutocomplete
|
||||
from "../../../../../components/Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
|
||||
import ShippingWarehouseAutocomplete from "../../../../../components/Selects/ShippingWarehouseAutocomplete/ShippingWarehouseAutocomplete.tsx";
|
||||
import { ButtonCopyControlled } from "../../../../../components/ButtonCopyControlled/ButtonCopyControlled.tsx";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import ButtonCopy from "../../../../../components/ButtonCopy/ButtonCopy.tsx";
|
||||
@@ -29,10 +36,10 @@ import { getCurrentDateTimeForFilename } from "../../../../../shared/lib/date.ts
|
||||
import { IconPrinter } from "@tabler/icons-react";
|
||||
|
||||
type Props = {
|
||||
deal: DealSchema
|
||||
}
|
||||
deal: DealSchema;
|
||||
};
|
||||
|
||||
type FormType = Omit<DealSchema, "statusHistory" | "services" | "products">
|
||||
type FormType = Omit<DealSchema, "statusHistory" | "services" | "products">;
|
||||
|
||||
const Content: FC<Props> = ({ deal }) => {
|
||||
const { setSelectedDeal } = useDealPageContext();
|
||||
@@ -40,31 +47,34 @@ const Content: FC<Props> = ({ deal }) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const initialValues: FormType = deal;
|
||||
const form = useForm<FormType>(
|
||||
{
|
||||
initialValues: initialValues,
|
||||
validate: {
|
||||
name: (value: string) => value.length > 0 ? null : "Название сделки не может быть пустым",
|
||||
},
|
||||
const form = useForm<FormType>({
|
||||
initialValues: initialValues,
|
||||
validate: {
|
||||
name: (value: string) =>
|
||||
value.length > 0
|
||||
? null
|
||||
: "Название сделки не может быть пустым",
|
||||
},
|
||||
);
|
||||
});
|
||||
const updateDealInfo = async (values: FormType) => {
|
||||
return DealService.updateDealGeneralInfo({
|
||||
requestBody: {
|
||||
dealId: deal.id,
|
||||
data: { ...values, shippingWarehouse: values.shippingWarehouse?.toString() },
|
||||
data: {
|
||||
...values,
|
||||
shippingWarehouse: values.shippingWarehouse?.toString(),
|
||||
},
|
||||
},
|
||||
}).then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
DealService.getDealById({ dealId: deal.id })
|
||||
.then((data) => {
|
||||
setSelectedDeal(data);
|
||||
form.setInitialValues(data);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["getDealSummaries"],
|
||||
});
|
||||
DealService.getDealById({ dealId: deal.id }).then(data => {
|
||||
setSelectedDeal(data);
|
||||
form.setInitialValues(data);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["getDealSummaries"],
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
const updateClientInfo = async (values: FormType) => {
|
||||
@@ -72,8 +82,7 @@ const Content: FC<Props> = ({ deal }) => {
|
||||
requestBody: {
|
||||
data: values.client,
|
||||
},
|
||||
}).then(({ ok, message }) =>
|
||||
notifications.guess(ok, { message }));
|
||||
}).then(({ ok, message }) => notifications.guess(ok, { message }));
|
||||
};
|
||||
const handleSubmit = async (values: FormType) => {
|
||||
// Updating client info if there changes
|
||||
@@ -83,9 +92,10 @@ const Content: FC<Props> = ({ deal }) => {
|
||||
// updating deal info
|
||||
await updateDealInfo(values);
|
||||
};
|
||||
const isShippingWarehouse = (value: (ShippingWarehouseSchema | string | null | undefined)): value is ShippingWarehouseSchema => {
|
||||
return !["string", "null", "undefined"].includes((typeof value));
|
||||
|
||||
const isShippingWarehouse = (
|
||||
value: ShippingWarehouseSchema | string | null | undefined
|
||||
): value is ShippingWarehouseSchema => {
|
||||
return !["string", "null", "undefined"].includes(typeof value);
|
||||
};
|
||||
|
||||
const onCopyGuestUrlClick = () => {
|
||||
@@ -94,18 +104,20 @@ const Content: FC<Props> = ({ deal }) => {
|
||||
dealId: deal.id,
|
||||
},
|
||||
}).then(({ ok, message, url }) => {
|
||||
if (!ok)
|
||||
notifications.guess(ok, { message });
|
||||
clipboard.copy(
|
||||
`${window.location.origin}/${url}`,
|
||||
);
|
||||
if (!ok) notifications.guess(ok, { message });
|
||||
clipboard.copy(`${window.location.origin}/${url}`);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
|
||||
<Flex direction={"column"} justify={"space-between"} h={"100%"}>
|
||||
<form onSubmit={form.onSubmit(values => handleSubmit(values))}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
justify={"space-between"}
|
||||
h={"100%"}>
|
||||
<Fieldset legend={`Общие параметры [ID: ${deal.id}]`}>
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<TextInput
|
||||
placeholder={"Название сделки"}
|
||||
label={"Название сделки"}
|
||||
@@ -115,20 +127,26 @@ const Content: FC<Props> = ({ deal }) => {
|
||||
disabled
|
||||
placeholder={"Дата создания"}
|
||||
label={"Дата создания"}
|
||||
value={new Date(deal.createdAt).toLocaleString("ru-RU")}
|
||||
value={new Date(deal.createdAt).toLocaleString(
|
||||
"ru-RU"
|
||||
)}
|
||||
/>
|
||||
<TextInput
|
||||
disabled
|
||||
placeholder={"Текущий статус"}
|
||||
label={"Текущий статус"}
|
||||
value={DealStatusDictionary[deal.currentStatus as DealStatus]} />
|
||||
value={
|
||||
DealStatusDictionary[
|
||||
deal.currentStatus as DealStatus
|
||||
]
|
||||
}
|
||||
/>
|
||||
{deal.category && (
|
||||
<TextInput
|
||||
disabled
|
||||
placeholder={"Категория"}
|
||||
label={"Категория"}
|
||||
value={deal.category.name}
|
||||
|
||||
/>
|
||||
)}
|
||||
<Textarea
|
||||
@@ -139,27 +157,46 @@ const Content: FC<Props> = ({ deal }) => {
|
||||
<ShippingWarehouseAutocomplete
|
||||
placeholder={"Введите склад отгрузки"}
|
||||
label={"Склад отгрузки"}
|
||||
value={isShippingWarehouse(form.values.shippingWarehouse) ? form.values.shippingWarehouse : undefined}
|
||||
value={
|
||||
isShippingWarehouse(
|
||||
form.values.shippingWarehouse
|
||||
)
|
||||
? form.values.shippingWarehouse
|
||||
: undefined
|
||||
}
|
||||
onChange={event => {
|
||||
if (isShippingWarehouse(event)) {
|
||||
form.getInputProps("shippingWarehouse").onChange(event.name);
|
||||
form.getInputProps(
|
||||
"shippingWarehouse"
|
||||
).onChange(event.name);
|
||||
return;
|
||||
}
|
||||
form.getInputProps("shippingWarehouse").onChange(event);
|
||||
form.getInputProps(
|
||||
"shippingWarehouse"
|
||||
).onChange(event);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Fieldset>
|
||||
<Flex mt={"md"} gap={rem(10)} align={"center"} justify={"flex-end"}>
|
||||
<Flex align={"center"} gap={rem(10)} justify={"center"}>
|
||||
|
||||
<Flex gap={rem(10)} align={"center"} justify={"space-between"}>
|
||||
<Tooltip
|
||||
label={"Распечатать сделку"}
|
||||
>
|
||||
<Flex
|
||||
mt={"md"}
|
||||
gap={rem(10)}
|
||||
align={"center"}
|
||||
justify={"flex-end"}>
|
||||
<Flex
|
||||
align={"center"}
|
||||
gap={rem(10)}
|
||||
justify={"center"}>
|
||||
<Flex
|
||||
gap={rem(10)}
|
||||
align={"center"}
|
||||
justify={"space-between"}>
|
||||
<Tooltip label={"Распечатать сделку"}>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
const pdfWindow = window.open(`${import.meta.env.VITE_API_URL}/deal/detailedDocument/${deal.id}`);
|
||||
const pdfWindow = window.open(
|
||||
`${import.meta.env.VITE_API_URL}/deal/detailedDocument/${deal.id}`
|
||||
);
|
||||
if (!pdfWindow) return;
|
||||
pdfWindow.print();
|
||||
}}
|
||||
@@ -169,38 +206,42 @@ const Content: FC<Props> = ({ deal }) => {
|
||||
</Tooltip>
|
||||
|
||||
<Flex gap={rem(10)}>
|
||||
|
||||
{(deal.billRequest && deal.billRequest.pdfUrl) ?
|
||||
{deal.billRequest && deal.billRequest.pdfUrl ? (
|
||||
<ButtonCopy
|
||||
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
|
||||
value={deal.billRequest.pdfUrl}
|
||||
>
|
||||
onCopiedLabel={
|
||||
"Ссылка скопирована в буфер обмена"
|
||||
}
|
||||
value={deal.billRequest.pdfUrl}>
|
||||
Ссылка на оплату
|
||||
</ButtonCopy>
|
||||
:
|
||||
) : (
|
||||
<ButtonCopyControlled
|
||||
onCopyClick={() => {
|
||||
const date = getCurrentDateTimeForFilename();
|
||||
FileSaver.saveAs(`${import.meta.env.VITE_API_URL}/deal/document/${deal.id}`,
|
||||
`bill_${deal.id}_${date}.pdf`);
|
||||
const date =
|
||||
getCurrentDateTimeForFilename();
|
||||
FileSaver.saveAs(
|
||||
`${import.meta.env.VITE_API_URL}/deal/document/${deal.id}`,
|
||||
`bill_${deal.id}_${date}.pdf`
|
||||
);
|
||||
}}
|
||||
copied={false}
|
||||
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
|
||||
>
|
||||
onCopiedLabel={
|
||||
"Ссылка скопирована в буфер обмена"
|
||||
}>
|
||||
Ссылка на оплату (PDF)
|
||||
</ButtonCopyControlled>
|
||||
}
|
||||
)}
|
||||
<ButtonCopyControlled
|
||||
onCopyClick={onCopyGuestUrlClick}
|
||||
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
|
||||
onCopiedLabel={
|
||||
"Ссылка скопирована в буфер обмена"
|
||||
}
|
||||
copied={clipboard.copied}>
|
||||
Ссылка на редактирование
|
||||
</ButtonCopyControlled>
|
||||
</Flex>
|
||||
|
||||
</Flex>
|
||||
<Flex gap={rem(10)}>
|
||||
|
||||
<Checkbox
|
||||
label={"Оплачен"}
|
||||
checked={deal.billRequest?.paid || false}
|
||||
@@ -216,36 +257,33 @@ const Content: FC<Props> = ({ deal }) => {
|
||||
{...form.getInputProps("isDeleted")}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
</Flex>
|
||||
<Divider
|
||||
orientation={"vertical"}
|
||||
/>
|
||||
<Group align={"center"} justify={"center"}>
|
||||
<Divider orientation={"vertical"} />
|
||||
<Group
|
||||
align={"center"}
|
||||
justify={"center"}>
|
||||
<Button
|
||||
color={"red"}
|
||||
type={"reset"}
|
||||
disabled={isEqual(initialValues, form.values)}
|
||||
onClick={() => form.reset()}
|
||||
>Отменить изменения</Button>
|
||||
onClick={() => form.reset()}>
|
||||
Отменить изменения
|
||||
</Button>
|
||||
<Button
|
||||
variant={"default"}
|
||||
type={"submit"}
|
||||
disabled={isEqual(initialValues, form.values)}
|
||||
>Сохранить изменения</Button>
|
||||
disabled={isEqual(initialValues, form.values)}>
|
||||
Сохранить изменения
|
||||
</Button>
|
||||
</Group>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
</form>
|
||||
|
||||
);
|
||||
};
|
||||
const DealEditDrawerGeneralTab: FC = () => {
|
||||
const { selectedDeal } = useDealPageContext();
|
||||
if (!selectedDeal) return <>No deal selected</>;
|
||||
return (
|
||||
<Content deal={selectedDeal} />
|
||||
);
|
||||
return <Content deal={selectedDeal} />;
|
||||
};
|
||||
export default DealEditDrawerGeneralTab;
|
||||
export default DealEditDrawerGeneralTab;
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {DealService} from "../../../client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { DealService } from "../../../client";
|
||||
import ObjectList from "../../../hooks/objectList.tsx";
|
||||
|
||||
export const useDealSummaries = () => {
|
||||
const {data: summariesRaw = [], refetch} = useQuery({
|
||||
queryKey: ['getDealSummaries'],
|
||||
queryFn: () => DealService.getDealSummaries({full: false}),
|
||||
select: data => data.summaries || [] // Трансформируем полученные данные
|
||||
const { data: summariesRaw = [], refetch } = useQuery({
|
||||
queryKey: ["getDealSummaries"],
|
||||
queryFn: () => DealService.getDealSummaries({ full: false }),
|
||||
select: data => data.summaries || [], // Трансформируем полученные данные
|
||||
});
|
||||
|
||||
// Теперь summaries будет содержать либо трансформированные данные, либо пустой массив по умолчанию
|
||||
// isLoading и isError могут быть использованы для отображения индикаторов загрузки или ошибки
|
||||
|
||||
return {summariesRaw, refetch};
|
||||
}
|
||||
export const useDealSummariesFull = () => ObjectList({
|
||||
queryFn: () => DealService.getDealSummaries({full: true}),
|
||||
queryKey: "getDealSummariesFull",
|
||||
getObjectsFn: response => response.summaries
|
||||
});
|
||||
return { summariesRaw, refetch };
|
||||
};
|
||||
export const useDealSummariesFull = () =>
|
||||
ObjectList({
|
||||
queryFn: () => DealService.getDealSummaries({ full: true }),
|
||||
queryKey: "getDealSummariesFull",
|
||||
getObjectsFn: response => response.summaries,
|
||||
});
|
||||
|
||||
@@ -1 +1 @@
|
||||
export {LeadsPage} from './ui/LeadsPage.tsx';
|
||||
export { LeadsPage } from "./ui/LeadsPage.tsx";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import BaseFormModal, { CreateEditFormProps } from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { DealProductSchema, DealProductServiceSchema } from "../../../client";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { NumberInput } from "@mantine/core";
|
||||
@@ -9,41 +11,48 @@ import { omit } from "lodash";
|
||||
type RestProps = {
|
||||
clientId: number;
|
||||
productIds?: number[];
|
||||
}
|
||||
};
|
||||
|
||||
type Props = CreateEditFormProps<DealProductSchema> & RestProps;
|
||||
const AddDealProductModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = 'element' in innerProps;
|
||||
const restProps = omit(innerProps, ['clientId']);
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "element" in innerProps;
|
||||
const restProps = omit(innerProps, ["clientId"]);
|
||||
|
||||
const validateServices = (services?: DealProductServiceSchema[]) => {
|
||||
if (!services || services.length == 0) return null;
|
||||
return services.find(service => service.service === undefined) ? "Удалите пустые услуги" : null;
|
||||
}
|
||||
return services.find(service => service.service === undefined)
|
||||
? "Удалите пустые услуги"
|
||||
: null;
|
||||
};
|
||||
const form = useForm<Partial<DealProductSchema>>({
|
||||
initialValues: isEditing ? innerProps.element : {
|
||||
product: undefined,
|
||||
services: [],
|
||||
quantity: 1
|
||||
},
|
||||
initialValues: isEditing
|
||||
? innerProps.element
|
||||
: {
|
||||
product: undefined,
|
||||
services: [],
|
||||
quantity: 1,
|
||||
},
|
||||
validate: {
|
||||
product: (product?: DealProductSchema['product']) => product !== undefined ? null : "Необходимо выбрать товар",
|
||||
quantity: (quantity?: number) => (quantity && quantity > 0) ? null : "Количество должно быть больше 0",
|
||||
services: validateServices
|
||||
}
|
||||
product: (product?: DealProductSchema["product"]) =>
|
||||
product !== undefined ? null : "Необходимо выбрать товар",
|
||||
quantity: (quantity?: number) =>
|
||||
quantity && quantity > 0
|
||||
? null
|
||||
: "Количество должно быть больше 0",
|
||||
services: validateServices,
|
||||
},
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
context.closeContextModal(id);
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
||||
<BaseFormModal
|
||||
{...restProps as CreateEditFormProps<DealProductSchema>}
|
||||
{...(restProps as CreateEditFormProps<DealProductSchema>)}
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={onClose}>
|
||||
@@ -54,14 +63,16 @@ const AddDealProductModal = ({
|
||||
label={"Товар"}
|
||||
clientId={innerProps.clientId}
|
||||
disabled={isEditing}
|
||||
filterBy={(item) => !(innerProps.productIds || []).includes(item.id)}
|
||||
{...form.getInputProps('product')}
|
||||
filterBy={item =>
|
||||
!(innerProps.productIds || []).includes(item.id)
|
||||
}
|
||||
{...form.getInputProps("product")}
|
||||
/>
|
||||
<NumberInput
|
||||
placeholder={"Введите количество"}
|
||||
label={"Количество"}
|
||||
min={1}
|
||||
{...form.getInputProps('quantity')}
|
||||
{...form.getInputProps("quantity")}
|
||||
/>
|
||||
{/*<Fieldset legend={'Услуги'}>*/}
|
||||
{/* <DealProductServiceTable*/}
|
||||
@@ -70,13 +81,10 @@ const AddDealProductModal = ({
|
||||
{/* BaseFormInputProps<DealProductServiceSchema[]>}*/}
|
||||
{/* />*/}
|
||||
{/*</Fieldset>*/}
|
||||
|
||||
</>
|
||||
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
);
|
||||
};
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default AddDealProductModal;
|
||||
export default AddDealProductModal;
|
||||
|
||||
@@ -1,50 +1,72 @@
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import BaseFormModal, {CreateEditFormProps} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { DealServiceSchema, ServicePriceCategorySchema } from "../../../client";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {ComboboxItem, ComboboxItemGroup, NumberInput, OptionsFilter} from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import {
|
||||
ComboboxItem,
|
||||
ComboboxItemGroup,
|
||||
NumberInput,
|
||||
OptionsFilter,
|
||||
} from "@mantine/core";
|
||||
import ServiceWithPriceInput from "../../../components/ServiceWithPriceInput/ServiceWithPriceInput.tsx";
|
||||
import {ServiceType} from "../../../shared/enums/ServiceType.ts";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState} from "../../../redux/store.ts";
|
||||
import { ServiceType } from "../../../shared/enums/ServiceType.ts";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../redux/store.ts";
|
||||
|
||||
type RestProps = {
|
||||
serviceIds?: number[];
|
||||
category?: ServicePriceCategorySchema;
|
||||
}
|
||||
};
|
||||
type Props = CreateEditFormProps<Partial<DealServiceSchema>> & RestProps;
|
||||
const AddDealServiceModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps
|
||||
}: ContextModalProps<Props>) => {
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
console.log(innerProps.category)
|
||||
const isEditing = 'element' in innerProps;
|
||||
console.log(innerProps.category);
|
||||
const isEditing = "element" in innerProps;
|
||||
const form = useForm<Partial<DealServiceSchema>>({
|
||||
initialValues: isEditing ? innerProps.element : {
|
||||
service: undefined,
|
||||
quantity: 1,
|
||||
employees: []
|
||||
},
|
||||
initialValues: isEditing
|
||||
? innerProps.element
|
||||
: {
|
||||
service: undefined,
|
||||
quantity: 1,
|
||||
employees: [],
|
||||
},
|
||||
validate: {
|
||||
service: (service?: DealServiceSchema['service']) => service !== undefined ? null : "Необходимо выбрать услугу",
|
||||
quantity: (quantity?: number) => (quantity && quantity > 0) ? null : "Количество должно быть больше 0"
|
||||
}
|
||||
service: (service?: DealServiceSchema["service"]) =>
|
||||
service !== undefined ? null : "Необходимо выбрать услугу",
|
||||
quantity: (quantity?: number) =>
|
||||
quantity && quantity > 0
|
||||
? null
|
||||
: "Количество должно быть больше 0",
|
||||
},
|
||||
});
|
||||
const onClose = () => {
|
||||
context.closeContextModal(id);
|
||||
}
|
||||
};
|
||||
|
||||
const serviceOptionsFilter = ({options}: { options: ComboboxItemGroup[] }) => {
|
||||
const serviceOptionsFilter = ({
|
||||
options,
|
||||
}: {
|
||||
options: ComboboxItemGroup[];
|
||||
}) => {
|
||||
if (!innerProps.serviceIds) return options;
|
||||
const productServiceIds = innerProps.serviceIds;
|
||||
return (options as ComboboxItemGroup[]).map(({items, group}) => {
|
||||
return (options as ComboboxItemGroup[]).map(({ items, group }) => {
|
||||
return {
|
||||
group,
|
||||
items: items.filter(item => !productServiceIds.includes(parseInt((item as ComboboxItem).value)))
|
||||
}
|
||||
})
|
||||
items: items.filter(
|
||||
item =>
|
||||
!productServiceIds.includes(
|
||||
parseInt((item as ComboboxItem).value)
|
||||
)
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
return (
|
||||
<BaseFormModal
|
||||
@@ -57,24 +79,24 @@ const AddDealServiceModal = ({
|
||||
<ServiceWithPriceInput
|
||||
category={innerProps.category}
|
||||
serviceProps={{
|
||||
...form.getInputProps('service'),
|
||||
...form.getInputProps("service"),
|
||||
label: "Услуга",
|
||||
placeholder: "Выберите услугу",
|
||||
style: {width: '100%'},
|
||||
style: { width: "100%" },
|
||||
disabled: isEditing,
|
||||
filter: serviceOptionsFilter as OptionsFilter
|
||||
filter: serviceOptionsFilter as OptionsFilter,
|
||||
}}
|
||||
priceProps={{
|
||||
...form.getInputProps('price'),
|
||||
...form.getInputProps("price"),
|
||||
label: "Цена",
|
||||
placeholder: "Введите цену",
|
||||
style: {width: '100%'},
|
||||
disabled: authState.isGuest
|
||||
style: { width: "100%" },
|
||||
disabled: authState.isGuest,
|
||||
}}
|
||||
quantity={form.values.quantity || 1}
|
||||
containerProps={{
|
||||
direction: "column",
|
||||
style: {width: "100%"}
|
||||
style: { width: "100%" },
|
||||
}}
|
||||
filterType={ServiceType.DEAL_SERVICE}
|
||||
lockOnEdit={isEditing}
|
||||
@@ -83,13 +105,12 @@ const AddDealServiceModal = ({
|
||||
placeholder={"Введите количество"}
|
||||
label={"Количество"}
|
||||
min={1}
|
||||
{...form.getInputProps('quantity')}
|
||||
{...form.getInputProps("quantity")}
|
||||
/>
|
||||
</>
|
||||
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default AddDealServiceModal;
|
||||
export default AddDealServiceModal;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import BaseFormModal, { CreateEditFormProps } from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { DealProductServiceSchema, ServicePriceCategorySchema, ServiceSchema } from "../../../client";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import {
|
||||
DealProductServiceSchema,
|
||||
ServicePriceCategorySchema,
|
||||
ServiceSchema,
|
||||
} from "../../../client";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { useForm, UseFormReturnType } from "@mantine/form";
|
||||
import { isNil, isNumber } from "lodash";
|
||||
@@ -13,26 +19,31 @@ type RestProps = {
|
||||
quantity: number;
|
||||
serviceIds: number[];
|
||||
category?: ServicePriceCategorySchema;
|
||||
}
|
||||
};
|
||||
type Props = CreateEditFormProps<DealProductServiceSchema> & RestProps;
|
||||
|
||||
const ProductServiceFormModal = ({
|
||||
context,
|
||||
id, innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const isEditing = "onChange" in innerProps;
|
||||
const initialValues: Partial<DealProductServiceSchema> = isEditing ? innerProps.element : {
|
||||
service: undefined,
|
||||
price: undefined,
|
||||
employees: [],
|
||||
};
|
||||
const initialValues: Partial<DealProductServiceSchema> = isEditing
|
||||
? innerProps.element
|
||||
: {
|
||||
service: undefined,
|
||||
price: undefined,
|
||||
employees: [],
|
||||
};
|
||||
const form = useForm<Partial<DealProductServiceSchema>>({
|
||||
initialValues,
|
||||
validate: {
|
||||
service: (service?: ServiceSchema) => isNil(service) || service.id < 0 ? "Укажите услугу" : null,
|
||||
price: (price?: number) => !isNumber(price) || price < 0 ? "Укажите цену" : null,
|
||||
service: (service?: ServiceSchema) =>
|
||||
isNil(service) || service.id < 0 ? "Укажите услугу" : null,
|
||||
price: (price?: number) =>
|
||||
!isNumber(price) || price < 0 ? "Укажите цену" : null,
|
||||
},
|
||||
});
|
||||
const onClose = () => {
|
||||
@@ -43,12 +54,10 @@ const ProductServiceFormModal = ({
|
||||
{...innerProps}
|
||||
form={form as UseFormReturnType<DealProductServiceSchema>}
|
||||
onClose={onClose}
|
||||
closeOnSubmit
|
||||
>
|
||||
closeOnSubmit>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Flex w={"100%"}>
|
||||
|
||||
<ServiceWithPriceInput
|
||||
category={innerProps.category}
|
||||
serviceProps={{
|
||||
@@ -56,7 +65,9 @@ const ProductServiceFormModal = ({
|
||||
label: "Услуга",
|
||||
placeholder: "Выберите услугу",
|
||||
disabled: isEditing,
|
||||
filterBy: (item) => !innerProps.serviceIds.includes(item.id) || isEditing,
|
||||
filterBy: item =>
|
||||
!innerProps.serviceIds.includes(item.id) ||
|
||||
isEditing,
|
||||
style: { width: "100%" },
|
||||
}}
|
||||
priceProps={{
|
||||
@@ -70,17 +81,14 @@ const ProductServiceFormModal = ({
|
||||
containerProps={{
|
||||
direction: "column",
|
||||
style: { width: "100%" },
|
||||
|
||||
}}
|
||||
lockOnEdit={isEditing}
|
||||
quantity={innerProps.quantity}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
|
||||
);
|
||||
};
|
||||
export default ProductServiceFormModal;
|
||||
export default ProductServiceFormModal;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {DealProductSchema} from "../../../client";
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {Button, Flex, rem} from "@mantine/core";
|
||||
import {useState} from "react";
|
||||
import { DealProductSchema } from "../../../client";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import { Button, Flex, rem } from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
import ObjectMultiSelect from "../../../components/ObjectMultiSelect/ObjectMultiSelect.tsx";
|
||||
import {notifications} from "../../../shared/lib/notifications.ts";
|
||||
import { notifications } from "../../../shared/lib/notifications.ts";
|
||||
|
||||
type Props = {
|
||||
dealProducts: DealProductSchema[];
|
||||
@@ -11,37 +11,46 @@ type Props = {
|
||||
onSelect: (
|
||||
sourceProduct: DealProductSchema,
|
||||
destinationProducts: DealProductSchema[]
|
||||
) => void
|
||||
}
|
||||
) => void;
|
||||
};
|
||||
|
||||
const SelectDealProductsModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps
|
||||
}: ContextModalProps<Props>) => {
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const [dealProducts, setDealProducts] = useState<DealProductSchema[]>([]);
|
||||
const onSelectClick = () => {
|
||||
if (!dealProducts) {
|
||||
notifications.error({message: "Выберите товары на которые необходимо продублировать услуги"});
|
||||
notifications.error({
|
||||
message:
|
||||
"Выберите товары на которые необходимо продублировать услуги",
|
||||
});
|
||||
return;
|
||||
}
|
||||
innerProps.onSelect(innerProps.dealProduct, dealProducts);
|
||||
context.closeContextModal(id);
|
||||
}
|
||||
};
|
||||
const onDuplicateAllClick = () => {
|
||||
innerProps.onSelect(
|
||||
innerProps.dealProduct,
|
||||
innerProps.dealProducts.filter(item => item !== innerProps.dealProduct)
|
||||
innerProps.dealProducts.filter(
|
||||
item => item !== innerProps.dealProduct
|
||||
)
|
||||
);
|
||||
context.closeContextModal(id);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<Flex>
|
||||
<ObjectMultiSelect<DealProductSchema>
|
||||
w={"100%"}
|
||||
label={"Товары"}
|
||||
placeholder={"Выберите товары на которые нужно продублировать услуги"}
|
||||
placeholder={
|
||||
"Выберите товары на которые нужно продублировать услуги"
|
||||
}
|
||||
onChange={setDealProducts}
|
||||
value={dealProducts}
|
||||
data={innerProps.dealProducts}
|
||||
@@ -50,27 +59,25 @@ const SelectDealProductsModal = ({
|
||||
filterBy={item => item !== innerProps.dealProduct}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex gap={rem(10)} justify={"flex-end"}>
|
||||
<Flex
|
||||
gap={rem(10)}
|
||||
justify={"flex-end"}>
|
||||
<Button
|
||||
variant={"subtle"}
|
||||
onClick={() => context.closeContextModal(id)}
|
||||
>
|
||||
onClick={() => context.closeContextModal(id)}>
|
||||
Отменить
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onDuplicateAllClick}
|
||||
>
|
||||
<Button onClick={onDuplicateAllClick}>
|
||||
Продублировать на все товары
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onSelectClick}
|
||||
variant={"default"}
|
||||
>
|
||||
variant={"default"}>
|
||||
Продублировать
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectDealProductsModal;
|
||||
export default SelectDealProductsModal;
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
.container-disabled {
|
||||
}
|
||||
|
||||
|
||||
.products-list {
|
||||
width: 60%;
|
||||
display: flex;
|
||||
@@ -28,7 +27,6 @@
|
||||
border: dashed var(--item-border-size) var(--mantine-color-default-border);
|
||||
border-radius: var(--item-border-radius);
|
||||
padding: rem(10);
|
||||
|
||||
}
|
||||
|
||||
.deal-container-buttons {
|
||||
@@ -37,4 +35,4 @@
|
||||
flex-direction: column;
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,66 @@
|
||||
import {FC} from "react";
|
||||
import styles from './ProductAndServiceTab.module.css';
|
||||
import { FC } from "react";
|
||||
import styles from "./ProductAndServiceTab.module.css";
|
||||
import ProductView from "./components/ProductView/ProductView.tsx";
|
||||
import {Button, Divider, Flex, rem, ScrollArea, Text, Title} from "@mantine/core";
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
rem,
|
||||
ScrollArea,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import DealServicesTable from "./components/DealServicesTable/DealServicesTable.tsx";
|
||||
import useDealProductAndServiceTabState from "./hooks/useProductAndServiceTabState.tsx";
|
||||
import {modals} from "@mantine/modals";
|
||||
import { modals } from "@mantine/modals";
|
||||
import {
|
||||
BillingService,
|
||||
DealProductSchema,
|
||||
DealService,
|
||||
GetServiceKitSchema,
|
||||
ProductSchema,
|
||||
ProductService
|
||||
ProductService,
|
||||
} from "../../../../client";
|
||||
import {notifications} from "../../../../shared/lib/notifications.ts";
|
||||
import {CreateProductRequest} from "../../../ProductsPage/types.ts";
|
||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||
import { CreateProductRequest } from "../../../ProductsPage/types.ts";
|
||||
import classNames from "classnames";
|
||||
|
||||
const ProductAndServiceTab: FC = () => {
|
||||
const {dealState, dealServicesState, dealProductsState} = useDealProductAndServiceTabState();
|
||||
const { dealState, dealServicesState, dealProductsState } =
|
||||
useDealProductAndServiceTabState();
|
||||
const isLocked = Boolean(dealState.deal?.billRequest);
|
||||
const onAddProductClick = () => {
|
||||
if (!dealProductsState.onCreate || !dealState.deal) return;
|
||||
const productIds = dealState.deal.products.map(product => product.product.id);
|
||||
const productIds = dealState.deal.products.map(
|
||||
product => product.product.id
|
||||
);
|
||||
modals.openContextModal({
|
||||
modal: "addDealProduct",
|
||||
innerProps: {
|
||||
onCreate: dealProductsState.onCreate,
|
||||
clientId: dealState.deal.clientId,
|
||||
productIds: productIds
|
||||
productIds: productIds,
|
||||
},
|
||||
withCloseButton: false
|
||||
})
|
||||
}
|
||||
withCloseButton: false,
|
||||
});
|
||||
};
|
||||
const getTotalPrice = () => {
|
||||
if (!dealState.deal) return 0
|
||||
const productServicesPrice = dealState.deal.products.reduce((acc, row) => acc + row.services.reduce((acc2, row2) => acc2 + row2.price * row.quantity, 0), 0);
|
||||
const dealServicesPrice = dealState.deal.services.reduce((acc, row) => acc + row.price * row.quantity, 0);
|
||||
if (!dealState.deal) return 0;
|
||||
const productServicesPrice = dealState.deal.products.reduce(
|
||||
(acc, row) =>
|
||||
acc +
|
||||
row.services.reduce(
|
||||
(acc2, row2) => acc2 + row2.price * row.quantity,
|
||||
0
|
||||
),
|
||||
0
|
||||
);
|
||||
const dealServicesPrice = dealState.deal.services.reduce(
|
||||
(acc, row) => acc + row.price * row.quantity,
|
||||
0
|
||||
);
|
||||
return dealServicesPrice + productServicesPrice;
|
||||
}
|
||||
};
|
||||
const onCopyServices = (
|
||||
sourceProduct: DealProductSchema,
|
||||
destinationProducts: DealProductSchema[]
|
||||
@@ -47,15 +69,17 @@ const ProductAndServiceTab: FC = () => {
|
||||
DealService.copyProductServices({
|
||||
requestBody: {
|
||||
dealId: dealState.deal.id,
|
||||
destinationProductIds: destinationProducts.map(product => product.product.id),
|
||||
sourceProductId: sourceProduct.product.id
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
destinationProductIds: destinationProducts.map(
|
||||
product => product.product.id
|
||||
),
|
||||
sourceProductId: sourceProduct.product.id,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await dealState.refetch()
|
||||
})
|
||||
}
|
||||
await dealState.refetch();
|
||||
});
|
||||
};
|
||||
const onCopyServicesClick = (product: DealProductSchema) => {
|
||||
modals.openContextModal({
|
||||
modal: "selectDealProductsModal",
|
||||
@@ -64,11 +88,11 @@ const ProductAndServiceTab: FC = () => {
|
||||
innerProps: {
|
||||
dealProducts: dealState.deal?.products || [],
|
||||
dealProduct: product,
|
||||
onSelect: onCopyServices
|
||||
onSelect: onCopyServices,
|
||||
},
|
||||
withCloseButton: false
|
||||
})
|
||||
}
|
||||
withCloseButton: false,
|
||||
});
|
||||
};
|
||||
|
||||
const onKitAdd = (item: DealProductSchema, kit: GetServiceKitSchema) => {
|
||||
if (!dealState.deal) return;
|
||||
@@ -76,55 +100,56 @@ const ProductAndServiceTab: FC = () => {
|
||||
requestBody: {
|
||||
dealId: dealState.deal.id,
|
||||
kitId: kit.id,
|
||||
productId: item.product.id
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
productId: item.product.id,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await dealState.refetch();
|
||||
});
|
||||
}
|
||||
};
|
||||
const onDealKitAdd = (kit: GetServiceKitSchema) => {
|
||||
if (!dealState.deal) return;
|
||||
DealService.addKitToDeal({
|
||||
requestBody: {
|
||||
dealId: dealState.deal.id,
|
||||
kitId: kit.id,
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await dealState.refetch();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onCreateProduct = (newProduct: CreateProductRequest) => {
|
||||
ProductService.createProduct({
|
||||
requestBody: newProduct
|
||||
}).then(({ok, message}) => {
|
||||
notifications.guess(ok, {message: message});
|
||||
})
|
||||
}
|
||||
requestBody: newProduct,
|
||||
}).then(({ ok, message }) => {
|
||||
notifications.guess(ok, { message: message });
|
||||
});
|
||||
};
|
||||
const onCreateProductClick = () => {
|
||||
if (!dealState.deal) return;
|
||||
modals.openContextModal({
|
||||
modal: "createProduct",
|
||||
title: 'Создание товара',
|
||||
title: "Создание товара",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
clientId: dealState.deal.clientId,
|
||||
onCreate: onCreateProduct
|
||||
}
|
||||
})
|
||||
}
|
||||
onCreate: onCreateProduct,
|
||||
},
|
||||
});
|
||||
};
|
||||
const onProductEdit = (product: ProductSchema) => {
|
||||
ProductService.updateProduct({requestBody: {product}})
|
||||
.then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
ProductService.updateProduct({ requestBody: { product } }).then(
|
||||
async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await dealState.refetch();
|
||||
})
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const onCreateBillClick = () => {
|
||||
if (!dealState.deal) return;
|
||||
@@ -132,67 +157,69 @@ const ProductAndServiceTab: FC = () => {
|
||||
modals.openConfirmModal({
|
||||
withCloseButton: false,
|
||||
size: "xl",
|
||||
children:
|
||||
<Text style={{textAlign: "justify"}}>
|
||||
Создание заявки на выставление счета, подтвержденное нажатием кнопки "Выставить", заблокирует
|
||||
возможность
|
||||
редактирования товаров и услуг сделки. Пожалуйста, проверьте всю информацию на точность и полноту
|
||||
перед подтверждением.
|
||||
</Text>,
|
||||
children: (
|
||||
<Text style={{ textAlign: "justify" }}>
|
||||
Создание заявки на выставление счета, подтвержденное
|
||||
нажатием кнопки "Выставить", заблокирует возможность
|
||||
редактирования товаров и услуг сделки. Пожалуйста, проверьте
|
||||
всю информацию на точность и полноту перед подтверждением.
|
||||
</Text>
|
||||
),
|
||||
onConfirm: () => {
|
||||
BillingService.createDealBill({
|
||||
requestBody: {
|
||||
dealId
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
if (ok) notifications.success({message: "Ссылка на оплату доступна во вкладе общее"});
|
||||
dealId,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (ok)
|
||||
notifications.success({
|
||||
message:
|
||||
"Ссылка на оплату доступна во вкладе общее",
|
||||
});
|
||||
await dealState.refetch();
|
||||
})
|
||||
});
|
||||
},
|
||||
labels: {
|
||||
confirm: "Выставить",
|
||||
cancel: "Отмена"
|
||||
}
|
||||
cancel: "Отмена",
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const onCancelBillClick = () => {
|
||||
if (!dealState.deal) return;
|
||||
const dealId = dealState.deal.id;
|
||||
modals.openConfirmModal({
|
||||
withCloseButton: false,
|
||||
children:
|
||||
<Text style={{textAlign: "justify"}}>
|
||||
children: (
|
||||
<Text style={{ textAlign: "justify" }}>
|
||||
Вы уверены что хотите отозвать заявку на оплату?
|
||||
</Text>,
|
||||
</Text>
|
||||
),
|
||||
onConfirm: () => {
|
||||
BillingService.cancelDealBill({
|
||||
requestBody: {
|
||||
dealId
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
dealId,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
await dealState.refetch();
|
||||
})
|
||||
});
|
||||
},
|
||||
labels: {
|
||||
confirm: "Отозвать",
|
||||
cancel: "Отмена"
|
||||
cancel: "Отмена",
|
||||
},
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
classNames(styles['container'],
|
||||
dealState.deal?.billRequest && styles['container-disabled']
|
||||
)
|
||||
}>
|
||||
|
||||
<div className={styles['products-list']}>
|
||||
className={classNames(
|
||||
styles["container"],
|
||||
dealState.deal?.billRequest && styles["container-disabled"]
|
||||
)}>
|
||||
<div className={styles["products-list"]}>
|
||||
<ScrollArea offsetScrollbars>
|
||||
|
||||
{dealState.deal?.products.map(product => (
|
||||
<ProductView
|
||||
onProductEdit={onProductEdit}
|
||||
@@ -205,61 +232,66 @@ const ProductAndServiceTab: FC = () => {
|
||||
/>
|
||||
))}
|
||||
</ScrollArea>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles['deal-container']}>
|
||||
<div className={styles["deal-container"]}>
|
||||
<ScrollArea offsetScrollbars>
|
||||
|
||||
<Flex direction={"column"} className={styles['deal-container-wrapper']}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
className={styles["deal-container-wrapper"]}>
|
||||
<DealServicesTable
|
||||
onKitAdd={onDealKitAdd}
|
||||
{...dealServicesState}
|
||||
/>
|
||||
|
||||
<Divider my={rem(15)}/>
|
||||
<div className={styles['deal-container-buttons']}>
|
||||
<Divider my={rem(15)} />
|
||||
<div className={styles["deal-container-buttons"]}>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
|
||||
variant={"default"}
|
||||
fullWidth
|
||||
onClick={onCreateProductClick}
|
||||
>Создать товар</Button>
|
||||
onClick={onCreateProductClick}>
|
||||
Создать товар
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
|
||||
onClick={onAddProductClick}
|
||||
variant={"default"}
|
||||
fullWidth>Добавить товар</Button>
|
||||
fullWidth>
|
||||
Добавить товар
|
||||
</Button>
|
||||
</div>
|
||||
<Divider my={rem(15)}/>
|
||||
<div className={styles['deal-container-buttons']}>
|
||||
{isLocked ? <Button
|
||||
<Divider my={rem(15)} />
|
||||
<div className={styles["deal-container-buttons"]}>
|
||||
{isLocked ? (
|
||||
<Button
|
||||
onClick={onCancelBillClick}
|
||||
color={"red"}
|
||||
>
|
||||
color={"red"}>
|
||||
Отозвать счет
|
||||
</Button> :
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
onClick={onCreateBillClick}
|
||||
variant={"default"}
|
||||
fullWidth>Выставить счет</Button>
|
||||
}
|
||||
|
||||
fullWidth>
|
||||
Выставить счет
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</Flex>
|
||||
<Flex direction={"column"} className={styles['deal-container-wrapper']}>
|
||||
<Title order={3}>Общая стоимость всех услуг: {getTotalPrice().toLocaleString("ru")}₽</Title>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
className={styles["deal-container-wrapper"]}>
|
||||
<Title order={3}>
|
||||
Общая стоимость всех услуг:{" "}
|
||||
{getTotalPrice().toLocaleString("ru")}₽
|
||||
</Title>
|
||||
</Flex>
|
||||
</ScrollArea>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductAndServiceTab;
|
||||
export default ProductAndServiceTab;
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
import { CRUDTableProps } from "../../../../../../types/CRUDTable.tsx";
|
||||
import { DealServiceSchema, GetServiceKitSchema, UserSchema } from "../../../../../../client";
|
||||
import {
|
||||
DealServiceSchema,
|
||||
GetServiceKitSchema,
|
||||
UserSchema,
|
||||
} from "../../../../../../client";
|
||||
import { FC, useState } from "react";
|
||||
import { ActionIcon, Button, Flex, Modal, NumberInput, rem, Text, Title, Tooltip } from "@mantine/core";
|
||||
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";
|
||||
@@ -12,16 +26,24 @@ import { RootState } from "../../../../../../redux/store.ts";
|
||||
import useDealProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
|
||||
|
||||
type RestProps = {
|
||||
onKitAdd?: (kit: GetServiceKitSchema) => void
|
||||
onKitAdd?: (kit: GetServiceKitSchema) => void;
|
||||
};
|
||||
type Props = CRUDTableProps<DealServiceSchema> & RestProps;
|
||||
const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onKitAdd }) => {
|
||||
const DealServicesTable: FC<Props> = ({
|
||||
items,
|
||||
onDelete,
|
||||
onCreate,
|
||||
onChange,
|
||||
onKitAdd,
|
||||
}) => {
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
|
||||
const { dealState } = useDealProductAndServiceTabState();
|
||||
const isLocked = Boolean(dealState.deal?.billRequest);
|
||||
|
||||
const [currentService, setCurrentService] = useState<DealServiceSchema | undefined>();
|
||||
const [currentService, setCurrentService] = useState<
|
||||
DealServiceSchema | undefined
|
||||
>();
|
||||
const [employeesModalVisible, setEmployeesModalVisible] = useState(false);
|
||||
|
||||
const onDeleteClick = (item: DealServiceSchema) => {
|
||||
@@ -30,7 +52,7 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
|
||||
};
|
||||
const onCreateClick = () => {
|
||||
if (!onCreate) return;
|
||||
console.log("228")
|
||||
console.log("228");
|
||||
const serviceIds = items.map(service => service.service.id);
|
||||
modals.openContextModal({
|
||||
modal: "addDealService",
|
||||
@@ -68,7 +90,9 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
|
||||
};
|
||||
const getCurrentEmployees = (): UserSchema[] => {
|
||||
if (!currentService) return [];
|
||||
const item = items.find(i => i.service.id === currentService.service.id);
|
||||
const item = items.find(
|
||||
i => i.service.id === currentService.service.id
|
||||
);
|
||||
if (!item) return [];
|
||||
return item.employees;
|
||||
};
|
||||
@@ -88,7 +112,6 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
|
||||
serviceType: ServiceType.DEAL_SERVICE,
|
||||
},
|
||||
withCloseButton: false,
|
||||
|
||||
});
|
||||
};
|
||||
return (
|
||||
@@ -96,9 +119,7 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}
|
||||
h={"100%"}
|
||||
|
||||
>
|
||||
h={"100%"}>
|
||||
<Flex
|
||||
h={"100%"}
|
||||
direction={"column"}>
|
||||
@@ -106,22 +127,20 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
|
||||
order={3}
|
||||
w={"100%"}
|
||||
style={{ textAlign: "center" }}
|
||||
mb={rem(10)}
|
||||
>Общие услуги</Title>
|
||||
mb={rem(10)}>
|
||||
Общие услуги
|
||||
</Title>
|
||||
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
gap={rem(10)}>
|
||||
{items.map(service => (
|
||||
<Flex
|
||||
key={service.service.id}
|
||||
w={"100%"}
|
||||
gap={rem(10)}
|
||||
align={"center"}
|
||||
>
|
||||
align={"center"}>
|
||||
<Tooltip
|
||||
|
||||
onClick={() => onDeleteClick(service)}
|
||||
label="Удалить услугу">
|
||||
<ActionIcon
|
||||
@@ -130,26 +149,34 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{!authState.isGuest &&
|
||||
{!authState.isGuest && (
|
||||
<Tooltip label="Сотрудники">
|
||||
<ActionIcon onClick={() => onEmployeeClick(service)} variant={"default"}>
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
onEmployeeClick(service)
|
||||
}
|
||||
variant={"default"}>
|
||||
<IconUsersGroup />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
}
|
||||
<Text
|
||||
flex={1}
|
||||
>{service.service.name}</Text>
|
||||
)}
|
||||
<Text flex={1}>{service.service.name}</Text>
|
||||
<NumberInput
|
||||
disabled={isLocked}
|
||||
flex={1}
|
||||
suffix={" шт."}
|
||||
onChange={event => isNumber(event) && onQuantityChange(service, event)}
|
||||
onChange={event =>
|
||||
isNumber(event) &&
|
||||
onQuantityChange(service, event)
|
||||
}
|
||||
value={service.quantity}
|
||||
/>
|
||||
<NumberInput
|
||||
flex={1}
|
||||
onChange={event => isNumber(event) && onPriceChange(service, event)}
|
||||
onChange={event =>
|
||||
isNumber(event) &&
|
||||
onPriceChange(service, event)
|
||||
}
|
||||
suffix={"₽"}
|
||||
value={service.price}
|
||||
disabled={authState.isGuest || isLocked}
|
||||
@@ -161,22 +188,31 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
|
||||
<Title
|
||||
style={{ textAlign: "end" }}
|
||||
mt={rem(10)}
|
||||
order={3}
|
||||
>Итог: {items.reduce((acc, item) => acc + (item.price * item.quantity), 0)}₽</Title>
|
||||
order={3}>
|
||||
Итог:{" "}
|
||||
{items.reduce(
|
||||
(acc, item) => acc + item.price * item.quantity,
|
||||
0
|
||||
)}
|
||||
₽
|
||||
</Title>
|
||||
</Flex>
|
||||
<Flex direction={"column"} gap={rem(10)} mt={"auto"}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}
|
||||
mt={"auto"}>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
onClick={onCreateClick}
|
||||
fullWidth
|
||||
variant={"default"}
|
||||
>Добавить услугу</Button>
|
||||
variant={"default"}>
|
||||
Добавить услугу
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
onClick={onAddKitClick}
|
||||
fullWidth
|
||||
variant={"default"}
|
||||
>
|
||||
variant={"default"}>
|
||||
Добавить набор услуг
|
||||
</Button>
|
||||
</Flex>
|
||||
@@ -185,19 +221,17 @@ const DealServicesTable: FC<Props> = ({ items, onDelete, onCreate, onChange, onK
|
||||
title={"Добавление сотрудника к услуге"}
|
||||
opened={employeesModalVisible}
|
||||
onClose={onEmployeeModalClose}
|
||||
size={"xl"}
|
||||
>
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
size={"xl"}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<SimpleUsersTable
|
||||
items={getCurrentEmployees()}
|
||||
onChange={onEmployeesChange}
|
||||
|
||||
/>
|
||||
|
||||
</Flex>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
export default DealServicesTable;
|
||||
export default DealServicesTable;
|
||||
|
||||
@@ -16,18 +16,18 @@ type RestProps = {
|
||||
quantity: number;
|
||||
onCopyServices?: () => void;
|
||||
onKitAdd?: () => void;
|
||||
}
|
||||
};
|
||||
|
||||
type Props = CRUDTableProps<DealProductServiceSchema> & RestProps;
|
||||
const ProductServicesTable: FC<Props> = ({
|
||||
items,
|
||||
quantity,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onChange,
|
||||
onCopyServices,
|
||||
onKitAdd,
|
||||
}) => {
|
||||
items,
|
||||
quantity,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onChange,
|
||||
onCopyServices,
|
||||
onKitAdd,
|
||||
}) => {
|
||||
const { dealState } = useDealProductAndServiceTabState();
|
||||
const isLocked = Boolean(dealState.deal?.billRequest);
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
@@ -35,7 +35,9 @@ const ProductServicesTable: FC<Props> = ({
|
||||
const columns = useProductServicesTableColumns({ data: items, quantity });
|
||||
const serviceIds = items.map(service => service.service.id);
|
||||
|
||||
const [currentService, setCurrentService] = useState<DealProductServiceSchema | undefined>();
|
||||
const [currentService, setCurrentService] = useState<
|
||||
DealProductServiceSchema | undefined
|
||||
>();
|
||||
const [employeesModalVisible, setEmployeesModalVisible] = useState(false);
|
||||
|
||||
const onCreateClick = () => {
|
||||
@@ -62,7 +64,6 @@ const ProductServicesTable: FC<Props> = ({
|
||||
serviceIds,
|
||||
quantity,
|
||||
category: dealState.deal?.category || undefined,
|
||||
|
||||
},
|
||||
withCloseButton: false,
|
||||
});
|
||||
@@ -78,7 +79,9 @@ const ProductServicesTable: FC<Props> = ({
|
||||
};
|
||||
const getCurrentEmployees = (): UserSchema[] => {
|
||||
if (!currentService) return [];
|
||||
const item = items.find(i => i.service.id === currentService.service.id);
|
||||
const item = items.find(
|
||||
i => i.service.id === currentService.service.id
|
||||
);
|
||||
if (!item) return [];
|
||||
return item.employees;
|
||||
};
|
||||
@@ -93,93 +96,99 @@ const ProductServicesTable: FC<Props> = ({
|
||||
<>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
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
|
||||
disabled={isLocked}
|
||||
onClick={() => onKitAdd && onKitAdd()}
|
||||
variant={"default"}
|
||||
>
|
||||
Добавить набор услуг
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
onClick={() => onCopyServices && onCopyServices()}
|
||||
variant={"default"}
|
||||
>
|
||||
Продублировать услуги
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
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={() => onChangeClick(row.original)}
|
||||
restProps={
|
||||
{
|
||||
enableColumnActions: false,
|
||||
enableSorting: false,
|
||||
enableRowActions: true,
|
||||
enableBottomToolbar: true,
|
||||
renderBottomToolbar: (
|
||||
<Flex
|
||||
justify={"flex-end"}
|
||||
gap={rem(10)}
|
||||
p={rem(10)}>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
onClick={() => onKitAdd && onKitAdd()}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{!authState.isGuest &&
|
||||
<Tooltip label="Сотрудники">
|
||||
<ActionIcon onClick={() => onEmployeeClick(row.original)} variant={"default"}>
|
||||
<IconUsersGroup />
|
||||
Добавить набор услуг
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
onClick={() =>
|
||||
onCopyServices && onCopyServices()
|
||||
}
|
||||
variant={"default"}>
|
||||
Продублировать услуги
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
disabled={isLocked}
|
||||
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>
|
||||
}
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<DealProductServiceSchema>}
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
onChangeClick(row.original)
|
||||
}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{!authState.isGuest && (
|
||||
<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)}>
|
||||
size={"xl"}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<SimpleUsersTable
|
||||
items={getCurrentEmployees()}
|
||||
onChange={onEmployeesChange}
|
||||
|
||||
/>
|
||||
|
||||
</Flex>
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
);
|
||||
};
|
||||
export default ProductServicesTable;
|
||||
export default ProductServicesTable;
|
||||
|
||||
@@ -1,38 +1,60 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {DealProductServiceSchema} from "../../../../../../client";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState} from "../../../../../../redux/store.ts";
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { DealProductServiceSchema } from "../../../../../../client";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../../../../../redux/store.ts";
|
||||
|
||||
type Props = {
|
||||
data: DealProductServiceSchema[];
|
||||
quantity: number;
|
||||
}
|
||||
};
|
||||
const useProductServicesTableColumns = (props: Props) => {
|
||||
const {data, quantity} = props;
|
||||
const { data, quantity } = props;
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
const totalPrice = useMemo(() => data.reduce((acc, row) => acc + (row.price * quantity), 0), [data, quantity]);
|
||||
const totalCost = useMemo(() => data.reduce((acc, row) => acc + ((row.service.cost || 0) * quantity), 0), [data, quantity]);
|
||||
const hideGuestColumns = [
|
||||
"service.cost"
|
||||
]
|
||||
return useMemo<MRT_ColumnDef<DealProductServiceSchema>[]>(() => [
|
||||
{
|
||||
accessorKey: "service.name",
|
||||
header: "Услуга",
|
||||
},
|
||||
{
|
||||
enableHiding: true,
|
||||
accessorKey: "service.cost",
|
||||
header: "Себестоимость",
|
||||
Footer: () => <>Итоговая себестоимость: {totalCost.toLocaleString("ru")}₽</>,
|
||||
},
|
||||
{
|
||||
accessorKey: "price",
|
||||
header: "Цена",
|
||||
Footer: () => <>Итог: {totalPrice.toLocaleString("ru")}₽</>,
|
||||
}
|
||||
], [totalPrice]).filter(columnDef => !(hideGuestColumns.includes(columnDef.accessorKey || "") && authState.isGuest));
|
||||
}
|
||||
const totalPrice = useMemo(
|
||||
() => data.reduce((acc, row) => acc + row.price * quantity, 0),
|
||||
[data, quantity]
|
||||
);
|
||||
const totalCost = useMemo(
|
||||
() =>
|
||||
data.reduce(
|
||||
(acc, row) => acc + (row.service.cost || 0) * quantity,
|
||||
0
|
||||
),
|
||||
[data, quantity]
|
||||
);
|
||||
const hideGuestColumns = ["service.cost"];
|
||||
return useMemo<MRT_ColumnDef<DealProductServiceSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "service.name",
|
||||
header: "Услуга",
|
||||
},
|
||||
{
|
||||
enableHiding: true,
|
||||
accessorKey: "service.cost",
|
||||
header: "Себестоимость",
|
||||
Footer: () => (
|
||||
<>
|
||||
Итоговая себестоимость: {totalCost.toLocaleString("ru")}
|
||||
₽
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "price",
|
||||
header: "Цена",
|
||||
Footer: () => <>Итог: {totalPrice.toLocaleString("ru")}₽</>,
|
||||
},
|
||||
],
|
||||
[totalPrice]
|
||||
).filter(
|
||||
columnDef =>
|
||||
!(
|
||||
hideGuestColumns.includes(columnDef.accessorKey || "") &&
|
||||
authState.isGuest
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default useProductServicesTableColumns;
|
||||
export default useProductServicesTableColumns;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
padding: rem(10);
|
||||
margin-bottom: rem(10);
|
||||
flex: 1;
|
||||
|
||||
}
|
||||
|
||||
.image-container {
|
||||
@@ -21,7 +20,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: rem(10);
|
||||
flex: 1
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.data-container {
|
||||
@@ -34,4 +33,4 @@
|
||||
|
||||
.attributes-container {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
import {FC} from "react";
|
||||
import { FC } from "react";
|
||||
import {
|
||||
DealProductSchema,
|
||||
DealProductServiceSchema,
|
||||
GetServiceKitSchema,
|
||||
ProductSchema
|
||||
ProductSchema,
|
||||
} from "../../../../../../client";
|
||||
import styles from './ProductView.module.css';
|
||||
import {ActionIcon, Box, Flex, Image, NumberInput, rem, Text, Title, Tooltip} from '@mantine/core';
|
||||
import styles from "./ProductView.module.css";
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Flex,
|
||||
Image,
|
||||
NumberInput,
|
||||
rem,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import ProductServicesTable from "../ProductServicesTable/ProductServicesTable.tsx";
|
||||
import {isNil, isNumber} from "lodash";
|
||||
import {IconBarcode, IconEdit, IconTrash} from "@tabler/icons-react";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {ServiceType} from "../../../../../../shared/enums/ServiceType.ts";
|
||||
import { isNil, isNumber } from "lodash";
|
||||
import { IconBarcode, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { ServiceType } from "../../../../../../shared/enums/ServiceType.ts";
|
||||
import useDealProductAndServiceTabState from "../../hooks/useProductAndServiceTabState.tsx";
|
||||
|
||||
type Props = {
|
||||
product: DealProductSchema;
|
||||
onChange?: (item: DealProductSchema) => void;
|
||||
onDelete?: (item: DealProductSchema) => void
|
||||
onDelete?: (item: DealProductSchema) => void;
|
||||
onCopyServices?: (item: DealProductSchema) => void;
|
||||
onKitAdd?: (item: DealProductSchema, kit: GetServiceKitSchema) => void;
|
||||
onProductEdit: (product: ProductSchema) => void;
|
||||
}
|
||||
};
|
||||
type ProductFieldNames = {
|
||||
[K in keyof ProductSchema]: string
|
||||
}
|
||||
[K in keyof ProductSchema]: string;
|
||||
};
|
||||
export const ProductFieldNames: Partial<ProductFieldNames> = {
|
||||
color: "Цвет",
|
||||
article: "Артикул",
|
||||
@@ -32,95 +42,94 @@ export const ProductFieldNames: Partial<ProductFieldNames> = {
|
||||
brand: "Бренд",
|
||||
composition: "Состав",
|
||||
additionalInfo: "Доп. информация",
|
||||
}
|
||||
};
|
||||
const ProductView: FC<Props> = ({
|
||||
product,
|
||||
onDelete,
|
||||
onChange,
|
||||
onCopyServices,
|
||||
onKitAdd,
|
||||
onProductEdit
|
||||
}) => {
|
||||
const {dealState} = useDealProductAndServiceTabState();
|
||||
product,
|
||||
onDelete,
|
||||
onChange,
|
||||
onCopyServices,
|
||||
onKitAdd,
|
||||
onProductEdit,
|
||||
}) => {
|
||||
const { dealState } = useDealProductAndServiceTabState();
|
||||
const isLocked = Boolean(dealState.deal?.billRequest);
|
||||
const onDeleteClick = () => {
|
||||
if (!onDelete) return;
|
||||
onDelete(product);
|
||||
}
|
||||
};
|
||||
|
||||
const onServiceDelete = (item: DealProductServiceSchema) => {
|
||||
if (!onChange) return;
|
||||
onChange({
|
||||
...product,
|
||||
services: product.services.filter(service => service.service.id !== item.service.id)
|
||||
services: product.services.filter(
|
||||
service => service.service.id !== item.service.id
|
||||
),
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
const onServiceCreate = (item: DealProductServiceSchema) => {
|
||||
if (!onChange) return;
|
||||
onChange({
|
||||
...product,
|
||||
services: [...product.services, item]
|
||||
})
|
||||
}
|
||||
services: [...product.services, item],
|
||||
});
|
||||
};
|
||||
|
||||
const onServiceChange = (item: DealProductServiceSchema) => {
|
||||
if (!onChange) return;
|
||||
onChange({
|
||||
...product,
|
||||
services: product.services.map(service => service.service.id === item.service.id ? item : service)
|
||||
})
|
||||
}
|
||||
services: product.services.map(service =>
|
||||
service.service.id === item.service.id ? item : service
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const onQuantityChange = (quantity: number) => {
|
||||
if (!onChange) return;
|
||||
onChange({
|
||||
...product,
|
||||
quantity
|
||||
})
|
||||
}
|
||||
quantity,
|
||||
});
|
||||
};
|
||||
const onPrintBarcodeClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "printBarcode",
|
||||
innerProps: {
|
||||
productId: product.product.id,
|
||||
defaultQuantity: product.quantity
|
||||
defaultQuantity: product.quantity,
|
||||
},
|
||||
title: 'Печать штрихкода',
|
||||
})
|
||||
}
|
||||
title: "Печать штрихкода",
|
||||
});
|
||||
};
|
||||
|
||||
const onKitAddClick = () => {
|
||||
if (!onKitAdd) return;
|
||||
modals.openContextModal({
|
||||
modal: "servicesKitSelectModal",
|
||||
innerProps: {
|
||||
onSelect: (kit) => onKitAdd(product, kit),
|
||||
serviceType: ServiceType.PRODUCT_SERVICE
|
||||
onSelect: kit => onKitAdd(product, kit),
|
||||
serviceType: ServiceType.PRODUCT_SERVICE,
|
||||
},
|
||||
withCloseButton: false
|
||||
})
|
||||
}
|
||||
|
||||
withCloseButton: false,
|
||||
});
|
||||
};
|
||||
|
||||
const onProductEditClick = () => {
|
||||
modals.openContextModal({
|
||||
modal: "createProduct",
|
||||
title: 'Редактирование товара',
|
||||
title: "Редактирование товара",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onChange: (newProduct) => onProductEdit(newProduct),
|
||||
onChange: newProduct => onProductEdit(newProduct),
|
||||
product: product.product,
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className={styles['container']}>
|
||||
<div className={styles['data-container']}>
|
||||
<div
|
||||
|
||||
|
||||
className={styles['image-container']}>
|
||||
<div className={styles["container"]}>
|
||||
<div className={styles["data-container"]}>
|
||||
<div className={styles["image-container"]}>
|
||||
<Image
|
||||
flex={1}
|
||||
radius={rem(10)}
|
||||
@@ -128,34 +137,44 @@ const ProductView: FC<Props> = ({
|
||||
src={product.product.imageUrl}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles['attributes-container']}>
|
||||
<div className={styles["attributes-container"]}>
|
||||
<Title order={3}>{product.product.name}</Title>
|
||||
{/*<Spoiler maxHeight={0} showLabel={"Показать характеристики"} hideLabel={"Скрыть"}>*/}
|
||||
|
||||
{Object.entries(product.product).map(([key, value]) => {
|
||||
const fieldName = ProductFieldNames[key as keyof ProductSchema];
|
||||
if (!fieldName || isNil(value) || value === '') return;
|
||||
return (<Text key={fieldName}>{fieldName}: {value.toString()} </Text>)
|
||||
})}
|
||||
<Text>Штрихкоды: {product.product.barcodes.join(', ')}</Text>
|
||||
{Object.entries(product.product).map(([key, value]) => {
|
||||
const fieldName =
|
||||
ProductFieldNames[key as keyof ProductSchema];
|
||||
if (!fieldName || isNil(value) || value === "") return;
|
||||
return (
|
||||
<Text key={fieldName}>
|
||||
{fieldName}: {value.toString()}{" "}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
<Text>
|
||||
Штрихкоды: {product.product.barcodes.join(", ")}
|
||||
</Text>
|
||||
{/*</Spoiler>*/}
|
||||
|
||||
</div>
|
||||
<Box/>
|
||||
<Box />
|
||||
<NumberInput
|
||||
mt={rem(10)}
|
||||
disabled={isLocked}
|
||||
suffix={" шт."}
|
||||
value={product.quantity}
|
||||
onChange={event => isNumber(event) && onQuantityChange(event)}
|
||||
onChange={event =>
|
||||
isNumber(event) && onQuantityChange(event)
|
||||
}
|
||||
placeholder={"Введите количество товара"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles['services-container']}>
|
||||
<div className={styles["services-container"]}>
|
||||
<ProductServicesTable
|
||||
onKitAdd={onKitAddClick}
|
||||
onCopyServices={() => onCopyServices && onCopyServices(product)}
|
||||
onCopyServices={() =>
|
||||
onCopyServices && onCopyServices(product)
|
||||
}
|
||||
items={product.services}
|
||||
quantity={product.quantity}
|
||||
onCreate={onServiceCreate}
|
||||
@@ -165,39 +184,34 @@ const ProductView: FC<Props> = ({
|
||||
<Flex
|
||||
mt={"auto"}
|
||||
ml={"auto"}
|
||||
gap={rem(10)}
|
||||
>
|
||||
gap={rem(10)}>
|
||||
<Tooltip
|
||||
|
||||
onClick={onPrintBarcodeClick}
|
||||
label="Печать штрихкода">
|
||||
<ActionIcon
|
||||
|
||||
variant={"default"}>
|
||||
<IconBarcode/>
|
||||
<ActionIcon variant={"default"}>
|
||||
<IconBarcode />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
onClick={onProductEditClick}
|
||||
label="Редактировать товар">
|
||||
<ActionIcon
|
||||
|
||||
variant={"default"}>
|
||||
<IconEdit/>
|
||||
<ActionIcon variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip onClick={onDeleteClick} label="Удалить товар">
|
||||
<Tooltip
|
||||
onClick={onDeleteClick}
|
||||
label="Удалить товар">
|
||||
<ActionIcon
|
||||
disabled={isLocked}
|
||||
variant={"default"}
|
||||
>
|
||||
<IconTrash/>
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductView;
|
||||
export default ProductView;
|
||||
|
||||
@@ -1,24 +1,30 @@
|
||||
import {CRUDTableProps} from "../../../../../types/CRUDTable.tsx";
|
||||
import {DealProductSchema, DealService, DealServiceSchema} from "../../../../../client";
|
||||
import {useDealPageContext} from "../../../contexts/DealPageContext.tsx";
|
||||
import {notifications} from "../../../../../shared/lib/notifications.ts";
|
||||
import { CRUDTableProps } from "../../../../../types/CRUDTable.tsx";
|
||||
import {
|
||||
DealProductSchema,
|
||||
DealService,
|
||||
DealServiceSchema,
|
||||
} from "../../../../../client";
|
||||
import { useDealPageContext } from "../../../contexts/DealPageContext.tsx";
|
||||
import { notifications } from "../../../../../shared/lib/notifications.ts";
|
||||
|
||||
const useDealState = () => {
|
||||
const {selectedDeal, setSelectedDeal} = useDealPageContext();
|
||||
const { selectedDeal, setSelectedDeal } = useDealPageContext();
|
||||
const refetch = async () => {
|
||||
if (!selectedDeal) return
|
||||
return DealService.getDealById({dealId: selectedDeal.id}).then((deal) => {
|
||||
setSelectedDeal(deal);
|
||||
})
|
||||
}
|
||||
if (!selectedDeal) return;
|
||||
return DealService.getDealById({ dealId: selectedDeal.id }).then(
|
||||
deal => {
|
||||
setSelectedDeal(deal);
|
||||
}
|
||||
);
|
||||
};
|
||||
return {
|
||||
deal: selectedDeal,
|
||||
refetch
|
||||
}
|
||||
}
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
const useDealServicesState = (): CRUDTableProps<DealServiceSchema> => {
|
||||
const {deal, refetch} = useDealState();
|
||||
const { deal, refetch } = useDealState();
|
||||
const onCreate = (item: DealServiceSchema) => {
|
||||
if (!deal) return;
|
||||
DealService.addDealService({
|
||||
@@ -26,92 +32,90 @@ const useDealServicesState = (): CRUDTableProps<DealServiceSchema> => {
|
||||
dealId: deal.id,
|
||||
serviceId: item.service.id,
|
||||
quantity: item.quantity,
|
||||
price: item.price
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
if (!ok) notifications.guess(ok, {message});
|
||||
price: item.price,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
if (!ok) notifications.guess(ok, { message });
|
||||
if (ok) await refetch();
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
const onDelete = (item: DealServiceSchema) => {
|
||||
if (!deal) return;
|
||||
DealService.deleteDealService({
|
||||
requestBody:
|
||||
{
|
||||
dealId: deal.id,
|
||||
serviceId: item.service.id
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
if (!ok) notifications.guess(ok, {message});
|
||||
requestBody: {
|
||||
dealId: deal.id,
|
||||
serviceId: item.service.id,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
if (!ok) notifications.guess(ok, { message });
|
||||
if (ok) await refetch();
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
const onChange = (item: DealServiceSchema) => {
|
||||
if (!deal) return;
|
||||
DealService.updateDealService({
|
||||
requestBody:
|
||||
{
|
||||
dealId: deal.id,
|
||||
service: item
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
if (!ok) notifications.guess(ok, {message});
|
||||
requestBody: {
|
||||
dealId: deal.id,
|
||||
service: item,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
if (!ok) notifications.guess(ok, { message });
|
||||
if (ok) await refetch();
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
return {
|
||||
items: deal?.services || [],
|
||||
onCreate,
|
||||
onDelete,
|
||||
onChange
|
||||
}
|
||||
}
|
||||
onChange,
|
||||
};
|
||||
};
|
||||
|
||||
const useDealProductsState = (): CRUDTableProps<DealProductSchema> => {
|
||||
const {deal, refetch} = useDealState();
|
||||
const { deal, refetch } = useDealState();
|
||||
const onCreate = (item: DealProductSchema) => {
|
||||
if (!deal) return;
|
||||
DealService.addDealProduct({
|
||||
requestBody: {
|
||||
dealId: deal.id,
|
||||
product: item
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
if (!ok) notifications.guess(ok, {message});
|
||||
product: item,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
if (!ok) notifications.guess(ok, { message });
|
||||
if (ok) await refetch();
|
||||
})
|
||||
});
|
||||
};
|
||||
const onDelete = (item: DealProductSchema) => {
|
||||
if (!deal) return;
|
||||
DealService.deleteDealProduct({
|
||||
requestBody: {
|
||||
dealId: deal.id,
|
||||
productId: item.product.id
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
if (!ok) notifications.guess(ok, {message});
|
||||
productId: item.product.id,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
if (!ok) notifications.guess(ok, { message });
|
||||
if (ok) await refetch();
|
||||
})
|
||||
});
|
||||
};
|
||||
const onChange = (item: DealProductSchema) => {
|
||||
if (!deal) return;
|
||||
DealService.updateDealProduct({
|
||||
requestBody: {
|
||||
dealId: deal.id,
|
||||
product: item
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
if (!ok) notifications.guess(ok, {message});
|
||||
product: item,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
if (!ok) notifications.guess(ok, { message });
|
||||
if (ok) await refetch();
|
||||
})
|
||||
});
|
||||
};
|
||||
return {
|
||||
items: deal?.products || [],
|
||||
onCreate,
|
||||
onDelete,
|
||||
onChange
|
||||
}
|
||||
}
|
||||
onChange,
|
||||
};
|
||||
};
|
||||
const useDealProductAndServiceTabState = () => {
|
||||
const dealState = useDealState();
|
||||
const dealProductsState = useDealProductsState();
|
||||
@@ -119,7 +123,7 @@ const useDealProductAndServiceTabState = () => {
|
||||
return {
|
||||
dealState,
|
||||
dealProductsState,
|
||||
dealServicesState
|
||||
}
|
||||
}
|
||||
export default useDealProductAndServiceTabState;
|
||||
dealServicesState,
|
||||
};
|
||||
};
|
||||
export default useDealProductAndServiceTabState;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
}
|
||||
|
||||
.search-input {
|
||||
|
||||
}
|
||||
|
||||
.boards {
|
||||
@@ -21,7 +20,6 @@
|
||||
}
|
||||
|
||||
.delete {
|
||||
|
||||
@mixin light {
|
||||
border-color: var(--mantine-color-gray-1);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ import styles from "./LeadsPage.module.css";
|
||||
import Board from "../../../components/Dnd/Board/Board.tsx";
|
||||
import { DragDropContext, Droppable, DropResult } from "@hello-pangea/dnd";
|
||||
import { useDealSummaries } from "../hooks/useDealSummaries.tsx";
|
||||
import { DealStatus, getDealStatusByName } from "../../../shared/enums/DealStatus.ts";
|
||||
import {
|
||||
DealStatus,
|
||||
getDealStatusByName,
|
||||
} from "../../../shared/enums/DealStatus.ts";
|
||||
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
|
||||
import DealEditDrawer from "../drawers/DealEditDrawer/DealEditDrawer.tsx";
|
||||
import { DealPageContextProvider } from "../contexts/DealPageContext.tsx";
|
||||
@@ -22,7 +25,7 @@ import { motion } from "framer-motion";
|
||||
|
||||
enum DisplayMode {
|
||||
BOARD,
|
||||
TABLE
|
||||
TABLE,
|
||||
}
|
||||
|
||||
export const LeadsPage: FC = () => {
|
||||
@@ -30,7 +33,9 @@ export const LeadsPage: FC = () => {
|
||||
|
||||
const { summariesRaw, refetch } = useDealSummaries();
|
||||
const [summaries, setSummaries] = useState(summariesRaw);
|
||||
const [displayMode, setDisplayMode] = useState<DisplayMode>(DisplayMode.BOARD);
|
||||
const [displayMode, setDisplayMode] = useState<DisplayMode>(
|
||||
DisplayMode.BOARD
|
||||
);
|
||||
const [isDragEnded, setIsDragEnded] = useState(true);
|
||||
useEffect(() => {
|
||||
setSummaries(summariesRaw);
|
||||
@@ -41,17 +46,19 @@ export const LeadsPage: FC = () => {
|
||||
if (!summary) return;
|
||||
modals.openConfirmModal({
|
||||
title: "Удаление сделки",
|
||||
children:
|
||||
children: (
|
||||
<Flex>
|
||||
Вы действительно хотите удалить сделку {summary.name}?
|
||||
</Flex>,
|
||||
</Flex>
|
||||
),
|
||||
onConfirm: () => {
|
||||
DealService.deleteDeal({ requestBody: { dealId: dealId } })
|
||||
.then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
});
|
||||
DealService.deleteDeal({
|
||||
requestBody: { dealId: dealId },
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
});
|
||||
},
|
||||
labels: {
|
||||
confirm: "Удалить",
|
||||
@@ -64,17 +71,19 @@ export const LeadsPage: FC = () => {
|
||||
if (!summary) return;
|
||||
modals.openConfirmModal({
|
||||
title: "Завершение сделки",
|
||||
children:
|
||||
children: (
|
||||
<Flex>
|
||||
Вы действительно хотите завершить сделку {summary.name}?
|
||||
</Flex>,
|
||||
</Flex>
|
||||
),
|
||||
onConfirm: () => {
|
||||
DealService.completeDeal({ requestBody: { dealId: dealId } })
|
||||
.then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
});
|
||||
DealService.completeDeal({
|
||||
requestBody: { dealId: dealId },
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await refetch();
|
||||
});
|
||||
},
|
||||
labels: {
|
||||
confirm: "Завершить",
|
||||
@@ -112,44 +121,42 @@ export const LeadsPage: FC = () => {
|
||||
status: status,
|
||||
};
|
||||
if (status == summary.status) {
|
||||
DealService.reorderDealSummaries({ requestBody: request as DealSummaryReorderRequest })
|
||||
.then(async response => {
|
||||
setSummaries(response.summaries);
|
||||
await refetch();
|
||||
});
|
||||
DealService.reorderDealSummaries({
|
||||
requestBody: request as DealSummaryReorderRequest,
|
||||
}).then(async response => {
|
||||
setSummaries(response.summaries);
|
||||
await refetch();
|
||||
});
|
||||
return;
|
||||
}
|
||||
modals.openContextModal({
|
||||
modal: "enterDeadline",
|
||||
title: "Необходимо указать дедлайн",
|
||||
innerProps: {
|
||||
onSubmit: (event) => DealService.reorderDealSummaries({ requestBody: event })
|
||||
.then(async response => {
|
||||
onSubmit: event =>
|
||||
DealService.reorderDealSummaries({
|
||||
requestBody: event,
|
||||
}).then(async response => {
|
||||
setSummaries(response.summaries);
|
||||
await refetch();
|
||||
}),
|
||||
request: request,
|
||||
},
|
||||
});
|
||||
|
||||
};
|
||||
const getTableBody = () => {
|
||||
return (
|
||||
<motion.div
|
||||
key={displayMode}
|
||||
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
|
||||
transition={{ duration: 0.2 }}>
|
||||
<DealsTable items={data} />
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
const getBoardBody = () => {
|
||||
return (
|
||||
|
||||
<motion.div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -159,9 +166,7 @@ export const LeadsPage: FC = () => {
|
||||
key={displayMode}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
|
||||
transition={{ duration: 0.2 }}>
|
||||
<DragDropContext
|
||||
onDragStart={() => {
|
||||
setIsDragEnded(false);
|
||||
@@ -170,124 +175,115 @@ export const LeadsPage: FC = () => {
|
||||
<Flex
|
||||
justify={"space-between"}
|
||||
direction={"column"}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
|
||||
style={{ flex: 1 }}>
|
||||
<div className={styles["boards"]}>
|
||||
<Board
|
||||
withCreateButton
|
||||
summaries={summaries
|
||||
.filter(summary => summary.status == DealStatus.AWAITING_ACCEPTANCE)}
|
||||
summaries={summaries.filter(
|
||||
summary =>
|
||||
summary.status ==
|
||||
DealStatus.AWAITING_ACCEPTANCE
|
||||
)}
|
||||
title={"Ожидает приемки"}
|
||||
droppableId={"AWAITING_ACCEPTANCE"}
|
||||
color={"#4A90E2"}
|
||||
/>
|
||||
<Board
|
||||
summaries={summaries
|
||||
.filter(summary => summary.status == DealStatus.PACKAGING)}
|
||||
summaries={summaries.filter(
|
||||
summary =>
|
||||
summary.status == DealStatus.PACKAGING
|
||||
)}
|
||||
title={"Упаковка"}
|
||||
droppableId={"PACKAGING"}
|
||||
color={"#F5A623"}
|
||||
|
||||
/>
|
||||
<Board
|
||||
summaries={summaries
|
||||
.filter(summary => summary.status == DealStatus.AWAITING_SHIPMENT)}
|
||||
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)}
|
||||
summaries={summaries.filter(
|
||||
summary =>
|
||||
summary.status ==
|
||||
DealStatus.AWAITING_PAYMENT
|
||||
)}
|
||||
title={"Ожидает оплаты"}
|
||||
droppableId={"AWAITING_PAYMENT"}
|
||||
color={"#D0021B"}
|
||||
|
||||
/>
|
||||
<Board
|
||||
summaries={summaries
|
||||
.filter(summary => summary.status == DealStatus.COMPLETED)}
|
||||
summaries={summaries.filter(
|
||||
summary =>
|
||||
summary.status == DealStatus.COMPLETED
|
||||
)}
|
||||
title={"Завершена"}
|
||||
droppableId={"COMPLETED"}
|
||||
color={"#417505"}
|
||||
/>
|
||||
</div>
|
||||
<Flex justify={"space-between"} gap={rem(10)}>
|
||||
<Flex
|
||||
justify={"space-between"}
|
||||
gap={rem(10)}>
|
||||
<div
|
||||
className={
|
||||
classNames(
|
||||
styles["delete"],
|
||||
isDragEnded && styles["delete-hidden"],
|
||||
)
|
||||
}
|
||||
>
|
||||
className={classNames(
|
||||
styles["delete"],
|
||||
isDragEnded && styles["delete-hidden"]
|
||||
)}>
|
||||
<Droppable droppableId={"DELETE"}>
|
||||
{(provided, snapshot) => (
|
||||
<>
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{
|
||||
!isDragEnded
|
||||
&&
|
||||
!snapshot.isDraggingOver &&
|
||||
<span>
|
||||
Удалить
|
||||
</span>
|
||||
|
||||
}
|
||||
|
||||
ref={provided.innerRef}>
|
||||
{!isDragEnded &&
|
||||
!snapshot.isDraggingOver && (
|
||||
<span>Удалить</span>
|
||||
)}
|
||||
</div>
|
||||
{provided.placeholder}
|
||||
</>
|
||||
|
||||
)}
|
||||
</Droppable>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
classNames(
|
||||
styles["delete"],
|
||||
isDragEnded && styles["delete-hidden"],
|
||||
)
|
||||
}
|
||||
>
|
||||
className={classNames(
|
||||
styles["delete"],
|
||||
isDragEnded && styles["delete-hidden"]
|
||||
)}>
|
||||
<Droppable droppableId={"SUCCESS"}>
|
||||
{(provided, snapshot) => (
|
||||
<>
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{
|
||||
!isDragEnded
|
||||
&&
|
||||
!snapshot.isDraggingOver &&
|
||||
<span>
|
||||
Успешно завершена
|
||||
</span>
|
||||
|
||||
}
|
||||
ref={provided.innerRef}>
|
||||
{!isDragEnded &&
|
||||
!snapshot.isDraggingOver && (
|
||||
<span>
|
||||
Успешно завершена
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{provided.placeholder}
|
||||
</>
|
||||
|
||||
)}
|
||||
</Droppable>
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
</DragDropContext>
|
||||
</motion.div>
|
||||
|
||||
);
|
||||
};
|
||||
const getBody = () => {
|
||||
return displayMode === DisplayMode.TABLE ? getTableBody() : getBoardBody();
|
||||
return displayMode === DisplayMode.TABLE
|
||||
? getTableBody()
|
||||
: getBoardBody();
|
||||
};
|
||||
return (
|
||||
<PageBlock
|
||||
@@ -298,78 +294,77 @@ export const LeadsPage: FC = () => {
|
||||
flexDirection: "column",
|
||||
backgroundColor: "transparent",
|
||||
boxShadow: "none",
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<DealPageContextProvider>
|
||||
<PageBlock
|
||||
style={{ flex: 0 }}
|
||||
>
|
||||
<PageBlock style={{ flex: 0 }}>
|
||||
<Flex
|
||||
align={"center"}
|
||||
justify={"space-between"}
|
||||
>
|
||||
justify={"space-between"}>
|
||||
<Flex
|
||||
gap={rem(10)}
|
||||
direction={"column"}
|
||||
align={"center"}
|
||||
>
|
||||
align={"center"}>
|
||||
<Text size={"xs"}>Вид</Text>
|
||||
<Flex gap={rem(10)}>
|
||||
<ActionIcon
|
||||
onClick={() => setDisplayMode(DisplayMode.BOARD)}
|
||||
onClick={() =>
|
||||
setDisplayMode(DisplayMode.BOARD)
|
||||
}
|
||||
variant={
|
||||
displayMode === DisplayMode.BOARD ?
|
||||
|
||||
"filled"
|
||||
:
|
||||
"default"
|
||||
|
||||
displayMode === DisplayMode.BOARD
|
||||
? "filled"
|
||||
: "default"
|
||||
}>
|
||||
<IconMenuDeep
|
||||
style={{ rotate: "-90deg" }}
|
||||
/>
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
onClick={() => setDisplayMode(DisplayMode.TABLE)}
|
||||
variant={
|
||||
displayMode === DisplayMode.TABLE ?
|
||||
"filled"
|
||||
:
|
||||
"default"
|
||||
onClick={() =>
|
||||
setDisplayMode(DisplayMode.TABLE)
|
||||
}
|
||||
>
|
||||
<IconMenu2
|
||||
/>
|
||||
variant={
|
||||
displayMode === DisplayMode.TABLE
|
||||
? "filled"
|
||||
: "default"
|
||||
}>
|
||||
<IconMenu2 />
|
||||
</ActionIcon>
|
||||
|
||||
</Flex>
|
||||
</Flex>
|
||||
<motion.div
|
||||
key={displayMode}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
transition={{ duration: 0.2 }}>
|
||||
<div
|
||||
|
||||
className={styles["top-panel"]}
|
||||
style={{ display: displayMode === DisplayMode.TABLE ? "flex" : "none" }}
|
||||
>
|
||||
style={{
|
||||
display:
|
||||
displayMode === DisplayMode.TABLE
|
||||
? "flex"
|
||||
: "none",
|
||||
}}>
|
||||
<DealStatusSelect
|
||||
onClear={() => form.setFieldValue("dealStatus", null)}
|
||||
onClear={() =>
|
||||
form.setFieldValue("dealStatus", null)
|
||||
}
|
||||
clearable
|
||||
placeholder={"Выберите статус "}
|
||||
{...form.getInputProps("dealStatus")}
|
||||
/>
|
||||
<BaseMarketplaceSelect
|
||||
onClear={() => form.setFieldValue("marketplace", null)}
|
||||
onClear={() =>
|
||||
form.setFieldValue("marketplace", null)
|
||||
}
|
||||
clearable
|
||||
|
||||
placeholder={"Выберите маркетплейс"}
|
||||
{...form.getInputProps("marketplace")}
|
||||
/>
|
||||
<ClientSelectNew
|
||||
onClear={() => form.setFieldValue("client", null)}
|
||||
onClear={() =>
|
||||
form.setFieldValue("client", null)
|
||||
}
|
||||
clearable
|
||||
searchable
|
||||
placeholder={"Выберите клиента"}
|
||||
@@ -388,11 +383,8 @@ export const LeadsPage: FC = () => {
|
||||
}}>
|
||||
{getBody()}
|
||||
</PageBlock>
|
||||
<DealEditDrawer
|
||||
/>
|
||||
<DealEditDrawer />
|
||||
</DealPageContextProvider>
|
||||
|
||||
</PageBlock>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.title {
|
||||
font-family:
|
||||
Greycliff CF,
|
||||
var(--mantine-font-family);
|
||||
font-weight: 900;
|
||||
}
|
||||
font-family:
|
||||
Greycliff CF,
|
||||
var(--mantine-font-family);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import {Button, Container, Paper, Title,} from '@mantine/core';
|
||||
import classes from './LoginPage.module.scss';
|
||||
import {RootState, useAppDispatch} from "../../redux/store.ts";
|
||||
import {AuthService} from "../../client";
|
||||
import TelegramLoginButton, {TelegramUser} from "../../components/TelegramAuthButton/TelegramAuthButton.tsx";
|
||||
import {notifications} from "../../shared/lib/notifications.ts";
|
||||
import {login} from "../../features/authSlice.ts";
|
||||
import {Navigate, useNavigate} from "@tanstack/react-router";
|
||||
import {useSelector} from "react-redux";
|
||||
import {useEffect} from "react";
|
||||
import { Button, Container, Paper, Title } from "@mantine/core";
|
||||
import classes from "./LoginPage.module.scss";
|
||||
import { RootState, useAppDispatch } from "../../redux/store.ts";
|
||||
import { AuthService } from "../../client";
|
||||
import TelegramLoginButton, {
|
||||
TelegramUser,
|
||||
} from "../../components/TelegramAuthButton/TelegramAuthButton.tsx";
|
||||
import { notifications } from "../../shared/lib/notifications.ts";
|
||||
import { login } from "../../features/authSlice.ts";
|
||||
import { Navigate, useNavigate } from "@tanstack/react-router";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const LoginPage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -16,52 +18,67 @@ const LoginPage = () => {
|
||||
useEffect(() => {
|
||||
if (authState.isAuthorized)
|
||||
// ???????????
|
||||
navigate({to: "/leads"}).then(() => {
|
||||
navigate({to: "/leads"}).then(() => {
|
||||
notifications.success({message: "Вы успешно вошли!"})
|
||||
navigate({ to: "/leads" }).then(() => {
|
||||
navigate({ to: "/leads" }).then(() => {
|
||||
notifications.success({ message: "Вы успешно вошли!" });
|
||||
});
|
||||
});
|
||||
}, [authState.isAuthorized])
|
||||
}, [authState.isAuthorized]);
|
||||
if (authState.isAuthorized) {
|
||||
return (<Navigate to={"/leads"}/>)
|
||||
return <Navigate to={"/leads"} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<Container size={420} my={40}>
|
||||
<Title ta="center" className={classes.title}>
|
||||
<Container
|
||||
size={420}
|
||||
my={40}>
|
||||
<Title
|
||||
ta="center"
|
||||
className={classes.title}>
|
||||
Добро пожаловать на DENCO CRM
|
||||
</Title>
|
||||
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||
<Paper
|
||||
withBorder
|
||||
shadow="md"
|
||||
p={30}
|
||||
mt={30}
|
||||
radius="md">
|
||||
<TelegramLoginButton
|
||||
botName={"DencoFulfillmentTestBot"}
|
||||
dataOnauth={() => {
|
||||
}}
|
||||
wrapperProps={{style: {display: "none"}}}
|
||||
dataOnauth={() => {}}
|
||||
wrapperProps={{ style: { display: "none" } }}
|
||||
/>
|
||||
<Button fullWidth onClick={() => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
window.Telegram.Login.auth(
|
||||
{
|
||||
bot_id: import.meta.env.VITE_BOT_ID,
|
||||
request_access: true
|
||||
},
|
||||
(data: TelegramUser) => {
|
||||
AuthService.loginAuthLoginPost({requestBody: data})
|
||||
.then(({accessToken}) => {
|
||||
dispatch(login({accessToken: accessToken}));
|
||||
}).catch(() => {
|
||||
notifications.error({message: "Неудалось войти!"})
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
}}>
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
window.Telegram.Login.auth(
|
||||
{
|
||||
bot_id: import.meta.env.VITE_BOT_ID,
|
||||
request_access: true,
|
||||
},
|
||||
(data: TelegramUser) => {
|
||||
AuthService.loginAuthLoginPost({
|
||||
requestBody: data,
|
||||
})
|
||||
.then(({ accessToken }) => {
|
||||
dispatch(
|
||||
login({ accessToken: accessToken })
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
notifications.error({
|
||||
message: "Неудалось войти!",
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}}>
|
||||
Войти через Telegram
|
||||
</Button>
|
||||
</Paper>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
export default LoginPage
|
||||
);
|
||||
};
|
||||
export default LoginPage;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.container {
|
||||
background-color: rebeccapurple;
|
||||
}
|
||||
background-color: rebeccapurple;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import {FC} from "react";
|
||||
import {Navigate} from "@tanstack/react-router";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState} from "../../redux/store.ts";
|
||||
import { FC } from "react";
|
||||
import { Navigate } from "@tanstack/react-router";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../redux/store.ts";
|
||||
|
||||
const MainPage: FC = () => {
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
if (authState.isAuthorized) {
|
||||
return (<Navigate to={"/leads"}/>)
|
||||
return <Navigate to={"/leads"} />;
|
||||
}
|
||||
return (<>
|
||||
return <></>;
|
||||
};
|
||||
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default MainPage;
|
||||
export default MainPage;
|
||||
|
||||
@@ -1,100 +1,116 @@
|
||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
||||
import {ClientSchema, MarketplaceSchema} from "../../../../client";
|
||||
import {FC} from "react";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { ClientSchema, MarketplaceSchema } from "../../../../client";
|
||||
import { FC } from "react";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import useMarketplacesTableColumns from "./columns.tsx";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import {ActionIcon, Button, Flex, rem, Text, Tooltip} from "@mantine/core";
|
||||
import {modals} from "@mantine/modals";
|
||||
import {IconEdit, IconRefresh, IconTrash} from "@tabler/icons-react";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { ActionIcon, Button, Flex, rem, Text, Tooltip } from "@mantine/core";
|
||||
import { modals } from "@mantine/modals";
|
||||
import { IconEdit, IconRefresh, IconTrash } from "@tabler/icons-react";
|
||||
|
||||
type RestProps = {
|
||||
client?: ClientSchema;
|
||||
onSynchronize?: (marketplace: MarketplaceSchema) => void;
|
||||
|
||||
}
|
||||
};
|
||||
type Props = CRUDTableProps<MarketplaceSchema> & RestProps;
|
||||
|
||||
const MarketplacesTable: FC<Props> = ({onDelete, onChange, onCreate, items, client, onSynchronize}) => {
|
||||
const MarketplacesTable: FC<Props> = ({
|
||||
onDelete,
|
||||
onChange,
|
||||
onCreate,
|
||||
items,
|
||||
client,
|
||||
onSynchronize,
|
||||
}) => {
|
||||
const columns = useMarketplacesTableColumns();
|
||||
const onDeleteClick = (marketplace: MarketplaceSchema) => {
|
||||
if (!onDelete) return;
|
||||
modals.openConfirmModal({
|
||||
title: 'Удаление маркетплейса',
|
||||
title: "Удаление маркетплейса",
|
||||
children: (
|
||||
<Text size="sm">
|
||||
Вы уверены что хотите удалить маркетплейс {marketplace.name}
|
||||
</Text>
|
||||
),
|
||||
labels: {confirm: 'Да', cancel: "Нет"},
|
||||
confirmProps: {color: 'red'},
|
||||
onConfirm: () => onDelete(marketplace)
|
||||
labels: { confirm: "Да", cancel: "Нет" },
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => onDelete(marketplace),
|
||||
});
|
||||
}
|
||||
};
|
||||
const onEditClick = (marketplace: MarketplaceSchema) => {
|
||||
if (!onChange) return;
|
||||
modals.openContextModal({
|
||||
modal: "marketplaceFormModal",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onChange: (event) => onChange(event),
|
||||
element: marketplace
|
||||
}
|
||||
})
|
||||
}
|
||||
onChange: event => onChange(event),
|
||||
element: marketplace,
|
||||
},
|
||||
});
|
||||
};
|
||||
const onCreateClick = () => {
|
||||
if (!onCreate || !client) return;
|
||||
modals.openContextModal({
|
||||
modal: "marketplaceFormModal",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onCreate: (event) => onCreate({...event, client: client})
|
||||
}
|
||||
})
|
||||
}
|
||||
onCreate: event => onCreate({ ...event, client: client }),
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<BaseTable
|
||||
data={items}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableTopToolbar: true,
|
||||
renderTopToolbar: (
|
||||
<Flex p={rem(10)} justify="end">
|
||||
<Button
|
||||
disabled={!client}
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}>
|
||||
Добавить
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon onClick={() => onDeleteClick(row.original)} variant={"default"}>
|
||||
<IconTrash/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Синхронизировать">
|
||||
<ActionIcon onClick={() => onSynchronize && onSynchronize(row.original)}
|
||||
variant={"default"}>
|
||||
<IconRefresh/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)
|
||||
} as MRT_TableOptions<MarketplaceSchema>}
|
||||
restProps={
|
||||
{
|
||||
enableSorting: false,
|
||||
enableColumnActions: false,
|
||||
enableTopToolbar: true,
|
||||
renderTopToolbar: (
|
||||
<Flex
|
||||
p={rem(10)}
|
||||
justify="end">
|
||||
<Button
|
||||
disabled={!client}
|
||||
variant={"default"}
|
||||
onClick={() => onCreateClick()}>
|
||||
Добавить
|
||||
</Button>
|
||||
</Flex>
|
||||
),
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => onDeleteClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Синхронизировать">
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
onSynchronize &&
|
||||
onSynchronize(row.original)
|
||||
}
|
||||
variant={"default"}>
|
||||
<IconRefresh />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<MarketplaceSchema>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default MarketplacesTable;
|
||||
);
|
||||
};
|
||||
export default MarketplacesTable;
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
import {MarketplaceSchema} from "../../../../client";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {useMemo} from "react";
|
||||
import {ActionIcon, Image} from "@mantine/core";
|
||||
import { MarketplaceSchema } from "../../../../client";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { useMemo } from "react";
|
||||
import { ActionIcon, Image } from "@mantine/core";
|
||||
|
||||
const useMarketplacesTableColumns = () => {
|
||||
return useMemo<MRT_ColumnDef<MarketplaceSchema>[]>(() => [
|
||||
{
|
||||
header: "Маркетплейс",
|
||||
size: 10,
|
||||
Cell: ({row}) => (
|
||||
<ActionIcon variant={"transparent"}>
|
||||
<Image src={row.original.baseMarketplace?.iconUrl || ""}/>
|
||||
</ActionIcon>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "client.name",
|
||||
header: "Клиент",
|
||||
enableSorting: false,
|
||||
},
|
||||
// {
|
||||
// accessorKey: "authData",
|
||||
// header: "Данные авторизации",
|
||||
// enableSorting: false,
|
||||
// },
|
||||
], []);
|
||||
}
|
||||
return useMemo<MRT_ColumnDef<MarketplaceSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "Маркетплейс",
|
||||
size: 10,
|
||||
Cell: ({ row }) => (
|
||||
<ActionIcon variant={"transparent"}>
|
||||
<Image
|
||||
src={row.original.baseMarketplace?.iconUrl || ""}
|
||||
/>
|
||||
</ActionIcon>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "client.name",
|
||||
header: "Клиент",
|
||||
enableSorting: false,
|
||||
},
|
||||
// {
|
||||
// accessorKey: "authData",
|
||||
// header: "Данные авторизации",
|
||||
// enableSorting: false,
|
||||
// },
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
export default useMarketplacesTableColumns;
|
||||
export default useMarketplacesTableColumns;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {ClientSchema, MarketplaceSchema, MarketplaceService, TaskService} from "../../../client";
|
||||
import {notifications} from "../../../shared/lib/notifications.ts";
|
||||
import {RootState, useAppDispatch} from "../../../redux/store.ts";
|
||||
import {addTask} from "../../../features/tasksSlice.tsx";
|
||||
import {useSelector} from "react-redux";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
ClientSchema,
|
||||
MarketplaceSchema,
|
||||
MarketplaceService,
|
||||
TaskService,
|
||||
} from "../../../client";
|
||||
import { notifications } from "../../../shared/lib/notifications.ts";
|
||||
import { RootState, useAppDispatch } from "../../../redux/store.ts";
|
||||
import { addTask } from "../../../features/tasksSlice.tsx";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const useMarketplacesPageState = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -14,12 +19,12 @@ const useMarketplacesPageState = () => {
|
||||
if (!client) return;
|
||||
MarketplaceService.getClientMarketplaces({
|
||||
requestBody: {
|
||||
clientId: client.id
|
||||
}
|
||||
}).then((response) => {
|
||||
clientId: client.id,
|
||||
},
|
||||
}).then(response => {
|
||||
setItems(response.marketplaces);
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onCreate = (marketplace: MarketplaceSchema) => {
|
||||
MarketplaceService.createMarketplace({
|
||||
@@ -27,77 +32,78 @@ const useMarketplacesPageState = () => {
|
||||
marketplace: {
|
||||
...marketplace,
|
||||
clientId: marketplace.client.id,
|
||||
baseMarketplaceKey: marketplace.baseMarketplace.key
|
||||
}
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
baseMarketplaceKey: marketplace.baseMarketplace.key,
|
||||
},
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await fetchMarketplaces();
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
const onDelete = (marketplace: MarketplaceSchema) => {
|
||||
MarketplaceService.deleteMarketplace({
|
||||
requestBody: {
|
||||
marketplaceId: marketplace.id
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
marketplaceId: marketplace.id,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await fetchMarketplaces();
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
const onChange = (marketplace: MarketplaceSchema) => {
|
||||
MarketplaceService.updateMarketplace({
|
||||
requestBody: {
|
||||
marketplace: marketplace
|
||||
}
|
||||
}).then(async ({ok, message}) => {
|
||||
notifications.guess(ok, {message});
|
||||
marketplace: marketplace,
|
||||
},
|
||||
}).then(async ({ ok, message }) => {
|
||||
notifications.guess(ok, { message });
|
||||
if (!ok) return;
|
||||
await fetchMarketplaces();
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onSynchronize = (marketplace: MarketplaceSchema) => {
|
||||
|
||||
const task = tasks.find(task => task.info.marketplaceId === marketplace.id);
|
||||
const task = tasks.find(
|
||||
task => task.info.marketplaceId === marketplace.id
|
||||
);
|
||||
if (task) {
|
||||
notifications.error({
|
||||
title: 'Ошибка',
|
||||
message: `Синхронизация маркетплейса ${marketplace.name} уже запущена`
|
||||
title: "Ошибка",
|
||||
message: `Синхронизация маркетплейса ${marketplace.name} уже запущена`,
|
||||
});
|
||||
return;
|
||||
|
||||
}
|
||||
TaskService.createSynchronizeMarketplaceTask({
|
||||
requestBody: {
|
||||
marketplaceId:
|
||||
marketplace.id
|
||||
}
|
||||
}).then(({taskId}) => {
|
||||
dispatch(addTask({
|
||||
id: taskId,
|
||||
config: {
|
||||
onErrorData: {
|
||||
title: 'Ошибка',
|
||||
message: `Ошибка синхронизации маркетплейса: ${marketplace.name}`
|
||||
marketplaceId: marketplace.id,
|
||||
},
|
||||
}).then(({ taskId }) => {
|
||||
dispatch(
|
||||
addTask({
|
||||
id: taskId,
|
||||
config: {
|
||||
onErrorData: {
|
||||
title: "Ошибка",
|
||||
message: `Ошибка синхронизации маркетплейса: ${marketplace.name}`,
|
||||
},
|
||||
onLoadingData: {
|
||||
title: "Синхронизация",
|
||||
message: `Синхронизация маркетплейса: ${marketplace.name}`,
|
||||
},
|
||||
onSuccessData: {
|
||||
title: "Успех",
|
||||
message: `Маркетплейс ${marketplace.name} успешно синхронизирован`,
|
||||
},
|
||||
},
|
||||
onLoadingData: {
|
||||
title: 'Синхронизация',
|
||||
message: `Синхронизация маркетплейса: ${marketplace.name}`
|
||||
info: {
|
||||
marketplaceId: marketplace.id,
|
||||
},
|
||||
onSuccessData: {
|
||||
title: 'Успех',
|
||||
message: `Маркетплейс ${marketplace.name} успешно синхронизирован`
|
||||
}
|
||||
},
|
||||
info: {
|
||||
marketplaceId: marketplace.id
|
||||
}
|
||||
}));
|
||||
})
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchMarketplaces();
|
||||
}, [client]);
|
||||
@@ -108,7 +114,7 @@ const useMarketplacesPageState = () => {
|
||||
onDelete,
|
||||
onChange,
|
||||
onCreate,
|
||||
onSynchronize
|
||||
}
|
||||
}
|
||||
export default useMarketplacesPageState;
|
||||
onSynchronize,
|
||||
};
|
||||
};
|
||||
export default useMarketplacesPageState;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export {MarketplacesPage} from './ui/MarketplacesPage.tsx';
|
||||
export { MarketplacesPage } from "./ui/MarketplacesPage.tsx";
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
import {TextInput} from "@mantine/core";
|
||||
import {BaseFormInputProps} from "../../../../types/utils.ts";
|
||||
import {FC} from "react";
|
||||
import {BaseMarketplaceSchema} from "../../../../client";
|
||||
import {BaseMarketplaceType} from "../../../../shared/enums/BaseMarketplaceType.ts";
|
||||
import { TextInput } from "@mantine/core";
|
||||
import { BaseFormInputProps } from "../../../../types/utils.ts";
|
||||
import { FC } from "react";
|
||||
import { BaseMarketplaceSchema } from "../../../../client";
|
||||
import { BaseMarketplaceType } from "../../../../shared/enums/BaseMarketplaceType.ts";
|
||||
|
||||
type RestProps = {
|
||||
baseMarketplace: BaseMarketplaceSchema;
|
||||
}
|
||||
};
|
||||
type Props = BaseFormInputProps<Record<string, string>> & RestProps;
|
||||
|
||||
const MarketplaceAuthDataInput: FC<Props> = (props: Props) => {
|
||||
console.log(props.baseMarketplace);
|
||||
const getWildberriesInputs = () => {
|
||||
// return input that sets record "Authorization" to value
|
||||
return <TextInput
|
||||
{...props}
|
||||
label={"Ключ авторизации"}
|
||||
placeholder={"Введите ключ авторизации"}
|
||||
value={props.value["Authorization"] || ""}
|
||||
onChange={(value) => props.onChange({...props.value, Authorization: value.target.value})}
|
||||
/>
|
||||
|
||||
}
|
||||
return (
|
||||
<TextInput
|
||||
{...props}
|
||||
label={"Ключ авторизации"}
|
||||
placeholder={"Введите ключ авторизации"}
|
||||
value={props.value["Authorization"] || ""}
|
||||
onChange={value =>
|
||||
props.onChange({
|
||||
...props.value,
|
||||
Authorization: value.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const getOzonInputs = () => {
|
||||
return (
|
||||
<>
|
||||
@@ -30,21 +36,29 @@ const MarketplaceAuthDataInput: FC<Props> = (props: Props) => {
|
||||
label={"Client-Id"}
|
||||
placeholder={"Введите Client-Id"}
|
||||
value={props.value["Client-Id"] || ""}
|
||||
onChange={(value) => props.onChange({...props.value, "Client-Id": value.target.value})}
|
||||
onChange={value =>
|
||||
props.onChange({
|
||||
...props.value,
|
||||
"Client-Id": value.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<TextInput
|
||||
{...props}
|
||||
label={"Api-Key"}
|
||||
placeholder={"Введите Api-Key"}
|
||||
value={props.value["Api-Key"] || ""}
|
||||
onChange={(value) => props.onChange({...props.value, "Api-Key": value.target.value})}
|
||||
onChange={value =>
|
||||
props.onChange({
|
||||
...props.value,
|
||||
"Api-Key": value.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
const getYandexMarketInputs = () => {
|
||||
}
|
||||
);
|
||||
};
|
||||
const getYandexMarketInputs = () => {};
|
||||
|
||||
const getInputs = () => {
|
||||
if (props.baseMarketplace.key === BaseMarketplaceType.WILDBERRIES) {
|
||||
@@ -56,14 +70,10 @@ const MarketplaceAuthDataInput: FC<Props> = (props: Props) => {
|
||||
if (props.baseMarketplace.key === BaseMarketplaceType.YANDEX_MARKET) {
|
||||
return getYandexMarketInputs();
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{getInputs()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
return <>{getInputs()}</>;
|
||||
};
|
||||
|
||||
export default MarketplaceAuthDataInput;
|
||||
export default MarketplaceAuthDataInput;
|
||||
|
||||
@@ -1,45 +1,51 @@
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import BaseFormModal, {CreateEditFormProps} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import {MarketplaceSchema} from "../../../../client";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {Fieldset, Flex, rem, TextInput} from "@mantine/core";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import BaseFormModal, {
|
||||
CreateEditFormProps,
|
||||
} from "../../../ClientsPage/modals/BaseFormModal/BaseFormModal.tsx";
|
||||
import { MarketplaceSchema } from "../../../../client";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { Fieldset, Flex, rem, TextInput } from "@mantine/core";
|
||||
import BaseMarketplaceSelect from "../../../../components/Selects/BaseMarketplaceSelect/BaseMarketplaceSelect.tsx";
|
||||
import MarketplaceAuthDataInput from "./MarketplaceAuthDataInput.tsx";
|
||||
|
||||
type Props = CreateEditFormProps<MarketplaceSchema>
|
||||
type Props = CreateEditFormProps<MarketplaceSchema>;
|
||||
const MarketplaceFormModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = 'element' in innerProps;
|
||||
const initialValue: Partial<MarketplaceSchema> = isEditing ? innerProps.element : {
|
||||
authData: {
|
||||
Authorization: '',
|
||||
"Client-Id": '',
|
||||
"Api-Key": ''
|
||||
|
||||
},
|
||||
};
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditing = "element" in innerProps;
|
||||
const initialValue: Partial<MarketplaceSchema> = isEditing
|
||||
? innerProps.element
|
||||
: {
|
||||
authData: {
|
||||
"Authorization": "",
|
||||
"Client-Id": "",
|
||||
"Api-Key": "",
|
||||
},
|
||||
};
|
||||
const form = useForm<Partial<MarketplaceSchema>>({
|
||||
initialValues: initialValue,
|
||||
validate: {
|
||||
baseMarketplace: (baseMarketplace) => !baseMarketplace && "Необходимо указать базовый маркетплейс",
|
||||
name: (name) => !name && "Необходимо указать название маркетплейса",
|
||||
authData: (authData) => !authData && "Необходимо указать данные авторизации"
|
||||
}
|
||||
baseMarketplace: baseMarketplace =>
|
||||
!baseMarketplace && "Необходимо указать базовый маркетплейс",
|
||||
name: name => !name && "Необходимо указать название маркетплейса",
|
||||
authData: authData =>
|
||||
!authData && "Необходимо указать данные авторизации",
|
||||
},
|
||||
});
|
||||
return (
|
||||
<BaseFormModal
|
||||
form={form}
|
||||
closeOnSubmit
|
||||
onClose={() => context.closeContextModal(id)}
|
||||
{...innerProps}
|
||||
>
|
||||
{...innerProps}>
|
||||
<BaseFormModal.Body>
|
||||
<>
|
||||
<Fieldset legend={"Общие параметры"}>
|
||||
<Flex direction={"column"} gap={rem(10)}>
|
||||
<Flex
|
||||
direction={"column"}
|
||||
gap={rem(10)}>
|
||||
<TextInput
|
||||
label={"Название"}
|
||||
placeholder={"Введите название маркетплейса"}
|
||||
@@ -51,20 +57,28 @@ const MarketplaceFormModal = ({
|
||||
{...form.getInputProps("baseMarketplace")}
|
||||
/>
|
||||
|
||||
{form.values.baseMarketplace &&
|
||||
{form.values.baseMarketplace && (
|
||||
<MarketplaceAuthDataInput
|
||||
baseMarketplace={form.values.baseMarketplace}
|
||||
value={form.values.authData as Record<string, string>}
|
||||
onChange={(value) => form.setFieldValue("authData", value)}
|
||||
baseMarketplace={
|
||||
form.values.baseMarketplace
|
||||
}
|
||||
value={
|
||||
form.values.authData as Record<
|
||||
string,
|
||||
string
|
||||
>
|
||||
}
|
||||
onChange={value =>
|
||||
form.setFieldValue("authData", value)
|
||||
}
|
||||
error={form.getInputProps("authData").error}
|
||||
/>
|
||||
}
|
||||
|
||||
)}
|
||||
</Flex>
|
||||
</Fieldset>
|
||||
</>
|
||||
</BaseFormModal.Body>
|
||||
</BaseFormModal>
|
||||
)
|
||||
}
|
||||
export default MarketplaceFormModal;
|
||||
);
|
||||
};
|
||||
export default MarketplaceFormModal;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
padding: rem(5);
|
||||
gap: rem(10);
|
||||
display: flex;
|
||||
|
||||
}
|
||||
|
||||
.top-panel-last-item {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import styles from './MarketplacesPage.module.css';
|
||||
import styles from "./MarketplacesPage.module.css";
|
||||
import PageBlock from "../../../components/PageBlock/PageBlock.tsx";
|
||||
import ClientSelectNew from "../../../components/Selects/ClientSelectNew/ClientSelectNew.tsx";
|
||||
import useMarketplacesPageState from "../hooks/useMarketplacesPageState.tsx";
|
||||
@@ -7,23 +7,20 @@ import MarketplacesTable from "../components/MarketplacesTable/MarketplacesTable
|
||||
export const MarketplacesPage = () => {
|
||||
const state = useMarketplacesPageState();
|
||||
return (
|
||||
<div className={styles['container']}>
|
||||
<div className={styles["container"]}>
|
||||
<PageBlock>
|
||||
<div className={styles['top-panel']}>
|
||||
<div className={styles["top-panel"]}>
|
||||
<ClientSelectNew
|
||||
placeholder={'Выберите клиента'}
|
||||
placeholder={"Выберите клиента"}
|
||||
onChange={state.setClient}
|
||||
/>
|
||||
</div>
|
||||
</PageBlock>
|
||||
<PageBlock>
|
||||
<>
|
||||
<MarketplacesTable
|
||||
{...state}
|
||||
/>
|
||||
|
||||
<MarketplacesTable {...state} />
|
||||
</>
|
||||
</PageBlock>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,50 +1,49 @@
|
||||
import {FC, ReactNode} from "react";
|
||||
import {AppShell, Flex, rem} from "@mantine/core";
|
||||
import {useSelector} from "react-redux";
|
||||
import {RootState} from "../../redux/store.ts";
|
||||
import styles from './PageWrapper.module.css';
|
||||
import {Navbar} from "../../components/Navbar/Navbar.tsx";
|
||||
import { FC, ReactNode } from "react";
|
||||
import { AppShell, Flex, rem } from "@mantine/core";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../redux/store.ts";
|
||||
import styles from "./PageWrapper.module.css";
|
||||
import { Navbar } from "../../components/Navbar/Navbar.tsx";
|
||||
|
||||
export type Props = {
|
||||
children: ReactNode;
|
||||
}
|
||||
const PageWrapper: FC<Props> = ({children}) => {
|
||||
};
|
||||
const PageWrapper: FC<Props> = ({ children }) => {
|
||||
const authState = useSelector((state: RootState) => state.auth);
|
||||
return (
|
||||
<AppShell
|
||||
layout={"alt"}
|
||||
withBorder={false}
|
||||
navbar={(authState.isAuthorized && !authState.isGuest) ? {
|
||||
width: "130px",
|
||||
breakpoint: "sm"
|
||||
} : undefined}
|
||||
>
|
||||
|
||||
navbar={
|
||||
authState.isAuthorized && !authState.isGuest
|
||||
? {
|
||||
width: "130px",
|
||||
breakpoint: "sm",
|
||||
}
|
||||
: undefined
|
||||
}>
|
||||
<AppShell.Navbar>
|
||||
{(authState.isAuthorized && !authState.isGuest) &&
|
||||
<Flex className={styles['main-container']} h={"100%"} w={"100%"}
|
||||
pl={rem(20)}
|
||||
py={rem(20)}
|
||||
>
|
||||
<Navbar/>
|
||||
|
||||
{authState.isAuthorized && !authState.isGuest && (
|
||||
<Flex
|
||||
className={styles["main-container"]}
|
||||
h={"100%"}
|
||||
w={"100%"}
|
||||
pl={rem(20)}
|
||||
py={rem(20)}>
|
||||
<Navbar />
|
||||
</Flex>
|
||||
}
|
||||
)}
|
||||
</AppShell.Navbar>
|
||||
<AppShell.Main
|
||||
style={
|
||||
authState.isGuest ? {backgroundColor: "var(--mantine-color-dark-8)"} : {}
|
||||
authState.isGuest
|
||||
? { backgroundColor: "var(--mantine-color-dark-8)" }
|
||||
: {}
|
||||
}
|
||||
className={styles['main-container']}
|
||||
>
|
||||
<div
|
||||
className={styles['container']}
|
||||
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
className={styles["main-container"]}>
|
||||
<div className={styles["container"]}>{children}</div>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
export default PageWrapper;
|
||||
);
|
||||
};
|
||||
export default PageWrapper;
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
}
|
||||
|
||||
.pagination {
|
||||
align-self: flex-end;
|
||||
}
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
@@ -1,74 +1,84 @@
|
||||
import {ProductSchema} from "../../../../client";
|
||||
import {FC} from "react";
|
||||
import {BaseTable} from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import {MRT_TableOptions} from "mantine-react-table";
|
||||
import {useProductsTableColumns} from "./columns.tsx";
|
||||
import {ActionIcon, Flex, Tooltip} from "@mantine/core";
|
||||
import {IconBarcode, IconEdit, IconTrash} from "@tabler/icons-react";
|
||||
import {CRUDTableProps} from "../../../../types/CRUDTable.tsx";
|
||||
import {modals} from "@mantine/modals";
|
||||
import { ProductSchema } from "../../../../client";
|
||||
import { FC } from "react";
|
||||
import { BaseTable } from "../../../../components/BaseTable/BaseTable.tsx";
|
||||
import { MRT_TableOptions } from "mantine-react-table";
|
||||
import { useProductsTableColumns } from "./columns.tsx";
|
||||
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
|
||||
import { IconBarcode, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
|
||||
import { modals } from "@mantine/modals";
|
||||
|
||||
|
||||
const ProductsTable: FC<CRUDTableProps<ProductSchema>> = ({items, onDelete, onChange, tableRef}) => {
|
||||
const ProductsTable: FC<CRUDTableProps<ProductSchema>> = ({
|
||||
items,
|
||||
onDelete,
|
||||
onChange,
|
||||
tableRef,
|
||||
}) => {
|
||||
const columns = useProductsTableColumns();
|
||||
const onEditClick = (product: ProductSchema) => {
|
||||
if (!onChange) return;
|
||||
modals.openContextModal({
|
||||
modal: "createProduct",
|
||||
title: 'Создание товара',
|
||||
title: "Создание товара",
|
||||
withCloseButton: false,
|
||||
innerProps: {
|
||||
onChange: (newProduct) => onChange(newProduct),
|
||||
onChange: newProduct => onChange(newProduct),
|
||||
product: product,
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
const onPrintBarcodeClick = (product: ProductSchema) => {
|
||||
modals.openContextModal({
|
||||
modal: "printBarcode",
|
||||
title: 'Печать штрихкода',
|
||||
title: "Печать штрихкода",
|
||||
withCloseButton: true,
|
||||
innerProps: {
|
||||
productId: product.id
|
||||
}
|
||||
})
|
||||
}
|
||||
productId: product.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<BaseTable
|
||||
ref={tableRef}
|
||||
data={items}
|
||||
columns={columns}
|
||||
restProps={{
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({row}) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Печать штрихкода">
|
||||
<ActionIcon
|
||||
onClick={() => onPrintBarcodeClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconBarcode/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon onClick={() => {
|
||||
if (onDelete) onDelete(row.original);
|
||||
}} variant={"default"}>
|
||||
<IconTrash/>
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
)
|
||||
} as MRT_TableOptions<ProductSchema>}
|
||||
restProps={
|
||||
{
|
||||
enableColumnActions: false,
|
||||
enableRowActions: true,
|
||||
renderRowActions: ({ row }) => (
|
||||
<Flex gap="md">
|
||||
<Tooltip label="Печать штрихкода">
|
||||
<ActionIcon
|
||||
onClick={() =>
|
||||
onPrintBarcodeClick(row.original)
|
||||
}
|
||||
variant={"default"}>
|
||||
<IconBarcode />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Редактировать">
|
||||
<ActionIcon
|
||||
onClick={() => onEditClick(row.original)}
|
||||
variant={"default"}>
|
||||
<IconEdit />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Удалить">
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
if (onDelete) onDelete(row.original);
|
||||
}}
|
||||
variant={"default"}>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
),
|
||||
} as MRT_TableOptions<ProductSchema>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductsTable;
|
||||
export default ProductsTable;
|
||||
|
||||
@@ -1,73 +1,76 @@
|
||||
import {useMemo} from "react";
|
||||
import {MRT_ColumnDef} from "mantine-react-table";
|
||||
import {ProductSchema} from "../../../../client";
|
||||
import {List, Spoiler, useMantineTheme} from "@mantine/core";
|
||||
import { useMemo } from "react";
|
||||
import { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { ProductSchema } from "../../../../client";
|
||||
import { List, Spoiler, useMantineTheme } from "@mantine/core";
|
||||
|
||||
export const useProductsTableColumns = () => {
|
||||
const theme = useMantineTheme();
|
||||
return useMemo<MRT_ColumnDef<ProductSchema>[]>(() => [
|
||||
{
|
||||
accessorKey: "article",
|
||||
header: "Артикул",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "barcodes",
|
||||
header: "Штрихкоды",
|
||||
Cell: ({cell}) => {
|
||||
return (
|
||||
<List size={"sm"}>
|
||||
<Spoiler maxHeight={parseFloat(theme.lineHeights.sm) * 25}
|
||||
showLabel={"Показать все"}
|
||||
hideLabel={"Скрыть"}>
|
||||
{cell.getValue<string[]>().map(barcode => (
|
||||
<List.Item key={barcode}>
|
||||
{barcode}
|
||||
</List.Item>
|
||||
))}
|
||||
</Spoiler>
|
||||
|
||||
</List>
|
||||
)
|
||||
return useMemo<MRT_ColumnDef<ProductSchema>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: "article",
|
||||
header: "Артикул",
|
||||
enableSorting: false,
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "barcodeTemplate.name",
|
||||
header: "Шаблон штрихкода",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "brand",
|
||||
header: "Бренд",
|
||||
enableSorting: false
|
||||
},
|
||||
{
|
||||
accessorKey: "composition",
|
||||
header: "Состав",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "color",
|
||||
header: "Цвет",
|
||||
enableSorting: false
|
||||
},
|
||||
{
|
||||
|
||||
accessorKey: "size",
|
||||
header: "Размер",
|
||||
enableSorting: false
|
||||
},
|
||||
{
|
||||
accessorKey: "additionalInfo",
|
||||
header: "Доп. информация",
|
||||
enableSorting: false
|
||||
}
|
||||
], []);
|
||||
|
||||
}
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Название",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "barcodes",
|
||||
header: "Штрихкоды",
|
||||
Cell: ({ cell }) => {
|
||||
return (
|
||||
<List size={"sm"}>
|
||||
<Spoiler
|
||||
maxHeight={
|
||||
parseFloat(theme.lineHeights.sm) * 25
|
||||
}
|
||||
showLabel={"Показать все"}
|
||||
hideLabel={"Скрыть"}>
|
||||
{cell.getValue<string[]>().map(barcode => (
|
||||
<List.Item key={barcode}>
|
||||
{barcode}
|
||||
</List.Item>
|
||||
))}
|
||||
</Spoiler>
|
||||
</List>
|
||||
);
|
||||
},
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "barcodeTemplate.name",
|
||||
header: "Шаблон штрихкода",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "brand",
|
||||
header: "Бренд",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "composition",
|
||||
header: "Состав",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "color",
|
||||
header: "Цвет",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "size",
|
||||
header: "Размер",
|
||||
enableSorting: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "additionalInfo",
|
||||
header: "Доп. информация",
|
||||
enableSorting: false,
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {ProductService} from "../../../client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ProductService } from "../../../client";
|
||||
|
||||
type Props = {
|
||||
clientId: number,
|
||||
page?: number,
|
||||
itemsPerPage?: number,
|
||||
searchInput: string
|
||||
}
|
||||
clientId: number;
|
||||
page?: number;
|
||||
itemsPerPage?: number;
|
||||
searchInput: string;
|
||||
};
|
||||
const useProductsList = (props: Props) => {
|
||||
const {clientId, page, itemsPerPage, searchInput} = props;
|
||||
const {data, refetch, isLoading} = useQuery({
|
||||
queryKey: ['getAllServices', clientId, page, itemsPerPage, searchInput],
|
||||
queryFn: () => ProductService.getProductsByClientId(props)
|
||||
const { clientId, page, itemsPerPage, searchInput } = props;
|
||||
const { data, refetch, isLoading } = useQuery({
|
||||
queryKey: ["getAllServices", clientId, page, itemsPerPage, searchInput],
|
||||
queryFn: () => ProductService.getProductsByClientId(props),
|
||||
});
|
||||
const products = !data ? [] : data.products;
|
||||
const paginationInfo = data?.paginationInfo;
|
||||
return {products, paginationInfo, refetch, isLoading}
|
||||
}
|
||||
export default useProductsList;
|
||||
return { products, paginationInfo, refetch, isLoading };
|
||||
};
|
||||
export default useProductsList;
|
||||
|
||||
@@ -1 +1 @@
|
||||
export {ProductsPage} from './ui/ProductsPage';
|
||||
export { ProductsPage } from "./ui/ProductsPage";
|
||||
|
||||
@@ -1,138 +1,174 @@
|
||||
import {ContextModalProps} from "@mantine/modals";
|
||||
import {Button, Fieldset, Flex, rem, TagsInput, TextInput} from "@mantine/core";
|
||||
import {useForm} from "@mantine/form";
|
||||
import {BaseProduct, CreateProductRequest} from "../../types.ts";
|
||||
import {ProductSchema} from "../../../../client";
|
||||
import { ContextModalProps } from "@mantine/modals";
|
||||
import {
|
||||
Button,
|
||||
Fieldset,
|
||||
Flex,
|
||||
rem,
|
||||
TagsInput,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { BaseProduct, CreateProductRequest } from "../../types.ts";
|
||||
import { ProductSchema } from "../../../../client";
|
||||
import BarcodeTemplateSelect from "../../../../components/Selects/BarcodeTemplateSelect/BarcodeTemplateSelect.tsx";
|
||||
import ImageDropzone from "../../../../components/ImageDropzone/ImageDropzone.tsx";
|
||||
import {BaseFormInputProps} from "../../../../types/utils.ts";
|
||||
import { BaseFormInputProps } from "../../../../types/utils.ts";
|
||||
|
||||
type CreateProps = {
|
||||
clientId: number;
|
||||
onCreate: (values: CreateProductRequest) => void
|
||||
}
|
||||
onCreate: (values: CreateProductRequest) => void;
|
||||
};
|
||||
|
||||
type EditProps = {
|
||||
product: ProductSchema,
|
||||
onChange: (values: ProductSchema) => void
|
||||
}
|
||||
product: ProductSchema;
|
||||
onChange: (values: ProductSchema) => void;
|
||||
};
|
||||
type Props = CreateProps | EditProps;
|
||||
|
||||
|
||||
const CreateProductModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditProps = 'product' in innerProps;
|
||||
const isCreatingProps = 'clientId' in innerProps;
|
||||
const initialValues: Omit<ProductSchema, 'id'> = isEditProps ? innerProps.product : {
|
||||
name: '',
|
||||
article: '',
|
||||
barcodes: [],
|
||||
clientId: innerProps.clientId
|
||||
};
|
||||
const form = useForm<Omit<ProductSchema, 'id'>>({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<Props>) => {
|
||||
const isEditProps = "product" in innerProps;
|
||||
const isCreatingProps = "clientId" in innerProps;
|
||||
const initialValues: Omit<ProductSchema, "id"> = isEditProps
|
||||
? innerProps.product
|
||||
: {
|
||||
name: "",
|
||||
article: "",
|
||||
barcodes: [],
|
||||
clientId: innerProps.clientId,
|
||||
};
|
||||
const form = useForm<Omit<ProductSchema, "id">>({
|
||||
initialValues: initialValues,
|
||||
validate: {
|
||||
name: (name) => name.trim() !== '' ? null : "Необходимо ввести название товара",
|
||||
name: name =>
|
||||
name.trim() !== "" ? null : "Необходимо ввести название товара",
|
||||
// article: (article) => article.trim() !== '' ? null : "Необходимо ввести артикул",
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
const onCancelClick = () => {
|
||||
context.closeContextModal(id);
|
||||
}
|
||||
};
|
||||
const onSubmit = (values: BaseProduct) => {
|
||||
if (isEditProps) innerProps.onChange({...innerProps.product, ...values})
|
||||
if (isEditProps)
|
||||
innerProps.onChange({ ...innerProps.product, ...values });
|
||||
if (isCreatingProps) {
|
||||
innerProps.onCreate({...values, clientId: innerProps.clientId});
|
||||
innerProps.onCreate({ ...values, clientId: innerProps.clientId });
|
||||
form.reset();
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={form.onSubmit((values) => onSubmit(values))}>
|
||||
<Flex gap={rem(10)} direction={"column"}>
|
||||
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
|
||||
<Flex
|
||||
gap={rem(10)}
|
||||
direction={"column"}>
|
||||
<Fieldset legend={"Основные характеристики"}>
|
||||
<TextInput
|
||||
placeholder={"Введите название товара"}
|
||||
label={"Название товара"}
|
||||
{...form.getInputProps('name')}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите артикул"}
|
||||
label={"Артикул"}
|
||||
{...form.getInputProps('article')}
|
||||
{...form.getInputProps("article")}
|
||||
/>
|
||||
<TagsInput
|
||||
placeholder={!form.values.barcodes.length ? "Добавьте штрихкоды к товару" : ""}
|
||||
placeholder={
|
||||
!form.values.barcodes.length
|
||||
? "Добавьте штрихкоды к товару"
|
||||
: ""
|
||||
}
|
||||
label={"Штрихкоды"}
|
||||
{...form.getInputProps('barcodes')}
|
||||
{...form.getInputProps("barcodes")}
|
||||
/>
|
||||
<BarcodeTemplateSelect
|
||||
placeholder={"Выберите шаблон штрихкода"}
|
||||
label={"Шаблон штрихкода"}
|
||||
{...form.getInputProps('barcodeTemplate')}
|
||||
{...form.getInputProps("barcodeTemplate")}
|
||||
/>
|
||||
</Fieldset>
|
||||
<Fieldset legend={"Дополнительные характеристики"}>
|
||||
<TextInput
|
||||
placeholder={"Введите бренд"}
|
||||
label={"Бренд"}
|
||||
{...form.getInputProps('brand')}
|
||||
{...form.getInputProps("brand")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите состав"}
|
||||
label={"Состав"}
|
||||
{...form.getInputProps('composition')}
|
||||
{...form.getInputProps("composition")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите цвет"}
|
||||
label={"Цвет"}
|
||||
{...form.getInputProps('color')}
|
||||
{...form.getInputProps("color")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите размер"}
|
||||
label={"Размер"}
|
||||
{...form.getInputProps('size')}
|
||||
{...form.getInputProps("size")}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={"Введите доп. информацию"}
|
||||
label={"Доп. информация"}
|
||||
{...form.getInputProps('additionalInfo')} />
|
||||
{...form.getInputProps("additionalInfo")}
|
||||
/>
|
||||
</Fieldset>
|
||||
|
||||
{isEditProps &&
|
||||
// <Fieldset legend={"Изображение"}>
|
||||
<ImageDropzone
|
||||
imageUrlInputProps={form.getInputProps('imageUrl') as BaseFormInputProps<string>}
|
||||
productId={innerProps.product.id}
|
||||
/>
|
||||
{
|
||||
isEditProps && (
|
||||
// <Fieldset legend={"Изображение"}>
|
||||
<ImageDropzone
|
||||
imageUrlInputProps={
|
||||
form.getInputProps(
|
||||
"imageUrl"
|
||||
) as BaseFormInputProps<string>
|
||||
}
|
||||
productId={innerProps.product.id}
|
||||
/>
|
||||
)
|
||||
// </Fieldset>
|
||||
}
|
||||
<Flex justify={"flex-end"} mt={rem(5)} gap={rem(10)}>
|
||||
<Button onClick={() => onCancelClick()} variant={"subtle"}>Отменить</Button>
|
||||
{isEditProps ?
|
||||
<Flex
|
||||
justify={"flex-end"}
|
||||
mt={rem(5)}
|
||||
gap={rem(10)}>
|
||||
<Button
|
||||
onClick={() => onCancelClick()}
|
||||
variant={"subtle"}>
|
||||
Отменить
|
||||
</Button>
|
||||
{isEditProps ? (
|
||||
<Button
|
||||
onClick={() => context.closeContextModal(id)}
|
||||
type={"submit"}
|
||||
variant={"default"}
|
||||
>Сохранить и закрыть</Button> :
|
||||
variant={"default"}>
|
||||
Сохранить и закрыть
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => context.closeContextModal(id)}
|
||||
onClick={() =>
|
||||
context.closeContextModal(id)
|
||||
}
|
||||
type={"submit"}
|
||||
variant={"default"}
|
||||
>Создать и закрыть</Button>
|
||||
variant={"default"}>
|
||||
Создать и закрыть
|
||||
</Button>
|
||||
{/*<Button*/}
|
||||
{/* type={"submit"}*/}
|
||||
{/* variant={"default"}*/}
|
||||
{/*>Создать</Button>*/}
|
||||
</>}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
export default CreateProductModal;
|
||||
export default CreateProductModal;
|
||||
|
||||
@@ -2,5 +2,5 @@ export type BaseProduct = {
|
||||
name: string;
|
||||
article?: string | null;
|
||||
barcodes: string[];
|
||||
}
|
||||
export type CreateProductRequest = BaseProduct & { clientId: number }
|
||||
};
|
||||
export type CreateProductRequest = BaseProduct & { clientId: number };
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
gap: rem(10);
|
||||
}
|
||||
|
||||
|
||||
.body-container {
|
||||
}
|
||||
|
||||
@@ -24,5 +23,4 @@
|
||||
|
||||
.table-pagination {
|
||||
align-self: flex-end;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user