Загрузка данных


import {DataGrid, GridColDef, GridColumnGroupingModel, GridRenderCellParams} from "@mui/x-data-grid";
import React, {useEffect, useState, useRef, useLayoutEffect } from "react";
import { Tooltip, Checkbox } from "@mui/material";

export interface HeadCell<T> {
  id: keyof T;            // Ключ данных
  label: string;          // Название столбца
  groupName?: string;     // Группа
  tooltipField?: keyof T; // Ключ для подскадки
  hidden?: boolean;       // Скрыть столбец
}

interface CustomTableProps<T> {
  onRowClick?: (row: T) => void;                            // событие одного клика по строке
  onRowDoubleClick?: (row: T) => void;                      // Событие двойного клика
  onRowContextMenu?: (row: T, e: React.MouseEvent) => void; // Вызов контекстного меню
  editable?: boolean;                                       // включает режим редактирования
  initialEmptyRows?: number;                                // сколько пустых строк создать при старте
  onAddRowRequest?: () => void;                             // Создание новой строки
  onSaveAllRequest?: () => void;                            // сохранить все строки в базу
  onDeleteRowRequest?: (row: T) => void;                    // Удалить строку
  onRowUpdate?: (row: T) => void;                           // Событие редактирования
}

interface OverflowTooltipProps {
  title: string;
  children: React.ReactElement;
  value: string;
}

const OverflowTooltip = ({ title, children, value }: OverflowTooltipProps) => {
  const [isOverflowed, setIsOverflowed] = useState(false);
  const textRef = useRef<HTMLDivElement>(null);
  const visibleColumns = columns.filter(col => !col.hidden);

  useLayoutEffect(() => {
    const element = textRef.current;
    if (!element) return;

    // Создаем наблюдатель за размером
    const resizeObserver = new ResizeObserver(() => {
      const hasOverflow = element.scrollWidth > element.clientWidth;
      setIsOverflowed(hasOverflow);
    });

    resizeObserver.observe(element);

    return () => {
      resizeObserver.disconnect(); // Чистим при размонтировании
    };
  }, [value]); // Переподключаем если сменилось значение, чтобы сбросить стейт

  // Приоритет:
  // 1. title из tooltipField.
  // 2. Если нет title и текст не влезает показываем значение ячейки.
  const finalTooltipText = title || (isOverflowed ? value : "");

  return (
    <Tooltip
      title={finalTooltipText}
      disableHoverListener={!finalTooltipText}
      arrow
      enterDelay={100}
    >
      {/* Оборачиваем в div, чтобы реф всегда был на контейнере, который мы меряем */}
      <div
        ref={textRef}
        style={{
          overflow: 'hidden',
          textOverflow: 'ellipsis',
          whiteSpace: 'nowrap',
          width: '100%'
        }}
      >
        {children}
      </div>
    </Tooltip>
  );
};


export default function CustomTable<T extends { row_id?: number | string }>({
  columns,
  rows: initialRows = [],
  onRowClick,
  onRowDoubleClick,
  onRowContextMenu,
  editable = false,
  onAddRowRequest,
  onSaveAllRequest,
  onDeleteRowRequest,
  onRowUpdate,
}: CustomTableProps<T>) {
  const [rows, setRows] = useState<T[]>([]);

  useEffect(() => {
    setRows(initialRows);
  }, [initialRows]);

  const handleKeyDown = (params: any, event: any) => {
    if (!editable || !onAddRowRequest) return;

    const isLastRow = params.id === rows.length - 1;

    if (isLastRow && (event.key === "Enter" || event.key === "ArrowDown")) {
      event.preventDefault();
      onAddRowRequest();
    }
  };

  const [selectedId, setSelectedId] = useState<number | string | null>(null); //выбор первой строки
  const gridColumns: GridColDef[] = columns.map((col) => {
    const isBoolean = typeof (rows[0]?.[col.id] ?? "") === "boolean";

    return {
      field: col.id as string,
      headerName: col.label,
      flex: 1,
      minWidth: 100,
      editable: editable, // включаем редактирование
      renderEditCell: isBoolean
        ? (params: GridRenderEditCellParams) => (
            <Checkbox
              checked={Boolean(params.value)}
              onChange={(e) => params.api.setEditCellValue({
                id: params.id,
                field: params.field,
                value: e.target.checked,
              })}
              autoFocus
            />
          )
        : undefined,
      renderCell: (params: GridRenderCellParams) => {
        const value = params.value;
        const tooltipTextFromField = col.tooltipField ? String(params.row[col.tooltipField] ?? '') : "";
        const cellValueString = String(value ?? '');

        // Если это булево, оставляем старую логику без авто-тултипа
        if (typeof value === 'boolean') {
          const boolContent = (
            <div style={{display: 'flex', justifyContent: 'center', width: '100%'}}>
              {value ? <span style={{color: '#2e7d32', fontWeight: 'bold', fontSize: '20px'}}>✓</span> : null}
            </div>
          );
          return tooltipTextFromField ?
            <Tooltip title={tooltipTextFromField} arrow>{boolContent}</Tooltip> : boolContent;
        }
        if (isBoolean) {
          return (
            <Checkbox
              checked={Boolean(params.value)}
              disabled
            />
          );
        }

        return (
          <OverflowTooltip title={tooltipTextFromField} value={cellValueString}>
            <span>{cellValueString}</span>
          </OverflowTooltip>
        );

        return params.value;
      },
    };
  });

  // Создание групп
  const groupingModel: GridColumnGroupingModel = [];

  const groupsMap = new Map<string, string[]>();

  columns.forEach((col) => {
    if (col.groupName) {
      const existing = groupsMap.get(col.groupName) || [];
      groupsMap.set(col.groupName, [...existing, col.id as string]);
    }
  });

  groupsMap.forEach((childrenFields, groupTitle) => {
    groupingModel.push({
      groupId: groupTitle,
      headerName: groupTitle, // Название группы в шапке
      headerAlign: 'center',
      children: childrenFields.map(field => ({field})),
    });
  });

  // Выбор первой строки
  useEffect(() => {
    if (rows.length > 0) {
      const firstId = rows[0].row_id ?? 0;
      setSelectedId(firstId);
      onRowClick?.(rows[0]);
    }
  }, [rows]);

  const handleGridContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
    console.log("[ContextMenu] ПКМ по таблице target:", e.target);
    e.preventDefault();

    if (!onRowContextMenu) return;

    // Пара десятков тестиков и всё заработало ЫЫЫЫЫЫЫЫЫ
    const rowNode = (e.target as HTMLElement).closest('[data-id]');
    const rowId = rowNode ? (rowNode as HTMLElement).getAttribute('data-id') : null;
    console.log("[ContextMenu] найден rowId:", rowId);

    if (rowId !== null) {
      const idx = Number(rowId);
      const clickedRow = rows[idx] as T | undefined;
      if (clickedRow) {
        console.log("[ContextMenu] передаём строку в onRowContextMenu:", clickedRow);
        onRowContextMenu(clickedRow, e);
      } else {
        console.log("[ContextMenu] строка по rowId не найдена.");
      }
    } else {
      console.log("[ContextMenu] клик вне строки");
    }
  };

  type PluralForm = {
    one: string;
    few: string;
    many: string;
  };

  function getPluralForm(count: number, options: PluralForm) {
    const penultimateDigit = Math.floor(count / 10) % 10;
    const lastDigit = count % 10;

    let pluralForm = options.many;
    if (penultimateDigit !== 1 && lastDigit > 1 && lastDigit < 5) {
      pluralForm = options.few;
    } else if (penultimateDigit !== 1 && lastDigit === 1) {
      pluralForm = options.one;
    }

    return `${count} ${pluralForm}`;
  }

  const russianLocale = {
    // Таблица
    noRowsLabel: 'Данные отсутствуют',

    // Иконки
    columnMenuLabel: 'Меню',
    columnHeaderSortIconLabel: "Сортировать",

    //Меню
    columnMenuShowColumns: 'Показать колонки',
    columnMenuFilter: 'Фильтр',
    columnMenuHideColumn: 'Скрыть',
    columnMenuUnsort: 'Снять сортировку',
    columnMenuSortAsc: 'Сортировать по возрастанию',
    columnMenuSortDesc: 'Сортировать по убыванию',

    //Мэнеджер колонок
    columnMenuManageColumns: "Управление колонками",
    columnsManagementSearchTitle: "Поиск",
    columnsManagementReset: "Сбросить",
    columnsManagementShowHideAllText: "Показать/Скрыть Всё",

    // Фильтр
    filterPanelDeleteIconLabel: 'Удалить',
    filterPanelOperator: 'Операторы',
    filterPanelColumns: 'Столбцы',
    filterPanelInputLabel: 'Значение',
    filterPanelInputPlaceholder: 'Значение фильтра',

    columnHeaderFiltersTooltipActive: (count) =>
      getPluralForm(count, {
        one: 'активный фильтр',
        few: 'активных фильтра',
        many: 'активных фильтров',
      }),

    filterOperatorContains: 'содержит',
    filterOperatorDoesNotContain: 'не содержит',
    filterOperatorEquals: 'равен',
    filterOperatorDoesNotEqual: 'не равен',
    filterOperatorStartsWith: 'начинается с',
    filterOperatorEndsWith: 'заканчивается на',
    filterOperatorIsEmpty: 'пустой',
    filterOperatorIsNotEmpty: 'не пустой',
    filterOperatorIsAnyOf: 'любой из',
  };

  return (
    <div
      style={{
        height: "100%",
        width: "100%",
      }}
      onContextMenu={handleGridContextMenu}
    >
      <DataGrid
        localeText={russianLocale}
        rows={rows.map((item, index)=>({...item, row_id: index}))}
        columns={gridColumns}
        columnGroupingModel={groupingModel}
        getRowId={(row) => row.row_id}
        onRowClick={(params) => onRowClick && onRowClick(params.row as T)}
        onRowDoubleClick={(params) =>
          onRowDoubleClick && onRowDoubleClick(params.row as T)
        }
        onContextMenu={(e) => {
          if (onRowContextMenu) {
            e.preventDefault();
            onRowContextMenu(rows, e);
          }
        }}
        processRowUpdate={(newRow) => {
          const updated = rows.map((r) =>
            r.row_id === newRow.row_id ? newRow : r
          );
          setRows(updated);
          onRowUpdate?.(newRow);
          return newRow;
        }}
        onCellKeyDown={handleKeyDown}
        experimentalFeatures={{ newEditingApi: true }}
        selectionModel={selectedId ? [selectedId] : []}
        paginationMode="server"
        autoPageSize={true}
        hideFooter={true}
        disableColumnSelector={false}
        columnGroupHeaderHeight={40}
        headerHeight={40}
        sx={{
            '& .MuiDataGrid-columnHeaderTitle': { fontWeight: 'bold' },
        }}
      />
    </div>
  );
}