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


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;       // Скрыть столбец
  vertical?: boolean;     // Вертикальный заголовок (работает пока что криво)
  widthCol?: number;      // Размер колонок
}

interface CustomTableProps<T> {
  onRowClick?: (row: T) => void;                               // событие одного клика по строке
  onRowDoubleClick?: (row: T) => void;                         // Событие двойного клика
  onRowContextMenu?: (row: T, e: React.MouseEvent) => void;    // Вызов контекстного меню
  editable?: boolean;                                          // включает режим редактирования
  onAddRowRequest?: () => void;                                // Создание новой строки
  onSaveAllRequest?: () => void;                               // сохранить все строки в базу
  onDeleteRowRequest?: (row: T) => void;                       // Удалить строку
  onRowUpdate?: (row: T) => void;                              // Событие редактирования
  deleteField?: keyof T;                                       // По какому столбцу отслеживать "удаление" (зачёркивание строки)
  expandField?: string;                                        // По какому столбцу отслеживать возможность раскрытия
  expandComponent?: React.ComponentType<{value: any; row: T}>; // Что показывается в раскрытой строке
}

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

  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,
  deleteField,
  expandComponent,
  expandField,
}: CustomTableProps<T>) {
  const [rows, setRows] = useState<T[]>([]);

  // Скрытие столбцов ---
  const initialVisibility = Object.fromEntries(
    columns.map(col => [col.id as string, !col.hidden])
  );
  const [columnVisibilityModel, setColumnVisibilityModel] = useState(initialVisibility);
  // ---

  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 [expandedRows, setExpandedRows] = useState<(string | number)[]>([]);
  const expandColumn: GridColDef[] = expandField && expandComponent ? [
    {
      field: "__expand__",
      headerName: "",
      width: 50,
      sortable: false,
      filterable: false,
      renderCell: (params) => {
        const hasExpand = Boolean(params.row[expandField as string]);

        if (!hasExpand) return null;

        const expanded = expandedRows.includes(params.id);

        return (
          <button
            onClick={(e) => {
              e.stopPropagation();

              setExpandedRows((prev) =>
                expanded ? prev.filter((id) => id !== params.id) : [...prev, params.id]
              );
            }}
          >
            {expanded ? "-" : "+"}
          </button>
        );
      },
    },
  ] : [];

  const gridColumns: GridColDef[] = columns.map((col) => {
    const isBoolean = typeof (rows[0]?.[col.id] ?? "") === "boolean";

    const baseCol: GridColDef = {
      field: col.id as string,
      headerName: col.label,
      flex: col.widthCol ? undefined : 1,
      width: col.widthCol,
      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;
      },
    };

    if (col.vertical) {
      baseCol.headerAlign = "center";
      baseCol.renderHeader = () => (
        <div
          style={{
            height: "100%",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            writingMode: "vertical-rl",
            transform: "rotate(180deg)",
            whiteSpace: "nowrap",
            // textAlign: "center",
            width: "20px",
            fontWeight: "bold",
          }}
        >
          {col.label}
        </div>
      );
    }

    return baseCol;
  });

  const finalColumns = [...expandColumn, ...gridColumns];

  // Создание групп
  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 = {
// мо перевод
  };

  return (
    <div
      style={{
        height: "100%",
        width: "100%",
      }}
      onContextMenu={handleGridContextMenu}
    >
      <DataGrid
        columnVisibilityModel={columnVisibilityModel}
        onColumnVisibilityModelChange={(newModel) => setColumnVisibilityModel(newModel)}
        localeText={russianLocale}
        rows={rows.map((item, index)=>({...item, row_id: index}))}
        columns={finalColumns}
        columnGroupingModel={groupingModel}
        getRowClassName={(params) => deleteField && params.row[deleteField] ? "row-delete" : ""}
        getRowId={(row) => row.row_id}
        getDetailPanelContent={(params) => {
          if (!expandField || !expandComponent) {
            return null;
          }
          const value = params.row[expandField as string];

          if (!value) {
            return null;
          }

          return (
            <div style={{padding: 12}}>
              <expandComponent
                value={value}
                row={params.row as T}
              />
            </div>
          );
        }}
        getDetailPanelHeight={() => 300}
        detailPanelExpandedRowIds={expandedRows}
        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={50}
        headerHeight={80}
        sx={{
          '& .MuiDataGrid-columnHeaderTitle': { fontWeight: 'bold', lineHeight: 1.1, whiteSpace: "normal" },
          // '& .MuiDataGrid-columnHeaders' : { height: 40, maxHeight: 55},
          "& .row-delete": {
            position: 'relative',

            '& .MuiDataGrid-cell': {
              opacity: 0.7,
            },

            // Красная линия
            '&::after': {
              content: '""',
              position: 'absolute',
              left: 0,
              right: 0,
              top: '50%',
              height: '2px',
              backgroundColor: '#d32f2f',
              transform: 'translateY(-50%)',
              zIndex: 1,

              pointerEvents: 'none',
            },
          }
        }}
      />
    </div>
  );
}