feat: Add explicit TypeScript types and improve error handling for employee reports.
This commit is contained in:
@@ -1,367 +1,590 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useConvexClient } from "convex-svelte";
|
import { useConvexClient } from 'convex-svelte';
|
||||||
import { api } from "@sgse-app/backend/convex/_generated/api";
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import { onMount } from "svelte";
|
import { onMount } from 'svelte';
|
||||||
import { resolve } from "$app/paths";
|
import { resolve } from '$app/paths';
|
||||||
|
|
||||||
const client = useConvexClient();
|
const client = useConvexClient();
|
||||||
|
|
||||||
type Row = { _id: string; nome: string; valor: number; count: number };
|
type Row = { _id: string; nome: string; valor: number; count: number };
|
||||||
let rows: Array<Row> = $state<Array<Row>>([]);
|
let rows: Array<Row> = $state<Array<Row>>([]);
|
||||||
let isLoading = $state(true);
|
let isLoading = $state(true);
|
||||||
let notice = $state<{ kind: "error" | "success"; text: string } | null>(null);
|
let notice = $state<{ kind: 'error' | 'success'; text: string } | null>(null);
|
||||||
let containerWidth = $state(1200);
|
let containerWidth = $state(1200);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
const simbolos = await client.query(api.simbolos.getAll, {} as any);
|
const simbolos = await client.query(api.simbolos.getAll, {});
|
||||||
const funcionarios = await client.query(api.funcionarios.getAll, {} as any);
|
const funcionarios = await client.query(api.funcionarios.getAll, {});
|
||||||
const counts: Record<string, number> = {};
|
|
||||||
for (const f of funcionarios) counts[f.simboloId] = (counts[f.simboloId] ?? 0) + 1;
|
|
||||||
rows = simbolos.map((s: any) => ({
|
|
||||||
_id: String(s._id),
|
|
||||||
nome: s.nome as string,
|
|
||||||
valor: Number(s.valor || 0),
|
|
||||||
count: counts[String(s._id)] ?? 0,
|
|
||||||
}));
|
|
||||||
} catch (e) {
|
|
||||||
notice = { kind: "error", text: "Falha ao carregar dados de relatórios." };
|
|
||||||
} finally {
|
|
||||||
isLoading = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dimensões responsivas
|
const counts: Record<string, number> = {};
|
||||||
$effect(() => {
|
for (const f of funcionarios) {
|
||||||
const updateSize = () => {
|
const sId = String(f.simboloId);
|
||||||
const container = document.querySelector('.chart-container');
|
counts[sId] = (counts[sId] ?? 0) + 1;
|
||||||
if (container) {
|
}
|
||||||
containerWidth = Math.min(container.clientWidth - 32, 1200);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateSize();
|
rows = simbolos.map((s) => ({
|
||||||
window.addEventListener('resize', updateSize);
|
_id: String(s._id),
|
||||||
|
nome: s.nome,
|
||||||
|
valor: Number(s.valor || 0),
|
||||||
|
count: counts[String(s._id)] ?? 0
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
notice = { kind: 'error', text: e.message };
|
||||||
|
} else {
|
||||||
|
notice = { kind: 'error', text: 'Falha ao carregar dados de relatórios.' };
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return () => window.removeEventListener('resize', updateSize);
|
// Dimensões responsivas
|
||||||
});
|
$effect(() => {
|
||||||
|
const updateSize = () => {
|
||||||
|
const container = document.querySelector('.chart-container');
|
||||||
|
if (container) {
|
||||||
|
containerWidth = Math.min(container.clientWidth - 32, 1200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const chartHeight = 350;
|
updateSize();
|
||||||
const padding = { top: 20, right: 20, bottom: 80, left: 70 };
|
window.addEventListener('resize', updateSize);
|
||||||
|
|
||||||
let chartWidth = $derived(containerWidth);
|
return () => window.removeEventListener('resize', updateSize);
|
||||||
|
});
|
||||||
|
|
||||||
function getMax<T>(arr: Array<T>, sel: (t: T) => number): number {
|
const chartHeight = 350;
|
||||||
let m = 0;
|
const padding = { top: 20, right: 20, bottom: 80, left: 70 };
|
||||||
for (const a of arr) m = Math.max(m, sel(a));
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scaleY(v: number, max: number): number {
|
let chartWidth = $derived(containerWidth);
|
||||||
if (max <= 0) return 0;
|
|
||||||
const innerH = chartHeight - padding.top - padding.bottom;
|
|
||||||
return (v / max) * innerH;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getX(i: number, n: number): number {
|
function getMax<T>(arr: Array<T>, sel: (t: T) => number): number {
|
||||||
const innerW = chartWidth - padding.left - padding.right;
|
let m = 0;
|
||||||
return padding.left + (innerW / (n - 1)) * i;
|
for (const a of arr) m = Math.max(m, sel(a));
|
||||||
}
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
function createAreaPath(data: Array<Row>, getValue: (r: Row) => number, max: number): string {
|
function scaleY(v: number, max: number): number {
|
||||||
if (data.length === 0) return "";
|
if (max <= 0) return 0;
|
||||||
const n = data.length;
|
const innerH = chartHeight - padding.top - padding.bottom;
|
||||||
let path = `M ${getX(0, n)} ${chartHeight - padding.bottom}`;
|
return (v / max) * innerH;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
function getX(i: number, n: number): number {
|
||||||
const x = getX(i, n);
|
const innerW = chartWidth - padding.left - padding.right;
|
||||||
const y = chartHeight - padding.bottom - scaleY(getValue(data[i]), max);
|
return padding.left + (innerW / (n - 1)) * i;
|
||||||
path += ` L ${x} ${y}`;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
path += ` L ${getX(n - 1, n)} ${chartHeight - padding.bottom}`;
|
function createAreaPath(data: Array<Row>, getValue: (r: Row) => number, max: number): string {
|
||||||
path += " Z";
|
if (data.length === 0) return '';
|
||||||
return path;
|
const n = data.length;
|
||||||
}
|
let path = `M ${getX(0, n)} ${chartHeight - padding.bottom}`;
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const x = getX(i, n);
|
||||||
|
const y = chartHeight - padding.bottom - scaleY(getValue(data[i]), max);
|
||||||
|
path += ` L ${x} ${y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
path += ` L ${getX(n - 1, n)} ${chartHeight - padding.bottom}`;
|
||||||
|
path += ' Z';
|
||||||
|
return path;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto px-4 py-6 max-w-7xl">
|
<div class="container mx-auto max-w-7xl px-4 py-6">
|
||||||
<!-- Breadcrumb -->
|
<!-- Breadcrumb -->
|
||||||
<div class="text-sm breadcrumbs mb-4">
|
<div class="breadcrumbs mb-4 text-sm">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href={resolve('/')} class="hover:text-primary">Dashboard</a></li>
|
<li><a href={resolve('/')} class="hover:text-primary">Dashboard</a></li>
|
||||||
<li><a href={resolve('/recursos-humanos')} class="hover:text-primary">Recursos Humanos</a></li>
|
<li>
|
||||||
<li><a href={resolve('/recursos-humanos/funcionarios')} class="hover:text-primary">Funcionários</a></li>
|
<a href={resolve('/recursos-humanos')} class="hover:text-primary">Recursos Humanos</a>
|
||||||
<li class="font-semibold text-primary">Relatórios</li>
|
</li>
|
||||||
</ul>
|
<li>
|
||||||
</div>
|
<a href={resolve('/recursos-humanos/funcionarios')} class="hover:text-primary"
|
||||||
|
>Funcionários</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="text-primary font-semibold">Relatórios</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center gap-4 mb-8">
|
<div class="mb-8 flex items-center gap-4">
|
||||||
<div class="p-3 bg-primary/10 rounded-xl">
|
<div class="bg-primary/10 rounded-xl p-3">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg
|
||||||
<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" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</svg>
|
class="text-primary h-8 w-8"
|
||||||
</div>
|
fill="none"
|
||||||
<div>
|
viewBox="0 0 24 24"
|
||||||
<h1 class="text-3xl font-bold text-base-content">Relatórios de Funcionários</h1>
|
stroke="currentColor"
|
||||||
<p class="text-base-content/60 mt-1">Análise de distribuição de salários e funcionários por símbolo</p>
|
>
|
||||||
</div>
|
<path
|
||||||
</div>
|
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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-base-content text-3xl font-bold">Relatórios de Funcionários</h1>
|
||||||
|
<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}
|
{#if notice}
|
||||||
<div class="alert mb-6" class:alert-error={notice.kind === "error"} class:alert-success={notice.kind === "success"}>
|
<div
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
|
class="alert mb-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>
|
class:alert-error={notice.kind === 'error'}
|
||||||
</svg>
|
class:alert-success={notice.kind === 'success'}
|
||||||
<span>{notice.text}</span>
|
>
|
||||||
</div>
|
<svg
|
||||||
{/if}
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="h-6 w-6 shrink-0 stroke-current"
|
||||||
|
>
|
||||||
|
<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}
|
{#if isLoading}
|
||||||
<div class="flex justify-center items-center py-20">
|
<div class="flex items-center justify-center py-20">
|
||||||
<span class="loading loading-spinner loading-lg text-primary"></span>
|
<span class="loading loading-spinner loading-lg text-primary"></span>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-6 chart-container">
|
<div class="chart-container space-y-6">
|
||||||
<!-- Gráfico 1: Símbolo x Salário (Valor) - Layer Chart -->
|
<!-- Gráfico 1: Símbolo x Salário (Valor) - Layer Chart -->
|
||||||
<div class="card bg-base-100 shadow-lg hover:shadow-xl transition-shadow border border-base-300">
|
<div
|
||||||
<div class="card-body p-6">
|
class="card bg-base-100 border-base-300 border shadow-lg transition-shadow hover:shadow-xl"
|
||||||
<div class="flex items-center gap-3 mb-6">
|
>
|
||||||
<div class="p-2.5 bg-primary/10 rounded-lg">
|
<div class="card-body p-6">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div class="mb-6 flex items-center gap-3">
|
||||||
<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" />
|
<div class="bg-primary/10 rounded-lg p-2.5">
|
||||||
</svg>
|
<svg
|
||||||
</div>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<div class="flex-1">
|
class="text-primary h-6 w-6"
|
||||||
<h3 class="text-lg font-bold text-base-content">Distribuição de Salários por Símbolo</h3>
|
fill="none"
|
||||||
<p class="text-sm text-base-content/60 mt-0.5">Valores dos símbolos cadastrados no sistema</p>
|
viewBox="0 0 24 24"
|
||||||
</div>
|
stroke="currentColor"
|
||||||
</div>
|
>
|
||||||
<div class="w-full overflow-x-auto bg-base-200/30 rounded-xl p-4">
|
<path
|
||||||
<svg width={chartWidth} height={chartHeight} role="img" aria-label="Gráfico de área: salário por símbolo">
|
stroke-linecap="round"
|
||||||
{#if rows.length === 0}
|
stroke-linejoin="round"
|
||||||
<text x="16" y="32" class="opacity-60">Sem dados</text>
|
stroke-width="2"
|
||||||
{:else}
|
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"
|
||||||
{@const max = getMax(rows, (r) => r.valor)}
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-base-content text-lg font-bold">
|
||||||
|
Distribuição de Salários por Símbolo
|
||||||
|
</h3>
|
||||||
|
<p class="text-base-content/60 mt-0.5 text-sm">
|
||||||
|
Valores dos símbolos cadastrados no sistema
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-base-200/30 w-full overflow-x-auto 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>
|
||||||
|
{:else}
|
||||||
|
{@const max = getMax(rows, (r) => r.valor)}
|
||||||
|
|
||||||
<!-- Grid lines -->
|
<!-- Grid lines -->
|
||||||
{#each [0,1,2,3,4,5] as t}
|
{#each [0, 1, 2, 3, 4, 5] as t (t)}
|
||||||
{@const val = Math.round((max/5) * t)}
|
{@const val = Math.round((max / 5) * t)}
|
||||||
{@const y = chartHeight - padding.bottom - scaleY(val, max)}
|
{@const y = chartHeight - padding.bottom - scaleY(val, max)}
|
||||||
<line x1={padding.left} y1={y} x2={chartWidth - padding.right} y2={y} stroke="currentColor" stroke-opacity="0.1" stroke-dasharray="4,4" />
|
<line
|
||||||
<text x={padding.left - 8} y={y + 4} text-anchor="end" class="text-[10px] opacity-70">{`R$ ${val.toLocaleString('pt-BR')}`}</text>
|
x1={padding.left}
|
||||||
{/each}
|
y1={y}
|
||||||
|
x2={chartWidth - padding.right}
|
||||||
|
y2={y}
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-opacity="0.1"
|
||||||
|
stroke-dasharray="4,4"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={padding.left - 8}
|
||||||
|
y={y + 4}
|
||||||
|
text-anchor="end"
|
||||||
|
class="text-[10px] opacity-70">{`R$ ${val.toLocaleString('pt-BR')}`}</text
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
|
||||||
<!-- Eixos -->
|
<!-- Eixos -->
|
||||||
<line x1={padding.left} y1={chartHeight - padding.bottom} x2={chartWidth - padding.right} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" stroke-width="2" />
|
<line
|
||||||
<line x1={padding.left} y1={padding.top} x2={padding.left} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" stroke-width="2" />
|
x1={padding.left}
|
||||||
|
y1={chartHeight - padding.bottom}
|
||||||
|
x2={chartWidth - padding.right}
|
||||||
|
y2={chartHeight - padding.bottom}
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-opacity="0.3"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1={padding.left}
|
||||||
|
y1={padding.top}
|
||||||
|
x2={padding.left}
|
||||||
|
y2={chartHeight - padding.bottom}
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-opacity="0.3"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Area fill (camada) -->
|
<!-- Area fill (camada) -->
|
||||||
<path
|
<path
|
||||||
d={createAreaPath(rows, (r) => r.valor, max)}
|
d={createAreaPath(rows, (r) => r.valor, max)}
|
||||||
fill="url(#gradient-salary)"
|
fill="url(#gradient-salary)"
|
||||||
opacity="0.7"
|
opacity="0.7"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Line -->
|
<!-- Line -->
|
||||||
<polyline
|
<polyline
|
||||||
points={rows.map((r, i) => {
|
points={rows
|
||||||
const x = getX(i, rows.length);
|
.map((r, i) => {
|
||||||
const y = chartHeight - padding.bottom - scaleY(r.valor, max);
|
const x = getX(i, rows.length);
|
||||||
return `${x},${y}`;
|
const y = chartHeight - padding.bottom - scaleY(r.valor, max);
|
||||||
}).join(' ')}
|
return `${x},${y}`;
|
||||||
fill="none"
|
})
|
||||||
stroke="rgb(59, 130, 246)"
|
.join(' ')}
|
||||||
stroke-width="3"
|
fill="none"
|
||||||
/>
|
stroke="rgb(59, 130, 246)"
|
||||||
|
stroke-width="3"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Data points -->
|
<!-- Data points -->
|
||||||
{#each rows as r, i}
|
{#each rows as r, i (r._id)}
|
||||||
{@const x = getX(i, rows.length)}
|
{@const x = getX(i, rows.length)}
|
||||||
{@const y = chartHeight - padding.bottom - scaleY(r.valor, max)}
|
{@const y = chartHeight - padding.bottom - scaleY(r.valor, max)}
|
||||||
<circle cx={x} cy={y} r="5" fill="rgb(59, 130, 246)" stroke="white" stroke-width="2" />
|
<circle
|
||||||
<text x={x} y={y - 12} text-anchor="middle" class="text-[10px] font-semibold fill-primary">
|
cx={x}
|
||||||
{`R$ ${r.valor.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`}
|
cy={y}
|
||||||
</text>
|
r="5"
|
||||||
{/each}
|
fill="rgb(59, 130, 246)"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
{x}
|
||||||
|
y={y - 12}
|
||||||
|
text-anchor="middle"
|
||||||
|
class="fill-primary text-[10px] font-semibold"
|
||||||
|
>
|
||||||
|
{`R$ ${r.valor.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`}
|
||||||
|
</text>
|
||||||
|
{/each}
|
||||||
|
|
||||||
<!-- Eixo X labels -->
|
<!-- Eixo X labels -->
|
||||||
{#each rows as r, i}
|
{#each rows as r, i (r._id)}
|
||||||
{@const x = getX(i, rows.length)}
|
{@const x = getX(i, rows.length)}
|
||||||
<foreignObject x={x - 40} y={chartHeight - padding.bottom + 15} width="80" height="70">
|
<foreignObject
|
||||||
<div class="flex items-center justify-center text-center">
|
x={x - 40}
|
||||||
<span class="text-[11px] font-medium text-base-content/80 leading-tight" style="word-wrap: break-word; hyphens: auto;">
|
y={chartHeight - padding.bottom + 15}
|
||||||
{r.nome}
|
width="80"
|
||||||
</span>
|
height="70"
|
||||||
</div>
|
>
|
||||||
</foreignObject>
|
<div class="flex items-center justify-center text-center">
|
||||||
{/each}
|
<span
|
||||||
|
class="text-base-content/80 text-[11px] leading-tight font-medium"
|
||||||
|
style="word-wrap: break-word; hyphens: auto;"
|
||||||
|
>
|
||||||
|
{r.nome}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
{/each}
|
||||||
|
|
||||||
<!-- Gradient definition -->
|
<!-- Gradient definition -->
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="gradient-salary" x1="0%" y1="0%" x2="0%" y2="100%">
|
<linearGradient id="gradient-salary" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
<stop offset="0%" style="stop-color:rgb(59, 130, 246);stop-opacity:0.8" />
|
<stop offset="0%" style="stop-color:rgb(59, 130, 246);stop-opacity:0.8" />
|
||||||
<stop offset="100%" style="stop-color:rgb(59, 130, 246);stop-opacity:0.1" />
|
<stop offset="100%" style="stop-color:rgb(59, 130, 246);stop-opacity:0.1" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
{/if}
|
{/if}
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Gráfico 2: Quantidade de Funcionários por Símbolo - Layer Chart -->
|
<!-- Gráfico 2: Quantidade de Funcionários por Símbolo - Layer Chart -->
|
||||||
<div class="card bg-base-100 shadow-lg hover:shadow-xl transition-shadow border border-base-300">
|
<div
|
||||||
<div class="card-body p-6">
|
class="card bg-base-100 border-base-300 border shadow-lg transition-shadow hover:shadow-xl"
|
||||||
<div class="flex items-center gap-3 mb-6">
|
>
|
||||||
<div class="p-2.5 bg-secondary/10 rounded-lg">
|
<div class="card-body p-6">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-secondary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div class="mb-6 flex items-center gap-3">
|
||||||
<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" />
|
<div class="bg-secondary/10 rounded-lg p-2.5">
|
||||||
</svg>
|
<svg
|
||||||
</div>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<div class="flex-1">
|
class="text-secondary h-6 w-6"
|
||||||
<h3 class="text-lg font-bold text-base-content">Distribuição de Funcionários por Símbolo</h3>
|
fill="none"
|
||||||
<p class="text-sm text-base-content/60 mt-0.5">Quantidade de funcionários alocados em cada símbolo</p>
|
viewBox="0 0 24 24"
|
||||||
</div>
|
stroke="currentColor"
|
||||||
</div>
|
>
|
||||||
<div class="w-full overflow-x-auto bg-base-200/30 rounded-xl p-4">
|
<path
|
||||||
<svg width={chartWidth} height={chartHeight} role="img" aria-label="Gráfico de área: quantidade por símbolo">
|
stroke-linecap="round"
|
||||||
{#if rows.length === 0}
|
stroke-linejoin="round"
|
||||||
<text x="16" y="32" class="opacity-60">Sem dados</text>
|
stroke-width="2"
|
||||||
{:else}
|
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"
|
||||||
{@const maxC = getMax(rows, (r) => r.count)}
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-base-content text-lg font-bold">
|
||||||
|
Distribuição de Funcionários por Símbolo
|
||||||
|
</h3>
|
||||||
|
<p class="text-base-content/60 mt-0.5 text-sm">
|
||||||
|
Quantidade de funcionários alocados em cada símbolo
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-base-200/30 w-full overflow-x-auto 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>
|
||||||
|
{:else}
|
||||||
|
{@const maxC = getMax(rows, (r) => r.count)}
|
||||||
|
|
||||||
<!-- Grid lines -->
|
<!-- Grid lines -->
|
||||||
{#each [0,1,2,3,4,5] as t}
|
{#each [0, 1, 2, 3, 4, 5] as t (t)}
|
||||||
{@const val = Math.round((maxC/5) * t)}
|
{@const val = Math.round((maxC / 5) * t)}
|
||||||
{@const y = chartHeight - padding.bottom - scaleY(val, Math.max(1, maxC))}
|
{@const y = chartHeight - padding.bottom - scaleY(val, Math.max(1, maxC))}
|
||||||
<line x1={padding.left} y1={y} x2={chartWidth - padding.right} y2={y} stroke="currentColor" stroke-opacity="0.1" stroke-dasharray="4,4" />
|
<line
|
||||||
<text x={padding.left - 6} y={y + 4} text-anchor="end" class="text-[10px] opacity-70">{val}</text>
|
x1={padding.left}
|
||||||
{/each}
|
y1={y}
|
||||||
|
x2={chartWidth - padding.right}
|
||||||
|
y2={y}
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-opacity="0.1"
|
||||||
|
stroke-dasharray="4,4"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={padding.left - 6}
|
||||||
|
y={y + 4}
|
||||||
|
text-anchor="end"
|
||||||
|
class="text-[10px] opacity-70">{val}</text
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
|
||||||
<!-- Eixos -->
|
<!-- Eixos -->
|
||||||
<line x1={padding.left} y1={chartHeight - padding.bottom} x2={chartWidth - padding.right} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" stroke-width="2" />
|
<line
|
||||||
<line x1={padding.left} y1={padding.top} x2={padding.left} y2={chartHeight - padding.bottom} stroke="currentColor" stroke-opacity="0.3" stroke-width="2" />
|
x1={padding.left}
|
||||||
|
y1={chartHeight - padding.bottom}
|
||||||
|
x2={chartWidth - padding.right}
|
||||||
|
y2={chartHeight - padding.bottom}
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-opacity="0.3"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1={padding.left}
|
||||||
|
y1={padding.top}
|
||||||
|
x2={padding.left}
|
||||||
|
y2={chartHeight - padding.bottom}
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-opacity="0.3"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Area fill (camada) -->
|
<!-- Area fill (camada) -->
|
||||||
<path
|
<path
|
||||||
d={createAreaPath(rows, (r) => r.count, Math.max(1, maxC))}
|
d={createAreaPath(rows, (r) => r.count, Math.max(1, maxC))}
|
||||||
fill="url(#gradient-count)"
|
fill="url(#gradient-count)"
|
||||||
opacity="0.7"
|
opacity="0.7"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Line -->
|
<!-- Line -->
|
||||||
<polyline
|
<polyline
|
||||||
points={rows.map((r, i) => {
|
points={rows
|
||||||
const x = getX(i, rows.length);
|
.map((r, i) => {
|
||||||
const y = chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC));
|
const x = getX(i, rows.length);
|
||||||
return `${x},${y}`;
|
const y = chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC));
|
||||||
}).join(' ')}
|
return `${x},${y}`;
|
||||||
fill="none"
|
})
|
||||||
stroke="rgb(236, 72, 153)"
|
.join(' ')}
|
||||||
stroke-width="3"
|
fill="none"
|
||||||
/>
|
stroke="rgb(236, 72, 153)"
|
||||||
|
stroke-width="3"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Data points -->
|
<!-- Data points -->
|
||||||
{#each rows as r, i}
|
{#each rows as r, i (r._id)}
|
||||||
{@const x = getX(i, rows.length)}
|
{@const x = getX(i, rows.length)}
|
||||||
{@const y = chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC))}
|
{@const y = chartHeight - padding.bottom - scaleY(r.count, Math.max(1, maxC))}
|
||||||
<circle cx={x} cy={y} r="5" fill="rgb(236, 72, 153)" stroke="white" stroke-width="2" />
|
<circle
|
||||||
<text x={x} y={y - 12} text-anchor="middle" class="text-[10px] font-semibold fill-secondary">
|
cx={x}
|
||||||
{r.count}
|
cy={y}
|
||||||
</text>
|
r="5"
|
||||||
{/each}
|
fill="rgb(236, 72, 153)"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
{x}
|
||||||
|
y={y - 12}
|
||||||
|
text-anchor="middle"
|
||||||
|
class="fill-secondary text-[10px] font-semibold"
|
||||||
|
>
|
||||||
|
{r.count}
|
||||||
|
</text>
|
||||||
|
{/each}
|
||||||
|
|
||||||
<!-- Eixo X labels -->
|
<!-- Eixo X labels -->
|
||||||
{#each rows as r, i}
|
{#each rows as r, i (r._id)}
|
||||||
{@const x = getX(i, rows.length)}
|
{@const x = getX(i, rows.length)}
|
||||||
<foreignObject x={x - 40} y={chartHeight - padding.bottom + 15} width="80" height="70">
|
<foreignObject
|
||||||
<div class="flex items-center justify-center text-center">
|
x={x - 40}
|
||||||
<span class="text-[11px] font-medium text-base-content/80 leading-tight" style="word-wrap: break-word; hyphens: auto;">
|
y={chartHeight - padding.bottom + 15}
|
||||||
{r.nome}
|
width="80"
|
||||||
</span>
|
height="70"
|
||||||
</div>
|
>
|
||||||
</foreignObject>
|
<div class="flex items-center justify-center text-center">
|
||||||
{/each}
|
<span
|
||||||
|
class="text-base-content/80 text-[11px] leading-tight font-medium"
|
||||||
|
style="word-wrap: break-word; hyphens: auto;"
|
||||||
|
>
|
||||||
|
{r.nome}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
{/each}
|
||||||
|
|
||||||
<!-- Gradient definition -->
|
<!-- Gradient definition -->
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="gradient-count" x1="0%" y1="0%" x2="0%" y2="100%">
|
<linearGradient id="gradient-count" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
<stop offset="0%" style="stop-color:rgb(236, 72, 153);stop-opacity:0.8" />
|
<stop offset="0%" style="stop-color:rgb(236, 72, 153);stop-opacity:0.8" />
|
||||||
<stop offset="100%" style="stop-color:rgb(236, 72, 153);stop-opacity:0.1" />
|
<stop offset="100%" style="stop-color:rgb(236, 72, 153);stop-opacity:0.1" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
{/if}
|
{/if}
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabela Resumo -->
|
<!-- Tabela Resumo -->
|
||||||
<div class="card bg-base-100 shadow-lg border border-base-300">
|
<div class="card bg-base-100 border-base-300 border shadow-lg">
|
||||||
<div class="card-body p-6">
|
<div class="card-body p-6">
|
||||||
<div class="flex items-center gap-3 mb-6">
|
<div class="mb-6 flex items-center gap-3">
|
||||||
<div class="p-2.5 bg-accent/10 rounded-lg">
|
<div class="bg-accent/10 rounded-lg p-2.5">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-accent" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg
|
||||||
<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" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</svg>
|
class="text-accent h-6 w-6"
|
||||||
</div>
|
fill="none"
|
||||||
<div class="flex-1">
|
viewBox="0 0 24 24"
|
||||||
<h3 class="text-lg font-bold text-base-content">Tabela Resumo - Símbolos e Funcionários</h3>
|
stroke="currentColor"
|
||||||
<p class="text-sm text-base-content/60 mt-0.5">Visão detalhada dos dados apresentados nos gráficos</p>
|
>
|
||||||
</div>
|
<path
|
||||||
</div>
|
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-base-content text-lg font-bold">
|
||||||
|
Tabela Resumo - Símbolos e Funcionários
|
||||||
|
</h3>
|
||||||
|
<p class="text-base-content/60 mt-0.5 text-sm">
|
||||||
|
Visão detalhada dos dados apresentados nos gráficos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="table table-zebra">
|
<table class="table-zebra table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="bg-base-200">Símbolo</th>
|
<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">Valor (R$)</th>
|
||||||
<th class="bg-base-200 text-right">Funcionários</th>
|
<th class="bg-base-200 text-right">Funcionários</th>
|
||||||
<th class="bg-base-200 text-right">Total (R$)</th>
|
<th class="bg-base-200 text-right">Total (R$)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#if rows.length === 0}
|
{#if rows.length === 0}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="4" class="text-center text-base-content/60 py-8">Nenhum dado disponível</td>
|
<td colspan="4" class="text-base-content/60 py-8 text-center"
|
||||||
</tr>
|
>Nenhum dado disponível</td
|
||||||
{:else}
|
>
|
||||||
{#each rows as row}
|
</tr>
|
||||||
<tr class="hover">
|
{:else}
|
||||||
<td class="font-semibold">{row.nome}</td>
|
{#each rows as row (row._id)}
|
||||||
<td class="text-right font-mono">
|
<tr class="hover">
|
||||||
{row.valor.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
<td class="font-semibold">{row.nome}</td>
|
||||||
</td>
|
<td class="text-right font-mono">
|
||||||
<td class="text-right">
|
{row.valor.toLocaleString('pt-BR', {
|
||||||
<span class="badge badge-primary badge-outline">{row.count}</span>
|
minimumFractionDigits: 2,
|
||||||
</td>
|
maximumFractionDigits: 2
|
||||||
<td class="text-right font-mono font-semibold text-primary">
|
})}
|
||||||
{(row.valor * row.count).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
</td>
|
||||||
</td>
|
<td class="text-right">
|
||||||
</tr>
|
<span class="badge badge-primary badge-outline">{row.count}</span>
|
||||||
{/each}
|
</td>
|
||||||
<!-- Total Geral -->
|
<td class="text-primary text-right font-mono font-semibold">
|
||||||
<tr class="font-bold bg-base-200 border-t-2 border-base-300">
|
{(row.valor * row.count).toLocaleString('pt-BR', {
|
||||||
<td>TOTAL GERAL</td>
|
minimumFractionDigits: 2,
|
||||||
<td class="text-right font-mono">
|
maximumFractionDigits: 2
|
||||||
{rows.reduce((sum, r) => sum + r.valor, 0).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
})}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
</tr>
|
||||||
<span class="badge badge-primary">{rows.reduce((sum, r) => sum + r.count, 0)}</span>
|
{/each}
|
||||||
</td>
|
<!-- Total Geral -->
|
||||||
<td class="text-right font-mono text-primary text-lg">
|
<tr class="bg-base-200 border-base-300 border-t-2 font-bold">
|
||||||
{rows.reduce((sum, r) => sum + (r.valor * r.count), 0).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
<td>TOTAL GERAL</td>
|
||||||
</td>
|
<td class="text-right font-mono">
|
||||||
</tr>
|
{rows
|
||||||
{/if}
|
.reduce((sum, r) => sum + r.valor, 0)
|
||||||
</tbody>
|
.toLocaleString('pt-BR', {
|
||||||
</table>
|
minimumFractionDigits: 2,
|
||||||
</div>
|
maximumFractionDigits: 2
|
||||||
</div>
|
})}
|
||||||
</div>
|
</td>
|
||||||
</div>
|
<td class="text-right">
|
||||||
{/if}
|
<span class="badge badge-primary"
|
||||||
|
>{rows.reduce((sum, r) => sum + r.count, 0)}</span
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td class="text-primary text-right font-mono 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>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user