feat: implement filtering and document management features in dashboard components, enhancing user experience with improved query capabilities and UI for managing documents in pedidos and compras

This commit is contained in:
2025-12-15 14:29:30 -03:00
parent f3288b9639
commit a5ad843b3e
7 changed files with 1135 additions and 34 deletions

View File

@@ -8,7 +8,21 @@
const client = useConvexClient();
// Reactive queries
const atasQuery = useQuery(api.atas.list, {});
// Filtros (listagem)
let filtroPeriodoInicio = $state('');
let filtroPeriodoFim = $state('');
let filtroNumero = $state('');
let filtroNumeroSei = $state('');
const atasTotalQuery = useQuery(api.atas.list, {});
let atasTotal = $derived(atasTotalQuery.data || []);
const atasQuery = useQuery(api.atas.list, () => ({
periodoInicio: filtroPeriodoInicio || undefined,
periodoFim: filtroPeriodoFim || undefined,
numero: filtroNumero.trim() || undefined,
numeroSei: filtroNumeroSei.trim() || undefined
}));
let atas = $derived(atasQuery.data || []);
let loadingAtas = $derived(atasQuery.isLoading);
let errorAtas = $derived(atasQuery.error?.message || null);
@@ -188,6 +202,13 @@
attachmentFiles = Array.from(input.files);
}
}
function limparFiltros() {
filtroPeriodoInicio = '';
filtroPeriodoFim = '';
filtroNumero = '';
filtroNumeroSei = '';
}
</script>
<main class="container mx-auto flex max-w-7xl flex-col px-4 py-4">
@@ -220,6 +241,71 @@
</div>
</div>
<div class="card bg-base-100/90 border-base-300 mb-6 border shadow-xl backdrop-blur-sm">
<div class="card-body">
<div class="grid gap-4 sm:grid-cols-[1fr_auto] sm:items-end">
<div class="grid grid-cols-1 gap-4 md:grid-cols-4">
<div class="form-control w-full">
<label class="label" for="filtro_numero">
<span class="label-text font-semibold">Número</span>
</label>
<input
id="filtro_numero"
class="input input-bordered focus:input-primary w-full"
type="text"
placeholder="Ex.: 12/2025"
bind:value={filtroNumero}
/>
</div>
<div class="form-control w-full">
<label class="label" for="filtro_numeroSei">
<span class="label-text font-semibold">Número SEI</span>
</label>
<input
id="filtro_numeroSei"
class="input input-bordered focus:input-primary w-full"
type="text"
placeholder="Ex.: 12345.000000/2025-00"
bind:value={filtroNumeroSei}
/>
</div>
<div class="form-control w-full">
<label class="label" for="filtro_inicio">
<span class="label-text font-semibold">Período (início)</span>
</label>
<input
id="filtro_inicio"
class="input input-bordered focus:input-primary w-full"
type="date"
bind:value={filtroPeriodoInicio}
/>
</div>
<div class="form-control w-full">
<label class="label" for="filtro_fim">
<span class="label-text font-semibold">Período (fim)</span>
</label>
<input
id="filtro_fim"
class="input input-bordered focus:input-primary w-full"
type="date"
bind:value={filtroPeriodoFim}
/>
</div>
</div>
<div class="flex items-center justify-between gap-3 sm:min-w-max sm:justify-end">
<div class="text-base-content/70 text-sm">
{atas.length} de {atasTotal.length}
</div>
<button type="button" class="btn btn-ghost" onclick={limparFiltros}>Limpar</button>
</div>
</div>
</div>
</div>
{#if loadingAtas}
<div class="flex items-center justify-center py-10">
<span class="loading loading-spinner loading-lg"></span>
@@ -258,8 +344,13 @@
<tr>
<td colspan="5" class="py-12 text-center">
<div class="text-base-content/60 flex flex-col items-center gap-2">
<p class="text-lg font-semibold">Nenhuma ata cadastrada</p>
<p class="text-sm">Clique em “Nova Ata” para cadastrar.</p>
{#if atasTotal.length === 0}
<p class="text-lg font-semibold">Nenhuma ata cadastrada</p>
<p class="text-sm">Clique em “Nova Ata” para cadastrar.</p>
{:else}
<p class="text-lg font-semibold">Nenhum resultado encontrado</p>
<p class="text-sm">Ajuste ou limpe os filtros para ver resultados.</p>
{/if}
</div>
</td>
</tr>

View File

@@ -9,7 +9,19 @@
const client = useConvexClient();
// Reactive queries
const objetosQuery = useQuery(api.objetos.list, {});
// Filtros (listagem)
let filtroNome = $state('');
let filtroTipo = $state<'todos' | 'material' | 'servico'>('todos');
let filtroCodigos = $state('');
const objetosTotalQuery = useQuery(api.objetos.list, {});
let objetosTotal = $derived(objetosTotalQuery.data || []);
const objetosQuery = useQuery(api.objetos.list, () => ({
nome: filtroNome.trim() || undefined,
tipo: filtroTipo === 'todos' ? undefined : filtroTipo,
codigos: filtroCodigos.trim() || undefined
}));
let objetos = $derived(objetosQuery.data || []);
let loading = $derived(objetosQuery.isLoading);
let error = $derived(objetosQuery.error?.message || null);
@@ -115,6 +127,12 @@
formData.atas = [...formData.atas, ataId];
}
}
function limparFiltros() {
filtroNome = '';
filtroTipo = 'todos';
filtroCodigos = '';
}
</script>
<main class="container mx-auto flex max-w-7xl flex-col px-4 py-4">
@@ -147,6 +165,62 @@
</div>
</div>
<div class="card bg-base-100/90 border-base-300 mb-6 border shadow-xl backdrop-blur-sm">
<div class="card-body">
<div class="grid gap-4 sm:grid-cols-[1fr_auto] sm:items-end">
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="form-control w-full">
<label class="label" for="filtro_nome">
<span class="label-text font-semibold">Nome</span>
</label>
<input
id="filtro_nome"
class="input input-bordered focus:input-primary w-full"
type="text"
placeholder="Digite para filtrar..."
bind:value={filtroNome}
/>
</div>
<div class="form-control w-full">
<label class="label" for="filtro_tipo">
<span class="label-text font-semibold">Tipo</span>
</label>
<select
id="filtro_tipo"
class="select select-bordered focus:select-primary w-full"
bind:value={filtroTipo}
>
<option value="todos">Todos</option>
<option value="material">Material</option>
<option value="servico">Serviço</option>
</select>
</div>
<div class="form-control w-full">
<label class="label" for="filtro_codigos">
<span class="label-text font-semibold">Códigos</span>
</label>
<input
id="filtro_codigos"
class="input input-bordered focus:input-primary w-full"
type="text"
placeholder="Efisco / Catmat / Catserv"
bind:value={filtroCodigos}
/>
</div>
</div>
<div class="flex items-center justify-between gap-3 sm:min-w-max sm:justify-end">
<div class="text-base-content/70 text-sm">
{objetos.length} de {objetosTotal.length}
</div>
<button type="button" class="btn btn-ghost" onclick={limparFiltros}>Limpar</button>
</div>
</div>
</div>
</div>
{#if loading}
<div class="flex items-center justify-center py-10">
<span class="loading loading-spinner loading-lg"></span>
@@ -185,8 +259,13 @@
<tr>
<td colspan="5" class="py-12 text-center">
<div class="text-base-content/60 flex flex-col items-center gap-2">
<p class="text-lg font-semibold">Nenhum objeto cadastrado</p>
<p class="text-sm">Clique em “Novo Objeto” para cadastrar.</p>
{#if objetosTotal.length === 0}
<p class="text-lg font-semibold">Nenhum objeto cadastrado</p>
<p class="text-sm">Clique em “Novo Objeto” para cadastrar.</p>
{:else}
<p class="text-lg font-semibold">Nenhum resultado encontrado</p>
<p class="text-sm">Ajuste ou limpe os filtros para ver resultados.</p>
{/if}
</div>
</td>
</tr>

View File

@@ -12,6 +12,7 @@
Edit,
Eye,
Plus,
Upload,
Save,
Send,
Trash2,
@@ -34,6 +35,9 @@
const acoesQuery = $derived.by(() => useQuery(api.acoes.list, {}));
const permissionsQuery = $derived.by(() => useQuery(api.pedidos.getPermissions, { pedidoId }));
const requestsQuery = $derived.by(() => useQuery(api.pedidos.getItemRequests, { pedidoId }));
const pedidoDocumentosQuery = $derived.by(() =>
useQuery(api.pedidos.listPedidoDocumentos, { pedidoId })
);
// Derived state
let pedido = $derived(pedidoQuery.data);
@@ -43,11 +47,19 @@
let acoes = $derived(acoesQuery.data || []);
let permissions = $derived(permissionsQuery.data);
let requests = $derived(requestsQuery.data || []);
let pedidoDocumentos = $derived(pedidoDocumentosQuery.data || []);
let currentFuncionarioId = $derived(permissions?.currentFuncionarioId ?? null);
type Modalidade = 'dispensa' | 'inexgibilidade' | 'adesao' | 'consumo';
function coerceModalidade(value: string): Modalidade {
if (value === 'dispensa' || value === 'inexgibilidade' || value === 'adesao' || value === 'consumo') {
return value;
}
return 'consumo';
}
type EditingItem = {
valorEstimado: string;
modalidade: Modalidade;
@@ -125,7 +137,8 @@
objetosQuery.isLoading ||
acoesQuery.isLoading ||
permissionsQuery.isLoading ||
requestsQuery.isLoading
requestsQuery.isLoading ||
pedidoDocumentosQuery.isLoading
);
let error = $derived(
@@ -137,6 +150,213 @@
null
);
// Documentos do Pedido
let showAddPedidoDocumento = $state(false);
let pedidoDocumentoDescricao = $state('');
let pedidoDocumentoFile = $state<File | null>(null);
let salvandoPedidoDocumento = $state(false);
function formatBytes(bytes: number) {
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
let value = bytes;
let idx = 0;
while (value >= 1024 && idx < units.length - 1) {
value /= 1024;
idx += 1;
}
return `${value.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`;
}
async function uploadToStorage(uploadUrl: string, file: File): Promise<Id<'_storage'>> {
const result = await fetch(uploadUrl, {
method: 'POST',
headers: { 'Content-Type': file.type },
body: file
});
const json = (await result.json()) as { storageId: Id<'_storage'> };
return json.storageId;
}
async function handleAddPedidoDocumento() {
if (!pedido) return;
if (!pedidoDocumentoFile) {
toast.error('Selecione um arquivo.');
return;
}
if (!pedidoDocumentoDescricao.trim()) {
toast.error('Informe uma descrição.');
return;
}
salvandoPedidoDocumento = true;
try {
const uploadUrl = await client.mutation(api.pedidos.generatePedidoUploadUrl, { pedidoId });
const storageId = await uploadToStorage(uploadUrl, pedidoDocumentoFile);
await client.mutation(api.pedidos.addPedidoDocumento, {
pedidoId,
descricao: pedidoDocumentoDescricao.trim(),
nome: pedidoDocumentoFile.name,
storageId,
tipo: pedidoDocumentoFile.type,
tamanho: pedidoDocumentoFile.size
});
pedidoDocumentoDescricao = '';
pedidoDocumentoFile = null;
showAddPedidoDocumento = false;
toast.success('Documento anexado ao pedido.');
} catch (e) {
toast.error('Erro ao anexar documento: ' + (e as Error).message);
} finally {
salvandoPedidoDocumento = false;
}
}
function handleOpenPedidoDocumento(url: string | null) {
if (!url) return;
window.open(url, '_blank');
}
function handleRemovePedidoDocumento(id: Id<'pedidoDocumentos'>) {
openConfirm(
'Remover documento',
'Tem certeza que deseja remover este documento do pedido?',
async () => {
await client.mutation(api.pedidos.removePedidoDocumento, { id });
toast.success('Documento removido.');
},
{ isDestructive: true, confirmText: 'Remover' }
);
}
// Documentos por Solicitação
let showSolicitacaoDocsModal = $state(false);
let solicitacaoDocsRequestId = $state<Id<'solicitacoesItens'> | null>(null);
let solicitacaoDocsSolicitadoPor = $state<Id<'funcionarios'> | null>(null);
let solicitacaoDocsTipo = $state<string | null>(null);
let solicitacaoDocs = $state<any[]>([]);
let carregandoSolicitacaoDocs = $state(false);
let solicitacaoDocumentoDescricao = $state('');
let solicitacaoDocumentoFile = $state<File | null>(null);
let salvandoSolicitacaoDocumento = $state(false);
const canAddDocsToSelectedRequest = $derived(
showSolicitacaoDocsModal &&
solicitacaoDocsTipo === 'adicao' &&
currentFuncionarioId !== null &&
solicitacaoDocsSolicitadoPor !== null &&
currentFuncionarioId === solicitacaoDocsSolicitadoPor
);
async function openSolicitacaoDocs(req: {
_id: Id<'solicitacoesItens'>;
solicitadoPor: Id<'funcionarios'>;
tipo: string;
}) {
solicitacaoDocsRequestId = req._id;
solicitacaoDocsSolicitadoPor = req.solicitadoPor;
solicitacaoDocsTipo = req.tipo;
solicitacaoDocumentoDescricao = '';
solicitacaoDocumentoFile = null;
showSolicitacaoDocsModal = true;
carregandoSolicitacaoDocs = true;
try {
solicitacaoDocs = await client.query(api.pedidos.listSolicitacaoDocumentos, {
requestId: req._id
});
} catch (e) {
toast.error('Erro ao carregar documentos da solicitação: ' + (e as Error).message);
solicitacaoDocs = [];
} finally {
carregandoSolicitacaoDocs = false;
}
}
function closeSolicitacaoDocs() {
showSolicitacaoDocsModal = false;
solicitacaoDocsRequestId = null;
solicitacaoDocsSolicitadoPor = null;
solicitacaoDocsTipo = null;
solicitacaoDocs = [];
solicitacaoDocumentoDescricao = '';
solicitacaoDocumentoFile = null;
}
function handleOpenSolicitacaoDocumento(url: string | null) {
if (!url) return;
window.open(url, '_blank');
}
async function refreshSolicitacaoDocs() {
if (!solicitacaoDocsRequestId) return;
carregandoSolicitacaoDocs = true;
try {
solicitacaoDocs = await client.query(api.pedidos.listSolicitacaoDocumentos, {
requestId: solicitacaoDocsRequestId
});
} finally {
carregandoSolicitacaoDocs = false;
}
}
async function handleAddSolicitacaoDocumento() {
if (!solicitacaoDocsRequestId) return;
if (!canAddDocsToSelectedRequest) {
toast.error('Apenas quem criou a solicitação pode anexar documentos.');
return;
}
if (!solicitacaoDocumentoFile) {
toast.error('Selecione um arquivo.');
return;
}
if (!solicitacaoDocumentoDescricao.trim()) {
toast.error('Informe uma descrição.');
return;
}
salvandoSolicitacaoDocumento = true;
try {
const uploadUrl = await client.mutation(api.pedidos.generateSolicitacaoUploadUrl, {
requestId: solicitacaoDocsRequestId
});
const storageId = await uploadToStorage(uploadUrl, solicitacaoDocumentoFile);
await client.mutation(api.pedidos.addSolicitacaoDocumento, {
requestId: solicitacaoDocsRequestId,
descricao: solicitacaoDocumentoDescricao.trim(),
nome: solicitacaoDocumentoFile.name,
storageId,
tipo: solicitacaoDocumentoFile.type,
tamanho: solicitacaoDocumentoFile.size
});
solicitacaoDocumentoDescricao = '';
solicitacaoDocumentoFile = null;
toast.success('Documento anexado à solicitação.');
await refreshSolicitacaoDocs();
} catch (e) {
toast.error('Erro ao anexar documento: ' + (e as Error).message);
} finally {
salvandoSolicitacaoDocumento = false;
}
}
function handleRemoveSolicitacaoDocumento(id: Id<'solicitacoesItensDocumentos'>) {
openConfirm(
'Remover documento',
'Tem certeza que deseja remover este documento da solicitação?',
async () => {
await client.mutation(api.pedidos.removeSolicitacaoDocumento, { id });
toast.success('Documento removido.');
await refreshSolicitacaoDocs();
},
{ isDestructive: true, confirmText: 'Remover' }
);
}
// Add Item State
let showAddItem = $state(false);
let newItem = $state({
@@ -1001,6 +1221,142 @@
</div>
</div>
<!-- Documentos do Pedido -->
<div class="mb-6 overflow-hidden rounded-lg bg-white shadow-md">
<div class="flex items-center justify-between border-b border-gray-200 px-6 py-4">
<h2 class="text-lg font-semibold">Documentos do Pedido</h2>
<button
type="button"
onclick={() => (showAddPedidoDocumento = !showAddPedidoDocumento)}
class="flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-800"
>
<Plus size={16} /> Adicionar documento
</button>
</div>
{#if showAddPedidoDocumento}
<div class="border-b border-gray-200 bg-gray-50 px-6 py-4">
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-medium text-gray-500" for="pedido-doc-desc"
>Descrição</label
>
<input
id="pedido-doc-desc"
type="text"
bind:value={pedidoDocumentoDescricao}
class="w-full rounded-md border-gray-300 text-sm shadow-sm"
placeholder="Ex: Cotação, Parecer, Termo de referência..."
disabled={salvandoPedidoDocumento}
/>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-500" for="pedido-doc-file"
>Arquivo</label
>
<input
id="pedido-doc-file"
type="file"
accept=".pdf,.jpg,.jpeg,.png"
class="w-full text-sm"
disabled={salvandoPedidoDocumento}
onchange={(e) => {
const f = e.currentTarget.files?.[0] ?? null;
pedidoDocumentoFile = f;
}}
/>
</div>
</div>
<div class="mt-4 flex justify-end gap-2">
<button
type="button"
onclick={() => {
showAddPedidoDocumento = false;
pedidoDocumentoDescricao = '';
pedidoDocumentoFile = null;
}}
class="rounded bg-gray-200 px-3 py-2 text-sm text-gray-700 hover:bg-gray-300"
disabled={salvandoPedidoDocumento}
>
Cancelar
</button>
<button
type="button"
onclick={handleAddPedidoDocumento}
disabled={salvandoPedidoDocumento}
class="flex items-center gap-2 rounded bg-blue-600 px-3 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
>
{#if salvandoPedidoDocumento}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<Upload size={16} />
{/if}
Anexar
</button>
</div>
</div>
{/if}
<div class="p-6">
{#if pedidoDocumentos.length === 0}
<p class="text-sm text-gray-500">Nenhum documento anexado ao pedido.</p>
{:else}
<div class="overflow-x-auto">
<table class="w-full text-left text-sm text-gray-600">
<thead class="bg-gray-50 text-xs font-medium text-gray-500 uppercase">
<tr>
<th class="px-4 py-2">Descrição</th>
<th class="px-4 py-2">Arquivo</th>
<th class="px-4 py-2">Tamanho</th>
<th class="px-4 py-2">Enviado por</th>
<th class="px-4 py-2">Data</th>
<th class="px-4 py-2 text-right">Ações</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{#each pedidoDocumentos as doc (doc._id)}
<tr class="hover:bg-gray-50">
<td class="px-4 py-2">{doc.descricao}</td>
<td class="px-4 py-2">
<span class="font-medium text-gray-900">{doc.nome}</span>
{#if doc.origemSolicitacaoId}
<span class="ml-2 text-xs text-gray-400"> (origem: solicitação)</span>
{/if}
</td>
<td class="px-4 py-2">{formatBytes(doc.tamanho)}</td>
<td class="px-4 py-2">{doc.criadoPorNome ?? 'Desconhecido'}</td>
<td class="px-4 py-2">
{new Date(doc.criadoEm).toLocaleString('pt-BR')}
</td>
<td class="px-4 py-2 text-right">
<button
type="button"
class="mr-2 rounded bg-indigo-100 p-1 text-indigo-700 hover:bg-indigo-200"
title="Ver documento"
onclick={() => handleOpenPedidoDocumento(doc.url)}
disabled={!doc.url}
>
<Eye size={16} />
</button>
<button
type="button"
class="rounded bg-red-100 p-1 text-red-700 hover:bg-red-200"
title="Remover documento"
onclick={() => handleRemovePedidoDocumento(doc._id)}
>
<Trash2 size={16} />
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>
<!-- Requests Section -->
{#if requests.length > 0}
<div class="mb-6 overflow-hidden rounded-lg border-l-4 border-yellow-400 bg-white shadow-md">
@@ -1064,24 +1420,53 @@
{/if}
</td>
<td class="px-4 py-2 text-right">
{#if permissions?.canManageRequests}
<div class="flex justify-end gap-2">
{#if req.tipo === 'adicao'}
{@const canAddDoc =
!!currentFuncionarioId && req.solicitadoPor === currentFuncionarioId}
<button
type="button"
onclick={() => openSolicitacaoDocs(req)}
class="rounded bg-blue-100 p-1 text-blue-700 hover:bg-blue-200"
title={canAddDoc
? 'Adicionar documento'
: 'Apenas quem criou a solicitação pode adicionar documentos'}
disabled={!canAddDoc}
>
<Plus size={16} />
</button>
{/if}
<button
onclick={() => handleApproveRequest(req._id)}
class="mr-2 rounded bg-green-100 p-1 text-green-700 hover:bg-green-200"
title="Aprovar"
type="button"
onclick={() => openSolicitacaoDocs(req)}
class="rounded bg-indigo-100 p-1 text-indigo-700 hover:bg-indigo-200"
title="Ver documentos"
>
<CheckCircle size={16} />
<Eye size={16} />
</button>
<button
onclick={() => handleRejectRequest(req._id)}
class="rounded bg-red-100 p-1 text-red-700 hover:bg-red-200"
title="Rejeitar"
>
<XCircle size={16} />
</button>
{:else}
<span class="text-xs text-gray-400">Aguardando Análise</span>
{/if}
{#if permissions?.canManageRequests}
<button
type="button"
onclick={() => handleApproveRequest(req._id)}
class="rounded bg-green-100 p-1 text-green-700 hover:bg-green-200"
title="Aprovar"
>
<CheckCircle size={16} />
</button>
<button
type="button"
onclick={() => handleRejectRequest(req._id)}
class="rounded bg-red-100 p-1 text-red-700 hover:bg-red-200"
title="Rejeitar"
>
<XCircle size={16} />
</button>
{:else}
<span class="ml-2 self-center text-xs text-gray-400">Aguardando Análise</span>
{/if}
</div>
</td>
</tr>
{/each}
@@ -1380,7 +1765,7 @@
setEditingField(
item._id,
'modalidade',
e.currentTarget.value as Modalidade
coerceModalidade(e.currentTarget.value)
);
void persistItemChanges(item);
}}
@@ -1698,6 +2083,147 @@
</div>
{/if}
{#if showSolicitacaoDocsModal && solicitacaoDocsRequestId}
<div
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40 p-4"
>
<div class="relative w-full max-w-3xl rounded-lg bg-white p-6 shadow-xl">
<button
type="button"
onclick={closeSolicitacaoDocs}
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
aria-label="Fechar"
>
<X size={20} />
</button>
<h3 class="mb-2 text-lg font-semibold text-gray-900">Documentos da solicitação</h3>
<p class="mb-4 text-xs text-gray-500">
Solicitação: {solicitacaoDocsRequestId.slice(-6)} tipo: {solicitacaoDocsTipo}
</p>
{#if canAddDocsToSelectedRequest}
<div class="mb-4 rounded-lg border border-blue-100 bg-blue-50 p-4">
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-medium text-gray-600" for="req-doc-desc"
>Descrição</label
>
<input
id="req-doc-desc"
type="text"
bind:value={solicitacaoDocumentoDescricao}
class="w-full rounded-md border-gray-300 text-sm shadow-sm"
placeholder="Ex: justificativa, anexo do item, planilha..."
disabled={salvandoSolicitacaoDocumento}
/>
</div>
<div>
<label class="mb-1 block text-xs font-medium text-gray-600" for="req-doc-file"
>Arquivo</label
>
<input
id="req-doc-file"
type="file"
accept=".pdf,.jpg,.jpeg,.png"
class="w-full text-sm"
disabled={salvandoSolicitacaoDocumento}
onchange={(e) => {
const f = e.currentTarget.files?.[0] ?? null;
solicitacaoDocumentoFile = f;
}}
/>
</div>
</div>
<div class="mt-3 flex justify-end">
<button
type="button"
onclick={handleAddSolicitacaoDocumento}
disabled={salvandoSolicitacaoDocumento}
class="flex items-center gap-2 rounded bg-blue-600 px-3 py-2 text-sm text-white hover:bg-blue-700 disabled:opacity-50"
>
{#if salvandoSolicitacaoDocumento}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<Upload size={16} />
{/if}
Anexar documento
</button>
</div>
</div>
{/if}
{#if carregandoSolicitacaoDocs}
<p class="text-sm text-gray-500">Carregando documentos...</p>
{:else if solicitacaoDocs.length === 0}
<p class="text-sm text-gray-500">Nenhum documento anexado à solicitação.</p>
{:else}
<div class="overflow-x-auto">
<table class="w-full text-left text-sm text-gray-600">
<thead class="bg-gray-50 text-xs font-medium text-gray-500 uppercase">
<tr>
<th class="px-4 py-2">Descrição</th>
<th class="px-4 py-2">Arquivo</th>
<th class="px-4 py-2">Tamanho</th>
<th class="px-4 py-2">Enviado por</th>
<th class="px-4 py-2">Data</th>
<th class="px-4 py-2 text-right">Ações</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{#each solicitacaoDocs as doc (doc._id)}
<tr class="hover:bg-gray-50">
<td class="px-4 py-2">{doc.descricao}</td>
<td class="px-4 py-2">
<span class="font-medium text-gray-900">{doc.nome}</span>
</td>
<td class="px-4 py-2">{formatBytes(doc.tamanho)}</td>
<td class="px-4 py-2">{doc.criadoPorNome ?? 'Desconhecido'}</td>
<td class="px-4 py-2">
{new Date(doc.criadoEm).toLocaleString('pt-BR')}
</td>
<td class="px-4 py-2 text-right">
<button
type="button"
class="mr-2 rounded bg-indigo-100 p-1 text-indigo-700 hover:bg-indigo-200"
title="Ver documento"
onclick={() => handleOpenSolicitacaoDocumento(doc.url)}
disabled={!doc.url}
>
<Eye size={16} />
</button>
{#if canAddDocsToSelectedRequest}
<button
type="button"
class="rounded bg-red-100 p-1 text-red-700 hover:bg-red-200"
title="Remover documento"
onclick={() => handleRemoveSolicitacaoDocumento(doc._id)}
>
<Trash2 size={16} />
</button>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
<div class="mt-5 flex justify-end">
<button
type="button"
onclick={closeSolicitacaoDocs}
class="rounded bg-gray-200 px-4 py-2 text-sm text-gray-800 hover:bg-gray-300"
>
Fechar
</button>
</div>
</div>
</div>
{/if}
{#if showRequestAdjustmentsModal}
<div
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40"