diff --git a/src/components/Table/Columns/index.ts b/src/components/Table/Columns/index.ts new file mode 100644 index 0000000..b6ebf7d --- /dev/null +++ b/src/components/Table/Columns/index.ts @@ -0,0 +1,63 @@ +import { ReactNode } from 'react' +import { Rule } from 'antd/lib/form' +import { ColumnProps } from 'antd/lib/table' + +export { + RegExpIsFloat, + makeNumericRender, + makeNumericColumn, + makeNumericColumnOptions, + makeNumericColumnPlanFact, + makeNumericStartEnd, + makeNumericMinMax, + makeNumericAvgRange +} from './numeric' +export { makeColumnsPlanFact } from './plan_fact' +export { makeSelectColumn } from './select' +export { makeTagColumn, makeTagInput } from './tag' +export { makeFilterTextMatch, makeTextColumn } from './text' +export { + rawTimezones, + timezoneOptions, + TimezoneSelect, + makeTimezoneColumn, + makeTimezoneRenderer +} from './timezone' + +export type { TagInputProps } from './tag' + +export type DataType = Record +export type RenderMethod = (value: T, dataset?: DataType, index?: number) => ReactNode +export type SorterMethod = (a?: DataType | null, b?: DataType | null) => number + +/* +other - объект с дополнительными свойствами колонки +поддерживаются все базовые свойства из описания https://ant.design/components/table/#Column +плю дополнительные для колонок EditableTable: */ +export type columnPropsOther = ColumnProps & { + // редактируемая колонка + editable?: boolean + // react компонента редактора + input?: ReactNode + // значение может быть пустым + isRequired?: boolean + // css класс для , если требуется + formItemClass?: string + // массив правил валидации значений https://ant.design/components/form/#Rule + formItemRules?: Rule[] + // дефолтное значение при добавлении новой строки + initialValue?: string | number + + render?: RenderMethod +} + +export const makeColumn = (title: ReactNode, key: string, other?: columnPropsOther) => ({ + title: title, + key: key, + dataIndex: key, + ...other, +}) + +export const makeGroupColumn = (title: ReactNode, children: object[]) => ({ title, children }) + +export default makeColumn diff --git a/src/components/Table/Columns/numeric.tsx b/src/components/Table/Columns/numeric.tsx new file mode 100644 index 0000000..1b2ed3b --- /dev/null +++ b/src/components/Table/Columns/numeric.tsx @@ -0,0 +1,111 @@ +import { InputNumber } from 'antd' +import { ReactNode } from 'react' + +import { makeNumericSorter } from '../sorters' +import { columnPropsOther, makeGroupColumn, RenderMethod } from '.' + +export const RegExpIsFloat = /^[-+]?\d+\.?\d*$/ + +export const makeNumericRender = (fixed?: number): RenderMethod => (value) => { + let val = '-' + if ((value ?? null) !== null && Number.isFinite(+value)) { + val = (fixed ?? null) !== null + ? (+value).toFixed(fixed) + : (+value).toPrecision(5) + } + + return ( +
+ {val} +
+ ) +} + +export const makeNumericColumnOptions = (fixed?: number, sorterKey?: string): columnPropsOther => ({ + editable: true, + initialValue: 0, + width: 100, + sorter: sorterKey ? makeNumericSorter(sorterKey) : undefined, + formItemRules: [{ + required: true, + message: 'Введите число', + pattern: RegExpIsFloat, + }], + render: makeNumericRender(fixed), +}) + +export const makeNumericColumn = ( + title: ReactNode, + dataIndex: string, + filters: object[], + filterDelegate: (key: string | number) => any, + renderDelegate: (_: any, row: object) => any, + width: string, + other?: columnPropsOther +) => ({ + title: title, + dataIndex: dataIndex, + key: dataIndex, + filters: filters, + onFilter: filterDelegate ? filterDelegate(dataIndex) : null, + sorter: makeNumericSorter(dataIndex), + width: width, + input: , + render: renderDelegate ?? makeNumericRender(), + align: 'right', + ...other +}) + +export const makeNumericColumnPlanFact = ( + title: ReactNode, + dataIndex: string, + filters: object[], + filterDelegate: (key: string | number) => any, + renderDelegate: (_: any, row: object) => any, + width: string +) => makeGroupColumn(title, [ + makeNumericColumn('п', dataIndex + 'Plan', filters, filterDelegate, renderDelegate, width), + makeNumericColumn('ф', dataIndex + 'Fact', filters, filterDelegate, renderDelegate, width), +]) + +export const makeNumericStartEnd = ( + title: ReactNode, + dataIndex: string, + fixed: number, + filters: object[], + filterDelegate: (key: string | number) => any, + renderDelegate: (_: any, row: object) => any, + width: string, +) => makeGroupColumn(title, [ + makeNumericColumn('старт', dataIndex + 'Start', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Start')), + makeNumericColumn('конец', dataIndex + 'End', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'End')) +]) + +export const makeNumericMinMax = ( + title: ReactNode, + dataIndex: string, + fixed: number, + filters: object[], + filterDelegate: (key: string | number) => any, + renderDelegate: (_: any, row: object) => any, + width: string, +) => makeGroupColumn(title, [ + makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')), + makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max')), +]) + +export const makeNumericAvgRange = ( + title: ReactNode, + dataIndex: string, + fixed: number, + filters: object[], + filterDelegate: (key: string | number) => any, + renderDelegate: (_: any, row: object) => any, + width: string +) => makeGroupColumn(title, [ + makeNumericColumn('мин', dataIndex + 'Min', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Min')), + makeNumericColumn('сред', dataIndex + 'Avg', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Avg')), + makeNumericColumn('макс', dataIndex + 'Max', filters, filterDelegate, renderDelegate, width, makeNumericColumnOptions(fixed, dataIndex + 'Max')) +]) + +export default makeNumericColumn diff --git a/src/components/Table/Columns/plan_fact.tsx b/src/components/Table/Columns/plan_fact.tsx new file mode 100644 index 0000000..e890eec --- /dev/null +++ b/src/components/Table/Columns/plan_fact.tsx @@ -0,0 +1,38 @@ +import { ReactNode } from 'react' + +import { columnPropsOther, makeColumn } from '.' + +export const makeColumnsPlanFact = ( + title: string | ReactNode, + key: string | string[], + columsOther?: columnPropsOther | [columnPropsOther, columnPropsOther], + gruopOther?: any +) => { + let keyPlanLocal: string + let keyFactLocal: string + + if (key instanceof Array) { + keyPlanLocal = key[0] + keyFactLocal = key[1] + } else { + keyPlanLocal = key + 'Plan' + keyFactLocal = key + 'Fact' + } + + let columsOtherLocal : any[2] + if (columsOther instanceof Array) + columsOtherLocal = [columsOther[0], columsOther[1]] + else + columsOtherLocal = [columsOther, columsOther] + + return { + title: title, + ...gruopOther, + children: [ + makeColumn('план', keyPlanLocal, columsOtherLocal[0]), + makeColumn('факт', keyFactLocal, columsOtherLocal[1]), + ] + } +} + +export default makeColumnsPlanFact diff --git a/src/components/Table/Columns/select.tsx b/src/components/Table/Columns/select.tsx new file mode 100644 index 0000000..85c8e5a --- /dev/null +++ b/src/components/Table/Columns/select.tsx @@ -0,0 +1,22 @@ +import { Select, SelectProps } from 'antd' +import { DefaultOptionType, SelectValue } from 'antd/lib/select' + +import { columnPropsOther, makeColumn } from '.' + +export const makeSelectColumn = ( + title: string, + dataIndex: string, + options: DefaultOptionType[], + defaultValue?: T, + other?: columnPropsOther, + selectOther?: SelectProps +) => makeColumn(title, dataIndex, { + ...other, + input: + ) +}) + +export const makeTagColumn = ( + title: ReactNode, + dataIndex: string, + options: T[], + value_key: keyof DataType, + label_key: keyof DataType, + other?: columnPropsOther, + tagOther?: TagInputProps +) => { + const InputComponent = makeTagInput(value_key, label_key) + + return makeColumn(title, dataIndex, { + ...other, + render: (item?: T[]) => item?.map((elm: T) => {other?.render?.(elm) ?? elm[label_key]}) ?? '-', + input: , + }) +} + +export default makeTagColumn diff --git a/src/components/Table/Columns/text.tsx b/src/components/Table/Columns/text.tsx new file mode 100644 index 0000000..992b984 --- /dev/null +++ b/src/components/Table/Columns/text.tsx @@ -0,0 +1,27 @@ +import { ReactNode } from 'react' + +import { columnPropsOther, DataType, RenderMethod, SorterMethod } from '.' +import { makeStringSorter } from '../sorters' + +export const makeFilterTextMatch = (key: keyof DataType) => + (filterValue: T, dataItem: DataType) => dataItem[key] === filterValue + +export const makeTextColumn = ( + title: ReactNode, + dataIndex: string, + filters: object[], + sorter?: SorterMethod, + render?: RenderMethod, + other?: columnPropsOther +) => ({ + title: title, + dataIndex: dataIndex, + key: dataIndex, + filters: filters, + onFilter: filters ? makeFilterTextMatch(dataIndex) : null, + sorter: sorter ?? makeStringSorter(dataIndex), + render: render, + ...other +}) + +export default makeTextColumn diff --git a/src/components/Table/Columns/timezone.tsx b/src/components/Table/Columns/timezone.tsx new file mode 100644 index 0000000..d4a9c4b --- /dev/null +++ b/src/components/Table/Columns/timezone.tsx @@ -0,0 +1,72 @@ +import { memo, ReactNode, useEffect, useState } from 'react' +import { Select, SelectProps } from 'antd' + +import { SimpleTimezoneDto } from '@api' + +import { columnPropsOther, makeColumn } from '.' + +export const rawTimezones = { + 'Калининград': 2, + 'Москва': 3, + 'Самара': 4, + 'Екатеринбург': 5, + 'Омск': 6, + 'Красноярск': 7, + 'Новосибирск': 7, + 'Иркутск': 8, + 'Чита': 9, + 'Владивосток': 10, + 'Магадан': 11, + 'Южно-Сахалинск': 11, + 'Среднеколымск': 11, + 'Анадырь': 12, + 'Петропавловск-Камчатский': 12, + } + +export const timezoneOptions = Object + .entries(rawTimezones) + .sort((a, b) => a[1] - b[1]) + .map(([id, hours]) => ({ + label: `UTC${hours > 0 ? '+':''}${('0' + hours).slice(-2)} :: ${id}`, + value: id, + })) + +export const TimezoneSelect = memo(({ onChange, ...other }) => { + const [id, setId] = useState(null) + + useEffect(() => onChange?.({ + timezoneId: id, + hours: id ? rawTimezones[id] : 0, + isOverride: false, + }, []), [id, onChange]) + + return (, - render: (value) => { - const item = options?.find(option => option?.value === value) - return other?.render?.(item?.value) ?? item?.label ?? defaultValue ?? value ?? '--' - } -}) - -const makeTagInput = >(value_key: string, label_key: string) => memo<{ - options: T[], - value?: T[], - onChange?: (values: T[]) => void, - other?: SelectProps, -}>(({ options, value, onChange, other }) => { - const [selectOptions, setSelectOptions] = useState([]) - const [selectedValue, setSelectedValue] = useState([]) - - useEffect(() => { - setSelectOptions(options.map((elm) => ({ - value: String(elm[value_key]), - label: elm[label_key], - }))) - }, [options]) - - useEffect(() => { - setSelectedValue(value?.map((elm) => String(elm[value_key])) ?? []) - }, [value]) - - const onSelectChange = (rawValues?: SelectValue) => { - let values: any[] = [] - if (typeof rawValues === 'string') - values = rawValues.split(',') - else if (Array.isArray(rawValues)) - values = rawValues - - const objectValues: T[] = values.reduce((out: T[], value: string) => { - const res = options.find((option) => String(option[value_key]) === String(value)) - if (res) out.push(res) - return out - }, []) - - onChange?.(objectValues) - } - - return ( - ) -}) - -export const makeTimezoneRenderer = () => (timezone?: SimpleTimezoneDto) => { - if (!timezone) return 'UTC~?? :: Неизвестно' - const { hours, timezoneId } = timezone - return `UTC${hours && hours > 0 ? '+':''}${hours ? ('0' + hours).slice(-2) : '~??'} :: ${timezoneId ?? 'Неизвестно'}` -} - -export const makeTimezoneColumn = ( - title: string = 'Зона', - key: string = 'timezone', - defaultValue: any = null, - allowClear: boolean = true, - other?: columnPropsOther -) => makeColumn(title, key, { - width: 100, - editable: true, - render: makeTimezoneRenderer(), - input: ( - - ), - ...other + ...other, + pageSize: сontainer.take, + total: сontainer.count ?? сontainer.items?.length ?? 0, + current: 1 + Math.floor((сontainer.skip ?? 0) / (сontainer.take ?? 1)) }) diff --git a/src/components/Table/sorters.ts b/src/components/Table/sorters.ts index 5e60a3b..bbccbbd 100644 --- a/src/components/Table/sorters.ts +++ b/src/components/Table/sorters.ts @@ -1,20 +1,25 @@ -import { RawDate } from "@asb/utils" +import { isRawDate } from '@utils' -export const makeNumericSorter = (key: string) => (a: Record, b: Record) => Number(a[key]) - Number(b[key]) +import { DataType } from './Columns' -export const makeStringSorter = (key: string) => (a: Record | null | undefined, b: Record | null | undefined) => { +export const makeNumericSorter = (key: keyof DataType) => + (a: DataType, b: DataType) => Number(a[key]) - Number(b[key]) + +export const makeStringSorter = (key: keyof DataType) => (a?: DataType | null, b?: DataType | null) => { if (!a && !b) return 0 if (!a) return 1 if (!b) return -1 - return ('' + a[key]).localeCompare(b[key]) + return String(a[key]).localeCompare(String(b[key])) } -export const makeDateSorter = (key: string) => (a: Record, b: Record) => { - const date = new Date(a[key]) - - if (Number.isNaN(date.getTime())) +export const makeDateSorter = (key: keyof DataType) => (a: DataType, b: DataType) => { + const adate = a[key] + const bdate = b[key] + if (!isRawDate(adate) || !isRawDate(bdate)) throw new Error('Date column contains not date formatted string(s)') - return date.getTime() - new Date(b[key]).getTime() + const date = new Date(adate) + + return date.getTime() - new Date(bdate).getTime() }