feat: modules, products, services, services kits

This commit is contained in:
2025-09-16 10:56:10 +04:00
parent f2746b8b65
commit 553e76d610
92 changed files with 8404 additions and 103 deletions

View File

@ -0,0 +1,108 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { groupBy, omit } from "lodash";
import { MultiSelect, MultiSelectProps } from "@mantine/core";
interface ObjectWithIdAndName {
id: number;
name: string;
}
export type MultiselectObjectType<T> = T;
type ControlledValueProps<T> = {
value: MultiselectObjectType<T>[];
onChange: (value: MultiselectObjectType<T>[]) => void;
};
type CustomLabelAndKeyProps<T> = {
getLabelFn: (item: MultiselectObjectType<T>) => string;
getValueFn: (item: MultiselectObjectType<T>) => string;
};
type RestProps<T> = {
defaultValue?: MultiselectObjectType<T>[];
onChange: (value: MultiselectObjectType<T>[]) => void;
data: MultiselectObjectType<T>[];
groupBy?: (item: MultiselectObjectType<T>) => string;
filterBy?: (item: MultiselectObjectType<T>) => boolean;
};
const defaultGetLabelFn = <T extends { name: string }>(item: T): string => {
return item.name;
};
const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
return item.id.toString();
};
export type ObjectMultiSelectProps<T> = (RestProps<T> &
Partial<ControlledValueProps<T>>) &
Omit<MultiSelectProps, "value" | "onChange" | "data"> &
(T extends ObjectWithIdAndName
? Partial<CustomLabelAndKeyProps<T>>
: CustomLabelAndKeyProps<T>);
const ObjectMultiSelect = <T,>(props: ObjectMultiSelectProps<T>) => {
const isControlled = "value" in props;
const haveGetValueFn = "getValueFn" in props;
const haveGetLabelFn = "getLabelFn" in props;
const [internalValue, setInternalValue] = useState<
MultiselectObjectType<T>[] | undefined
>(props.defaultValue);
const value = (isControlled ? props.value : internalValue) || [];
const getValueFn =
(haveGetValueFn && props.getValueFn) || defaultGetValueFn;
const getLabelFn =
(haveGetLabelFn && props.getLabelFn) || defaultGetLabelFn;
const data = useMemo(() => {
const propsData = props.filterBy
? props.data.filter(props.filterBy)
: props.data;
if (props.groupBy) {
const groupedData = groupBy(propsData, props.groupBy);
return Object.entries(groupedData).map(([group, items]) => ({
group,
items: items.map(item => ({
label: getLabelFn(item),
value: getValueFn(item),
})),
}));
}
return propsData.map(item => ({
label: getLabelFn(item),
value: getValueFn(item),
}));
}, [props.data, props.groupBy]);
const handleOnChange = (event: string[]) => {
const objects = props.data.filter(item =>
event.includes(getValueFn(item))
);
if (isControlled) {
props.onChange(objects);
return;
}
setInternalValue(objects);
};
useEffect(() => {
if (isControlled || !internalValue) return;
props.onChange(internalValue);
}, [internalValue]);
const restProps = omit(props, "getValueFn", "getLabelFn", "filterBy");
return (
<MultiSelect
{...restProps}
value={value.map(item => getValueFn(item))}
onChange={handleOnChange}
data={data}
/>
);
};
export default ObjectMultiSelect;

View File

@ -1,3 +1,5 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { groupBy, omit } from "lodash";
import { Select, SelectProps } from "@mantine/core";
@ -33,6 +35,7 @@ const defaultGetValueFn = <T extends { id: number }>(item: T): string => {
if (!item) return item;
return item.id.toString();
};
export type ObjectSelectProps<T> = (RestProps<T> &
Partial<ControlledValueProps<T>>) &
Omit<SelectProps, "value" | "onChange" | "data"> &

View File

@ -4,11 +4,10 @@ import { DataTable, DataTableProps } from "mantine-datatable";
function BaseTable<T>(props: DataTableProps<T>) {
return (
<DataTable
withTableBorder={false}
withTableBorder
withRowBorders
striped={false}
verticalAlign={"center"}
borderRadius={"lg"}
backgroundColor={"transparent"}
{...props}
/>

View File

@ -0,0 +1,119 @@
import { FC } from "react";
import { IconPhoto, IconUpload, IconX } from "@tabler/icons-react";
import { omit } from "lodash";
import {
Button,
Fieldset,
Flex,
Group,
Image,
Loader,
rem,
Text,
} from "@mantine/core";
import { Dropzone, DropzoneProps, FileWithPath } from "@mantine/dropzone";
import UseImageDropzone from "./types";
interface RestProps {
imageDropzone: UseImageDropzone;
onDrop: (files: FileWithPath[]) => void;
}
type Props = Omit<DropzoneProps, "onDrop"> & RestProps;
const ImageDropzone: FC<Props> = (props: Props) => {
const { showDropzone, setShowDropzone, isLoading, imageUrlInputProps } =
props.imageDropzone;
const restProps = omit(props, ["imageDropzone"]);
const getDropzone = () => (
<Dropzone
{...restProps}
accept={[
"image/png",
"image/jpeg",
"image/gif",
"image/bmp",
"image/tiff",
"image/x-icon",
"image/webp",
"image/svg+xml",
"image/heic",
]}
multiple={false}
onDrop={props.onDrop}>
<Group
justify="center"
gap="xl"
style={{ pointerEvents: "none" }}>
<Dropzone.Accept>
<IconUpload
style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-blue-6)",
}}
stroke={1.5}
/>
</Dropzone.Accept>
<Dropzone.Reject>
<IconX
style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-red-6)",
}}
stroke={1.5}
/>
</Dropzone.Reject>
<Dropzone.Idle>
<IconPhoto
style={{
width: rem(52),
height: rem(52),
color: "var(--mantine-color-dimmed)",
}}
stroke={1.5}
/>
</Dropzone.Idle>
<div style={{ textAlign: "center" }}>
<Text
size="xl"
inline>
Перенесите изображение или нажмите чтоб выбрать файл
</Text>
</div>
</Group>
</Dropzone>
);
const getBody = () => {
if (imageUrlInputProps?.value && !showDropzone) {
return <Image src={imageUrlInputProps.value} />;
}
return getDropzone();
};
return (
<Flex
gap={rem(10)}
direction={"column"}>
<Fieldset legend={"Изображение"}>
<Flex justify={"center"}>
{isLoading ? <Loader /> : getBody()}
</Flex>
</Fieldset>
{!showDropzone && (
<Button
onClick={() => setShowDropzone(true)}
variant={"default"}>
Заменить изображение {}
</Button>
)}
</Flex>
);
};
export default ImageDropzone;

View File

@ -0,0 +1,12 @@
import { Dispatch, SetStateAction } from "react";
import BaseFormInputProps from "@/utils/baseFormInputProps";
type UseImageDropzone = {
showDropzone: boolean;
setShowDropzone: Dispatch<SetStateAction<boolean>>;
isLoading: boolean;
setIsLoading: Dispatch<SetStateAction<boolean>>;
imageUrlInputProps?: BaseFormInputProps<string>;
};
export default UseImageDropzone;