feat: projects and boards

This commit is contained in:
2025-02-07 20:07:10 +04:00
parent 58d397ed0b
commit 580552bd47
185 changed files with 3352 additions and 1284 deletions

View File

@@ -0,0 +1,86 @@
import useShippingTableColumns from "../hooks/shippingTableColumns.tsx";
import { BaseTable } from "../../../../../components/BaseTable/BaseTable.tsx";
import { BoxSchema, ShippingService } from "../../../../../client";
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
import { IconEdit, IconTrash } from "@tabler/icons-react";
import { MRT_TableOptions } from "mantine-react-table";
import { modals } from "@mantine/modals";
import { useDealPageContext } from "../../../contexts/DealPageContext.tsx";
import useUpdateDeal from "../hooks/useUpdateDeal.tsx";
import { notifications } from "../../../../../shared/lib/notifications.ts";
type Props = {
items: BoxSchema[];
}
const BoxesTable = ({ items }: Props) => {
const columns = useShippingTableColumns<BoxSchema>({ isBox: true });
const { update } = useUpdateDeal();
const { selectedDeal: deal } = useDealPageContext();
const onDeleteClick = (box: BoxSchema) => {
ShippingService.deleteBox({
boxId: box.id,
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
if (ok) update();
})
.catch(err => console.log(err));
};
const onEditClick = (box: BoxSchema) => {
if (!deal) return;
modals.openContextModal({
modal: "shippingProductModal",
title: "Редактирование короба",
withCloseButton: false,
innerProps: {
deal,
updateOnSubmit: update,
isBox: true,
shippingData: {
boxId: box.id,
productId: box.product?.id,
quantity: box.quantity,
},
},
});
};
return (
<BaseTable
data={items}
columns={columns}
restProps={
{
enableSorting: false,
enableColumnActions: false,
enableRowActions: true,
positionActionsColumn: "last",
renderRowActions: ({ row }) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon
onClick={() => onDeleteClick(row.original)}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
<Tooltip label="Редактировать">
<ActionIcon
onClick={() => onEditClick(row.original)}
variant={"default"}>
<IconEdit />
</ActionIcon>
</Tooltip>
</Flex>
),
} as MRT_TableOptions<BoxSchema>
}
/>
);
};
export default BoxesTable;

View File

@@ -0,0 +1,40 @@
import { ProductSchema } from "../../../../../client";
import { FC, useState } from "react";
import ObjectSelect, { ObjectSelectProps } from "../../../../../components/ObjectSelect/ObjectSelect.tsx";
import getRenderOptions from "../../../../../components/ProductSelect/utils/getRenderOptions.tsx";
type Props = Omit<ObjectSelectProps<ProductSchema>, "searchValue" | "onSearchChange">;
const ShippingProductSelect: FC<Props> = (props: Props) => {
const [search, setSearch] = useState("");
const getFilteredData = () => {
const searchValue: string = search.toLowerCase();
const data: ProductSchema[] = props.data.filter(product => {
return (
product.name.toLowerCase().includes(searchValue) ||
product.article?.toLowerCase().includes(searchValue) ||
product.barcodes && product.barcodes[0].toLowerCase().includes(searchValue)
);
});
return data;
};
return (
<ObjectSelect
label={"Товар"}
placeholder={"Выберите товар"}
searchable
{...props}
data={getFilteredData()}
searchValue={search}
onSearchChange={setSearch}
renderOption={getRenderOptions(props.data)}
filter={({ options }) => options}
/>
);
};
export default ShippingProductSelect;

View File

@@ -0,0 +1,87 @@
import useShippingTableColumns from "../hooks/shippingTableColumns.tsx";
import { BaseTable } from "../../../../../components/BaseTable/BaseTable.tsx";
import { ShippingProductSchema, ShippingService } from "../../../../../client";
import { ActionIcon, Flex, Tooltip } from "@mantine/core";
import { IconEdit, IconTrash } from "@tabler/icons-react";
import { MRT_TableOptions } from "mantine-react-table";
import { notifications } from "../../../../../shared/lib/notifications.ts";
import useUpdateDeal from "../hooks/useUpdateDeal.tsx";
import { modals } from "@mantine/modals";
import { useDealPageContext } from "../../../contexts/DealPageContext.tsx";
type Props = {
items: ShippingProductSchema[];
}
const ShippingProductsTable = ({ items }: Props) => {
const columns = useShippingTableColumns<ShippingProductSchema>({ isBox: false });
const { update } = useUpdateDeal();
const { selectedDeal: deal } = useDealPageContext();
const onDeleteClick = (shippingProduct: ShippingProductSchema) => {
ShippingService.deleteShippingProduct({
shippingProductId: shippingProduct.id,
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
update();
})
.catch(err => console.log(err));
};
const onEditClick = (shippingProduct: ShippingProductSchema) => {
if (!deal) return;
modals.openContextModal({
modal: "shippingProductModal",
title: "Редактирование товара на паллете",
withCloseButton: false,
innerProps: {
deal,
updateOnSubmit: update,
isBox: false,
shippingData: {
shippingProductId: shippingProduct.id,
productId: shippingProduct.product.id,
quantity: shippingProduct.quantity,
},
},
});
};
return (
<BaseTable
data={items}
columns={columns}
restProps={
{
enableSorting: false,
enableColumnActions: false,
enableRowActions: true,
enableRowNumbers: true,
positionActionsColumn: "last",
renderRowActions: ({ row }) => (
<Flex gap="md">
<Tooltip label="Удалить">
<ActionIcon
onClick={() => onDeleteClick(row.original)}
variant={"default"}>
<IconTrash />
</ActionIcon>
</Tooltip>
<Tooltip label="Редактировать">
<ActionIcon
onClick={() => onEditClick(row.original)}
variant={"default"}>
<IconEdit />
</ActionIcon>
</Tooltip>
</Flex>
),
} as MRT_TableOptions<ShippingProductSchema>
}
/>
);
};
export default ShippingProductsTable;

View File

@@ -0,0 +1,151 @@
import { useDealPageContext } from "../../../contexts/DealPageContext.tsx";
import { Accordion, ActionIcon, Button, Center, Group, rem, Stack, Title, Tooltip } from "@mantine/core";
import { BoxSchema, PalletSchema, ShippingProductSchema } from "../../../../../client";
import ShippingProductsTable from "./ShippingProductsTable.tsx";
import BoxesTable from "./BoxesTable.tsx";
import { IconBox, IconPlus, IconSpace, IconTrash } from "@tabler/icons-react";
import useShipping from "../hooks/useShipping.tsx";
const ShippingTree = () => {
const { selectedDeal: deal } = useDealPageContext();
const {
onCreateBoxInPallet,
onCreateShippingProduct,
onDeletePalletClick,
palletIds,
} = useShipping();
const sortById = (data?: PalletSchema[] | BoxSchema[] | ShippingProductSchema[]) => {
return data?.sort((a, b) => a.id - b.id);
};
const getPallets = () => {
const sortedPallets = sortById(deal?.pallets) as PalletSchema[];
const pallets = sortedPallets?.map((pallet => {
palletIds.push(pallet.id.toString());
return (
<Accordion.Item key={pallet.id} value={pallet.id.toString()}>
<Center>
<Accordion.Control icon={<IconSpace />}>
Паллет - П{pallet.id}
</Accordion.Control>
{removePalletButton(pallet.id)}
</Center>
<Accordion.Panel>
{getPalletContent(pallet)}
</Accordion.Panel>
</Accordion.Item>
);
})) ?? [];
if (deal?.boxes && deal?.boxes.length > 0) {
const boxes = deal?.boxes.sort((b1, b2) => (b1.id - b2.id));
const itemValue = "noPallets";
const boxesWithoutPallet = (
<Accordion.Item key={-1} value={itemValue}>
<Accordion.Control icon={<IconBox />}>
Короба без паллетов
</Accordion.Control>
<Accordion.Panel>
<BoxesTable items={boxes} />
</Accordion.Panel>
</Accordion.Item>
);
pallets.unshift(boxesWithoutPallet);
palletIds.push(itemValue);
}
return pallets;
};
const removePalletButton = (palletId: number) => {
return (
<Tooltip label="Удалить паллет">
<ActionIcon
variant={"default"}
onClick={() => onDeletePalletClick(palletId)}
mx={"md"}
>
<IconTrash />
</ActionIcon>
</Tooltip>
);
};
const createBoxOrShippingProductButton = (palletId: number, isBox: boolean) => {
const createButtonLabel = isBox ? "Добавить короб" : "Добавить товар";
return (
<Button
variant={"default"}
onClick={() => {
if (isBox) {
onCreateBoxInPallet(palletId);
} else {
onCreateShippingProduct(palletId);
}
}}
>
<Group gap={rem(5)}>
<IconPlus />
{createButtonLabel}
</Group>
</Button>
);
};
const getPalletContent = (pallet: PalletSchema) => {
const isEmpty = pallet.boxes.length === 0 && pallet.shippingProducts.length === 0;
const isBox = pallet.boxes.length > 0;
const title = isEmpty ? "Пустой" : isBox ? "Короба" : "Товары";
let palletButtons;
if (isEmpty) {
palletButtons = [
createBoxOrShippingProductButton(pallet.id, true),
createBoxOrShippingProductButton(pallet.id, false),
];
} else {
palletButtons = [
createBoxOrShippingProductButton(pallet.id, isBox),
];
}
const boxes = sortById(pallet.boxes) as BoxSchema[];
const shippingProducts = sortById(pallet.shippingProducts) as ShippingProductSchema[];
let table;
if (!isEmpty) {
if (isBox) {
table = (<BoxesTable items={boxes} />);
} else {
table = (<ShippingProductsTable items={shippingProducts} />);
}
}
return (
<Stack gap={rem(5)}>
<Group justify={"space-between"}>
<Title order={6}>{title}</Title>
<Group gap={rem(10)}>
{...palletButtons}
</Group>
</Group>
{table}
</Stack>
);
};
return (
<Accordion
multiple={true}
defaultValue={palletIds}
bd={"solid 1px gray"}
>
{getPallets()}
</Accordion>
);
};
export default ShippingTree;