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:
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import { useQuery, useConvexClient } from 'convex-svelte';
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { notificacoesCount } from '$lib/stores/chatStore';
|
import { notificacoesCount } from '$lib/stores/chatStore';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
import { ptBR } from 'date-fns/locale';
|
import { ptBR } from 'date-fns/locale';
|
||||||
@@ -18,9 +20,10 @@
|
|||||||
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
||||||
|
|
||||||
let modalOpen = $state(false);
|
let modalOpen = $state(false);
|
||||||
|
let usuarioId = $derived((currentUser?.data?._id as Id<'usuarios'> | undefined) ?? null);
|
||||||
let notificacoesFerias = $state<
|
let notificacoesFerias = $state<
|
||||||
Array<{
|
Array<{
|
||||||
_id: string;
|
_id: Id<'notificacoesFerias'>;
|
||||||
mensagem: string;
|
mensagem: string;
|
||||||
tipo: string;
|
tipo: string;
|
||||||
_creationTime: number;
|
_creationTime: number;
|
||||||
@@ -28,7 +31,7 @@
|
|||||||
>([]);
|
>([]);
|
||||||
let notificacoesAusencias = $state<
|
let notificacoesAusencias = $state<
|
||||||
Array<{
|
Array<{
|
||||||
_id: string;
|
_id: Id<'notificacoesAusencias'>;
|
||||||
mensagem: string;
|
mensagem: string;
|
||||||
tipo: string;
|
tipo: string;
|
||||||
_creationTime: number;
|
_creationTime: number;
|
||||||
@@ -47,43 +50,40 @@
|
|||||||
// Separar notificações lidas e não lidas
|
// Separar notificações lidas e não lidas
|
||||||
let notificacoesNaoLidas = $derived(todasNotificacoes.filter((n) => !n.lida));
|
let notificacoesNaoLidas = $derived(todasNotificacoes.filter((n) => !n.lida));
|
||||||
let notificacoesLidas = $derived(todasNotificacoes.filter((n) => n.lida));
|
let notificacoesLidas = $derived(todasNotificacoes.filter((n) => n.lida));
|
||||||
|
let totalCount = $derived(count + (notificacoesFerias?.length || 0));
|
||||||
|
|
||||||
// Atualizar contador no store
|
// Atualizar contador no store
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const totalNotificacoes =
|
const totalNotificacoes =
|
||||||
count + (notificacoesFerias?.length || 0) + (notificacoesAusencias?.length || 0);
|
count + (notificacoesFerias?.length || 0) + (notificacoesAusencias?.length || 0);
|
||||||
notificacoesCount.set(totalNotificacoes);
|
$notificacoesCount = totalNotificacoes;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Buscar notificações de férias
|
// Buscar notificações de férias
|
||||||
async function buscarNotificacoesFerias() {
|
async function buscarNotificacoesFerias(id: Id<'usuarios'> | null) {
|
||||||
try {
|
try {
|
||||||
const usuarioId = currentUser?.data?._id;
|
if (!id) return;
|
||||||
if (usuarioId) {
|
|
||||||
const notifsFerias = await client.query(api.ferias.obterNotificacoesNaoLidas, {
|
const notifsFerias = await client.query(api.ferias.obterNotificacoesNaoLidas, {
|
||||||
usuarioId
|
usuarioId: id
|
||||||
});
|
});
|
||||||
notificacoesFerias = notifsFerias || [];
|
notificacoesFerias = notifsFerias || [];
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Erro ao buscar notificações de férias:', e);
|
console.error('Erro ao buscar notificações de férias:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buscar notificações de ausências
|
// Buscar notificações de ausências
|
||||||
async function buscarNotificacoesAusencias() {
|
async function buscarNotificacoesAusencias(id: Id<'usuarios'> | null) {
|
||||||
try {
|
try {
|
||||||
const usuarioId = currentUser?.data?._id;
|
if (!id) return;
|
||||||
if (usuarioId) {
|
|
||||||
try {
|
try {
|
||||||
const notifsAusencias = await client.query(api.ausencias.obterNotificacoesNaoLidas, {
|
const notifsAusencias = await client.query(api.ausencias.obterNotificacoesNaoLidas, {
|
||||||
usuarioId
|
usuarioId: id
|
||||||
});
|
});
|
||||||
notificacoesAusencias = notifsAusencias || [];
|
notificacoesAusencias = notifsAusencias || [];
|
||||||
} catch (queryError: unknown) {
|
} catch (queryError: unknown) {
|
||||||
// Silenciar erros de timeout e função não encontrada
|
// Silenciar erros de timeout e função não encontrada
|
||||||
const errorMessage =
|
const errorMessage = queryError instanceof Error ? queryError.message : String(queryError);
|
||||||
queryError instanceof Error ? queryError.message : String(queryError);
|
|
||||||
const isTimeout = errorMessage.includes('timed out') || errorMessage.includes('timeout');
|
const isTimeout = errorMessage.includes('timed out') || errorMessage.includes('timeout');
|
||||||
const isFunctionNotFound = errorMessage.includes('Could not find public function');
|
const isFunctionNotFound = errorMessage.includes('Could not find public function');
|
||||||
|
|
||||||
@@ -92,7 +92,6 @@
|
|||||||
}
|
}
|
||||||
notificacoesAusencias = [];
|
notificacoesAusencias = [];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Erro geral - silenciar se for sobre função não encontrada ou timeout
|
// Erro geral - silenciar se for sobre função não encontrada ou timeout
|
||||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||||
@@ -106,13 +105,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Atualizar notificações periodicamente
|
// Atualizar notificações periodicamente
|
||||||
$effect(() => {
|
onMount(() => {
|
||||||
buscarNotificacoesFerias();
|
void buscarNotificacoesFerias(usuarioId);
|
||||||
buscarNotificacoesAusencias();
|
void buscarNotificacoesAusencias(usuarioId);
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
buscarNotificacoesFerias();
|
void buscarNotificacoesFerias(usuarioId);
|
||||||
buscarNotificacoesAusencias();
|
void buscarNotificacoesAusencias(usuarioId);
|
||||||
}, 30000); // A cada 30s
|
}, 30000); // A cada 30s
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,30 +128,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMarcarTodasLidas() {
|
|
||||||
await client.mutation(api.chat.marcarTodasNotificacoesLidas, {});
|
|
||||||
// Marcar todas as notificações de férias como lidas
|
|
||||||
for (const notif of notificacoesFerias) {
|
|
||||||
await client.mutation(api.ferias.marcarComoLida, {
|
|
||||||
notificacaoId: notif._id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Marcar todas as notificações de ausências como lidas
|
|
||||||
for (const notif of notificacoesAusencias) {
|
|
||||||
await client.mutation(api.ausencias.marcarComoLida, {
|
|
||||||
notificacaoId: notif._id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await buscarNotificacoesFerias();
|
|
||||||
await buscarNotificacoesAusencias();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleLimparTodasNotificacoes() {
|
async function handleLimparTodasNotificacoes() {
|
||||||
limpandoNotificacoes = true;
|
limpandoNotificacoes = true;
|
||||||
try {
|
try {
|
||||||
await client.mutation(api.chat.limparTodasNotificacoes, {});
|
await client.mutation(api.chat.limparTodasNotificacoes, {});
|
||||||
await buscarNotificacoesFerias();
|
await buscarNotificacoesFerias(usuarioId);
|
||||||
await buscarNotificacoesAusencias();
|
await buscarNotificacoesAusencias(usuarioId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erro ao limpar notificações:', error);
|
console.error('Erro ao limpar notificações:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -162,8 +145,8 @@
|
|||||||
limpandoNotificacoes = true;
|
limpandoNotificacoes = true;
|
||||||
try {
|
try {
|
||||||
await client.mutation(api.chat.limparNotificacoesNaoLidas, {});
|
await client.mutation(api.chat.limparNotificacoesNaoLidas, {});
|
||||||
await buscarNotificacoesFerias();
|
await buscarNotificacoesFerias(usuarioId);
|
||||||
await buscarNotificacoesAusencias();
|
await buscarNotificacoesAusencias(usuarioId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erro ao limpar notificações não lidas:', error);
|
console.error('Erro ao limpar notificações não lidas:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -173,24 +156,24 @@
|
|||||||
|
|
||||||
async function handleClickNotificacao(notificacaoId: string) {
|
async function handleClickNotificacao(notificacaoId: string) {
|
||||||
await client.mutation(api.chat.marcarNotificacaoLida, {
|
await client.mutation(api.chat.marcarNotificacaoLida, {
|
||||||
notificacaoId: notificacaoId as any
|
notificacaoId: notificacaoId as Id<'notificacoes'>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClickNotificacaoFerias(notificacaoId: string) {
|
async function handleClickNotificacaoFerias(notificacaoId: Id<'notificacoesFerias'>) {
|
||||||
await client.mutation(api.ferias.marcarComoLida, {
|
await client.mutation(api.ferias.marcarComoLida, {
|
||||||
notificacaoId: notificacaoId
|
notificacaoId: notificacaoId
|
||||||
});
|
});
|
||||||
await buscarNotificacoesFerias();
|
await buscarNotificacoesFerias(usuarioId);
|
||||||
// Redirecionar para a página de férias
|
// Redirecionar para a página de férias
|
||||||
window.location.href = '/recursos-humanos/ferias';
|
window.location.href = '/recursos-humanos/ferias';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClickNotificacaoAusencias(notificacaoId: string) {
|
async function handleClickNotificacaoAusencias(notificacaoId: Id<'notificacoesAusencias'>) {
|
||||||
await client.mutation(api.ausencias.marcarComoLida, {
|
await client.mutation(api.ausencias.marcarComoLida, {
|
||||||
notificacaoId: notificacaoId
|
notificacaoId: notificacaoId
|
||||||
});
|
});
|
||||||
await buscarNotificacoesAusencias();
|
await buscarNotificacoesAusencias(usuarioId);
|
||||||
// Redirecionar para a página de perfil na aba de ausências
|
// Redirecionar para a página de perfil na aba de ausências
|
||||||
window.location.href = '/perfil?aba=minhas-ausencias';
|
window.location.href = '/perfil?aba=minhas-ausencias';
|
||||||
}
|
}
|
||||||
@@ -204,19 +187,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fechar popup ao clicar fora ou pressionar Escape
|
// Fechar popup ao clicar fora ou pressionar Escape
|
||||||
$effect(() => {
|
onMount(() => {
|
||||||
if (!modalOpen) return;
|
|
||||||
|
|
||||||
function handleClickOutside(event: MouseEvent) {
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (!modalOpen) return;
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
if (!target.closest('.notification-popup') && !target.closest('.notification-bell')) {
|
if (!target.closest('.notification-popup') && !target.closest('.notification-bell')) {
|
||||||
modalOpen = false;
|
closeModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEscape(event: KeyboardEvent) {
|
function handleEscape(event: KeyboardEvent) {
|
||||||
|
if (!modalOpen) return;
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
modalOpen = false;
|
closeModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,56 +213,32 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="notification-bell relative">
|
<div class="notification-bell relative">
|
||||||
<!-- Botão de Notificação ULTRA MODERNO (igual ao perfil) -->
|
<!-- Botão de Notificação (padrão do tema) -->
|
||||||
<button
|
<div class="indicator">
|
||||||
type="button"
|
{#if totalCount > 0}
|
||||||
tabindex="0"
|
<span class="indicator-item badge badge-error badge-sm">
|
||||||
class="group relative flex h-14 w-14 items-center justify-center overflow-hidden rounded-2xl transition-all duration-300 hover:scale-105"
|
|
||||||
style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 8px 24px -4px rgba(102, 126, 234, 0.4);"
|
|
||||||
onclick={openModal}
|
|
||||||
aria-label="Notificações"
|
|
||||||
>
|
|
||||||
<!-- Efeito de brilho no hover -->
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 bg-linear-to-br from-white/0 to-white/20 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Anel de pulso sutil -->
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 rounded-2xl"
|
|
||||||
style="animation: pulse-ring-subtle 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Glow effect quando tem notificações -->
|
|
||||||
{#if count && count > 0}
|
|
||||||
<div class="bg-error/30 absolute inset-0 animate-pulse rounded-2xl blur-lg"></div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Ícone do sino PREENCHIDO moderno -->
|
|
||||||
<Bell
|
|
||||||
class="relative z-10 h-7 w-7 text-white transition-all duration-300 group-hover:scale-110"
|
|
||||||
style="filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3)); animation: {count && count > 0
|
|
||||||
? 'bell-ring 2s ease-in-out infinite'
|
|
||||||
: 'none'};"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Badge premium MODERNO com gradiente -->
|
|
||||||
{#if count + (notificacoesFerias?.length || 0) > 0}
|
|
||||||
{@const totalCount = count + (notificacoesFerias?.length || 0)}
|
|
||||||
<span
|
|
||||||
class="absolute -top-1 -right-1 z-20 flex h-6 w-6 items-center justify-center rounded-full text-[10px] font-black text-white shadow-xl ring-2 ring-white"
|
|
||||||
style="background: linear-gradient(135deg, #ff416c, #ff4b2b); box-shadow: 0 8px 24px -4px rgba(255, 65, 108, 0.6), 0 4px 12px -2px rgba(255, 75, 43, 0.4); animation: badge-bounce 2s ease-in-out infinite;"
|
|
||||||
>
|
|
||||||
{totalCount > 9 ? '9+' : totalCount}
|
{totalCount > 9 ? '9+' : totalCount}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
tabindex="0"
|
||||||
|
class="btn ring-base-200 hover:ring-primary/50 size-10 p-0 ring-2 ring-offset-2 transition-all"
|
||||||
|
onclick={openModal}
|
||||||
|
aria-label="Notificações"
|
||||||
|
aria-expanded={modalOpen}
|
||||||
|
>
|
||||||
|
<Bell
|
||||||
|
class="size-6 transition-colors {totalCount > 0 ? 'text-primary' : 'text-base-content/70'}"
|
||||||
|
style="animation: {totalCount > 0 ? 'bell-ring 2s ease-in-out infinite' : 'none'};"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Popup Flutuante de Notificações -->
|
<!-- Popup Flutuante de Notificações -->
|
||||||
{#if modalOpen}
|
{#if modalOpen}
|
||||||
<div
|
<div
|
||||||
class="notification-popup bg-base-100 border-base-300 fixed top-24 right-4 z-[100] flex max-h-[calc(100vh-7rem)] w-[calc(100vw-2rem)] max-w-2xl flex-col overflow-hidden rounded-2xl border shadow-2xl backdrop-blur-sm"
|
class="notification-popup bg-base-100 border-base-300 fixed top-24 right-4 z-100 flex max-h-[calc(100vh-7rem)] w-[calc(100vw-2rem)] max-w-2xl flex-col overflow-hidden rounded-2xl border shadow-2xl backdrop-blur-sm"
|
||||||
style="animation: slideDown 0.2s ease-out;"
|
style="animation: slideDown 0.2s ease-out;"
|
||||||
>
|
>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
@@ -310,7 +269,7 @@
|
|||||||
Limpar todas
|
Limpar todas
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button type="button" class="btn btn-sm btn-circle btn-ghost" onclick={closeModal}>
|
<button type="button" class="btn btn-sm btn-circle" onclick={closeModal}>
|
||||||
<X class="h-5 w-5" />
|
<X class="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -439,17 +398,17 @@
|
|||||||
<!-- Notificações de Férias -->
|
<!-- Notificações de Férias -->
|
||||||
{#if notificacoesFerias.length > 0}
|
{#if notificacoesFerias.length > 0}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4 class="mb-2 px-2 text-sm font-semibold text-purple-600">Férias</h4>
|
<h4 class="text-secondary mb-2 px-2 text-sm font-semibold">Férias</h4>
|
||||||
{#each notificacoesFerias as notificacao (notificacao._id)}
|
{#each notificacoesFerias as notificacao (notificacao._id)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="hover:bg-base-200 mb-2 w-full rounded-lg border-l-4 border-purple-600 px-4 py-3 text-left transition-colors"
|
class="hover:bg-base-200 border-secondary mb-2 w-full rounded-lg border-l-4 px-4 py-3 text-left transition-colors"
|
||||||
onclick={() => handleClickNotificacaoFerias(notificacao._id)}
|
onclick={() => handleClickNotificacaoFerias(notificacao._id)}
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<!-- Ícone -->
|
<!-- Ícone -->
|
||||||
<div class="mt-1 shrink-0">
|
<div class="mt-1 shrink-0">
|
||||||
<Calendar class="h-5 w-5 text-purple-600" strokeWidth={2} />
|
<Calendar class="text-secondary h-5 w-5" strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Conteúdo -->
|
<!-- Conteúdo -->
|
||||||
@@ -464,7 +423,7 @@
|
|||||||
|
|
||||||
<!-- Badge -->
|
<!-- Badge -->
|
||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
<div class="badge badge-primary badge-xs"></div>
|
<div class="badge badge-secondary badge-xs"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -475,17 +434,17 @@
|
|||||||
<!-- Notificações de Ausências -->
|
<!-- Notificações de Ausências -->
|
||||||
{#if notificacoesAusencias.length > 0}
|
{#if notificacoesAusencias.length > 0}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4 class="mb-2 px-2 text-sm font-semibold text-orange-600">Ausências</h4>
|
<h4 class="text-warning mb-2 px-2 text-sm font-semibold">Ausências</h4>
|
||||||
{#each notificacoesAusencias as notificacao (notificacao._id)}
|
{#each notificacoesAusencias as notificacao (notificacao._id)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="hover:bg-base-200 mb-2 w-full rounded-lg border-l-4 border-orange-600 px-4 py-3 text-left transition-colors"
|
class="hover:bg-base-200 border-warning mb-2 w-full rounded-lg border-l-4 px-4 py-3 text-left transition-colors"
|
||||||
onclick={() => handleClickNotificacaoAusencias(notificacao._id)}
|
onclick={() => handleClickNotificacaoAusencias(notificacao._id)}
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<!-- Ícone -->
|
<!-- Ícone -->
|
||||||
<div class="mt-1 shrink-0">
|
<div class="mt-1 shrink-0">
|
||||||
<Clock class="h-5 w-5 text-orange-600" strokeWidth={2} />
|
<Clock class="text-warning h-5 w-5" strokeWidth={2} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Conteúdo -->
|
<!-- Conteúdo -->
|
||||||
@@ -539,28 +498,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@keyframes badge-bounce {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-ring-subtle {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 0.1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.3;
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bell-ring {
|
@keyframes bell-ring {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Building2, FileText, Package, ShoppingCart } from 'lucide-svelte';
|
import { Building2, FileText, Package, ShoppingCart } from 'lucide-svelte';
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import ProtectedRoute from '$lib/components/ProtectedRoute.svelte';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ProtectedRoute>
|
|
||||||
<main class="container mx-auto px-4 py-4">
|
<main class="container mx-auto px-4 py-4">
|
||||||
<div class="breadcrumbs mb-4 text-sm">
|
<div class="breadcrumbs mb-4 text-sm">
|
||||||
<ul>
|
<ul>
|
||||||
@@ -95,4 +93,3 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</ProtectedRoute>
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
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();
|
const client = useConvexClient();
|
||||||
|
|
||||||
@@ -189,140 +190,173 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto p-6">
|
<main class="container mx-auto flex max-w-7xl flex-col px-4 py-4">
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<div class="breadcrumbs mb-4 text-sm">
|
||||||
<h1 class="text-2xl font-bold">Atas de Registro de Preços</h1>
|
<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
|
<button
|
||||||
|
class="btn btn-primary gap-2 shadow-md transition-all hover:shadow-lg"
|
||||||
onclick={() => openModal()}
|
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} />
|
<Plus class="h-5 w-5" strokeWidth={2} />
|
||||||
Nova Ata
|
Nova Ata
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if loadingAtas}
|
{#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}
|
{:else if errorAtas}
|
||||||
<p class="text-red-600">{errorAtas}</p>
|
<div class="alert alert-error">
|
||||||
|
<span>{errorAtas}</span>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-hidden rounded-lg bg-white shadow-md">
|
<div class="card bg-base-100/90 border-base-300 border shadow-xl backdrop-blur-sm">
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
<div class="card-body p-0">
|
||||||
<thead class="bg-gray-50">
|
<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>
|
<tr>
|
||||||
<th
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>Número</th
|
>Número</th
|
||||||
>
|
>
|
||||||
<th
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>SEI</th
|
>SEI</th
|
||||||
>
|
>
|
||||||
<th
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>Empresa</th
|
>Empresa</th
|
||||||
>
|
>
|
||||||
<th
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>Vigência</th
|
>Vigência</th
|
||||||
>
|
>
|
||||||
<th
|
<th
|
||||||
class="px-6 py-3 text-right text-xs font-medium tracking-wider text-gray-500 uppercase"
|
class="text-base-content border-base-400 border-b text-right font-bold whitespace-nowrap"
|
||||||
>Ações</th
|
>Ações</th
|
||||||
>
|
>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 bg-white">
|
<tbody>
|
||||||
{#each atas as ata (ata._id)}
|
{#if atas.length === 0}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-6 py-4 font-medium whitespace-nowrap">{ata.numero}</td>
|
<td colspan="5" class="py-12 text-center">
|
||||||
<td class="px-6 py-4 whitespace-nowrap">{ata.numeroSei}</td>
|
<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
|
<td
|
||||||
class="max-w-md truncate px-6 py-4 whitespace-nowrap"
|
class="max-w-md truncate whitespace-nowrap"
|
||||||
title={getEmpresaNome(ata.empresaId)}
|
title={getEmpresaNome(ata.empresaId)}
|
||||||
>
|
>
|
||||||
{getEmpresaNome(ata.empresaId)}
|
{getEmpresaNome(ata.empresaId)}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-sm whitespace-nowrap text-gray-500">
|
<td class="text-base-content/70 whitespace-nowrap">
|
||||||
{ata.dataInicio || '-'} a {ata.dataFim || '-'}
|
{ata.dataInicio || '-'} a {ata.dataFim || '-'}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-right text-sm font-medium whitespace-nowrap">
|
<td class="text-right whitespace-nowrap">
|
||||||
|
<div class="flex items-center justify-end gap-1">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-sm"
|
||||||
|
aria-label="Editar ata"
|
||||||
onclick={() => openModal(ata)}
|
onclick={() => openModal(ata)}
|
||||||
class="mr-4 text-indigo-600 hover:text-indigo-900"
|
|
||||||
>
|
>
|
||||||
<Pencil size={18} />
|
<Pencil size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-sm text-error"
|
||||||
|
aria-label="Excluir ata"
|
||||||
onclick={() => handleDelete(ata._id)}
|
onclick={() => handleDelete(ata._id)}
|
||||||
class="text-red-600 hover:text-red-900"
|
|
||||||
>
|
>
|
||||||
<Trash2 size={18} />
|
<Trash2 size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/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}
|
{/if}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showModal}
|
{#if showModal}
|
||||||
<div
|
<div class="modal modal-open">
|
||||||
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40"
|
<div class="modal-box max-w-4xl">
|
||||||
>
|
|
||||||
<div class="relative my-8 w-full max-w-2xl rounded-lg bg-white p-8 shadow-xl">
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-circle absolute top-2 right-2"
|
||||||
onclick={closeModal}
|
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>
|
</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 class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
<div>
|
<div class="space-y-4">
|
||||||
<div class="mb-4">
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="numero">
|
<label class="label" for="numero">
|
||||||
Número da Ata
|
<span class="label-text font-semibold">Número da Ata</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<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"
|
id="numero"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={formData.numero}
|
bind:value={formData.numero}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="numeroSei">
|
<label class="label" for="numeroSei">
|
||||||
Número SEI
|
<span class="label-text font-semibold">Número SEI</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<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"
|
id="numeroSei"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={formData.numeroSei}
|
bind:value={formData.numeroSei}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="empresa">
|
<label class="label" for="empresa">
|
||||||
Empresa
|
<span class="label-text font-semibold">Empresa</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
class="focus:shadow-outline w-full rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
|
||||||
id="empresa"
|
id="empresa"
|
||||||
|
class="select select-bordered focus:select-primary w-full"
|
||||||
bind:value={formData.empresaId}
|
bind:value={formData.empresaId}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
@@ -333,25 +367,25 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4 grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div>
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="dataInicio">
|
<label class="label" for="dataInicio">
|
||||||
Data Início
|
<span class="label-text font-semibold">Data Início</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<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"
|
id="dataInicio"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
type="date"
|
type="date"
|
||||||
bind:value={formData.dataInicio}
|
bind:value={formData.dataInicio}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="dataFim">
|
<label class="label" for="dataFim">
|
||||||
Data Fim
|
<span class="label-text font-semibold">Data Fim</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<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"
|
id="dataFim"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
type="date"
|
type="date"
|
||||||
bind:value={formData.dataFim}
|
bind:value={formData.dataFim}
|
||||||
/>
|
/>
|
||||||
@@ -359,81 +393,89 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="space-y-4">
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="objetos">
|
<div class="flex items-center justify-between">
|
||||||
Objetos Vinculados ({selectedObjetos.length})
|
<div class="font-semibold">Objetos Vinculados</div>
|
||||||
</label>
|
<span class="badge badge-outline">{selectedObjetos.length}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="relative mb-2">
|
<div class="form-control w-full">
|
||||||
|
<label class="label" for="buscar_objeto">
|
||||||
|
<span class="label-text font-semibold">Buscar objetos</span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
<Search size={16} class="text-gray-400" />
|
<Search size={16} class="text-base-content/40" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
id="buscar_objeto"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Buscar objetos..."
|
placeholder="Digite para filtrar..."
|
||||||
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"
|
class="input input-bordered focus:input-primary w-full pl-10"
|
||||||
bind:value={searchObjeto}
|
bind:value={searchObjeto}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="border-base-300 max-h-52 overflow-y-auto rounded-lg border p-2">
|
||||||
class="mb-4 flex-1 overflow-y-auto rounded border bg-gray-50 p-2"
|
|
||||||
style="max-height: 200px;"
|
|
||||||
>
|
|
||||||
{#if filteredObjetos.length === 0}
|
{#if filteredObjetos.length === 0}
|
||||||
<p class="py-4 text-center text-sm text-gray-500">Nenhum objeto encontrado.</p>
|
<p class="text-base-content/60 px-2 py-3 text-center text-sm">
|
||||||
|
Nenhum objeto encontrado.
|
||||||
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-1">
|
|
||||||
{#each filteredObjetos as objeto (objeto._id)}
|
{#each filteredObjetos as objeto (objeto._id)}
|
||||||
<button
|
{@const isSelected = selectedObjetos.includes(objeto._id)}
|
||||||
type="button"
|
<label
|
||||||
class="flex w-full items-center justify-between rounded px-3 py-2 text-left text-sm hover:bg-gray-200 {selectedObjetos.includes(
|
class="hover:bg-base-200/50 flex cursor-pointer items-center gap-3 rounded-md px-2 py-2 {isSelected
|
||||||
objeto._id
|
? 'bg-primary/5'
|
||||||
)
|
|
||||||
? 'bg-blue-50 text-blue-700'
|
|
||||||
: ''}"
|
: ''}"
|
||||||
onclick={() => toggleObjeto(objeto._id)}
|
|
||||||
>
|
>
|
||||||
<span class="truncate">{objeto.nome}</span>
|
<input
|
||||||
{#if selectedObjetos.includes(objeto._id)}
|
type="checkbox"
|
||||||
<Check size={16} class="text-blue-600" />
|
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}
|
{/if}
|
||||||
</button>
|
</label>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t pt-4">
|
<div class="border-base-300 border-t pt-4">
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="anexos">
|
<div class="font-semibold">Anexos</div>
|
||||||
Anexos
|
<div class="mt-2 space-y-2">
|
||||||
</label>
|
|
||||||
<input
|
<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"
|
id="anexos"
|
||||||
type="file"
|
type="file"
|
||||||
multiple
|
multiple
|
||||||
|
class="file-input file-input-bordered w-full"
|
||||||
onchange={handleAttachmentsSelect}
|
onchange={handleAttachmentsSelect}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if attachments.length > 0}
|
{#if attachments.length > 0}
|
||||||
<div class="mt-2 max-h-40 space-y-2 overflow-y-auto">
|
|
||||||
{#each attachments as doc (doc._id)}
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between rounded bg-gray-100 p-2 text-sm"
|
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
|
<a
|
||||||
href={doc.url}
|
href={doc.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="max-w-[150px] truncate text-blue-600 hover:underline"
|
class="link link-primary max-w-[260px] truncate"
|
||||||
>
|
>
|
||||||
{doc.nome}
|
{doc.nome}
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs text-error"
|
||||||
onclick={() => handleDeleteAttachment(doc._id)}
|
onclick={() => handleDeleteAttachment(doc._id)}
|
||||||
class="text-red-500 hover:text-red-700"
|
aria-label="Excluir anexo"
|
||||||
>
|
>
|
||||||
<X size={16} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
@@ -444,25 +486,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex items-center justify-end border-t pt-4">
|
<div class="modal-action">
|
||||||
<button
|
<button type="button" class="btn" onclick={closeModal} disabled={saving || uploading}>
|
||||||
type="button"
|
|
||||||
onclick={closeModal}
|
|
||||||
class="mr-2 rounded bg-gray-300 px-4 py-2 font-bold text-gray-800 hover:bg-gray-400"
|
|
||||||
>
|
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="submit" class="btn btn-primary" disabled={saving || uploading}>
|
||||||
type="submit"
|
{#if saving || uploading}
|
||||||
disabled={saving || uploading}
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
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"
|
{/if}
|
||||||
>
|
|
||||||
{saving || uploading ? 'Salvando...' : 'Salvar'}
|
{saving || uploading ? 'Salvando...' : 'Salvar'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="modal-backdrop" onclick={closeModal} aria-label="Fechar modal"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</main>
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Doc, Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { useConvexClient, useQuery } from 'convex-svelte';
|
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 { maskCurrencyBRL } from '$lib/utils/masks';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
@@ -116,56 +117,86 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto p-6">
|
<main class="container mx-auto flex max-w-7xl flex-col px-4 py-4">
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<div class="breadcrumbs mb-4 text-sm">
|
||||||
<h1 class="text-2xl font-bold">Objetos</h1>
|
<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
|
<button
|
||||||
|
class="btn btn-primary gap-2 shadow-md transition-all hover:shadow-lg"
|
||||||
onclick={() => openModal()}
|
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} />
|
<Plus class="h-5 w-5" strokeWidth={2} />
|
||||||
Novo Objeto
|
Novo Objeto
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if loading}
|
{#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}
|
{:else if error}
|
||||||
<p class="text-red-600">{error}</p>
|
<div class="alert alert-error">
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="overflow-hidden rounded-lg bg-white shadow-md">
|
<div class="card bg-base-100/90 border-base-300 border shadow-xl backdrop-blur-sm">
|
||||||
<table class="min-w-full divide-y divide-gray-200">
|
<div class="card-body p-0">
|
||||||
<thead class="bg-gray-50">
|
<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>
|
<tr>
|
||||||
<th
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>Nome</th
|
>Nome</th
|
||||||
>
|
>
|
||||||
<th
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>Tipo</th
|
>Tipo</th
|
||||||
>
|
>
|
||||||
<th
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>Unidade</th
|
>Unidade</th
|
||||||
>
|
>
|
||||||
<th
|
<th class="text-base-content border-base-400 border-b font-bold whitespace-nowrap"
|
||||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
|
||||||
>Valor Estimado</th
|
>Valor Estimado</th
|
||||||
>
|
>
|
||||||
<th
|
<th
|
||||||
class="px-6 py-3 text-right text-xs font-medium tracking-wider text-gray-500 uppercase"
|
class="text-base-content border-base-400 border-b text-right font-bold whitespace-nowrap"
|
||||||
>Ações</th
|
>Ações</th
|
||||||
>
|
>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 bg-white">
|
<tbody>
|
||||||
{#each objetos as objeto (objeto._id)}
|
{#if objetos.length === 0}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<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">
|
<div class="flex flex-col">
|
||||||
<span class="font-medium">{objeto.nome}</span>
|
<span class="font-medium">{objeto.nome}</span>
|
||||||
<span class="text-xs text-gray-500">
|
<span class="text-base-content/60 text-xs">
|
||||||
Efisco: {objeto.codigoEfisco}
|
Efisco: {objeto.codigoEfisco}
|
||||||
{#if objeto.codigoCatmat}
|
{#if objeto.codigoCatmat}
|
||||||
| Catmat: {objeto.codigoCatmat}{/if}
|
| Catmat: {objeto.codigoCatmat}{/if}
|
||||||
@@ -174,92 +205,99 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="whitespace-nowrap">
|
||||||
<span
|
<span
|
||||||
class="inline-flex rounded-full px-2 text-xs leading-5 font-semibold
|
class="badge badge-sm {objeto.tipo === 'servico'
|
||||||
{objeto.tipo === 'servico'
|
? 'badge-success'
|
||||||
? 'bg-green-100 text-green-800'
|
: 'badge-info'}"
|
||||||
: 'bg-blue-100 text-blue-800'}"
|
|
||||||
>
|
>
|
||||||
{objeto.tipo === 'material' ? 'Material' : 'Serviço'}
|
{objeto.tipo === 'material' ? 'Material' : 'Serviço'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">{objeto.unidade}</td>
|
<td class="whitespace-nowrap">{objeto.unidade}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="whitespace-nowrap">
|
||||||
{maskCurrencyBRL(objeto.valorEstimado) || 'R$ 0,00'}
|
{maskCurrencyBRL(objeto.valorEstimado) || 'R$ 0,00'}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-right text-sm font-medium whitespace-nowrap">
|
<td class="text-right whitespace-nowrap">
|
||||||
|
<div class="flex items-center justify-end gap-1">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-sm"
|
||||||
|
aria-label="Editar objeto"
|
||||||
onclick={() => openModal(objeto)}
|
onclick={() => openModal(objeto)}
|
||||||
class="mr-4 text-indigo-600 hover:text-indigo-900"
|
|
||||||
>
|
>
|
||||||
<Pencil size={18} />
|
<Pencil size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-sm text-error"
|
||||||
|
aria-label="Excluir objeto"
|
||||||
onclick={() => handleDelete(objeto._id)}
|
onclick={() => handleDelete(objeto._id)}
|
||||||
class="text-red-600 hover:text-red-900"
|
|
||||||
>
|
>
|
||||||
<Trash2 size={18} />
|
<Trash2 size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/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}
|
{/if}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showModal}
|
{#if showModal}
|
||||||
<div
|
<div class="modal modal-open">
|
||||||
class="fixed inset-0 z-50 flex h-full w-full items-center justify-center overflow-y-auto bg-black/40"
|
<div class="modal-box max-w-2xl">
|
||||||
>
|
|
||||||
<div class="relative w-full max-w-md rounded-lg bg-white p-8 shadow-xl">
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sm btn-circle absolute top-2 right-2"
|
||||||
onclick={closeModal}
|
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>
|
</button>
|
||||||
<h2 class="mb-6 text-xl font-bold">{editingId ? 'Editar' : 'Novo'} Objeto</h2>
|
|
||||||
|
|
||||||
<form onsubmit={handleSubmit}>
|
<h3 class="text-lg font-bold">{editingId ? 'Editar' : 'Novo'} Objeto</h3>
|
||||||
<div class="mb-4">
|
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="nome"> Nome </label>
|
<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
|
<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"
|
id="nome"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={formData.nome}
|
bind:value={formData.nome}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4 grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div>
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="tipo"> Tipo </label>
|
<label class="label" for="tipo">
|
||||||
|
<span class="label-text font-semibold">Tipo</span>
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
class="focus:shadow-outline w-full rounded border px-3 py-2 leading-tight text-gray-700 shadow focus:outline-none"
|
|
||||||
id="tipo"
|
id="tipo"
|
||||||
|
class="select select-bordered focus:select-primary w-full"
|
||||||
bind:value={formData.tipo}
|
bind:value={formData.tipo}
|
||||||
>
|
>
|
||||||
<option value="material">Material</option>
|
<option value="material">Material</option>
|
||||||
<option value="servico">Serviço</option>
|
<option value="servico">Serviço</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="unidade">
|
<div class="form-control w-full">
|
||||||
Unidade
|
<label class="label" for="unidade">
|
||||||
|
<span class="label-text font-semibold">Unidade</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<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"
|
id="unidade"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={formData.unidade}
|
bind:value={formData.unidade}
|
||||||
required
|
required
|
||||||
@@ -267,13 +305,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="codigoEfisco">
|
<label class="label" for="codigoEfisco">
|
||||||
Código Efisco
|
<span class="label-text font-semibold">Código Efisco</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<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"
|
id="codigoEfisco"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={formData.codigoEfisco}
|
bind:value={formData.codigoEfisco}
|
||||||
required
|
required
|
||||||
@@ -281,88 +319,86 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if formData.tipo === 'material'}
|
{#if formData.tipo === 'material'}
|
||||||
<div class="mb-4">
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="codigoCatmat">
|
<label class="label" for="codigoCatmat">
|
||||||
Código Catmat
|
<span class="label-text font-semibold">Código Catmat</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<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"
|
id="codigoCatmat"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={formData.codigoCatmat}
|
bind:value={formData.codigoCatmat}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="mb-4">
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="codigoCatserv">
|
<label class="label" for="codigoCatserv">
|
||||||
Código Catserv
|
<span class="label-text font-semibold">Código Catserv</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<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"
|
id="codigoCatserv"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={formData.codigoCatserv}
|
bind:value={formData.codigoCatserv}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="valor">
|
<label class="label" for="valor">
|
||||||
Valor Estimado
|
<span class="label-text font-semibold">Valor Estimado</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<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"
|
id="valor"
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
type="text"
|
type="text"
|
||||||
|
placeholder="R$ 0,00"
|
||||||
bind:value={formData.valorEstimado}
|
bind:value={formData.valorEstimado}
|
||||||
oninput={(e) => (formData.valorEstimado = maskCurrencyBRL(e.currentTarget.value))}
|
oninput={(e) => (formData.valorEstimado = maskCurrencyBRL(e.currentTarget.value))}
|
||||||
placeholder="R$ 0,00"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="form-control w-full">
|
||||||
<label class="mb-2 block text-sm font-bold text-gray-700" for="atas">
|
<label class="label" for="atas">
|
||||||
Vincular Atas
|
<span class="label-text font-semibold">Vincular Atas</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="max-h-40 overflow-y-auto rounded border p-2">
|
<div class="border-base-300 max-h-48 overflow-y-auto rounded-lg border p-2">
|
||||||
|
{#if atas.length === 0}
|
||||||
|
<p class="text-base-content/60 px-2 py-3 text-sm">Nenhuma ata disponível.</p>
|
||||||
|
{:else}
|
||||||
{#each atas as ata (ata._id)}
|
{#each atas as ata (ata._id)}
|
||||||
<div class="mb-2 flex items-center">
|
<label
|
||||||
|
class="hover:bg-base-200/50 flex cursor-pointer items-center gap-3 rounded-md px-2 py-2"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={`ata-${ata._id}`}
|
class="checkbox checkbox-primary checkbox-sm"
|
||||||
checked={formData.atas.includes(ata._id)}
|
checked={formData.atas.includes(ata._id)}
|
||||||
onchange={() => toggleAtaSelection(ata._id)}
|
onchange={() => toggleAtaSelection(ata._id)}
|
||||||
class="mr-2 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
aria-label="Vincular ata {ata.numero}"
|
||||||
/>
|
/>
|
||||||
<label for={`ata-${ata._id}`} class="text-sm text-gray-700">
|
<span class="text-sm">{ata.numero} ({ata.numeroSei})</span>
|
||||||
{ata.numero} ({ata.numeroSei})
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
{/each}
|
{/each}
|
||||||
{#if atas.length === 0}
|
|
||||||
<p class="text-sm text-gray-500">Nenhuma ata disponível.</p>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-end">
|
<div class="modal-action">
|
||||||
<button
|
<button type="button" class="btn" onclick={closeModal} disabled={saving}>
|
||||||
type="button"
|
|
||||||
onclick={closeModal}
|
|
||||||
class="mr-2 rounded bg-gray-300 px-4 py-2 font-bold text-gray-800 hover:bg-gray-400"
|
|
||||||
>
|
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="submit" class="btn btn-primary" disabled={saving}>
|
||||||
type="submit"
|
{#if saving}
|
||||||
disabled={saving}
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
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"
|
{/if}
|
||||||
>
|
|
||||||
{saving ? 'Salvando...' : 'Salvar'}
|
{saving ? 'Salvando...' : 'Salvar'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="modal-backdrop" onclick={closeModal} aria-label="Fechar modal"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</main>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
Users,
|
Users,
|
||||||
Inbox,
|
Inbox,
|
||||||
Search,
|
Search,
|
||||||
AlertTriangle,
|
TriangleAlert,
|
||||||
User
|
User
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import type { FunctionReturnType } from 'convex/server';
|
import type { FunctionReturnType } from 'convex/server';
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
const list = $derived(funcionariosQuery.data ?? []);
|
const list = $derived(funcionariosQuery.data ?? []);
|
||||||
|
|
||||||
let funcionarioToDelete = $derived<Funcionario | null>(null);
|
let funcionarioToDelete = $derived<Funcionario | null>(null);
|
||||||
|
let deleting = $state(false);
|
||||||
|
|
||||||
let filtro = $state('');
|
let filtro = $state('');
|
||||||
let notice: { kind: 'success' | 'error'; text: string } | null = $state(null);
|
let notice: { kind: 'success' | 'error'; text: string } | null = $state(null);
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
if (!funcionarioToDelete) return;
|
if (!funcionarioToDelete) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
deleting = true;
|
||||||
await client.mutation(api.funcionarios.remove, { id: funcionarioToDelete._id });
|
await client.mutation(api.funcionarios.remove, { id: funcionarioToDelete._id });
|
||||||
closeDeleteModal();
|
closeDeleteModal();
|
||||||
notice = {
|
notice = {
|
||||||
@@ -56,6 +58,8 @@
|
|||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
notice = { kind: 'error', text: 'Erro ao excluir cadastro. Tente novamente.' };
|
notice = { kind: 'error', text: 'Erro ao excluir cadastro. Tente novamente.' };
|
||||||
|
} finally {
|
||||||
|
deleting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,12 +247,12 @@
|
|||||||
<dialog id="delete_modal_func_excluir" class="modal">
|
<dialog id="delete_modal_func_excluir" class="modal">
|
||||||
<div class="modal-box max-w-md">
|
<div class="modal-box max-w-md">
|
||||||
<h3 class="text-error mb-4 flex items-center gap-2 text-2xl font-bold">
|
<h3 class="text-error mb-4 flex items-center gap-2 text-2xl font-bold">
|
||||||
<AlertTriangle class="h-7 w-7" strokeWidth={2} />
|
<TriangleAlert class="h-7 w-7" strokeWidth={2} />
|
||||||
Confirmar Exclusão
|
Confirmar Exclusão
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div class="alert alert-warning mb-4">
|
<div class="alert alert-warning mb-4">
|
||||||
<AlertTriangle class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
<TriangleAlert class="h-6 w-6 shrink-0 stroke-current" strokeWidth={2} />
|
||||||
<div>
|
<div>
|
||||||
<span class="font-bold">Atenção!</span>
|
<span class="font-bold">Atenção!</span>
|
||||||
<p class="text-sm">Esta ação não pode ser desfeita!</p>
|
<p class="text-sm">Esta ação não pode ser desfeita!</p>
|
||||||
@@ -280,16 +284,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="modal-action justify-between">
|
<div class="modal-action justify-between">
|
||||||
<button class="btn gap-2" onclick={closeDeleteModal} disabled={funcionarioToDelete !== null}>
|
<button class="btn gap-2" onclick={closeDeleteModal} disabled={deleting}>
|
||||||
<X class="h-5 w-5" strokeWidth={2} />
|
<X class="h-5 w-5" strokeWidth={2} />
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-error gap-2"
|
class="btn btn-error gap-2"
|
||||||
onclick={confirmDelete}
|
onclick={confirmDelete}
|
||||||
disabled={funcionarioToDelete !== null}
|
disabled={deleting || funcionarioToDelete === null}
|
||||||
>
|
>
|
||||||
{#if funcionarioToDelete}
|
{#if deleting}
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
Excluindo...
|
Excluindo...
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
Reference in New Issue
Block a user