🚀 Initial commit: Versão atual do TrackSteel APP
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { StandardPageLayout } from '@/components/layout/StandardPageLayout';
|
||||
import { StandardCard } from '@/components/layout/StandardCard';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Plus, Printer, Edit, Trash2, Eye, RefreshCw, Check, ShoppingCart } from 'lucide-react';
|
||||
import { useSolicitacoesCompra } from '@/hooks/useSolicitacoesCompra';
|
||||
import { useUserFunction } from '@/hooks/useUserFunction';
|
||||
import { SolicitacaoComprasModal } from '@/components/solicitacao-compras/SolicitacaoComprasModal';
|
||||
import { SolicitacaoComprasPreviewModal } from '@/components/solicitacao-compras/SolicitacaoComprasPreviewModal';
|
||||
import { UserAvatar } from '@/components/ui/user-avatar';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
|
||||
const statusColors = {
|
||||
'Em planejamento': 'bg-yellow-500',
|
||||
'Revisado': 'bg-orange-500',
|
||||
'Solicitado': 'bg-blue-500',
|
||||
'Comprado': 'bg-blue-600',
|
||||
'Recebido': 'bg-lime-500',
|
||||
'Arquivado': 'bg-red-500',
|
||||
};
|
||||
|
||||
export default function SolicitacaoCompras() {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||
const [selectedSolicitacao, setSelectedSolicitacao] = useState(null);
|
||||
const { isComprador } = useUserFunction();
|
||||
const {
|
||||
solicitacoes,
|
||||
isLoading,
|
||||
canEdit,
|
||||
canDelete,
|
||||
deleteSolicitacao,
|
||||
updateStatus,
|
||||
revisar,
|
||||
aceitar,
|
||||
comprar,
|
||||
comprarDireto,
|
||||
isRevisando,
|
||||
isAceitando,
|
||||
isComprando,
|
||||
isComprandoDireto
|
||||
} = useSolicitacoesCompra();
|
||||
|
||||
const handlePrint = (solicitacao: any) => {
|
||||
try {
|
||||
const printContent = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Solicitação de Compra - ${solicitacao.numero_sc}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
-webkit-print-color-adjust: exact;
|
||||
color-adjust: exact;
|
||||
}
|
||||
.print-container {
|
||||
box-shadow: none;
|
||||
margin: 0;
|
||||
max-width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100 p-4 sm:p-6">
|
||||
|
||||
<div class="print-container max-w-4xl mx-auto bg-white rounded-xl shadow-lg overflow-hidden">
|
||||
<header class="bg-gray-100 text-gray-800 p-4 md:p-5 border-b border-gray-200">
|
||||
<h1 class="text-xl md:text-2xl font-bold">Solicitação de Compra</h1>
|
||||
<p class="text-gray-600 text-sm">Revise os detalhes da sua solicitação abaixo.</p>
|
||||
</header>
|
||||
|
||||
<main class="p-4 md:p-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||
<div class="bg-gray-50 p-3 rounded-lg">
|
||||
<h3 class="font-semibold text-gray-500 text-xs">Número SC</h3>
|
||||
<p class="text-gray-900 text-base font-medium">${solicitacao.numero_sc}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 rounded-lg">
|
||||
<h3 class="font-semibold text-gray-500 text-xs">Data da Solicitação</h3>
|
||||
<p class="text-gray-900 text-base font-medium">${format(new Date(solicitacao.data_solicitacao), 'dd/MM/yyyy', { locale: ptBR })}</p>
|
||||
</div>
|
||||
<div class="bg-blue-50 border-l-4 border-blue-500 p-3 rounded-lg col-span-1 sm:col-span-2 lg:col-span-1">
|
||||
<h3 class="font-semibold text-blue-800 text-xs">Status</h3>
|
||||
<p class="text-blue-900 text-base font-medium">${solicitacao.status}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 rounded-lg">
|
||||
<h3 class="font-semibold text-gray-500 text-xs">Solicitante</h3>
|
||||
<p class="text-gray-900 text-base font-medium">${solicitacao.creator?.full_name || 'Usuário não encontrado'}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 rounded-lg">
|
||||
<h3 class="font-semibold text-gray-500 text-xs">Objetivo</h3>
|
||||
<p class="text-gray-900 text-base font-medium">${solicitacao.objetivo || 'N/A'}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 rounded-lg">
|
||||
<h3 class="font-semibold text-gray-500 text-xs">OF / Revisão</h3>
|
||||
<p class="text-gray-900 text-base font-medium">${solicitacao.of_number || 'N/A'} / Rev. ${solicitacao.revisao}</p>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-3 rounded-lg col-span-1 sm:col-span-2 lg:col-span-3">
|
||||
<h3 class="font-semibold text-gray-500 text-xs">Justificativa</h3>
|
||||
<p class="text-gray-900 text-base font-medium">${solicitacao.justificativa || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${solicitacao.itens && solicitacao.itens.length > 0 ? `
|
||||
<div class="mb-6">
|
||||
<h2 class="text-lg font-bold text-gray-800 mb-3">Itens Solicitados</h2>
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-200">
|
||||
<table class="min-w-full divide-y divide-gray-200 text-xs">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-4 py-2 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">Descrição</th>
|
||||
<th scope="col" class="px-4 py-2 text-center text-xs font-bold text-gray-600 uppercase tracking-wider">Unidade</th>
|
||||
<th scope="col" class="px-4 py-2 text-center text-xs font-bold text-gray-600 uppercase tracking-wider">Quantidade</th>
|
||||
<th scope="col" class="px-4 py-2 text-right text-xs font-bold text-gray-600 uppercase tracking-wider">Prazo Recebimento</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
${solicitacao.itens.map((item: any, index: number) => `
|
||||
<tr${index % 2 === 1 ? ' class="hover:bg-gray-50"' : ''}>
|
||||
<td class="px-4 py-3 text-xs font-medium text-gray-800 break-words">${item.material?.descricao || 'Material não encontrado'}</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap text-center text-gray-600 text-xs">${item.material?.unidade || 'UN'}</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap text-center text-gray-600 text-xs">${item.quantidade}</td>
|
||||
<td class="px-4 py-3 whitespace-nowrap text-right text-gray-600 text-xs">${format(new Date(item.prazo_recebimento), 'dd/MM/yyyy', { locale: ptBR })}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</main>
|
||||
|
||||
<footer class="text-center text-xs text-gray-400 p-3 bg-gray-50 border-t">
|
||||
<p>Gerado em: ${new Date().toLocaleString('pt-BR')}</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const printWindow = window.open('', '_blank');
|
||||
if (printWindow) {
|
||||
printWindow.document.write(printContent);
|
||||
printWindow.document.close();
|
||||
|
||||
// Aguardar o carregamento antes de focar e imprimir
|
||||
printWindow.onload = function() {
|
||||
printWindow.focus();
|
||||
setTimeout(() => {
|
||||
printWindow.print();
|
||||
}, 250);
|
||||
};
|
||||
|
||||
// Fallback caso onload não funcione
|
||||
setTimeout(() => {
|
||||
if (printWindow && !printWindow.closed) {
|
||||
printWindow.focus();
|
||||
printWindow.print();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao imprimir:', error);
|
||||
alert('Erro ao imprimir. Tente novamente.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (solicitacao: any) => {
|
||||
if (!canEdit(solicitacao)) {
|
||||
alert('Você só pode editar suas próprias solicitações.');
|
||||
return;
|
||||
}
|
||||
setSelectedSolicitacao(solicitacao);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handlePreview = (solicitacao: any) => {
|
||||
setSelectedSolicitacao(solicitacao);
|
||||
setIsPreviewOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = (solicitacao: any) => {
|
||||
if (!canDelete(solicitacao)) {
|
||||
if (solicitacao.status !== 'Em planejamento') {
|
||||
alert('⚠️ PERMISSÃO NEGADA\n\nSolicitações com status diferente de "Em planejamento" só podem ser excluídas por compradores.');
|
||||
} else {
|
||||
alert('⚠️ PERMISSÃO NEGADA\n\nVocê só pode excluir suas próprias solicitações.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.confirm('⚠️ CONFIRMAÇÃO DE EXCLUSÃO\n\nTem certeza que deseja excluir esta solicitação de compra?\n\nEsta ação não pode ser desfeita!')) {
|
||||
deleteSolicitacao(solicitacao.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRevisar = (solicitacao: any) => {
|
||||
if (window.confirm(`Tem certeza que deseja enviar a solicitação ${solicitacao.numero_sc} para revisão?`)) {
|
||||
revisar({ id: solicitacao.id, created_by: solicitacao.created_by });
|
||||
}
|
||||
};
|
||||
|
||||
const handleAceitar = (solicitacao: any) => {
|
||||
if (window.confirm(`Tem certeza que deseja aceitar a solicitação ${solicitacao.numero_sc}?`)) {
|
||||
aceitar({ id: solicitacao.id, created_by: solicitacao.created_by });
|
||||
}
|
||||
};
|
||||
|
||||
const handleComprar = (solicitacao: any) => {
|
||||
if (window.confirm(`Tem certeza que deseja marcar a solicitação ${solicitacao.numero_sc} como comprada?`)) {
|
||||
comprar({ id: solicitacao.id, created_by: solicitacao.created_by });
|
||||
}
|
||||
};
|
||||
|
||||
const handleComprarDireto = (solicitacao: any) => {
|
||||
if (window.confirm(`Você deseja passar o status dessa SC ${solicitacao.numero_sc} para "Comprado"?`)) {
|
||||
comprarDireto({ id: solicitacao.id, created_by: solicitacao.created_by });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StandardPageLayout
|
||||
title="Solicitação de Compras"
|
||||
subtitle="Gerencie suas solicitações de compras de materiais"
|
||||
>
|
||||
<StandardCard title="Solicitações de Compra">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<Button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Nova Solicitação
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8">Carregando solicitações...</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-border">
|
||||
<thead className="bg-muted">
|
||||
<tr>
|
||||
<th className="px-2 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-20">
|
||||
Número SC
|
||||
</th>
|
||||
<th className="px-2 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-32">
|
||||
Usuário
|
||||
</th>
|
||||
<th className="px-2 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-20">
|
||||
Data
|
||||
</th>
|
||||
<th className="px-2 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-24">
|
||||
OF/Objetivo
|
||||
</th>
|
||||
<th className="px-2 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-20">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-2 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-16">
|
||||
Revisão
|
||||
</th>
|
||||
<th className="px-2 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-16">
|
||||
Itens
|
||||
</th>
|
||||
<th className="px-2 py-3 text-right text-xs font-medium text-muted-foreground uppercase tracking-wider w-32">
|
||||
Ações
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-background divide-y divide-border">
|
||||
{solicitacoes.map((solicitacao) => (
|
||||
<tr key={solicitacao.id} className="hover:bg-muted/50">
|
||||
<td className="px-2 py-3 whitespace-nowrap">
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{solicitacao.numero_sc}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-3 whitespace-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<UserAvatar
|
||||
imageUrl={solicitacao.creator?.profile_image_url}
|
||||
name={solicitacao.creator?.full_name}
|
||||
email={solicitacao.creator?.email}
|
||||
size="sm"
|
||||
/>
|
||||
<div className="text-sm text-foreground">
|
||||
{solicitacao.creator?.full_name || 'Usuário não encontrado'}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-3 whitespace-nowrap">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{format(new Date(solicitacao.data_solicitacao), 'dd/MM/yyyy', { locale: ptBR })}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-3 whitespace-nowrap">
|
||||
<div className="text-sm text-foreground">
|
||||
{solicitacao.of_number || solicitacao.objetivo}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-3 whitespace-nowrap">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`${statusColors[solicitacao.status as keyof typeof statusColors]} text-white text-xs ${
|
||||
isComprador && solicitacao.status === 'Em planejamento' ? 'cursor-pointer hover:opacity-80 select-none' : ''
|
||||
}`}
|
||||
onDoubleClick={
|
||||
isComprador && solicitacao.status === 'Em planejamento'
|
||||
? (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleComprarDireto(solicitacao);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
title={
|
||||
isComprador && solicitacao.status === 'Em planejamento'
|
||||
? 'Duplo clique para marcar como "Comprado"'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{solicitacao.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-2 py-3 whitespace-nowrap">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{solicitacao.revisao}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-3 whitespace-nowrap">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{solicitacao.itens?.length || 0}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-2 py-3 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div className="flex justify-end space-x-1">
|
||||
{/* Ações básicas - sempre visíveis */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handlePreview(solicitacao)}
|
||||
title="Visualizar"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handlePrint(solicitacao)}
|
||||
title="Imprimir"
|
||||
>
|
||||
<Printer className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* Ações condicionais baseadas em permissões */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(solicitacao)}
|
||||
title="Editar"
|
||||
disabled={!canEdit(solicitacao)}
|
||||
className={!canEdit(solicitacao) ? "opacity-50 cursor-not-allowed" : ""}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(solicitacao)}
|
||||
title="Excluir"
|
||||
disabled={!canDelete(solicitacao)}
|
||||
className={`${!canDelete(solicitacao) ? "opacity-50 cursor-not-allowed" : "text-red-600 hover:text-red-700"}`}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* Ações exclusivas para compradores */}
|
||||
{isComprador && (
|
||||
<>
|
||||
{/* Botão Revisar - só para status 'Em planejamento' */}
|
||||
{solicitacao.status === 'Em planejamento' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRevisar(solicitacao)}
|
||||
title="Enviar para Revisão"
|
||||
disabled={isRevisando}
|
||||
className="text-orange-600 hover:text-orange-700"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Botão Aceitar - para status 'Em planejamento' e 'Revisado' */}
|
||||
{(solicitacao.status === 'Em planejamento' || solicitacao.status === 'Revisado') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleAceitar(solicitacao)}
|
||||
title="Aceitar Solicitação"
|
||||
disabled={isAceitando}
|
||||
className="text-green-600 hover:text-green-700"
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Botão Comprar - só para status 'Solicitado' */}
|
||||
{solicitacao.status === 'Solicitado' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleComprar(solicitacao)}
|
||||
title="Marcar como Comprado"
|
||||
disabled={isComprando}
|
||||
className="text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
<ShoppingCart className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</StandardCard>
|
||||
|
||||
<SolicitacaoComprasModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedSolicitacao(null);
|
||||
}}
|
||||
solicitacao={selectedSolicitacao}
|
||||
/>
|
||||
|
||||
<SolicitacaoComprasPreviewModal
|
||||
isOpen={isPreviewOpen}
|
||||
onClose={() => {
|
||||
setIsPreviewOpen(false);
|
||||
setSelectedSolicitacao(null);
|
||||
}}
|
||||
solicitacao={selectedSolicitacao}
|
||||
/>
|
||||
</StandardPageLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user