feat: enhance notification bell component by refactoring notification fetching logic, improving type safety, and updating UI elements for better user experience

This commit is contained in:
2025-12-15 11:33:51 -03:00
parent c272ca05e8
commit f3288b9639
5 changed files with 589 additions and 575 deletions

View File

@@ -1,98 +1,95 @@
<script lang="ts">
import { Building2, FileText, Package, ShoppingCart } from 'lucide-svelte';
import { resolve } from '$app/paths';
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
</script>
<ProtectedRoute>
<main class="container mx-auto px-4 py-4">
<div class="breadcrumbs mb-4 text-sm">
<ul>
<li><a href={resolve('/')} class="text-primary hover:underline">Dashboard</a></li>
<li>Compras</li>
</ul>
</div>
<main class="container mx-auto px-4 py-4">
<div class="breadcrumbs mb-4 text-sm">
<ul>
<li><a href={resolve('/')} class="text-primary hover:underline">Dashboard</a></li>
<li>Compras</li>
</ul>
</div>
<div class="mb-6">
<div class="mb-2 flex items-center gap-4">
<div class="rounded-xl bg-cyan-500/20 p-3">
<ShoppingCart class="h-8 w-8 text-cyan-600" strokeWidth={2} />
</div>
<div>
<h1 class="text-primary text-3xl font-bold">Compras</h1>
<p class="text-base-content/70">Gestão de compras e aquisições</p>
</div>
<div class="mb-6">
<div class="mb-2 flex items-center gap-4">
<div class="rounded-xl bg-cyan-500/20 p-3">
<ShoppingCart class="h-8 w-8 text-cyan-600" strokeWidth={2} />
</div>
<div>
<h1 class="text-primary text-3xl font-bold">Compras</h1>
<p class="text-base-content/70">Gestão de compras e aquisições</p>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<a
href={resolve('/compras/objetos')}
class="card bg-base-100 border-base-200 hover:border-primary border shadow-md transition-shadow hover:shadow-lg"
>
<div class="card-body">
<div class="mb-2 flex items-center gap-3">
<div class="bg-primary/10 rounded-lg p-2">
<Package class="text-primary h-6 w-6" strokeWidth={2} />
</div>
<h4 class="font-semibold">Objetos</h4>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<a
href={resolve('/compras/objetos')}
class="card bg-base-100 border-base-200 hover:border-primary border shadow-md transition-shadow hover:shadow-lg"
>
<div class="card-body">
<div class="mb-2 flex items-center gap-3">
<div class="bg-primary/10 rounded-lg p-2">
<Package class="text-primary h-6 w-6" strokeWidth={2} />
</div>
<p class="text-base-content/70 text-sm">
Cadastro, listagem e edição de objetos e serviços disponíveis para compra.
</p>
<h4 class="font-semibold">Objetos</h4>
</div>
</a>
<p class="text-base-content/70 text-sm">
Cadastro, listagem e edição de objetos e serviços disponíveis para compra.
</p>
</div>
</a>
<a
href={resolve('/compras/atas')}
class="card bg-base-100 border-base-200 hover:border-accent border shadow-md transition-shadow hover:shadow-lg"
>
<div class="card-body">
<div class="mb-2 flex items-center gap-3">
<div class="bg-accent/10 rounded-lg p-2">
<FileText class="text-accent h-6 w-6" strokeWidth={2} />
</div>
<h4 class="font-semibold">Atas de Registro</h4>
<a
href={resolve('/compras/atas')}
class="card bg-base-100 border-base-200 hover:border-accent border shadow-md transition-shadow hover:shadow-lg"
>
<div class="card-body">
<div class="mb-2 flex items-center gap-3">
<div class="bg-accent/10 rounded-lg p-2">
<FileText class="text-accent h-6 w-6" strokeWidth={2} />
</div>
<p class="text-base-content/70 text-sm">
Gerencie Atas de Registro de Preços e seus vínculos com objetos.
</p>
<h4 class="font-semibold">Atas de Registro</h4>
</div>
</a>
<p class="text-base-content/70 text-sm">
Gerencie Atas de Registro de Preços e seus vínculos com objetos.
</p>
</div>
</a>
<a
href={resolve('/licitacoes/empresas')}
class="card bg-base-100 border-base-200 hover:border-info border shadow-md transition-shadow hover:shadow-lg"
>
<div class="card-body">
<div class="mb-2 flex items-center gap-3">
<div class="bg-info/10 rounded-lg p-2">
<Building2 class="text-info h-6 w-6" strokeWidth={2} />
</div>
<h4 class="font-semibold">Empresas</h4>
<a
href={resolve('/licitacoes/empresas')}
class="card bg-base-100 border-base-200 hover:border-info border shadow-md transition-shadow hover:shadow-lg"
>
<div class="card-body">
<div class="mb-2 flex items-center gap-3">
<div class="bg-info/10 rounded-lg p-2">
<Building2 class="text-info h-6 w-6" strokeWidth={2} />
</div>
<p class="text-base-content/70 text-sm">
Cadastro e gestão de empresas fornecedoras e seus contatos.
</p>
<h4 class="font-semibold">Empresas</h4>
</div>
</a>
<p class="text-base-content/70 text-sm">
Cadastro e gestão de empresas fornecedoras e seus contatos.
</p>
</div>
</a>
<a
href={resolve('/pedidos')}
class="card bg-base-100 border-base-200 hover:border-secondary border shadow-md transition-shadow hover:shadow-lg"
>
<div class="card-body">
<div class="mb-2 flex items-center gap-3">
<div class="bg-secondary/10 rounded-lg p-2">
<FileText class="text-secondary h-6 w-6" strokeWidth={2} />
</div>
<h4 class="font-semibold">Pedidos</h4>
<a
href={resolve('/pedidos')}
class="card bg-base-100 border-base-200 hover:border-secondary border shadow-md transition-shadow hover:shadow-lg"
>
<div class="card-body">
<div class="mb-2 flex items-center gap-3">
<div class="bg-secondary/10 rounded-lg p-2">
<FileText class="text-secondary h-6 w-6" strokeWidth={2} />
</div>
<p class="text-base-content/70 text-sm">
Gerencie pedidos de compra, acompanhe status e histórico de aquisições.
</p>
<h4 class="font-semibold">Pedidos</h4>
</div>
</a>
</div>
</main>
</ProtectedRoute>
<p class="text-base-content/70 text-sm">
Gerencie pedidos de compra, acompanhe status e histórico de aquisições.
</p>
</div>
</a>
</div>
</main>

View File

@@ -2,7 +2,8 @@
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { useConvexClient, useQuery } from 'convex-svelte';
import { Pencil, Plus, Trash2, X, Search, Check } from 'lucide-svelte';
import { Pencil, Plus, Trash2, X, Search, Check, FileText } from 'lucide-svelte';
import { resolve } from '$app/paths';
const client = useConvexClient();
@@ -189,140 +190,173 @@
}
</script>
<div class="container mx-auto p-6">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold">Atas de Registro de Preços</h1>
<button
onclick={() => openModal()}
class="flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
<Plus size={20} />
Nova Ata
</button>
<main class="container mx-auto flex max-w-7xl flex-col px-4 py-4">
<div class="breadcrumbs mb-4 text-sm">
<ul>
<li><a href={resolve('/')} class="text-primary hover:underline">Dashboard</a></li>
<li><a href={resolve('/compras')} class="text-primary hover:underline">Compras</a></li>
<li>Atas</li>
</ul>
</div>
<div class="mb-6">
<div class="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
<div class="flex items-center gap-4">
<div class="bg-accent/10 rounded-xl p-3">
<FileText class="text-accent h-8 w-8" strokeWidth={2} />
</div>
<div>
<h1 class="text-primary text-3xl font-bold">Atas de Registro de Preços</h1>
<p class="text-base-content/70">Gerencie atas, vigência, empresa e anexos</p>
</div>
</div>
<button
class="btn btn-primary gap-2 shadow-md transition-all hover:shadow-lg"
onclick={() => openModal()}
>
<Plus class="h-5 w-5" strokeWidth={2} />
Nova Ata
</button>
</div>
</div>
{#if loadingAtas}
<p>Carregando...</p>
<div class="flex items-center justify-center py-10">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else if errorAtas}
<p class="text-red-600">{errorAtas}</p>
<div class="alert alert-error">
<span>{errorAtas}</span>
</div>
{:else}
<div class="overflow-hidden rounded-lg bg-white shadow-md">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
>Número</th
>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
>SEI</th
>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
>Empresa</th
>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
>Vigência</th
>
<th
class="px-6 py-3 text-right text-xs font-medium tracking-wider text-gray-500 uppercase"
>Ações</th
>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{#each atas as ata (ata._id)}
<tr>
<td class="px-6 py-4 font-medium whitespace-nowrap">{ata.numero}</td>
<td class="px-6 py-4 whitespace-nowrap">{ata.numeroSei}</td>
<td
class="max-w-md truncate px-6 py-4 whitespace-nowrap"
title={getEmpresaNome(ata.empresaId)}
>
{getEmpresaNome(ata.empresaId)}
</td>
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-500">
{ata.dataInicio || '-'} a {ata.dataFim || '-'}
</td>
<td class="px-6 py-4 text-right text-sm font-medium whitespace-nowrap">
<button
onclick={() => openModal(ata)}
class="mr-4 text-indigo-600 hover:text-indigo-900"
<div class="card bg-base-100/90 border-base-300 border shadow-xl backdrop-blur-sm">
<div class="card-body p-0">
<div class="overflow-x-auto">
<table class="table-zebra table w-full">
<thead class="from-base-300 to-base-200 sticky top-0 z-10 bg-linear-to-r shadow-md">
<tr>
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
>Número</th
>
<Pencil size={18} />
</button>
<button
onclick={() => handleDelete(ata._id)}
class="text-red-600 hover:text-red-900"
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
>SEI</th
>
<Trash2 size={18} />
</button>
</td>
</tr>
{/each}
{#if atas.length === 0}
<tr>
<td colspan="5" class="px-6 py-4 text-center text-gray-500"
>Nenhuma ata cadastrada.</td
>
</tr>
{/if}
</tbody>
</table>
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
>Empresa</th
>
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
>Vigência</th
>
<th
class="text-base-content border-base-400 border-b text-right font-bold whitespace-nowrap"
>Ações</th
>
</tr>
</thead>
<tbody>
{#if atas.length === 0}
<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>
</div>
</td>
</tr>
{:else}
{#each atas as ata (ata._id)}
<tr class="hover:bg-base-200/50 transition-colors">
<td class="font-medium whitespace-nowrap">{ata.numero}</td>
<td class="whitespace-nowrap">{ata.numeroSei}</td>
<td
class="max-w-md truncate whitespace-nowrap"
title={getEmpresaNome(ata.empresaId)}
>
{getEmpresaNome(ata.empresaId)}
</td>
<td class="text-base-content/70 whitespace-nowrap">
{ata.dataInicio || '-'} a {ata.dataFim || '-'}
</td>
<td class="text-right whitespace-nowrap">
<div class="flex items-center justify-end gap-1">
<button
type="button"
class="btn btn-ghost btn-sm"
aria-label="Editar ata"
onclick={() => openModal(ata)}
>
<Pencil size={18} />
</button>
<button
type="button"
class="btn btn-ghost btn-sm text-error"
aria-label="Excluir ata"
onclick={() => handleDelete(ata._id)}
>
<Trash2 size={18} />
</button>
</div>
</td>
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
</div>
</div>
{/if}
{#if showModal}
<div
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40"
>
<div class="relative my-8 w-full max-w-2xl rounded-lg bg-white p-8 shadow-xl">
<div class="modal modal-open">
<div class="modal-box max-w-4xl">
<button
type="button"
class="btn btn-sm btn-circle absolute top-2 right-2"
onclick={closeModal}
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
aria-label="Fechar modal"
>
<X size={24} />
<X class="h-5 w-5" />
</button>
<h2 class="mb-6 text-xl font-bold">{editingId ? 'Editar' : 'Nova'} Ata</h2>
<form onsubmit={handleSubmit}>
<h3 class="text-lg font-bold">{editingId ? 'Editar' : 'Nova'} Ata</h3>
<form class="mt-6 space-y-6" onsubmit={handleSubmit}>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<div class="mb-4">
<label class="mb-2 block text-sm font-bold text-gray-700" for="numero">
Número da Ata
<div class="space-y-4">
<div class="form-control w-full">
<label class="label" for="numero">
<span class="label-text font-semibold">Número da Ata</span>
</label>
<input
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
id="numero"
class="input input-bordered focus:input-primary w-full"
type="text"
bind:value={formData.numero}
required
/>
</div>
<div class="mb-4">
<label class="mb-2 block text-sm font-bold text-gray-700" for="numeroSei">
Número SEI
<div class="form-control w-full">
<label class="label" for="numeroSei">
<span class="label-text font-semibold">Número SEI</span>
</label>
<input
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
id="numeroSei"
class="input input-bordered focus:input-primary w-full"
type="text"
bind:value={formData.numeroSei}
required
/>
</div>
<div class="mb-4">
<label class="mb-2 block text-sm font-bold text-gray-700" for="empresa">
Empresa
<div class="form-control w-full">
<label class="label" for="empresa">
<span class="label-text font-semibold">Empresa</span>
</label>
<select
class="focus:shadow-outline w-full rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
id="empresa"
class="select select-bordered focus:select-primary w-full"
bind:value={formData.empresaId}
required
>
@@ -333,25 +367,25 @@
</select>
</div>
<div class="mb-4 grid grid-cols-2 gap-4">
<div>
<label class="mb-2 block text-sm font-bold text-gray-700" for="dataInicio">
Data Início
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control w-full">
<label class="label" for="dataInicio">
<span class="label-text font-semibold">Data Início</span>
</label>
<input
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
id="dataInicio"
class="input input-bordered focus:input-primary w-full"
type="date"
bind:value={formData.dataInicio}
/>
</div>
<div>
<label class="mb-2 block text-sm font-bold text-gray-700" for="dataFim">
Data Fim
<div class="form-control w-full">
<label class="label" for="dataFim">
<span class="label-text font-semibold">Data Fim</span>
</label>
<input
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
id="dataFim"
class="input input-bordered focus:input-primary w-full"
type="date"
bind:value={formData.dataFim}
/>
@@ -359,110 +393,116 @@
</div>
</div>
<div class="flex flex-col">
<label class="mb-2 block text-sm font-bold text-gray-700" for="objetos">
Objetos Vinculados ({selectedObjetos.length})
</label>
<div class="relative mb-2">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<Search size={16} class="text-gray-400" />
</div>
<input
type="text"
placeholder="Buscar objetos..."
class="focus:shadow-outline w-full rounded border py-2 pr-3 pl-10 text-sm leading-tight text-gray-700 shadow focus:outline-none"
bind:value={searchObjeto}
/>
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="font-semibold">Objetos Vinculados</div>
<span class="badge badge-outline">{selectedObjetos.length}</span>
</div>
<div
class="mb-4 flex-1 overflow-y-auto rounded border bg-gray-50 p-2"
style="max-height: 200px;"
>
{#if filteredObjetos.length === 0}
<p class="py-4 text-center text-sm text-gray-500">Nenhum objeto encontrado.</p>
{:else}
<div class="space-y-1">
{#each filteredObjetos as objeto (objeto._id)}
<button
type="button"
class="flex w-full items-center justify-between rounded px-3 py-2 text-left text-sm hover:bg-gray-200 {selectedObjetos.includes(
objeto._id
)
? 'bg-blue-50 text-blue-700'
: ''}"
onclick={() => toggleObjeto(objeto._id)}
>
<span class="truncate">{objeto.nome}</span>
{#if selectedObjetos.includes(objeto._id)}
<Check size={16} class="text-blue-600" />
{/if}
</button>
{/each}
</div>
{/if}
</div>
<div class="border-t pt-4">
<label class="mb-2 block text-sm font-bold text-gray-700" for="anexos">
Anexos
<div class="form-control w-full">
<label class="label" for="buscar_objeto">
<span class="label-text font-semibold">Buscar objetos</span>
</label>
<input
class="focus:shadow-outline mb-2 w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
id="anexos"
type="file"
multiple
onchange={handleAttachmentsSelect}
/>
{#if attachments.length > 0}
<div class="mt-2 max-h-40 space-y-2 overflow-y-auto">
{#each attachments as doc (doc._id)}
<div
class="flex items-center justify-between rounded bg-gray-100 p-2 text-sm"
>
<a
href={doc.url}
target="_blank"
rel="noopener noreferrer"
class="max-w-[150px] truncate text-blue-600 hover:underline"
>
{doc.nome}
</a>
<button
type="button"
onclick={() => handleDeleteAttachment(doc._id)}
class="text-red-500 hover:text-red-700"
>
<X size={16} />
</button>
</div>
{/each}
<div class="relative">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<Search size={16} class="text-base-content/40" />
</div>
<input
id="buscar_objeto"
type="text"
placeholder="Digite para filtrar..."
class="input input-bordered focus:input-primary w-full pl-10"
bind:value={searchObjeto}
/>
</div>
</div>
<div class="border-base-300 max-h-52 overflow-y-auto rounded-lg border p-2">
{#if filteredObjetos.length === 0}
<p class="text-base-content/60 px-2 py-3 text-center text-sm">
Nenhum objeto encontrado.
</p>
{:else}
{#each filteredObjetos as objeto (objeto._id)}
{@const isSelected = selectedObjetos.includes(objeto._id)}
<label
class="hover:bg-base-200/50 flex cursor-pointer items-center gap-3 rounded-md px-2 py-2 {isSelected
? 'bg-primary/5'
: ''}"
>
<input
type="checkbox"
class="checkbox checkbox-primary checkbox-sm"
checked={isSelected}
onchange={() => toggleObjeto(objeto._id)}
aria-label="Vincular objeto {objeto.nome}"
/>
<span class="flex-1 truncate text-sm">{objeto.nome}</span>
{#if isSelected}
<Check size={16} class="text-primary" />
{/if}
</label>
{/each}
{/if}
</div>
<div class="border-base-300 border-t pt-4">
<div class="font-semibold">Anexos</div>
<div class="mt-2 space-y-2">
<input
id="anexos"
type="file"
multiple
class="file-input file-input-bordered w-full"
onchange={handleAttachmentsSelect}
/>
{#if attachments.length > 0}
<div
class="border-base-300 max-h-40 space-y-2 overflow-y-auto rounded-lg border p-2"
>
{#each attachments as doc (doc._id)}
<div class="flex items-center justify-between gap-2 text-sm">
<a
href={doc.url}
target="_blank"
rel="noopener noreferrer"
class="link link-primary max-w-[260px] truncate"
>
{doc.nome}
</a>
<button
type="button"
class="btn btn-ghost btn-xs text-error"
onclick={() => handleDeleteAttachment(doc._id)}
aria-label="Excluir anexo"
>
<X size={16} />
</button>
</div>
{/each}
</div>
{/if}
</div>
</div>
</div>
</div>
<div class="mt-6 flex items-center justify-end border-t pt-4">
<button
type="button"
onclick={closeModal}
class="mr-2 rounded bg-gray-300 px-4 py-2 font-bold text-gray-800 hover:bg-gray-400"
>
<div class="modal-action">
<button type="button" class="btn" onclick={closeModal} disabled={saving || uploading}>
Cancelar
</button>
<button
type="submit"
disabled={saving || uploading}
class="focus:shadow-outline rounded bg-blue-600 px-4 py-2 font-bold text-white hover:bg-blue-700 focus:outline-none disabled:opacity-50"
>
<button type="submit" class="btn btn-primary" disabled={saving || uploading}>
{#if saving || uploading}
<span class="loading loading-spinner loading-sm"></span>
{/if}
{saving || uploading ? 'Salvando...' : 'Salvar'}
</button>
</div>
</form>
</div>
<button type="button" class="modal-backdrop" onclick={closeModal} aria-label="Fechar modal"
></button>
</div>
{/if}
</div>
</main>

View File

@@ -2,8 +2,9 @@
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { useConvexClient, useQuery } from 'convex-svelte';
import { Pencil, Plus, Trash2, X } from 'lucide-svelte';
import { Pencil, Plus, Trash2, X, Package } from 'lucide-svelte';
import { maskCurrencyBRL } from '$lib/utils/masks';
import { resolve } from '$app/paths';
const client = useConvexClient();
@@ -116,150 +117,187 @@
}
</script>
<div class="container mx-auto p-6">
<div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold">Objetos</h1>
<button
onclick={() => openModal()}
class="flex items-center gap-2 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
>
<Plus size={20} />
Novo Objeto
</button>
<main class="container mx-auto flex max-w-7xl flex-col px-4 py-4">
<div class="breadcrumbs mb-4 text-sm">
<ul>
<li><a href={resolve('/')} class="text-primary hover:underline">Dashboard</a></li>
<li><a href={resolve('/compras')} class="text-primary hover:underline">Compras</a></li>
<li>Objetos</li>
</ul>
</div>
<div class="mb-6">
<div class="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
<div class="flex items-center gap-4">
<div class="bg-primary/10 rounded-xl p-3">
<Package class="text-primary h-8 w-8" strokeWidth={2} />
</div>
<div>
<h1 class="text-primary text-3xl font-bold">Objetos</h1>
<p class="text-base-content/70">Cadastro e gestão de objetos e serviços</p>
</div>
</div>
<button
class="btn btn-primary gap-2 shadow-md transition-all hover:shadow-lg"
onclick={() => openModal()}
>
<Plus class="h-5 w-5" strokeWidth={2} />
Novo Objeto
</button>
</div>
</div>
{#if loading}
<p>Carregando...</p>
<div class="flex items-center justify-center py-10">
<span class="loading loading-spinner loading-lg"></span>
</div>
{:else if error}
<p class="text-red-600">{error}</p>
<div class="alert alert-error">
<span>{error}</span>
</div>
{:else}
<div class="overflow-hidden rounded-lg bg-white shadow-md">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
>Nome</th
>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
>Tipo</th
>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
>Unidade</th
>
<th
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
>Valor Estimado</th
>
<th
class="px-6 py-3 text-right text-xs font-medium tracking-wider text-gray-500 uppercase"
>Ações</th
>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{#each objetos as objeto (objeto._id)}
<tr>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex flex-col">
<span class="font-medium">{objeto.nome}</span>
<span class="text-xs text-gray-500">
Efisco: {objeto.codigoEfisco}
{#if objeto.codigoCatmat}
| Catmat: {objeto.codigoCatmat}{/if}
{#if objeto.codigoCatserv}
| Catserv: {objeto.codigoCatserv}{/if}
</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span
class="inline-flex rounded-full px-2 text-xs leading-5 font-semibold
{objeto.tipo === 'servico'
? 'bg-green-100 text-green-800'
: 'bg-blue-100 text-blue-800'}"
<div class="card bg-base-100/90 border-base-300 border shadow-xl backdrop-blur-sm">
<div class="card-body p-0">
<div class="overflow-x-auto">
<table class="table-zebra table w-full">
<thead class="from-base-300 to-base-200 sticky top-0 z-10 bg-linear-to-r shadow-md">
<tr>
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
>Nome</th
>
{objeto.tipo === 'material' ? 'Material' : 'Serviço'}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">{objeto.unidade}</td>
<td class="px-6 py-4 whitespace-nowrap">
{maskCurrencyBRL(objeto.valorEstimado) || 'R$ 0,00'}
</td>
<td class="px-6 py-4 text-right text-sm font-medium whitespace-nowrap">
<button
onclick={() => openModal(objeto)}
class="mr-4 text-indigo-600 hover:text-indigo-900"
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
>Tipo</th
>
<Pencil size={18} />
</button>
<button
onclick={() => handleDelete(objeto._id)}
class="text-red-600 hover:text-red-900"
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
>Unidade</th
>
<Trash2 size={18} />
</button>
</td>
</tr>
{/each}
{#if objetos.length === 0}
<tr>
<td colspan="5" class="px-6 py-4 text-center text-gray-500"
>Nenhum objeto cadastrado.</td
>
</tr>
{/if}
</tbody>
</table>
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
>Valor Estimado</th
>
<th
class="text-base-content border-base-400 border-b text-right font-bold whitespace-nowrap"
>Ações</th
>
</tr>
</thead>
<tbody>
{#if objetos.length === 0}
<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>
</div>
</td>
</tr>
{:else}
{#each objetos as objeto (objeto._id)}
<tr class="hover:bg-base-200/50 transition-colors">
<td>
<div class="flex flex-col">
<span class="font-medium">{objeto.nome}</span>
<span class="text-base-content/60 text-xs">
Efisco: {objeto.codigoEfisco}
{#if objeto.codigoCatmat}
| Catmat: {objeto.codigoCatmat}{/if}
{#if objeto.codigoCatserv}
| Catserv: {objeto.codigoCatserv}{/if}
</span>
</div>
</td>
<td class="whitespace-nowrap">
<span
class="badge badge-sm {objeto.tipo === 'servico'
? 'badge-success'
: 'badge-info'}"
>
{objeto.tipo === 'material' ? 'Material' : 'Serviço'}
</span>
</td>
<td class="whitespace-nowrap">{objeto.unidade}</td>
<td class="whitespace-nowrap">
{maskCurrencyBRL(objeto.valorEstimado) || 'R$ 0,00'}
</td>
<td class="text-right whitespace-nowrap">
<div class="flex items-center justify-end gap-1">
<button
type="button"
class="btn btn-ghost btn-sm"
aria-label="Editar objeto"
onclick={() => openModal(objeto)}
>
<Pencil size={18} />
</button>
<button
type="button"
class="btn btn-ghost btn-sm text-error"
aria-label="Excluir objeto"
onclick={() => handleDelete(objeto._id)}
>
<Trash2 size={18} />
</button>
</div>
</td>
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
</div>
</div>
{/if}
{#if showModal}
<div
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40"
>
<div class="relative w-full max-w-md rounded-lg bg-white p-8 shadow-xl">
<div class="modal modal-open">
<div class="modal-box max-w-2xl">
<button
type="button"
class="btn btn-sm btn-circle absolute top-2 right-2"
onclick={closeModal}
class="absolute top-4 right-4 text-gray-400 hover:text-gray-600"
aria-label="Fechar modal"
>
<X size={24} />
<X class="h-5 w-5" />
</button>
<h2 class="mb-6 text-xl font-bold">{editingId ? 'Editar' : 'Novo'} Objeto</h2>
<form onsubmit={handleSubmit}>
<div class="mb-4">
<label class="mb-2 block text-sm font-bold text-gray-700" for="nome"> Nome </label>
<h3 class="text-lg font-bold">{editingId ? 'Editar' : 'Novo'} Objeto</h3>
<form class="mt-6 space-y-4" onsubmit={handleSubmit}>
<div class="form-control w-full">
<label class="label" for="nome">
<span class="label-text font-semibold">Nome</span>
</label>
<input
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
id="nome"
class="input input-bordered focus:input-primary w-full"
type="text"
bind:value={formData.nome}
required
/>
</div>
<div class="mb-4 grid grid-cols-2 gap-4">
<div>
<label class="mb-2 block text-sm font-bold text-gray-700" for="tipo"> Tipo </label>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="form-control w-full">
<label class="label" for="tipo">
<span class="label-text font-semibold">Tipo</span>
</label>
<select
class="focus:shadow-outline w-full rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
id="tipo"
class="select select-bordered focus:select-primary w-full"
bind:value={formData.tipo}
>
<option value="material">Material</option>
<option value="servico">Serviço</option>
</select>
</div>
<div>
<label class="mb-2 block text-sm font-bold text-gray-700" for="unidade">
Unidade
<div class="form-control w-full">
<label class="label" for="unidade">
<span class="label-text font-semibold">Unidade</span>
</label>
<input
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
id="unidade"
class="input input-bordered focus:input-primary w-full"
type="text"
bind:value={formData.unidade}
required
@@ -267,13 +305,13 @@
</div>
</div>
<div class="mb-4">
<label class="mb-2 block text-sm font-bold text-gray-700" for="codigoEfisco">
Código Efisco
<div class="form-control w-full">
<label class="label" for="codigoEfisco">
<span class="label-text font-semibold">Código Efisco</span>
</label>
<input
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
id="codigoEfisco"
class="input input-bordered focus:input-primary w-full"
type="text"
bind:value={formData.codigoEfisco}
required
@@ -281,88 +319,86 @@
</div>
{#if formData.tipo === 'material'}
<div class="mb-4">
<label class="mb-2 block text-sm font-bold text-gray-700" for="codigoCatmat">
Código Catmat
<div class="form-control w-full">
<label class="label" for="codigoCatmat">
<span class="label-text font-semibold">Código Catmat</span>
</label>
<input
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
id="codigoCatmat"
class="input input-bordered focus:input-primary w-full"
type="text"
bind:value={formData.codigoCatmat}
/>
</div>
{:else}
<div class="mb-4">
<label class="mb-2 block text-sm font-bold text-gray-700" for="codigoCatserv">
Código Catserv
<div class="form-control w-full">
<label class="label" for="codigoCatserv">
<span class="label-text font-semibold">Código Catserv</span>
</label>
<input
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
id="codigoCatserv"
class="input input-bordered focus:input-primary w-full"
type="text"
bind:value={formData.codigoCatserv}
/>
</div>
{/if}
<div class="mb-6">
<label class="mb-2 block text-sm font-bold text-gray-700" for="valor">
Valor Estimado
<div class="form-control w-full">
<label class="label" for="valor">
<span class="label-text font-semibold">Valor Estimado</span>
</label>
<input
class="focus:shadow-outline w-full appearance-none rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
id="valor"
class="input input-bordered focus:input-primary w-full"
type="text"
placeholder="R$ 0,00"
bind:value={formData.valorEstimado}
oninput={(e) => (formData.valorEstimado = maskCurrencyBRL(e.currentTarget.value))}
placeholder="R$ 0,00"
/>
</div>
<div class="mb-6">
<label class="mb-2 block text-sm font-bold text-gray-700" for="atas">
Vincular Atas
<div class="form-control w-full">
<label class="label" for="atas">
<span class="label-text font-semibold">Vincular Atas</span>
</label>
<div class="max-h-40 overflow-y-auto rounded border p-2">
{#each atas as ata (ata._id)}
<div class="mb-2 flex items-center">
<input
type="checkbox"
id={`ata-${ata._id}`}
checked={formData.atas.includes(ata._id)}
onchange={() => toggleAtaSelection(ata._id)}
class="mr-2 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<label for={`ata-${ata._id}`} class="text-sm text-gray-700">
{ata.numero} ({ata.numeroSei})
</label>
</div>
{/each}
<div class="border-base-300 max-h-48 overflow-y-auto rounded-lg border p-2">
{#if atas.length === 0}
<p class="text-sm text-gray-500">Nenhuma ata disponível.</p>
<p class="text-base-content/60 px-2 py-3 text-sm">Nenhuma ata disponível.</p>
{:else}
{#each atas as ata (ata._id)}
<label
class="hover:bg-base-200/50 flex cursor-pointer items-center gap-3 rounded-md px-2 py-2"
>
<input
type="checkbox"
class="checkbox checkbox-primary checkbox-sm"
checked={formData.atas.includes(ata._id)}
onchange={() => toggleAtaSelection(ata._id)}
aria-label="Vincular ata {ata.numero}"
/>
<span class="text-sm">{ata.numero} ({ata.numeroSei})</span>
</label>
{/each}
{/if}
</div>
</div>
<div class="flex items-center justify-end">
<button
type="button"
onclick={closeModal}
class="mr-2 rounded bg-gray-300 px-4 py-2 font-bold text-gray-800 hover:bg-gray-400"
>
<div class="modal-action">
<button type="button" class="btn" onclick={closeModal} disabled={saving}>
Cancelar
</button>
<button
type="submit"
disabled={saving}
class="focus:shadow-outline rounded bg-blue-600 px-4 py-2 font-bold text-white hover:bg-blue-700 focus:outline-none disabled:opacity-50"
>
<button type="submit" class="btn btn-primary" disabled={saving}>
{#if saving}
<span class="loading loading-spinner loading-sm"></span>
{/if}
{saving ? 'Salvando...' : 'Salvar'}
</button>
</div>
</form>
</div>
<button type="button" class="modal-backdrop" onclick={closeModal} aria-label="Fechar modal"
></button>
</div>
{/if}
</div>
</main>