first
This commit is contained in:
466
src/components/orders/OrderItemsTable.tsx
Normal file
466
src/components/orders/OrderItemsTable.tsx
Normal file
@@ -0,0 +1,466 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user