Feat chat #3

Merged
deyvisonwanderley merged 11 commits from feat-chat into master 2025-10-28 15:05:03 +00:00
156 changed files with 35125 additions and 1083 deletions
Showing only changes of commit d41a7cea1b - Show all commits

View File

@@ -25,9 +25,9 @@
nome: string;
tipo: SimboloTipo;
descricao: string;
}> = [];
}> = $state([]);
let tipo: SimboloTipo = "cargo_comissionado";
let tipo = $state<SimboloTipo>("cargo_comissionado");
let loading = $state(false);
let loadingData = $state(true);
let notice = $state<{ kind: "success" | "error"; text: string } | null>(null);
@@ -318,6 +318,12 @@
init();
}
});
// Resetar simboloId quando tipo mudar
$effect(() => {
tipo; // track tipo
simboloId = ""; // reset selection when tipo changes
});
</script>
{#if loadingData}

View File

@@ -22,9 +22,9 @@
nome: string;
tipo: SimboloTipo;
descricao: string;
}> = [];
}> = $state([]);
let tipo: SimboloTipo = "cargo_comissionado";
let tipo = $state<SimboloTipo>("cargo_comissionado");
let loading = $state(false);
let notice = $state<{ kind: "success" | "error"; text: string } | null>(null);
@@ -246,7 +246,15 @@
}
}
loadSimbolos();
$effect(() => {
loadSimbolos();
});
// Resetar simboloId quando tipo mudar
$effect(() => {
tipo; // track tipo
simboloId = ""; // reset selection when tipo changes
});
</script>
<main class="container mx-auto px-4 py-4 max-w-7xl">

View File

@@ -6,9 +6,10 @@
const client = useConvexClient();
type Row = { _id: string; nome: string; valor: number; count: number };
let rows: Array<Row> = [];
let isLoading = true;
let notice: { kind: "error" | "success"; text: string } | null = null;
let rows: Array<Row> = $state<Array<Row>>([]);
let isLoading = $state(true);
let notice = $state<{ kind: "error" | "success"; text: string } | null>(null);
let containerWidth = $state(1200);
onMount(async () => {
try {
@@ -29,9 +30,25 @@
}
});
let chartWidth = 900;
let chartHeight = 400;
const padding = { top: 40, right: 30, bottom: 100, left: 80 };
// Dimensões responsivas
$effect(() => {
const updateSize = () => {
const container = document.querySelector('.chart-container');
if (container) {
containerWidth = Math.min(container.clientWidth - 32, 1200);
}
};
updateSize();
window.addEventListener('resize', updateSize);
return () => window.removeEventListener('resize', updateSize);
});
const chartHeight = 350;
const padding = { top: 20, right: 20, bottom: 80, left: 70 };
let chartWidth = $derived(containerWidth);
function getMax<T>(arr: Array<T>, sel: (t: T) => number): number {
let m = 0;
@@ -67,19 +84,19 @@
}
</script>
<div class="container mx-auto px-4 py-6 space-y-6">
<div class="container mx-auto px-4 py-6 max-w-7xl">
<!-- Breadcrumb -->
<div class="text-sm breadcrumbs">
<div class="text-sm breadcrumbs mb-4">
<ul>
<li><a href="/">Dashboard</a></li>
<li><a href="/recursos-humanos">Recursos Humanos</a></li>
<li><a href="/recursos-humanos/funcionarios">Funcionários</a></li>
<li class="font-semibold">Relatórios</li>
<li><a href="/" class="hover:text-primary">Dashboard</a></li>
<li><a href="/recursos-humanos" class="hover:text-primary">Recursos Humanos</a></li>
<li><a href="/recursos-humanos/funcionarios" class="hover:text-primary">Funcionários</a></li>
<li class="font-semibold text-primary">Relatórios</li>
</ul>
</div>
<!-- Header -->
<div class="flex items-center gap-4 mb-6">
<div class="flex items-center gap-4 mb-8">
<div class="p-3 bg-primary/10 rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
@@ -87,37 +104,40 @@
</div>
<div>
<h1 class="text-3xl font-bold text-base-content">Relatórios de Funcionários</h1>
<p class="text-base-content/60">Análise de distribuição de salários e funcionários por símbolo</p>
<p class="text-base-content/60 mt-1">Análise de distribuição de salários e funcionários por símbolo</p>
</div>
</div>
{#if notice}
<div class="alert" class:alert-error={notice.kind === "error"} class:alert-success={notice.kind === "success"}>
<div class="alert mb-6" class:alert-error={notice.kind === "error"} class:alert-success={notice.kind === "success"}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>{notice.text}</span>
</div>
{/if}
{#if isLoading}
<div class="flex justify-center items-center py-12">
<span class="loading loading-spinner loading-lg"></span>
<div class="flex justify-center items-center py-20">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
{:else}
<div class="grid gap-6">
<div class="space-y-6 chart-container">
<!-- Gráfico 1: Símbolo x Salário (Valor) - Layer Chart -->
<div class="card bg-gradient-to-br from-base-100 to-base-200 shadow-xl border border-base-300">
<div class="card-body">
<div class="flex items-center gap-3 mb-4">
<div class="p-2 bg-primary/10 rounded-lg">
<div class="card bg-base-100 shadow-lg hover:shadow-xl transition-shadow border border-base-300">
<div class="card-body p-6">
<div class="flex items-center gap-3 mb-6">
<div class="p-2.5 bg-primary/10 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<h3 class="text-xl font-bold text-base-content">Distribuição de Salários por Símbolo</h3>
<p class="text-sm text-base-content/60">Valores dos símbolos cadastrados no sistema</p>
<div class="flex-1">
<h3 class="text-lg font-bold text-base-content">Distribuição de Salários por Símbolo</h3>
<p class="text-sm text-base-content/60 mt-0.5">Valores dos símbolos cadastrados no sistema</p>
</div>
</div>
<div class="w-full overflow-x-auto bg-base-100 rounded-lg p-4">
<div class="w-full overflow-x-auto bg-base-200/30 rounded-xl p-4">
<svg width={chartWidth} height={chartHeight} role="img" aria-label="Gráfico de área: salário por símbolo">
{#if rows.length === 0}
<text x="16" y="32" class="opacity-60">Sem dados</text>
@@ -191,20 +211,20 @@
</div>
<!-- Gráfico 2: Quantidade de Funcionários por Símbolo - Layer Chart -->
<div class="card bg-gradient-to-br from-base-100 to-base-200 shadow-xl border border-base-300">
<div class="card-body">
<div class="flex items-center gap-3 mb-4">
<div class="p-2 bg-secondary/10 rounded-lg">
<div class="card bg-base-100 shadow-lg hover:shadow-xl transition-shadow border border-base-300">
<div class="card-body p-6">
<div class="flex items-center gap-3 mb-6">
<div class="p-2.5 bg-secondary/10 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-secondary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
<div>
<h3 class="text-xl font-bold text-base-content">Distribuição de Funcionários por Símbolo</h3>
<p class="text-sm text-base-content/60">Quantidade de funcionários alocados em cada símbolo</p>
<div class="flex-1">
<h3 class="text-lg font-bold text-base-content">Distribuição de Funcionários por Símbolo</h3>
<p class="text-sm text-base-content/60 mt-0.5">Quantidade de funcionários alocados em cada símbolo</p>
</div>
</div>
<div class="w-full overflow-x-auto bg-base-100 rounded-lg p-4">
<div class="w-full overflow-x-auto bg-base-200/30 rounded-xl p-4">
<svg width={chartWidth} height={chartHeight} role="img" aria-label="Gráfico de área: quantidade por símbolo">
{#if rows.length === 0}
<text x="16" y="32" class="opacity-60">Sem dados</text>
@@ -276,8 +296,71 @@
</div>
</div>
</div>
<!-- Tabela Resumo -->
<div class="card bg-base-100 shadow-lg border border-base-300">
<div class="card-body p-6">
<div class="flex items-center gap-3 mb-6">
<div class="p-2.5 bg-accent/10 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
</div>
<div class="flex-1">
<h3 class="text-lg font-bold text-base-content">Tabela Resumo - Símbolos e Funcionários</h3>
<p class="text-sm text-base-content/60 mt-0.5">Visão detalhada dos dados apresentados nos gráficos</p>
</div>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th class="bg-base-200">Símbolo</th>
<th class="bg-base-200 text-right">Valor (R$)</th>
<th class="bg-base-200 text-right">Funcionários</th>
<th class="bg-base-200 text-right">Total (R$)</th>
</tr>
</thead>
<tbody>
{#if rows.length === 0}
<tr>
<td colspan="4" class="text-center text-base-content/60 py-8">Nenhum dado disponível</td>
</tr>
{:else}
{#each rows as row}
<tr class="hover">
<td class="font-semibold">{row.nome}</td>
<td class="text-right font-mono">
{row.valor.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</td>
<td class="text-right">
<span class="badge badge-primary badge-outline">{row.count}</span>
</td>
<td class="text-right font-mono font-semibold text-primary">
{(row.valor * row.count).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</td>
</tr>
{/each}
<!-- Total Geral -->
<tr class="font-bold bg-base-200 border-t-2 border-base-300">
<td>TOTAL GERAL</td>
<td class="text-right font-mono">
{rows.reduce((sum, r) => sum + r.valor, 0).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</td>
<td class="text-right">
<span class="badge badge-primary">{rows.reduce((sum, r) => sum + r.count, 0)}</span>
</td>
<td class="text-right font-mono text-primary text-lg">
{rows.reduce((sum, r) => sum + (r.valor * r.count), 0).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</td>
</tr>
{/if}
</tbody>
</table>
</div>
</div>
</div>
</div>
{/if}
</div>

View File

@@ -137,4 +137,52 @@ export const update = mutation({
});
return null;
},
});
/**
* Remove símbolos duplicados, mantendo apenas a primeira ocorrência de cada símbolo
*/
export const removerDuplicados = mutation({
args: {},
returns: v.object({
removidos: v.number(),
mantidos: v.number(),
}),
handler: async (ctx) => {
const todosSimbolos = await ctx.db.query("simbolos").collect();
// Agrupar símbolos por nome
const simbolosPorNome = new Map<string, typeof todosSimbolos>();
for (const simbolo of todosSimbolos) {
const key = simbolo.nome.trim().toLowerCase();
if (!simbolosPorNome.has(key)) {
simbolosPorNome.set(key, []);
}
simbolosPorNome.get(key)!.push(simbolo);
}
let removidos = 0;
let mantidos = 0;
// Para cada grupo de símbolos com o mesmo nome
for (const [nome, simbolos] of simbolosPorNome) {
// Ordenar por _creationTime (mais antigo primeiro)
simbolos.sort((a, b) => a._creationTime - b._creationTime);
// Manter o primeiro (mais antigo) e remover os demais
const [primeiro, ...duplicados] = simbolos;
mantidos++;
// Remover duplicados
for (const duplicado of duplicados) {
await ctx.db.delete(duplicado._id);
removidos++;
}
}
console.log(`✅ Remoção concluída: ${mantidos} símbolos mantidos, ${removidos} duplicados removidos`);
return { removidos, mantidos };
},
});