🚀 Initial commit: Versão atual do TrackSteel APP

This commit is contained in:
2026-03-18 21:17:53 +00:00
commit bde410c9ad
633 changed files with 108150 additions and 0 deletions
@@ -0,0 +1,162 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Paperclip, X, FileText, Image, FileSpreadsheet } from 'lucide-react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
interface FileUploadSectionProps {
onFilesChange: (files: File[], urls: string[]) => void;
initialFiles?: string[];
}
const MAX_FILE_SIZE = 300 * 1024; // 300KB
const ALLOWED_TYPES = [
'text/plain',
'application/pdf',
'image/jpeg',
'image/png',
'image/gif',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/csv'
];
export function FileUploadSection({ onFilesChange, initialFiles = [] }: FileUploadSectionProps) {
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [uploadedUrls, setUploadedUrls] = useState<string[]>(initialFiles);
const [uploading, setUploading] = useState(false);
const getFileIcon = (type: string) => {
if (type.startsWith('image/')) return <Image className="h-4 w-4" />;
if (type.includes('sheet') || type.includes('excel') || type.includes('csv')) return <FileSpreadsheet className="h-4 w-4" />;
return <FileText className="h-4 w-4" />;
};
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(event.target.files || []);
// Validar arquivos
const validFiles = files.filter(file => {
if (file.size > MAX_FILE_SIZE) {
toast.error(`Arquivo ${file.name} excede 300KB`);
return false;
}
if (!ALLOWED_TYPES.includes(file.type)) {
toast.error(`Tipo de arquivo ${file.type} não permitido`);
return false;
}
return true;
});
const newFiles = [...selectedFiles, ...validFiles];
setSelectedFiles(newFiles);
onFilesChange(newFiles, uploadedUrls);
};
const removeFile = (index: number) => {
const newFiles = selectedFiles.filter((_, i) => i !== index);
setSelectedFiles(newFiles);
onFilesChange(newFiles, uploadedUrls);
};
const uploadFiles = async () => {
if (selectedFiles.length === 0) return;
setUploading(true);
const newUrls: string[] = [];
try {
const { data: userData } = await supabase.auth.getUser();
if (!userData.user) throw new Error('Usuário não autenticado');
for (const file of selectedFiles) {
const fileName = `${userData.user.id}/${Date.now()}-${file.name}`;
const { error: uploadError } = await supabase.storage
.from('solicitacao-compras-anexos')
.upload(fileName, file);
if (uploadError) throw uploadError;
const { data: { publicUrl } } = supabase.storage
.from('solicitacao-compras-anexos')
.getPublicUrl(fileName);
newUrls.push(publicUrl);
}
const allUrls = [...uploadedUrls, ...newUrls];
setUploadedUrls(allUrls);
setSelectedFiles([]);
onFilesChange([], allUrls);
toast.success('Arquivos enviados com sucesso!');
} catch (error) {
console.error('Erro no upload:', error);
toast.error('Erro ao enviar arquivos');
} finally {
setUploading(false);
}
};
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Input
type="file"
multiple
accept=".txt,.pdf,.jpg,.jpeg,.png,.gif,.xls,.xlsx,.csv"
onChange={handleFileSelect}
className="flex-1"
/>
<Button
type="button"
variant="outline"
onClick={uploadFiles}
disabled={selectedFiles.length === 0 || uploading}
>
<Paperclip className="h-4 w-4 mr-2" />
{uploading ? 'Enviando...' : 'Anexar'}
</Button>
</div>
{selectedFiles.length > 0 && (
<div className="space-y-2">
<p className="text-sm text-muted-foreground">Arquivos selecionados:</p>
<div className="flex flex-wrap gap-2">
{selectedFiles.map((file, index) => (
<Badge key={index} variant="secondary" className="flex items-center gap-1 pr-1">
{getFileIcon(file.type)}
<span className="text-xs">{file.name}</span>
<Button
size="sm"
variant="ghost"
className="h-4 w-4 p-0 hover:bg-destructive hover:text-destructive-foreground"
onClick={() => removeFile(index)}
>
<X className="h-3 w-3" />
</Button>
</Badge>
))}
</div>
</div>
)}
{uploadedUrls.length > 0 && (
<div className="space-y-2">
<p className="text-sm text-muted-foreground">Arquivos anexados:</p>
<div className="flex flex-wrap gap-2">
{uploadedUrls.map((url, index) => (
<Badge key={index} variant="default" className="flex items-center gap-1">
<Paperclip className="h-3 w-3" />
<span className="text-xs">Anexo {index + 1}</span>
</Badge>
))}
</div>
</div>
)}
</div>
);
}
@@ -0,0 +1,235 @@
import React, { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useMateriaisEstoque } from '@/hooks/useMateriaisEstoque';
import { useTiposMateriais } from '@/hooks/useTiposMateriais';
interface SeletorMateriaisModalProps {
isOpen: boolean;
onClose: () => void;
onAddItem: (item: {
material_id: string;
descricao: string;
unidade: string;
quantidade: number;
prazo_recebimento: string;
}) => void;
editingItem?: {
material_id: string;
descricao: string;
unidade: string;
quantidade: number;
prazo_recebimento: string;
};
}
export const SeletorMateriaisModal: React.FC<SeletorMateriaisModalProps> = ({
isOpen,
onClose,
onAddItem,
editingItem
}) => {
const [filtros, setFiltros] = useState({
tipo: '',
busca: ''
});
const [selectedMaterial, setSelectedMaterial] = useState('');
const [quantidade, setQuantidade] = useState('');
const [prazoRecebimento, setPrazoRecebimento] = useState('');
const { materiais, isLoading: isLoadingMateriais } = useMateriaisEstoque(filtros);
const { tiposMateriais, isLoading: isLoadingTipos } = useTiposMateriais();
// Carregar dados do item em edição
useEffect(() => {
if (editingItem) {
setSelectedMaterial(editingItem.material_id);
setQuantidade(editingItem.quantidade.toString());
setPrazoRecebimento(editingItem.prazo_recebimento);
} else {
// Reset form apenas quando não estiver editando
setSelectedMaterial('');
setQuantidade('');
setPrazoRecebimento('');
}
}, [editingItem, isOpen]);
// Reset form quando modal fechar
useEffect(() => {
if (!isOpen && !editingItem) {
setSelectedMaterial('');
setQuantidade('');
setPrazoRecebimento('');
}
}, [isOpen, editingItem]);
const handleAddItem = () => {
if (!selectedMaterial || !quantidade || !prazoRecebimento) {
alert('Preencha todos os campos obrigatórios');
return;
}
const materialData = materiais.find(m => m.id === selectedMaterial);
if (!materialData) {
alert('Material não encontrado');
return;
}
const quantidadeNum = parseFloat(quantidade);
if (isNaN(quantidadeNum) || quantidadeNum <= 0) {
alert('Quantidade deve ser um número válido maior que zero');
return;
}
onAddItem({
material_id: selectedMaterial,
descricao: materialData.descricao,
unidade: materialData.unidade,
quantidade: quantidadeNum,
prazo_recebimento: prazoRecebimento
});
// Reset form apenas se não estiver editando
if (!editingItem) {
setSelectedMaterial('');
setQuantidade('');
setPrazoRecebimento('');
}
onClose();
};
const selectedMaterialData = materiais.find(m => m.id === selectedMaterial);
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{editingItem ? 'Editar Material' : 'Adicionar Material'}</DialogTitle>
</DialogHeader>
<div className="space-y-6">
{/* Filtros */}
<div className="bg-muted p-4 rounded-lg">
<h3 className="text-lg font-medium mb-4">Filtros</h3>
<div className="flex flex-col sm:flex-row space-y-4 sm:space-y-0 sm:space-x-4">
<div className="flex-1">
<Label htmlFor="tipo-filter">Tipo de Material</Label>
<Select value={filtros.tipo} onValueChange={(value) => setFiltros(prev => ({ ...prev, tipo: value === 'todos' ? '' : value }))}>
<SelectTrigger>
<SelectValue placeholder="Todos os Tipos" />
</SelectTrigger>
<SelectContent>
<SelectItem value="todos">Todos os Tipos</SelectItem>
{tiposMateriais.map((tipo) => (
<SelectItem key={tipo.id} value={tipo.nome}>
{tipo.nome}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex-1">
<Label htmlFor="busca-filter">Buscar Material</Label>
<Input
id="busca-filter"
placeholder="Digite para buscar..."
value={filtros.busca}
onChange={(e) => setFiltros(prev => ({ ...prev, busca: e.target.value }))}
/>
</div>
</div>
</div>
{/* Seleção de Material */}
<div className="space-y-4">
<div>
<Label htmlFor="material">Material *</Label>
<Select value={selectedMaterial} onValueChange={setSelectedMaterial}>
<SelectTrigger>
<SelectValue placeholder="Selecione um material" />
</SelectTrigger>
<SelectContent>
{isLoadingMateriais ? (
<SelectItem value="loading" disabled>Carregando materiais...</SelectItem>
) : materiais.length === 0 ? (
<SelectItem value="empty" disabled>Nenhum material encontrado</SelectItem>
) : (
materiais.map((material) => (
<SelectItem key={material.id} value={material.id}>
{material.codigo} - {material.descricao}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
{selectedMaterialData && (
<div className="bg-muted p-3 rounded-lg">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<strong>Código:</strong> {selectedMaterialData.codigo}
</div>
<div>
<strong>Unidade:</strong> {selectedMaterialData.unidade}
</div>
<div>
<strong>Tipo:</strong> {selectedMaterialData.tipo_material_nome || 'N/A'}
</div>
<div>
<strong>Disponível:</strong> {selectedMaterialData.quantidade_disponivel || 0}
</div>
</div>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="quantidade">Quantidade *</Label>
<Input
id="quantidade"
type="number"
step="0.01"
min="0.01"
placeholder="Digite a quantidade"
value={quantidade}
onChange={(e) => setQuantidade(e.target.value)}
/>
</div>
<div>
<Label htmlFor="prazo">Prazo de Recebimento *</Label>
<Input
id="prazo"
type="date"
value={prazoRecebimento}
onChange={(e) => setPrazoRecebimento(e.target.value)}
/>
</div>
</div>
</div>
{/* Botões */}
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={onClose}>
Cancelar
</Button>
<Button
onClick={handleAddItem}
disabled={!selectedMaterial || !quantidade || !prazoRecebimento}
>
{editingItem ? 'Atualizar Item' : 'Adicionar Item'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,338 @@
import React, { useState, useEffect } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Plus, Trash2 } from 'lucide-react';
import { toast } from 'sonner';
import { useSolicitacoesCompra } from '@/hooks/useSolicitacoesCompra';
import { useOFs } from '@/hooks/useOFs';
import { SeletorMateriaisModal } from './SeletorMateriaisModal';
import { FileUploadSection } from './FileUploadSection';
interface ItemSolicitacao {
material_id: string;
quantidade: number;
prazo_recebimento: string;
material?: {
descricao: string;
unidade: string;
};
}
interface SolicitacaoComprasModalProps {
isOpen: boolean;
onClose: () => void;
solicitacao?: any;
}
export function SolicitacaoComprasModal({ isOpen, onClose, solicitacao }: SolicitacaoComprasModalProps) {
const [vinculo, setVinculo] = useState<'of' | 'objetivo'>('of');
const [formData, setFormData] = useState({
data_solicitacao: new Date().toISOString().split('T')[0],
of_number: '',
objetivo: '',
justificativa: '',
});
const [itens, setItens] = useState<ItemSolicitacao[]>([]);
const [isSeletorOpen, setIsSeletorOpen] = useState(false);
const [anexosFiles, setAnexosFiles] = useState<File[]>([]);
const [anexosUrls, setAnexosUrls] = useState<string[]>([]);
const { createSolicitacao, isCreating } = useSolicitacoesCompra();
const { ofs, loading } = useOFs();
useEffect(() => {
if (solicitacao) {
setFormData({
data_solicitacao: solicitacao.data_solicitacao,
of_number: solicitacao.of_number || '',
objetivo: solicitacao.objetivo || '',
justificativa: solicitacao.justificativa || '',
});
setVinculo(solicitacao.of_number ? 'of' : 'objetivo');
setItens(solicitacao.itens || []);
setAnexosUrls(solicitacao.anexos_urls || []);
} else {
resetForm();
}
}, [solicitacao]);
const resetForm = () => {
setVinculo('of');
setFormData({
data_solicitacao: new Date().toISOString().split('T')[0],
of_number: '',
objetivo: '',
justificativa: '',
});
setItens([]);
setAnexosFiles([]);
setAnexosUrls([]);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.justificativa.trim()) {
toast.error('Justificativa é obrigatória');
return;
}
if (vinculo === 'of' && !formData.of_number.trim()) {
toast.error('Selecione uma OF');
return;
}
if (vinculo === 'objetivo' && !formData.objetivo.trim()) {
toast.error('Informe o objetivo');
return;
}
if (itens.length === 0) {
toast.error('Adicione pelo menos um item à solicitação');
return;
}
const solicitacaoData = {
...formData,
of_number: vinculo === 'of' ? formData.of_number : null,
objetivo: vinculo === 'objetivo' ? formData.objetivo : null,
itens: itens.map(item => ({
material_id: item.material_id,
quantidade: item.quantidade,
prazo_recebimento: item.prazo_recebimento,
})),
anexos_urls: anexosUrls,
};
createSolicitacao(solicitacaoData);
onClose();
resetForm();
};
const adicionarItem = (item: {
material_id: string;
descricao: string;
unidade: string;
quantidade: number;
prazo_recebimento: string;
}) => {
const novoItem: ItemSolicitacao = {
material_id: item.material_id,
quantidade: item.quantidade,
prazo_recebimento: item.prazo_recebimento,
material: {
descricao: item.descricao,
unidade: item.unidade,
},
};
setItens([...itens, novoItem]);
};
const removerItem = (index: number) => {
setItens(itens.filter((_, i) => i !== index));
};
const atualizarItem = (index: number, campo: string, valor: any) => {
const novosItens = [...itens];
novosItens[index] = { ...novosItens[index], [campo]: valor };
setItens(novosItens);
};
const handleFilesChange = (files: File[], urls: string[]) => {
setAnexosFiles(files);
setAnexosUrls(urls);
};
return (
<>
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{solicitacao ? 'Editar Solicitação de Compra' : 'Nova Solicitação de Compra'}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="data_solicitacao">Data da Solicitação</Label>
<Input
id="data_solicitacao"
type="date"
value={formData.data_solicitacao}
onChange={(e) => setFormData({ ...formData, data_solicitacao: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label>Vínculo</Label>
<RadioGroup
value={vinculo}
onValueChange={(value: 'of' | 'objetivo') => {
setVinculo(value);
setFormData({ ...formData, of_number: '', objetivo: '' });
}}
className="flex space-x-4"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="of" id="vinculo-of" />
<Label htmlFor="vinculo-of">Vincular a uma OF</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="objetivo" id="vinculo-objetivo" />
<Label htmlFor="vinculo-objetivo">Objetivo (ADM, Fábrica, Estoque)</Label>
</div>
</RadioGroup>
</div>
</div>
{vinculo === 'of' && (
<div className="space-y-2">
<Label htmlFor="of_number">Número da OF</Label>
<Select
value={formData.of_number}
onValueChange={(value) => setFormData({ ...formData, of_number: value })}
>
<SelectTrigger>
<SelectValue placeholder="Selecione uma OF" />
</SelectTrigger>
<SelectContent>
{ofs.map((of) => (
<SelectItem key={of.num_of} value={of.num_of}>
{of.num_of} - {of.descritivo || 'Sem descrição'}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{vinculo === 'objetivo' && (
<div className="space-y-2">
<Label htmlFor="objetivo">Objetivo</Label>
<Input
id="objetivo"
value={formData.objetivo}
onChange={(e) => setFormData({ ...formData, objetivo: e.target.value })}
placeholder="Descreva o objetivo da solicitação"
/>
</div>
)}
<div className="space-y-2">
<Label htmlFor="justificativa">Justificativa / Comentários *</Label>
<Textarea
id="justificativa"
value={formData.justificativa}
onChange={(e) => setFormData({ ...formData, justificativa: e.target.value })}
placeholder="Digite a justificativa para a solicitação..."
required
/>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label>Anexos</Label>
</div>
<FileUploadSection
onFilesChange={handleFilesChange}
initialFiles={anexosUrls}
/>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label>Adicione os materiais necessários para a sua solicitação de compra.</Label>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setIsSeletorOpen(true)}
>
<Plus className="h-4 w-4 mr-2" />
Adicionar Item
</Button>
</div>
{itens.length > 0 && (
<div className="space-y-3">
<div className="grid grid-cols-12 gap-2 text-sm font-medium text-muted-foreground border-b pb-2">
<div className="col-span-4">DESCRIÇÃO DO MATERIAL</div>
<div className="col-span-2">QUANTIDADE</div>
<div className="col-span-2">UNIDADE</div>
<div className="col-span-3">PRAZO DE RECEBIMENTO</div>
<div className="col-span-1">AÇÕES</div>
</div>
{itens.map((item, index) => (
<div key={index} className="grid grid-cols-12 gap-2 items-center p-2 border rounded">
<div className="col-span-4">
<p className="text-sm">{item.material?.descricao}</p>
</div>
<div className="col-span-2">
<Input
type="number"
min="0.01"
step="0.01"
value={item.quantidade}
onChange={(e) => atualizarItem(index, 'quantidade', parseFloat(e.target.value) || 0)}
className="text-sm"
/>
</div>
<div className="col-span-2">
<p className="text-sm">{item.material?.unidade}</p>
</div>
<div className="col-span-3">
<Input
type="date"
value={item.prazo_recebimento}
onChange={(e) => atualizarItem(index, 'prazo_recebimento', e.target.value)}
className="text-sm"
/>
</div>
<div className="col-span-1">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removerItem(index)}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
)}
</div>
<div className="flex justify-end space-x-3">
<Button type="button" variant="outline" onClick={onClose}>
Cancelar
</Button>
<Button type="submit" disabled={isCreating}>
{isCreating ? 'Salvando...' : solicitacao ? 'Atualizar' : 'Salvar Solicitação'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
<SeletorMateriaisModal
isOpen={isSeletorOpen}
onClose={() => setIsSeletorOpen(false)}
onAddItem={adicionarItem}
/>
</>
);
}
@@ -0,0 +1,211 @@
import React from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Download, X } from 'lucide-react';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import { UserAvatar } from '@/components/ui/user-avatar';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
interface SolicitacaoComprasPreviewModalProps {
isOpen: boolean;
onClose: () => void;
solicitacao: any;
}
const statusColors = {
'Em planejamento': 'bg-yellow-500',
'Revisado': 'bg-orange-500',
'Solicitado': 'bg-blue-500',
'Comprado': 'bg-green-500',
'Recebido': 'bg-lime-500',
'Arquivado': 'bg-red-500',
};
export function SolicitacaoComprasPreviewModal({ isOpen, onClose, solicitacao }: SolicitacaoComprasPreviewModalProps) {
if (!solicitacao) return null;
const downloadAnexo = async (url: string) => {
try {
// Extrair o caminho do arquivo da URL
const filePath = url.split('/').slice(-2).join('/'); // pega bucket/path
const { data, error } = await supabase.storage
.from('solicitacao-compras-anexos')
.download(filePath);
if (error) {
console.error('Erro ao baixar arquivo:', error);
toast.error('Erro ao baixar arquivo');
return;
}
const fileName = url.split('/').pop() || 'anexo';
const link = document.createElement('a');
link.href = URL.createObjectURL(data);
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
toast.success('Arquivo baixado com sucesso!');
} catch (error) {
console.error('Erro ao baixar anexo:', error);
toast.error('Erro ao baixar arquivo');
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader className="flex flex-row items-center justify-between">
<DialogTitle>Preview - Solicitação de Compra {solicitacao.numero_sc}</DialogTitle>
<Button onClick={onClose} variant="ghost" size="sm">
<X className="w-4 h-4" />
</Button>
</DialogHeader>
<div className="space-y-6">
{/* Informações básicas */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-muted rounded-lg">
<div>
<h3 className="font-semibold mb-2">Informações Gerais</h3>
<div className="space-y-2">
<div>
<span className="font-medium">Número SC: </span>
{solicitacao.numero_sc}
</div>
<div>
<span className="font-medium">Data: </span>
{format(new Date(solicitacao.data_solicitacao), 'dd/MM/yyyy', { locale: ptBR })}
</div>
<div>
<span className="font-medium">Status: </span>
<Badge
variant="secondary"
className={`${statusColors[solicitacao.status as keyof typeof statusColors]} text-white ml-2`}
>
{solicitacao.status}
</Badge>
</div>
<div>
<span className="font-medium">Revisão: </span>
{solicitacao.revisao}
</div>
</div>
</div>
<div>
<h3 className="font-semibold mb-2">Solicitante</h3>
<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>
<div className="font-medium">{solicitacao.creator?.full_name}</div>
<div className="text-sm text-muted-foreground">{solicitacao.creator?.email}</div>
</div>
</div>
</div>
</div>
{/* OF/Objetivo */}
{(solicitacao.of_number || solicitacao.objetivo) && (
<div className="p-4 bg-muted rounded-lg">
<h3 className="font-semibold mb-2">
{solicitacao.of_number ? 'Ordem de Fabricação' : 'Objetivo'}
</h3>
<p>{solicitacao.of_number || solicitacao.objetivo}</p>
</div>
)}
{/* Justificativa */}
{solicitacao.justificativa && (
<div className="p-4 bg-muted rounded-lg">
<h3 className="font-semibold mb-2">Justificativa</h3>
<p>{solicitacao.justificativa}</p>
</div>
)}
{/* Itens */}
{solicitacao.itens && solicitacao.itens.length > 0 && (
<div className="space-y-3">
<h3 className="font-semibold">Itens Solicitados</h3>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-border">
<thead className="bg-muted">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Descrição
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Unidade
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Quantidade
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Prazo Recebimento
</th>
</tr>
</thead>
<tbody className="bg-background divide-y divide-border">
{solicitacao.itens.map((item: any, index: number) => (
<tr key={index}>
<td className="px-4 py-3 text-sm">
{item.material?.descricao || 'Material não encontrado'}
</td>
<td className="px-4 py-3 text-sm">
{item.material?.unidade || 'UN'}
</td>
<td className="px-4 py-3 text-sm">
{item.quantidade}
</td>
<td className="px-4 py-3 text-sm">
{format(new Date(item.prazo_recebimento), 'dd/MM/yyyy', { locale: ptBR })}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Anexos */}
{solicitacao.anexos_urls && solicitacao.anexos_urls.length > 0 && (
<div className="space-y-3">
<h3 className="font-semibold">Anexos</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{solicitacao.anexos_urls.map((url: string, index: number) => {
const fileName = url.split('/').pop();
return (
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
<span className="text-sm truncate">{fileName}</span>
<Button
variant="outline"
size="sm"
onClick={() => downloadAnexo(url)}
className="flex items-center gap-2"
>
<Download className="w-4 h-4" />
Baixar
</Button>
</div>
);
})}
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}