466 lines
18 KiB
TypeScript
466 lines
18 KiB
TypeScript
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/src/components/ui/table";
|
|
import { Button } from "@/src/components/ui/button";
|
|
import { Eye, GripVertical } from "lucide-react";
|
|
import { OrderItem } from "@/src/components/types";
|
|
import { cellClass } from "@/src/constants/status-options";
|
|
import { formatCurrency } from "@/src/utils/format";
|
|
import { useState, useEffect, } from "react";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuLabel,
|
|
DropdownMenuTrigger,
|
|
DropdownMenuCheckboxItem,
|
|
} from "@/src/components/ui/dropdown-menu";
|
|
|
|
interface Item {
|
|
quantity: number | null | undefined;
|
|
}
|
|
|
|
const displayQuantity = (item?: Item) => {
|
|
if (!item?.quantity && item?.quantity !== 0) {
|
|
return "-";
|
|
}
|
|
return item.quantity >= 0 ? item.quantity.toString() : "0";
|
|
};
|
|
|
|
const ALL_COLUMNS = [
|
|
"Código",
|
|
"Descrição",
|
|
"Embalagem",
|
|
"Cor",
|
|
"F. Estoque",
|
|
"Tipo Entrega",
|
|
"Quantidade",
|
|
"Preço Unitário",
|
|
"Preço Total",
|
|
"Departamento",
|
|
"Marca",
|
|
];
|
|
|
|
interface OrderItemsTableProps {
|
|
orderItems: OrderItem[];
|
|
loadingItems: boolean;
|
|
itemsError: string | null;
|
|
onViewDetails?: (item: OrderItem) => void;
|
|
}
|
|
|
|
export function OrderItemsTable({
|
|
orderItems,
|
|
loadingItems,
|
|
itemsError,
|
|
onViewDetails,
|
|
}: OrderItemsTableProps) {
|
|
// Carregar as colunas visíveis do localStorage na inicialização
|
|
const initVisibleColumns = () => {
|
|
try {
|
|
const saved = localStorage.getItem('visibleOrderItemColumns');
|
|
if (saved) {
|
|
const parsed = JSON.parse(saved);
|
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
return parsed;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao carregar configurações de colunas:', error);
|
|
}
|
|
return ALL_COLUMNS;
|
|
};
|
|
|
|
// Inicializar a ordem das colunas
|
|
const initColumnOrder = () => {
|
|
try {
|
|
const saved = localStorage.getItem('orderItemColumnsOrder');
|
|
if (saved) {
|
|
const parsed = JSON.parse(saved);
|
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
return parsed;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Erro ao carregar ordem das colunas:', error);
|
|
}
|
|
return ALL_COLUMNS;
|
|
};
|
|
|
|
// Estado para controlar quais colunas estão visíveis
|
|
const [visibleColumns, setVisibleColumns] = useState<string[]>(initVisibleColumns);
|
|
// Estado para controlar a ordem das colunas
|
|
const [columnOrder, setColumnOrder] = useState<string[]>(initColumnOrder);
|
|
// Estado para rastrear coluna sendo arrastada
|
|
const [draggedColumn, setDraggedColumn] = useState<string | null>(null);
|
|
|
|
// Salvar as colunas visíveis no localStorage sempre que elas mudarem
|
|
useEffect(() => {
|
|
try {
|
|
localStorage.setItem('visibleOrderItemColumns', JSON.stringify(visibleColumns));
|
|
} catch (error) {
|
|
console.error('Erro ao salvar configurações de colunas:', error);
|
|
}
|
|
}, [visibleColumns]);
|
|
|
|
// Salvar a ordem das colunas no localStorage
|
|
useEffect(() => {
|
|
try {
|
|
localStorage.setItem('orderItemColumnsOrder', JSON.stringify(columnOrder));
|
|
} catch (error) {
|
|
console.error('Erro ao salvar ordem das colunas:', error);
|
|
}
|
|
}, [columnOrder]);
|
|
|
|
// Função para alternar a visibilidade de uma coluna
|
|
const toggleColumn = (column: string) => {
|
|
setVisibleColumns((current) => {
|
|
if (current.includes(column)) {
|
|
return current.filter((c) => c !== column);
|
|
} else {
|
|
return [...current, column];
|
|
}
|
|
});
|
|
};
|
|
|
|
// Função para selecionar todas as colunas
|
|
const selectAllColumns = () => {
|
|
setVisibleColumns(ALL_COLUMNS);
|
|
};
|
|
|
|
// Função para desmarcar todas as colunas
|
|
const unselectAllColumns = () => {
|
|
// Mantém pelo menos uma coluna para garantir que a tabela não fique vazia
|
|
setVisibleColumns(["Código"]);
|
|
};
|
|
|
|
// Função para verificar se uma coluna está visível
|
|
const isColumnVisible = (column: string) => visibleColumns.includes(column);
|
|
|
|
// Funções para drag & drop
|
|
const handleDragStart = (e: React.DragEvent<HTMLDivElement>, column: string) => {
|
|
setDraggedColumn(column);
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
e.dataTransfer.setData('text/plain', column);
|
|
|
|
// Adicionar um estilo visual para o elemento sendo arrastado
|
|
if (e.currentTarget) {
|
|
setTimeout(() => {
|
|
if (e.currentTarget) {
|
|
e.currentTarget.classList.add('opacity-50');
|
|
}
|
|
}, 0);
|
|
}
|
|
};
|
|
|
|
const handleDragEnd = (e: React.DragEvent<HTMLDivElement>) => {
|
|
setDraggedColumn(null);
|
|
e.currentTarget.classList.remove('opacity-50');
|
|
};
|
|
|
|
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'move';
|
|
};
|
|
|
|
const handleDrop = (e: React.DragEvent<HTMLDivElement>, targetColumn: string) => {
|
|
e.preventDefault();
|
|
|
|
if (!draggedColumn || draggedColumn === targetColumn) return;
|
|
|
|
// Reordenar as colunas
|
|
const newColumnOrder = [...columnOrder];
|
|
const draggedIndex = newColumnOrder.indexOf(draggedColumn);
|
|
const targetIndex = newColumnOrder.indexOf(targetColumn);
|
|
|
|
if (draggedIndex !== -1 && targetIndex !== -1) {
|
|
newColumnOrder.splice(draggedIndex, 1);
|
|
newColumnOrder.splice(targetIndex, 0, draggedColumn);
|
|
setColumnOrder(newColumnOrder);
|
|
}
|
|
};
|
|
|
|
const getColumnClass = (title: string) => {
|
|
const textRight = ["Preço Unitário", "Preço Total", "Quantidade"];
|
|
const cellWidth: { [key: string]: string } = {
|
|
"Código": "w-20",
|
|
"Descrição": "w-56",
|
|
"Embalagem": "w-24",
|
|
"Cor": "w-20",
|
|
"F. Estoque": "w-24",
|
|
"Tipo Entrega": "w-28",
|
|
"Quantidade": "w-24",
|
|
"Preço Unitário": "w-24",
|
|
"Preço Total": "w-24",
|
|
"Departamento": "w-48",
|
|
"Marca": "w-24",
|
|
};
|
|
|
|
let classes = "";
|
|
|
|
// Não aplicar whitespace-nowrap para Departamento
|
|
if (title !== "Departamento") {
|
|
classes += "whitespace-nowrap ";
|
|
}
|
|
|
|
// Alinhar à direita
|
|
if (textRight.includes(title)) {
|
|
classes += "text-right ";
|
|
}
|
|
|
|
// Aplicar larguras
|
|
if (cellWidth[title]) {
|
|
classes += cellWidth[title] + " ";
|
|
}
|
|
|
|
return classes;
|
|
};
|
|
|
|
// Função para ordenar as colunas visíveis na ordem definida pelo usuário
|
|
const getOrderedVisibleColumns = () => {
|
|
// Primeiro, filtrar as colunas que são visíveis
|
|
const visibleColumnSet = new Set(visibleColumns);
|
|
|
|
// Depois, ordenar as colunas de acordo com a ordem definida pelo usuário
|
|
return columnOrder.filter(column => visibleColumnSet.has(column));
|
|
};
|
|
|
|
// Resetar a ordem das colunas
|
|
const resetColumnOrder = () => {
|
|
setColumnOrder(ALL_COLUMNS);
|
|
};
|
|
|
|
const handleEvent = (e: React.SyntheticEvent) => {
|
|
const element = e.currentTarget;
|
|
if (element instanceof HTMLElement) {
|
|
const timeoutId = setTimeout(() => {
|
|
element.classList.add('opacity-50');
|
|
}, 0);
|
|
|
|
// Limpeza em caso de desmontagem
|
|
return () => clearTimeout(timeoutId);
|
|
}
|
|
};
|
|
|
|
if (loadingItems) {
|
|
return <div className="py-4 text-center">Carregando itens...</div>;
|
|
}
|
|
|
|
if (itemsError) {
|
|
return <div className="py-2 text-red-500">{itemsError}</div>;
|
|
}
|
|
|
|
if (orderItems.length === 0) {
|
|
return <div className="py-2">Nenhum item encontrado para este pedido.</div>;
|
|
}
|
|
|
|
const orderedVisibleColumns = getOrderedVisibleColumns();
|
|
|
|
return (
|
|
<div className="rounded-md border-0">
|
|
<div className="flex justify-end mb-2 gap-2">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="w-56 p-2 bg-white shadow-lg border border-gray-200">
|
|
<DropdownMenuLabel className="text-sm font-bold pb-2 mb-2 border-b border-gray-200">Visibilidade das Colunas</DropdownMenuLabel>
|
|
<div className="flex justify-between mb-2 border-b pb-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={selectAllColumns}
|
|
className="text-xs h-7 hover:bg-gray-100"
|
|
>
|
|
Marcar Todas
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={unselectAllColumns}
|
|
className="text-xs h-7 hover:bg-gray-100"
|
|
>
|
|
Desmarcar Todas
|
|
</Button>
|
|
</div>
|
|
<div className="max-h-[300px] overflow-y-auto">
|
|
{ALL_COLUMNS.map((column) => (
|
|
<DropdownMenuCheckboxItem
|
|
key={column}
|
|
checked={isColumnVisible(column)}
|
|
onCheckedChange={() => toggleColumn(column)}
|
|
className="flex items-center space-x-2 cursor-pointer py-1.5 hover:bg-gray-100 px-2 rounded"
|
|
>
|
|
<div className="flex items-center gap-2 text-sm">
|
|
{isColumnVisible(column) ? (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="h-4 w-4 text-blue-600"
|
|
>
|
|
<rect width="16" height="16" x="4" y="4" rx="2" />
|
|
<path d="m9 12 2 2 4-4" />
|
|
</svg>
|
|
) : (
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className="h-4 w-4 text-gray-400"
|
|
>
|
|
<rect width="16" height="16" x="4" y="4" rx="2" />
|
|
</svg>
|
|
)}
|
|
{column}
|
|
</div>
|
|
</DropdownMenuCheckboxItem>
|
|
))}
|
|
</div>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
<div className="border border-gray-300">
|
|
<div className="overflow-x-auto">
|
|
<div className="max-h-[600px] overflow-y-auto">
|
|
<Table className="border-collapse border-gray-200">
|
|
<TableHeader className="sticky top-0 z-10 bg-white border-b border-gray-200">
|
|
<TableRow className="hover:bg-transparent">
|
|
{orderedVisibleColumns.map((title, index, array) => (
|
|
<TableHead
|
|
key={index}
|
|
className={`text-xs font-medium text-gray-600 p-2 ${getColumnClass(title)} ${
|
|
index < array.length - 1 ? "border-r border-gray-200" : ""
|
|
} relative`}
|
|
draggable={true}
|
|
onDragStart={(e) => handleDragStart(e, title)}
|
|
onDragEnd={handleDragEnd}
|
|
onDragOver={handleDragOver}
|
|
onDrop={(e) => handleDrop(e, title)}
|
|
>
|
|
<div className="flex items-center">
|
|
<GripVertical className="h-3 w-3 mr-1 cursor-move text-gray-400" />
|
|
{title}
|
|
</div>
|
|
</TableHead>
|
|
))}
|
|
{onViewDetails && (
|
|
<TableHead className="text-xs font-medium text-gray-600 p-2 w-16">
|
|
Ações
|
|
</TableHead>
|
|
)}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{orderItems.map((item, index) => (
|
|
<TableRow
|
|
key={`${item.productId}-${index}`}
|
|
className="odd:bg-white even:bg-gray-50 hover:bg-gray-100"
|
|
>
|
|
{orderedVisibleColumns.map((columnName, colIndex) => {
|
|
switch (columnName) {
|
|
case "Código":
|
|
return (
|
|
<TableCell key={colIndex} className={`${cellClass} border-r border-gray-200 p-2 text-xs`}>
|
|
{item.productId ?? "-"}
|
|
</TableCell>
|
|
);
|
|
case "Descrição":
|
|
return (
|
|
<TableCell key={colIndex} className={`${cellClass} border-r border-gray-200 p-2 text-xs`}>
|
|
{item.description ?? "-"}
|
|
</TableCell>
|
|
);
|
|
case "Embalagem":
|
|
return (
|
|
<TableCell key={colIndex} className={`${cellClass} border-r border-gray-200 p-2 text-xs`}>
|
|
{item.pacth ?? "-"}
|
|
</TableCell>
|
|
);
|
|
case "Cor":
|
|
return (
|
|
<TableCell key={colIndex} className={`${cellClass} border-r border-gray-200 p-2 text-xs`}>
|
|
{item.color ?? "-"}
|
|
</TableCell>
|
|
);
|
|
case "F. Estoque":
|
|
return (
|
|
<TableCell key={colIndex} className={`${cellClass} border-r border-gray-200 p-2 text-xs`}>
|
|
{item.stockId ?? "-"}
|
|
</TableCell>
|
|
);
|
|
case "Tipo Entrega":
|
|
return (
|
|
<TableCell key={colIndex} className={`${cellClass} border-r border-gray-200 p-2 text-xs`}>
|
|
{item.deliveryType ?? "-"}
|
|
</TableCell>
|
|
);
|
|
case "Quantidade":
|
|
return (
|
|
<TableCell key={colIndex} className={`${cellClass} border-r border-gray-200 p-2 text-right text-xs`}>
|
|
{displayQuantity(item)}
|
|
</TableCell>
|
|
);
|
|
case "Preço Unitário":
|
|
return (
|
|
<TableCell key={colIndex} className={`${cellClass} border-r border-gray-200 p-2 text-right text-xs`}>
|
|
{formatCurrency(item.salePrice)}
|
|
</TableCell>
|
|
);
|
|
case "Preço Total":
|
|
return (
|
|
<TableCell key={colIndex} className={`${cellClass} border-r border-gray-200 p-2 text-right text-xs`}>
|
|
{item.total != null ? formatCurrency(item.total) : "-"}
|
|
</TableCell>
|
|
);
|
|
case "Departamento":
|
|
return (
|
|
<TableCell key={colIndex} className={`${cellClass} border-r border-gray-200 p-2 text-xs`}>
|
|
<div className="break-words hyphens-auto w-full">
|
|
{item.department ?? "-"}
|
|
</div>
|
|
</TableCell>
|
|
);
|
|
case "Marca":
|
|
return (
|
|
<TableCell key={colIndex} className={`${cellClass} border-r border-gray-200 p-2 text-xs`}>
|
|
{item.brand ?? "-"}
|
|
</TableCell>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
})}
|
|
{onViewDetails && (
|
|
<TableCell className="p-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => onViewDetails(item)}
|
|
title="Ver detalhes do item"
|
|
className="h-7 w-7"
|
|
>
|
|
<Eye className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</TableCell>
|
|
)}
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |