diff --git a/.agent/rules/convex-typing.md b/.agent/rules/convex-typing.md new file mode 100644 index 0000000..b3a8929 --- /dev/null +++ b/.agent/rules/convex-typing.md @@ -0,0 +1,69 @@ +--- +trigger: glob +description: Regras de tipagem para queries e mutations do Convex +globs: **/*.svelte.ts,**/*.svelte +--- + +# Regras de Tipagem do Convex + +## Regra Principal + +**NUNCA** crie anotações de tipo manuais para queries ou mutations do Convex. Os tipos já são inferidos automaticamente pelo Convex. + +### ❌ Errado - Não faça isso: + +```typescript +// NÃO crie tipos manuais para o retorno de queries +type Funcionario = { + _id: Id<'funcionarios'>; + nome: string; + email: string; + // ... outras propriedades +}; + +const funcionarios: Funcionario[] = useQuery(api.funcionarios.getAll) ?? []; +``` + +### ✅ Correto - Use inferência automática: + +```typescript +// O tipo já vem inferido automaticamente +const funcionarios = useQuery(api.funcionarios.getAll); +``` + +--- + +## Quando Tipar É Necessário + +Em situações onde você **realmente precisa** de um tipo explícito (ex: props de componentes, variáveis de estado, etc.), use `FunctionReturnType` para inferir o tipo: + +```typescript +import { FunctionReturnType } from 'convex/server'; +import { api } from '$convex/_generated/api'; + +// Infere o tipo de retorno da query +type FuncionariosQueryResult = FunctionReturnType; + +// Agora pode usar em props de componentes +interface Props { + funcionarios: FuncionariosQueryResult; +} +``` + +### Casos de Uso Válidos para `FunctionReturnType`: + +1. **Props de componentes** - quando um componente filho recebe dados de uma query +2. **Variáveis derivadas** - quando precisa tipar uma transformação dos dados +3. **Funções auxiliares** - quando cria funções que operam sobre os dados da query +4. **Stores/Estado global** - quando armazena dados em estado externo ao componente + +--- + +## Resumo + +| Situação | Abordagem | +| --------------------------- | ------------------------------------------------- | +| Usar `useQuery` diretamente | Deixe o tipo ser inferido automaticamente | +| Props de componentes | Use `FunctionReturnType` | +| Transformações de dados | Use `FunctionReturnType` | +| Anotações manuais de tipo | **NUNCA** - sempre infira do Convex | diff --git a/.agent/rules/svelte-rules.md b/.agent/rules/svelte-rules.md index 208d7ff..1737d67 100644 --- a/.agent/rules/svelte-rules.md +++ b/.agent/rules/svelte-rules.md @@ -1,5 +1,6 @@ --- -trigger: glob +trigger: model_decision +description: whenever you're working with Svelte files globs: **/*.svelte.ts,**/*.svelte --- diff --git a/.vscode/settings.json b/.vscode/settings.json index 324766b..bdba50b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,28 +19,20 @@ "pattern": "packages/*" } ], - "eslint.validate": [ - "javascript", - "javascriptreact", - "typescript", - "typescriptreact", - "svelte" - ], + "eslint.validate": ["javascript", "typescript", "svelte"], "eslint.options": { "cache": true, "cacheLocation": ".eslintcache" }, "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit", - "source.addMissingImports": "always", - "source.removeUnusedImports": "always", - }, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features" }, + "[svelte]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, "editor.tabSize": 2 -} \ No newline at end of file +} diff --git a/apps/web/package.json b/apps/web/package.json index db9e0ba..49ad8c6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -57,6 +57,7 @@ "jspdf-autotable": "^5.0.2", "lib-jitsi-meet": "^1.0.6", "lucide-svelte": "^0.552.0", + "marked": "^17.0.1", "papaparse": "^5.4.1", "svelte-sonner": "^1.0.5", "xlsx": "^0.18.5", diff --git a/apps/web/src/hooks.server.ts b/apps/web/src/hooks.server.ts index df2d022..a04f98e 100644 --- a/apps/web/src/hooks.server.ts +++ b/apps/web/src/hooks.server.ts @@ -1,9 +1,91 @@ -import type { Handle } from '@sveltejs/kit'; +import type { Handle, HandleServerError } from '@sveltejs/kit'; import { createAuth } from '@sgse-app/backend/convex/auth'; -import { getToken } from '@mmailaender/convex-better-auth-svelte/sveltekit'; +import { getToken, createConvexHttpClient } from '@mmailaender/convex-better-auth-svelte/sveltekit'; +import { api } from '@sgse-app/backend/convex/_generated/api'; export const handle: Handle = async ({ event, resolve }) => { event.locals.token = await getToken(createAuth, event.cookies); return resolve(event); }; + +export const handleError: HandleServerError = async ({ error, event, status, message }) => { + // Notificar erros 404 e 500+ (erros internos do servidor) + if (status === 404 || status === 500 || status >= 500) { + // Evitar loop infinito: não registrar erros relacionados à própria página de erros + const urlPath = event.url.pathname; + if (urlPath.includes('/ti/erros-servidor')) { + console.warn( + `⚠️ Erro na página de erros do servidor (${status}): Não será registrado para evitar loop.` + ); + } else { + try { + // Obter token do usuário (se autenticado) + const token = event.locals.token; + + // Criar cliente Convex para chamar a action + const client = createConvexHttpClient({ + token: token || undefined + }); + + // Extrair informações do erro + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + const url = event.url.toString(); + const method = event.request.method; + const ipAddress = event.getClientAddress(); + const userAgent = event.request.headers.get('user-agent') || undefined; + + // Log para debug + console.log(`📝 Registrando erro ${status} no servidor:`, { + url, + method, + mensagem: errorMessage.substring(0, 100) + }); + + // Chamar action para registrar e notificar erro + // Aguardar a promise mas não bloquear a resposta se falhar + try { + // Usar Promise.race com timeout para evitar bloquear a resposta + const actionPromise = client.action(api.errosServidor.registrarErroServidor, { + statusCode: status, + mensagem: errorMessage, + stack: errorStack, + url, + method, + ipAddress, + userAgent, + usuarioId: undefined // Pode ser implementado depois para obter do token + }); + + // Timeout de 3 segundos + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Timeout ao registrar erro')), 3000); + }); + + const resultado = await Promise.race([actionPromise, timeoutPromise]); + console.log(`✅ Erro ${status} registrado com sucesso:`, resultado); + } catch (actionError) { + // Log do erro de notificação, mas não falhar a resposta + console.error( + `❌ Erro ao registrar notificação de erro ${status}:`, + actionError instanceof Error ? actionError.message : actionError + ); + } + } catch (err) { + // Se falhar ao criar cliente ou chamar action, apenas logar + // Não queremos que falhas na notificação quebrem a resposta de erro + console.error( + `❌ Erro ao tentar notificar equipe técnica sobre erro ${status}:`, + err instanceof Error ? err.message : err + ); + } + } + } + + // Retornar mensagem de erro padrão + return { + message: message || 'Erro interno do servidor', + status + }; +}; diff --git a/apps/web/src/lib/components/ActionGuard.svelte b/apps/web/src/lib/components/ActionGuard.svelte index 3a774ac..6b718fb 100644 --- a/apps/web/src/lib/components/ActionGuard.svelte +++ b/apps/web/src/lib/components/ActionGuard.svelte @@ -1,9 +1,9 @@ + +{#if open} + +{/if} + + + diff --git a/apps/web/src/lib/components/AlterarStatusFerias.svelte b/apps/web/src/lib/components/AlterarStatusFerias.svelte index 5e80f6f..cad9d34 100644 --- a/apps/web/src/lib/components/AlterarStatusFerias.svelte +++ b/apps/web/src/lib/components/AlterarStatusFerias.svelte @@ -1,7 +1,8 @@ + +{#if open} + +{/if} + + + diff --git a/apps/web/src/lib/components/FileUpload.svelte b/apps/web/src/lib/components/FileUpload.svelte index b70ca05..7898d94 100644 --- a/apps/web/src/lib/components/FileUpload.svelte +++ b/apps/web/src/lib/components/FileUpload.svelte @@ -2,14 +2,13 @@ import { useConvexClient } from 'convex-svelte'; import { ExternalLink, - Eye, - File as FileIcon, FileText, - RefreshCw, + File as FileIcon, + Upload, Trash2, - Upload + Eye, + RefreshCw } from 'lucide-svelte'; - import { resolve } from '$app/paths'; interface Props { label: string; @@ -21,7 +20,7 @@ onRemove: () => Promise; } - const { + let { label, helpUrl, value = $bindable(), diff --git a/apps/web/src/lib/components/MenuProtection.svelte b/apps/web/src/lib/components/MenuProtection.svelte index e9d013c..bb772a9 100644 --- a/apps/web/src/lib/components/MenuProtection.svelte +++ b/apps/web/src/lib/components/MenuProtection.svelte @@ -1,10 +1,9 @@ -```
@@ -188,21 +196,16 @@ placeholder="Buscar usuários (nome, email, matrícula)..." class="input input-bordered w-full pl-10" bind:value={searchQuery} + aria-label="Buscar usuários ou conversas" + aria-describedby="search-help" /> - Digite para buscar usuários por nome, email ou matrícula - - +
@@ -222,7 +225,7 @@ class={`tab flex-1 ${activeTab === 'conversas' ? 'tab-active' : ''}`} onclick={() => (activeTab = 'conversas')} > - 💬 Conversas ({conversasFiltradas().length}) + 💬 Conversas ({conversasFiltradas.length}) @@ -235,16 +238,7 @@ title="Nova conversa (grupo ou sala de reunião)" aria-label="Nova conversa" > - - - + Nova Conversa @@ -263,34 +257,24 @@ : 'cursor-pointer'}" onclick={() => handleClickUsuario(usuario)} disabled={processando} + aria-label="Abrir conversa com {usuario.nome}" + aria-describedby="usuario-status-{usuario._id}" >
- - - - +
@@ -321,6 +305,9 @@ {usuario.statusMensagem || usuario.email}

+ + Status: {getStatusLabel(usuario.statusPresenca)} +
{/each} @@ -332,27 +319,14 @@ {:else}
- - - +

Nenhum usuário encontrado

{/if} {:else} - {#if conversas?.data && conversasFiltradas().length > 0} - {#each conversasFiltradas() as conversa (conversa._id)} + {#if conversas?.data && conversasFiltradas.length > 0} + {#each conversasFiltradas as conversa (conversa._id)} @@ -981,21 +1198,11 @@
- - - + strokeWidth={2.5} + /> @@ -1009,20 +1216,11 @@
- - - - + strokeWidth={2.5} + /> @@ -1117,6 +1315,8 @@ {/if} + + {#if showGlobalNotificationPopup && globalNotificationMessage} {@const notificationMsg = globalNotificationMessage} @@ -1157,20 +1357,7 @@ >
- - - +

@@ -1194,16 +1381,7 @@ } }} > - - - +

@@ -1249,7 +1427,7 @@ } } - /* Rotação para anel de brilho */ + /* Rotação para anel de brilho - suavizada */ @keyframes rotate { from { transform: rotate(0deg); @@ -1259,6 +1437,19 @@ } } + /* Efeito de pulso de brilho durante arrasto */ + @keyframes pulse-glow { + 0%, + 100% { + opacity: 0.2; + transform: scale(1); + } + 50% { + opacity: 0.4; + transform: scale(1.05); + } + } + /* Efeito shimmer para o header */ @keyframes shimmer { 0% { diff --git a/apps/web/src/lib/components/chat/ChatWindow.svelte b/apps/web/src/lib/components/chat/ChatWindow.svelte index 31b4d9b..4e54ca1 100644 --- a/apps/web/src/lib/components/chat/ChatWindow.svelte +++ b/apps/web/src/lib/components/chat/ChatWindow.svelte @@ -1,31 +1,35 @@ + +{#if showIndicator} +
+ {#if !isOnline} + + Sem conexão + {:else if !convexConnected} + + Reconectando... + {:else} + + Conectado + {/if} +
+{/if} + diff --git a/apps/web/src/lib/components/chat/E2EManagementModal.svelte b/apps/web/src/lib/components/chat/E2EManagementModal.svelte new file mode 100644 index 0000000..bfb336e --- /dev/null +++ b/apps/web/src/lib/components/chat/E2EManagementModal.svelte @@ -0,0 +1,267 @@ + + + + diff --git a/apps/web/src/lib/components/chat/MessageInput.svelte b/apps/web/src/lib/components/chat/MessageInput.svelte index a9369fd..21c258b 100644 --- a/apps/web/src/lib/components/chat/MessageInput.svelte +++ b/apps/web/src/lib/components/chat/MessageInput.svelte @@ -1,9 +1,18 @@
- {#if mensagens?.data && mensagens.data.length > 0} - {@const gruposPorDia = agruparMensagensPorDia(mensagens.data)} + {#if todasMensagens.length > 0} + {@const mensagensParaExibir = + mensagensComConteudo.length > 0 ? mensagensComConteudo : todasMensagens} + {@const gruposPorDia = agruparMensagensPorDia(mensagensParaExibir)} {#each Object.entries(gruposPorDia) as [dia, mensagensDia]}
@@ -511,12 +951,27 @@ cancelarEdicao(); } }} + aria-label="Editar mensagem" + aria-describedby="edicao-help" > +

+ Pressione Ctrl+Enter para salvar ou Escape para cancelar +

- - +
{:else if mensagem.deletada} @@ -525,7 +980,7 @@

- {mensagem.conteudo} + {mensagem.conteudoDescriptografado ?? mensagem.conteudo}

{#if mensagem.editadaEm} (editado) @@ -573,37 +1028,30 @@ {:else if mensagem.tipo === 'imagem'}
{mensagem.arquivoNome} { + if (mensagem.criptografado) { + (e.target as HTMLImageElement).alt = '🔒 Erro ao descriptografar imagem'; + } + }} />
{#if mensagem.conteudo}

- {mensagem.conteudo} + {mensagem.conteudoDescriptografado ?? mensagem.conteudo}

{/if} {:else if mensagem.tipo === 'arquivo'} - - - +

{mensagem.arquivoNome}

{#if mensagem.arquivoTamanho} @@ -637,6 +1085,7 @@ class="text-base-content/50 hover:text-primary mt-1 text-xs transition-colors" onclick={() => responderMensagem(mensagem)} title="Responder" + aria-label="Responder à mensagem de {mensagem.remetente?.nome || 'usuário'}" > ↪️ Responder @@ -655,45 +1104,15 @@
{#if mensagemFoiLida(mensagem)} - - - - - - + /> + {:else} - - - + {/if}
{/if} @@ -705,6 +1124,7 @@ class="text-base-content/50 hover:text-primary text-xs transition-colors" onclick={() => editarMensagem(mensagem)} title="Editar mensagem" + aria-label="Editar esta mensagem" > ✏️ @@ -712,6 +1132,7 @@ class="text-base-content/50 hover:text-error text-xs transition-colors" onclick={() => deletarMensagem(mensagem._id, false)} title="Deletar mensagem" + aria-label="Deletar esta mensagem" > 🗑️ @@ -721,6 +1142,7 @@ class="text-base-content/50 hover:text-error text-xs transition-colors" onclick={() => deletarMensagem(mensagem._id, true)} title="Deletar mensagem (como administrador)" + aria-label="Deletar esta mensagem como administrador" > 🗑️ Admin @@ -753,7 +1175,22 @@

{/if} - {:else if !mensagens?.data} + + + {#if carregandoMais} +
+ + Carregando mensagens anteriores... +
+ {/if} + + + {#if !hasMore && todasMensagens.length > 0} +
+ Não há mais mensagens +
+ {/if} + {:else if !mensagensQuery?.data && todasMensagens.length === 0}
@@ -761,20 +1198,7 @@ {:else}
- - - +

Nenhuma mensagem ainda

Envie a primeira mensagem!

@@ -796,20 +1220,7 @@ >
- - - +

@@ -831,16 +1242,7 @@ } }} > - - - +

diff --git a/apps/web/src/lib/components/chat/NotificationBell.svelte b/apps/web/src/lib/components/chat/NotificationBell.svelte index 26f066d..d270e47 100644 --- a/apps/web/src/lib/components/chat/NotificationBell.svelte +++ b/apps/web/src/lib/components/chat/NotificationBell.svelte @@ -1,11 +1,10 @@
- {@html config.icon} +
diff --git a/apps/web/src/lib/components/ferias/WizardSolicitacaoFerias.svelte b/apps/web/src/lib/components/ferias/WizardSolicitacaoFerias.svelte index 34cb88b..67df941 100644 --- a/apps/web/src/lib/components/ferias/WizardSolicitacaoFerias.svelte +++ b/apps/web/src/lib/components/ferias/WizardSolicitacaoFerias.svelte @@ -1,8 +1,7 @@ + +
+ +
+

Banco de Horas Mensal

+
+ + +
+ + + {formatarMes(mesSelecionado)} + + +
+
+
+ + + {#if saldoNegativo && bancoMensal} +
+ +
+

Atenção: Saldo Negativo Acumulado

+
+ Seu saldo acumulado está negativo em{' '} + + {Math.abs(bancoMensal.saldoFormatado.final.horas)}h{' '} + {Math.abs(bancoMensal.saldoFormatado.final.minutos)}min + + . Considere compensar horas ou entrar em contato com seu gestor. +
+
+
+ {/if} + + {#if bancoMensalQuery?.isLoading} +
+ +
+ {:else if bancoMensal} + +
+ +
+
+
+
+

Saldo Inicial

+

+ {bancoMensal.saldoFormatado.inicial.positivo ? '+' : '-'} + {bancoMensal.saldoFormatado.inicial.horas}h{' '} + {bancoMensal.saldoFormatado.inicial.minutos}min +

+
+
+ {#if bancoMensal.saldoFormatado.inicial.positivo} + + {:else} + + {/if} +
+
+
+
+ + +
+
+
+
+

Saldo do Mês

+

+ {bancoMensal.saldoFormatado.mes.positivo ? '+' : '-'} + {bancoMensal.saldoFormatado.mes.horas}h{' '} + {bancoMensal.saldoFormatado.mes.minutos}min +

+
+
+ {#if bancoMensal.saldoFormatado.mes.positivo} + + {:else} + + {/if} +
+
+
+
+ + +
+
+
+
+

Saldo Final

+

+ {bancoMensal.saldoFormatado.final.positivo ? '+' : '-'} + {bancoMensal.saldoFormatado.final.horas}h{' '} + {bancoMensal.saldoFormatado.final.minutos}min +

+
+
+ {#if bancoMensal.saldoFormatado.final.positivo} + + {:else} + + {/if} +
+
+
+
+
+ + +
+ +
+
+
+
+ +
+
+

Horas Extras

+

+ {Math.floor(bancoMensal.horasExtras / 60)}h{' '} + {bancoMensal.horasExtras % 60}min +

+
+
+
+
+ + +
+
+
+
+ +
+
+

Déficit de Horas

+

+ {Math.floor(bancoMensal.horasDeficit / 60)}h{' '} + {bancoMensal.horasDeficit % 60}min +

+
+
+
+
+
+ + +
+
+

+ + Informações do Mês +

+
+
+

Dias Trabalhados

+

{bancoMensal.diasTrabalhados} dias

+
+
+

Última Atualização

+

+ {new Date(bancoMensal.atualizadoEm).toLocaleDateString('pt-BR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} +

+
+
+
+
+ + + {#if ajustes && ajustes.length > 0} +
+
+

+ + Ajustes Aplicados - {formatarMes(mesSelecionado)} +

+
+ + + + + + + + + + + + + {#each ajustes as ajuste} + + + + + + + + + {/each} + +
DataTipoMotivoValorGestorStatus
+ {new Date(ajuste.dataAplicacao).toLocaleDateString('pt-BR', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + })} + + + {ajuste.tipo === 'abonar' + ? 'Abonar' + : ajuste.tipo === 'descontar' + ? 'Descontar' + : 'Compensar'} + + +
+ + {ajuste.motivoTipo === 'atestado' + ? 'Atestado Médico' + : ajuste.motivoTipo === 'licenca' + ? 'Licença' + : ajuste.motivoTipo === 'ausencia' + ? 'Ausência' + : 'Manual'} + + {#if ajuste.motivoDescricao} + + {ajuste.motivoDescricao} + + {/if} +
+
+ = 0 ? 'text-success' : 'text-error'} + > + {ajuste.valorMinutos >= 0 ? '+' : ''} + {Math.floor(Math.abs(ajuste.valorMinutos) / 60)}h{' '} + {Math.abs(ajuste.valorMinutos) % 60}min + + {ajuste.gestor?.nome || 'Sistema'} + {#if ajuste.aplicado} + + + Aplicado + + {:else} + + + Pendente + + {/if} +
+
+
+
+ {/if} + + + {#if inconsistencias && inconsistencias.length > 0} +
+
+

+ + Inconsistências Detectadas - {formatarMes(mesSelecionado)} +

+
+ {#each inconsistencias as inconsistencia} +
+
+
+
+ + {inconsistencia.status === 'resolvida' + ? 'Resolvida' + : inconsistencia.status === 'ignorada' + ? 'Ignorada' + : 'Pendente'} + + + {inconsistencia.tipo === 'ponto_com_atestado' + ? 'Registro de Ponto com Atestado' + : inconsistencia.tipo === 'ponto_com_licenca' + ? 'Registro de Ponto com Licença' + : inconsistencia.tipo === 'ponto_com_ausencia' + ? 'Registro de Ponto com Ausência' + : inconsistencia.tipo === 'registro_duplicado' + ? 'Registro Duplicado' + : inconsistencia.tipo === 'sequencia_invalida' + ? 'Sequência Inválida' + : 'Saldo Inconsistente'} + +
+

{inconsistencia.descricao}

+

+ Detectada em:{' '} + {new Date(inconsistencia.dataDetectada).toLocaleDateString('pt-BR', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + })} +

+
+ {#if inconsistencia.status === 'pendente'} + + {:else if inconsistencia.status === 'resolvida'} + + {/if} +
+
+ {/each} +
+
+
+ {/if} + + + {#if bancoMensal && (bancoMensal.totalAjustes || bancoMensal.totalAbonos || bancoMensal.totalDescontos || bancoMensal.inconsistenciasResolvidas !== undefined)} +
+
+

+ + Resumo de Ajustes e Inconsistências +

+
+ {#if bancoMensal.totalAjustes !== undefined} +
+

Total de Ajustes

+

+ {Math.floor(Math.abs(bancoMensal.totalAjustes) / 60)}h{' '} + {Math.abs(bancoMensal.totalAjustes) % 60}min +

+
+ {/if} + {#if bancoMensal.totalAbonos !== undefined} +
+

Total de Abonos

+

+ +{Math.floor(bancoMensal.totalAbonos / 60)}h{' '} + {bancoMensal.totalAbonos % 60}min +

+
+ {/if} + {#if bancoMensal.totalDescontos !== undefined} +
+

Total de Descontos

+

+ -{Math.floor(bancoMensal.totalDescontos / 60)}h{' '} + {bancoMensal.totalDescontos % 60}min +

+
+ {/if} + {#if bancoMensal.inconsistenciasResolvidas !== undefined} +
+

+ Inconsistências Resolvidas +

+

{bancoMensal.inconsistenciasResolvidas}

+
+ {/if} +
+
+
+ {/if} + {:else} +
+
+ +

Nenhum dado disponível para este mês

+

+ Não há registros de banco de horas para {formatarMes(mesSelecionado)}. +

+
+
+ {/if} + + + {#if chartData && historico && historico.length > 0} +
+
+

+ + Evolução do Banco de Horas +

+
+ +
+
+
+ {/if} + + + {#if historico && historico.length > 0} +
+
+

+ + Histórico dos Últimos 6 Meses +

+
+ + + + + + + + + + + + {#each historico as item} + + + + + + + + {/each} + +
MêsSaldo InicialSaldo do MêsSaldo FinalDias
{formatarMes(item.mes)} + + {item.saldoFormatado.inicial.positivo ? '+' : '-'} + {item.saldoFormatado.inicial.horas}h{' '} + {item.saldoFormatado.inicial.minutos}min + + + + {item.saldoFormatado.mes.positivo ? '+' : '-'} + {item.saldoFormatado.mes.horas}h{' '} + {item.saldoFormatado.mes.minutos}min + + + + {item.saldoFormatado.final.positivo ? '+' : '-'} + {item.saldoFormatado.final.horas}h{' '} + {item.saldoFormatado.final.minutos}min + + {item.diasTrabalhados}
+
+
+
+ {/if} + + + {#if historicoAlteracoes && historicoAlteracoes.length > 0} +
+
+

+ + Histórico de Alterações - {formatarMes(mesSelecionado)} +

+
+ {#each historicoAlteracoes as alteracao} +
+
+
+
+ {#if alteracao.tipoAlteracao === 'edicao_registro'} + + Edição de Registro + {:else if alteracao.tipoAlteracao === 'ajuste_banco'} + + + Ajuste de Banco de Horas + + {:else} + + Outro + {/if} + + {alteracao.dataFormatada} + +
+ + {#if alteracao.tipoAlteracao === 'edicao_registro' && alteracao.registro} +
+

+ Registro: {alteracao.registro.tipo} em{' '} + {alteracao.registro.data} +

+

+ Alteração:{' '} + + {alteracao.registro.horaAnterior} + {' '} + →{' '} + + {alteracao.registro.horaNova} + +

+ {#if alteracao.diferencaMinutos !== undefined} +

+ Diferença:{' '} + = 0 + ? 'text-success' + : 'text-error'} + > + {alteracao.diferencaMinutos >= 0 ? '+' : ''} + {Math.floor(Math.abs(alteracao.diferencaMinutos) / 60)}h{' '} + {Math.abs(alteracao.diferencaMinutos) % 60}min + +

+ {/if} +
+ {:else if alteracao.tipoAlteracao === 'ajuste_banco'} +
+

+ Tipo: {alteracao.tipoAjuste === 'compensar' + ? 'Compensar' + : alteracao.tipoAjuste === 'abonar' + ? 'Abonar' + : 'Descontar'} +

+ {#if alteracao.ajusteMinutos !== undefined} +

+ Ajuste:{' '} + = 0 + ? 'text-success' + : 'text-error'} + > + {alteracao.ajusteMinutos >= 0 ? '+' : ''} + {Math.floor(Math.abs(alteracao.ajusteMinutos) / 60)}h{' '} + {Math.abs(alteracao.ajusteMinutos) % 60}min + +

+ {/if} +
+ {/if} + + {#if alteracao.motivoDescricao} +

+ Motivo: {alteracao.motivoDescricao} +

+ {/if} + + {#if alteracao.observacoes} +

+ Observações: {alteracao.observacoes} +

+ {/if} + + {#if alteracao.gestor} +

+ Alterado por: {alteracao.gestor.nome} +

+ {/if} +
+
+
+ {/each} +
+
+
+ {:else if historicoAlteracoesQuery?.data && historicoAlteracoesQuery.data.length === 0} +
+
+ +

Nenhuma alteração registrada

+

+ Não há histórico de alterações para {formatarMes(mesSelecionado)}. +

+
+
+ {/if} +
+ diff --git a/apps/web/src/lib/components/ponto/PrintPontoModal.svelte b/apps/web/src/lib/components/ponto/PrintPontoModal.svelte index 33c661a..82e2939 100644 --- a/apps/web/src/lib/components/ponto/PrintPontoModal.svelte +++ b/apps/web/src/lib/components/ponto/PrintPontoModal.svelte @@ -43,7 +43,8 @@ function handleGenerate() { onGenerate(sections); - onClose(); + // Não chamar onClose() aqui - o modal será fechado pelo callback onSuccess + // após a geração do PDF ser concluída com sucesso } function handleClose() { diff --git a/apps/web/src/lib/components/ponto/RegistroPonto.svelte b/apps/web/src/lib/components/ponto/RegistroPonto.svelte index 40abeeb..6371e05 100644 --- a/apps/web/src/lib/components/ponto/RegistroPonto.svelte +++ b/apps/web/src/lib/components/ponto/RegistroPonto.svelte @@ -1,33 +1,37 @@ diff --git a/apps/web/src/lib/components/ponto/TimePicker.svelte b/apps/web/src/lib/components/ponto/TimePicker.svelte new file mode 100644 index 0000000..5bbe728 --- /dev/null +++ b/apps/web/src/lib/components/ponto/TimePicker.svelte @@ -0,0 +1,163 @@ + + +
+ {#if label} +
{label}
+ {/if} + +
+ +
+ + + + horas +
+ + +
:
+ + +
+ + + + min +
+ + +
+ Total + {displayText} +
+
+
+ + + diff --git a/apps/web/src/lib/components/ponto/WebcamCapture.svelte b/apps/web/src/lib/components/ponto/WebcamCapture.svelte index 9ecb551..9001b2f 100644 --- a/apps/web/src/lib/components/ponto/WebcamCapture.svelte +++ b/apps/web/src/lib/components/ponto/WebcamCapture.svelte @@ -11,7 +11,7 @@ fotoObrigatoria?: boolean; // Se true, não permite continuar sem foto } - const { + let { onCapture, onCancel, onError, diff --git a/apps/web/src/lib/components/ponto/registro-pontos/EstatisticasCards.svelte b/apps/web/src/lib/components/ponto/registro-pontos/EstatisticasCards.svelte new file mode 100644 index 0000000..3a50852 --- /dev/null +++ b/apps/web/src/lib/components/ponto/registro-pontos/EstatisticasCards.svelte @@ -0,0 +1,104 @@ + + +{#if estatisticas} +
+ +
+
+
+
+

Total de Registros

+

{estatisticas.totalRegistros}

+
+
+ +
+
+
+
+ + +
+
+
+
+

Dentro do Prazo

+

{estatisticas.dentroDoPrazo}

+

+ {estatisticas.totalRegistros > 0 + ? ((estatisticas.dentroDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1) + : 0}% do total +

+
+
+ +
+
+
+
+ + +
+
+
+
+

Fora do Prazo

+

{estatisticas.foraDoPrazo}

+

+ {estatisticas.totalRegistros > 0 + ? ((estatisticas.foraDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1) + : 0}% do total +

+
+
+ +
+
+
+
+ + +
+
+
+
+

Funcionários

+

{estatisticas.totalFuncionarios}

+

+ {estatisticas.funcionariosDentroPrazo} dentro, {estatisticas.funcionariosForaPrazo} fora +

+
+
+ +
+
+
+
+
+{/if} + diff --git a/apps/web/src/lib/components/ponto/registro-pontos/GraficoEstatisticas.svelte b/apps/web/src/lib/components/ponto/registro-pontos/GraficoEstatisticas.svelte new file mode 100644 index 0000000..460ac03 --- /dev/null +++ b/apps/web/src/lib/components/ponto/registro-pontos/GraficoEstatisticas.svelte @@ -0,0 +1,218 @@ + + +
+
+
+

+
+ +
+ Visão Geral das Estatísticas +

+
+
+ {#if isLoading} +
+
+ + Carregando estatísticas... +
+
+ {:else if error} +
+
+ +
+

Erro ao carregar estatísticas

+
+ {error?.message || String(error) || 'Erro desconhecido'} +
+
+
+
+ {:else if !estatisticas || !chartData} +
+
+ +

Nenhuma estatística disponível

+
+
+ {:else} + + {/if} +
+
+
+ diff --git a/apps/web/src/lib/components/ponto/registro-pontos/HeaderRegistroPontos.svelte b/apps/web/src/lib/components/ponto/registro-pontos/HeaderRegistroPontos.svelte new file mode 100644 index 0000000..0007918 --- /dev/null +++ b/apps/web/src/lib/components/ponto/registro-pontos/HeaderRegistroPontos.svelte @@ -0,0 +1,66 @@ + + +
+
+
+
+
+
+ +
+
+

+ Registro de Pontos +

+

+ Gerencie e visualize os registros de ponto dos funcionários com informações detalhadas e + relatórios +

+
+
+ {#if estatisticas} +
+
+

Total de Registros

+

{estatisticas.totalRegistros}

+
+
+

Funcionários

+

{estatisticas.totalFuncionarios}

+
+
+
+ + {estatisticas.totalRegistros > 0 + ? ((estatisticas.dentroDoPrazo / estatisticas.totalRegistros) * 100).toFixed(1) + : 0}% dentro do prazo + + Ativo +
+
+ {/if} +
+
+ diff --git a/apps/web/src/lib/components/ti/AlertConfigModal.svelte b/apps/web/src/lib/components/ti/AlertConfigModal.svelte index 5617882..6b5b178 100644 --- a/apps/web/src/lib/components/ti/AlertConfigModal.svelte +++ b/apps/web/src/lib/components/ti/AlertConfigModal.svelte @@ -1,17 +1,24 @@ -