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


import { DataGrid, GridColDef, GridColumnGroupingModel, GridRenderCellParams, GridRenderEditCellParams } 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;                                       // включает режим редактирования
  initialEmptyRows?: number;                                // сколько пустых строк создать при старте
  onAddRowRequest?: () => void;                             // Создание новой строки
  onSaveAllRequest?: () => void;                            // сохранить все строки в базу
  onDeleteRowRequest?: (row: T) => void;                    // Удалить строку
  onRowUpdate?: (row: T) => void;                           // Событие редактирования
  deleteField?: keyof T;                                    // По какому столбцу отслеживать "удаление" (зачёркивание строки)
  
  // НОВЫЕ ПРОПСЫ ДЛЯ РАСКРЫТИЯ СТРОК
  expandField?: keyof T;                                    // По какому столбцу отслеживать возможность раскрытия
  expandComponent?: React.ComponentType<{ id: string | number; row: T }>; // Компонент раскрытия, принимает id (или value) и всю строку
}

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,
  
  // Деструктуризация новых пропсов
  expandField,
  expandComponent: ExpandComponent,
}: CustomTableProps<T>) {
  const [baseRows, setBaseRows] = useState<T[]>([]);
  const [expandedRowIds, setExpandedRowIds] = useState<(string | number)[]>([]);

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

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

  const handleKeyDown = (params: any, event: any) => {
    if (!editable || !onAddRowRequest) return;
    const isLastRow = params.id === displayRows.length - 1; // проверяем по итоговому массиву

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

  const [selectedId, setSelectedId] = useState<number | string | null>(null);

  // --- ФОРМИРОВАНИЕ ИТОГОВЫХ СТРОК С УЧЕТОМ РАСКРЫТЫХ ПАНЕЛЕЙ ---
  const displayRows = React.useMemo(() => {
    const result: any[] = [];
    baseRows.forEach((row, index) => {
      // Присваиваем row_id на основе индекса, как в вашем исходном коде [1]
      const actualRowId = row.row_id ?? index;
      const rowWithId = { ...row, row_id: actualRowId };
      
      result.push(rowWithId);

      // Если строка раскрыта, пушим вслед за ней специальный служебный объект панели
      if (expandedRowIds.includes(actualRowId)) {
        result.push({
          row_id: `panel-${actualRowId}`, // Уникальный ID для DataGrid
          isExpandedPanel: true,
          parentRow: rowWithId,          // Передаем родительские данные внутрь панели
        });
      }
    });
    return result;
  }, [baseRows, expandedRowIds]);
  // -------------------------------------------------------------

  // Колонка для кнопки "+" / "-"
  const expandColumn: GridColDef[] = expandField && ExpandComponent ? [
    {
      field: "__expand__",
      headerName: "",
      width: 50,
      sortable: false,
      filterable: false,
      disableColumnMenu: true,
      renderCell: (params) => {
        // Если это строка-панель, кнопку не рисуем
        if (params.row.isExpandedPanel) return null;

        // Проверяем наличие данных в указанном столбце expandField
        const hasExpandData = Boolean(params.row[expandField]);
        if (!hasExpandData) return null;

        const isExpanded = expandedRowIds.includes(params.id);

        return (
          <button
            style={{
              cursor: 'pointer',
              width: '24px',
              height: '24px',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              fontWeight: 'bold'
            }}
            onClick={(e) => {
              e.stopPropagation(); // Чтобы не срабатывал клик по строке
              setExpandedRowIds(prev => 
                isExpanded ? prev.filter(id => id !== params.id) : [...prev, params.id]
              );
            }}
          >
            {isExpanded ? "−" : "+"}
          </button>
        );
      }
    }
  ] : [];

  const gridColumns: GridColDef[] = columns.map((col) => {
    const isBoolean = typeof (baseRows[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) => {
        // ЕСЛИ ЭТО НАША КАСTОМНАЯ СТРОКА-ПАНЕЛЬ
        if (params.row.isExpandedPanel) {
          // Рендерим кастомный компонент только в первой основной колонке данных,
          // чтобы интерфейс не дублировался в каждой ячейке строки
          const firstDataColumn = columns[0]?.id;
          if (params.field !== firstDataColumn) return null;

          const parentRow = params.row.parentRow;
          const targetId = parentRow[expandField!]; // Берем значение из указанного столбца

          return (
            <div 
              style={{ 
                position: 'absolute', 
                left: 0,
                width: '100%',
                padding: '10px 20px',
                background: '#f9f9f9',
                boxSizing: 'border-box',
                zIndex: 2,
              }}
              onClick={(e) => e.stopPropagation()} // Изолируем клики внутри панели
            >
              {/* Рендерим переданный извне компонент вида <Test test={id} /> */}
              <ExpandComponent id={targetId} row={parentRow} />
            </div>
          );
        }

        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 = {true} />;
        }

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

  // Объединяем служебную колонку "+" и обычные колонки [1]
  const finalColumns = [...expandColumn, ...gridColumns];
// Создание групп [1]
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 })),
});
});
// Выбор первой строки [1]
useEffect(() => {
if (baseRows.length > 0) {
const firstId = baseRows[0].row_id ?? 0;
setSelectedId(firstId);
onRowClick?.(baseRows[0]);
}
}, [baseRows]);
const handleGridContextMenu = (e: React.MouseEvent) => {
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) {
// Игнорируем контекстное меню для раскрытых панелей-заглушек
if (String(rowId).startsWith('panel-')) return;
const idx = Number(rowId);
const clickedRow = baseRows[idx] as T | undefined;
if (clickedRow) {
onRowContextMenu(clickedRow, e);
}
}
};
const russianLocale = {};
return (
<div
style={{ height: "100%", width: "100%" }}
onContextMenu={handleGridContextMenu}
>
<DataGrid
columnVisibilityModel={columnVisibilityModel}
onColumnVisibilityModelChange={(newModel) => setColumnVisibilityModel(newModel)}
localeText={russianLocale}
// Передаем модифицированные строки (данные + раскрытые панели)
rows={displayRows}
columns={finalColumns}
columnGroupingModel={groupingModel}
// Динамически меняем высоту строки, если это раскрытая панель (например, 150px или авто)
getRowHeight={(params) => (params.model.isExpandedPanel ? 150 : 'auto')}
getRowClassName={(params) => {
if (params.row.isExpandedPanel) return "row-expanded-panel";
return deleteField && params.row[deleteField] ? "row-delete" : "";
}}
getRowId={(row) => row.row_id}
// Защищаем системные клики по строке панели
onRowClick={(params) => {
if (params.row.isExpandedPanel) return;
onRowClick && onRowClick(params.row as T);
}}
onRowDoubleClick={(params) => {
if (params.row.isExpandedPanel) return;
onRowDoubleClick && onRowDoubleClick(params.row as T);
}}
processRowUpdate={(newRow) => {
if (newRow.isExpandedPanel) return newRow;
const updated = baseRows.map((r, idx) => {
const currentId = r.row_id ?? idx;
return currentId === newRow.row_id ? newRow : r;
});
setBaseRows(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" },
// Стилизация строки-панели для объединения ячеек визуально
"& .row-expanded-panel": {
backgroundColor: "#f9f9f9 !important",
"& .MuiDataGrid-cell": {
borderBottom: "1px solid #e0e0e0",
overflow: "visible !important", // Позволяет компоненту растягиваться на всю ширину
}
},
"& .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',
},
}
}}
/>

);
}