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


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> {
  columns: HeadCell<T>[];
  rows?: 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.ReactNode;                           // Принимает готовый JSX: <Comp x={} y={} />
}

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

  const finalTooltipText = title || (isOverflowed ? value : "");

  return (
    <Tooltip
      title={finalTooltipText}
      disableHoverListener={!finalTooltipText}
      arrow
      enterDelay={100}
    >
      <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 [columnVisibilityModel, setColumnVisibilityModel] = useState(
    Object.fromEntries(columns.map(col => [col.id as string, !col.hidden]))
  );
  const [expandedRows, setExpandedRows] = useState<(string | number)[]>([]);
  const [selectedId, setSelectedId] = useState<number | string | null>(null);

  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 expandColumn: GridColDef[] = expandField && expandComponent ? [
    {
      field: "__expand__",
      headerName: "",
      width: 50,
      sortable: false,
      filterable: false,
      // В детальной строке растягиваем эту ячейку на всю ширину таблицы (colSpan)
      colSpan: (params) => {
        if (params.row.isDetailPanel) {
          return finalColumns.length;
        }
        return undefined;
      },
      renderCell: (params) => {
        // Если это строка подвала (детальный компонент), рендерим переданный извне компонент
        if (params.row.isDetailPanel) {
          return (
            <div style={{ padding: '12px', width: '100%', whiteSpace: 'normal' }}>
              {expandComponent}
            </div>
          );
        }

        const hasExpand = Boolean(params.row[expandField as string]);
        if (!hasExpand) return null;

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

        return (
          <button
            style={{ cursor: 'pointer', width: 24, height: 24 }}
            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) => (
            <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) => {
        // Если родительская ячейка занята colSpan-ом детальной панели, ничего не рендерим
        if (params.row.isDetailPanel) return null;

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

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

    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",
          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>) => {
    e.preventDefault();
    if (!onRowContextMenu) return;

    const rowNode = (e.target as HTMLElement).closest('[data-id]');
    const rowId = rowNode ? (rowNode as HTMLElement).getAttribute('data-id') : null;

    if (rowId !== null) {
      // Ищем строку в исходном массиве rows, игнорируя detail-панели
      const clickedRow = rows.find((r, idx) => String(r.row_id ?? idx) === String(rowId));
      if (clickedRow) {
        onRowContextMenu(clickedRow, e);
      }
    }
  };

  // Преобразуем плоский список строк: если ID строки есть в expandedRows,
  // сразу за ней внедряем искусственную строку для отображения компонента
  const displayRows = rows.reduce((acc: any[], item, index) => {
    const rowId = item.row_id ?? index;
    const actualRow = { ...item, row_id: rowId };
    acc.push(actualRow);

    if (expandedRows.includes(rowId)) {
      acc.push({
        row_id: `detail-${rowId}`, // Уникальный ID для DataGrid
        isDetailPanel: true,       // Флаг для кастомного рендера
      });
    }
    return acc;
  }, []);

  return (
    <div style={{ height: "100%", width: "100%" }} onContextMenu={handleGridContextMenu}>
      <DataGrid
        columnVisibilityModel={columnVisibilityModel}
        onColumnVisibilityModelChange={(newModel) => setColumnVisibilityModel(newModel)}
        rows={displayRows}
        columns={finalColumns}
        columnGroupingModel={groupingModel}
        getRowClassName={(params) => {
          if (params.row.isDetailPanel) return "row-detail-panel";
return deleteField && params.row[deleteField] ? "row-delete" : "";
}}
getRowId={(row) => row.row_id}
// Динамическая высота: для детальной панели авто-высота, для обычных строк стандартная
getRowHeight={(params) => (params.model.isDetailPanel ? 'auto' : null)}
onRowClick={(params) => {
if (params.row.isDetailPanel) return; // Игнорируем клики по детальной панели
onRowClick && onRowClick(params.row as T);
}}
onRowDoubleClick={(params) => {
if (params.row.isDetailPanel) return;
onRowDoubleClick && onRowDoubleClick(params.row as T);
}}
processRowUpdate={(newRow) => {
const updated = rows.map((r, idx) => {
const rId = r.row_id === undefined ? idx : r.row_id;
return rId === 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}
columnGroupHeaderHeight={50}
headerHeight={80}
sx={{
'& .MuiDataGrid-columnHeaderTitle': { fontWeight: 'bold', lineHeight: 1.1, whiteSpace: "normal" },
'& .row-detail-panel': {
backgroundColor: '#f9f9f9',
'&:hover': { backgroundColor: '#f9f9f9 !important' },
},
'& .row-detail-panel .MuiDataGrid-cell': {
padding: 0,
overflow: 'visible',
},
"& .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',
},
}
}}
/>

);
}