feat: prettier

This commit is contained in:
2024-09-27 04:47:04 +03:00
parent c5f839d9ef
commit de4fe450ab
253 changed files with 11322 additions and 10004 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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")}`,
},
],
[]
);
};

View File

@@ -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;

View File

@@ -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),
},
],
[]
);
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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: "Ключ",
},
],
[]
);
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 />,
},
],
[]
);
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -9,4 +9,4 @@
padding: rem(5);
gap: rem(10);
display: flex;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 />,
},
],
[]
);
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -9,4 +9,4 @@
padding: rem(5);
gap: rem(10);
display: flex;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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: "Шаблон штрихкодов",
},
],
[]
);
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1 +1 @@
export {DealPage} from './ui/DealPage'
export { DealPage } from "./ui/DealPage";

View File

@@ -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>
)
}
);
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1 +1 @@
export {DealsPage} from './ui/DealsPage.tsx';
export { DealsPage } from "./ui/DealsPage.tsx";

View File

@@ -5,7 +5,6 @@
gap: rem(10);
}
.body-container {
}
@@ -23,5 +22,4 @@
.table-pagination {
align-self: flex-end;
}
}

View File

@@ -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>
</>
)
}
);
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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}</>,
},
],
[]
);
};

View File

@@ -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;

View File

@@ -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>
),
},
],
[]
);
};

View File

@@ -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;

View File

@@ -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: "Должность",
},
],
[]
);
};

View File

@@ -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;
}
};

View File

@@ -7,4 +7,4 @@
@mixin dark {
background-color: var(--mantine-color-dark-5);
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,
});

View File

@@ -1 +1 @@
export {LeadsPage} from './ui/LeadsPage.tsx';
export { LeadsPage } from "./ui/LeadsPage.tsx";

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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%;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -6,7 +6,6 @@
}
.search-input {
}
.boards {
@@ -21,7 +20,6 @@
}
.delete {
@mixin light {
border-color: var(--mantine-color-gray-1);
}

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -1,3 +1,3 @@
.container {
background-color: rebeccapurple;
}
background-color: rebeccapurple;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1 +1 @@
export {MarketplacesPage} from './ui/MarketplacesPage.tsx';
export { MarketplacesPage } from "./ui/MarketplacesPage.tsx";

View File

@@ -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;

View File

@@ -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;

View File

@@ -9,7 +9,6 @@
padding: rem(5);
gap: rem(10);
display: flex;
}
.top-panel-last-item {

View File

@@ -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>
)
}
);
};

View File

@@ -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;

View File

@@ -5,5 +5,5 @@
}
.pagination {
align-self: flex-end;
}
align-self: flex-end;
}

View File

@@ -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;

View File

@@ -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,
},
],
[]
);
};

View File

@@ -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;

View File

@@ -1 +1 @@
export {ProductsPage} from './ui/ProductsPage';
export { ProductsPage } from "./ui/ProductsPage";

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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