From b85021d924d4f2eaaacd64ed829c6194a0f817ac Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Sun, 30 Nov 2025 00:30:38 -0300 Subject: [PATCH] feat: implement area charts for total days by type and monthly trends in the employee leave dashboard, enhancing data visualization and user insights --- .../atestados-licencas/+page.svelte | 361 +++++------------- 1 file changed, 97 insertions(+), 264 deletions(-) diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/atestados-licencas/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/atestados-licencas/+page.svelte index 96f290a..d9e4470 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/atestados-licencas/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/atestados-licencas/+page.svelte @@ -10,6 +10,7 @@ import FileUpload from '$lib/components/FileUpload.svelte'; import ErrorModal from '$lib/components/ErrorModal.svelte'; import CalendarioAfastamentos from '$lib/components/CalendarioAfastamentos.svelte'; + import AreaChart from '$lib/components/ti/charts/AreaChart.svelte'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import jsPDF from 'jspdf'; import autoTable from 'jspdf-autotable'; @@ -141,6 +142,86 @@ }; } + // Dados para gráfico de área - Total de Dias por Tipo (Layerchart) + const chartDataTotalDiasPorTipo = $derived.by(() => { + if (!graficosQuery?.data?.totalDiasPorTipo) { + return { + labels: [], + datasets: [] + }; + } + + const dados = graficosQuery.data.totalDiasPorTipo; + const cores = ['#ef4444', '#f97316', '#ec4899', '#3b82f6', '#10b981']; + + return { + labels: dados.map((d) => d.tipo), + datasets: [ + { + label: 'Total de Dias', + data: dados.map((d) => d.dias), + backgroundColor: dados.map((_, i) => `${cores[i % cores.length]}80`), + borderColor: dados.map((_, i) => cores[i % cores.length]), + borderWidth: 3, + pointBackgroundColor: dados.map((_, i) => cores[i % cores.length]), + pointBorderColor: '#ffffff', + pointBorderWidth: 2, + pointRadius: 6, + pointHoverRadius: 8, + pointHoverBackgroundColor: dados.map((_, i) => cores[i % cores.length]), + pointHoverBorderColor: '#ffffff', + pointHoverBorderWidth: 3, + fill: true, + tension: 0.4, + spanGaps: false + } + ] + }; + }); + + // Dados para gráfico de área - Tendências Mensais (Layerchart empilhado) + const chartDataTendenciasMensais = $derived.by(() => { + if (!graficosQuery?.data?.tendenciasMensais) { + return { + labels: [], + datasets: [] + }; + } + + const tendencias = graficosQuery.data.tendenciasMensais; + const tipos = [ + 'atestado_medico', + 'declaracao_comparecimento', + 'maternidade', + 'paternidade', + 'ferias' + ]; + const cores = ['#ef4444', '#f97316', '#ec4899', '#3b82f6', '#10b981']; + const nomes = ['Atestado Médico', 'Declaração', 'Maternidade', 'Paternidade', 'Férias']; + + return { + labels: tendencias.map((t) => t.mes), + datasets: tipos.map((tipo, idx) => ({ + label: nomes[idx], + data: tendencias.map((t) => (t[tipo as keyof typeof t] as number) || 0), + backgroundColor: `${cores[idx]}60`, + borderColor: cores[idx], + borderWidth: 2, + pointBackgroundColor: cores[idx], + pointBorderColor: '#ffffff', + pointBorderWidth: 2, + pointRadius: 4, + pointHoverRadius: 6, + pointHoverBackgroundColor: cores[idx], + pointHoverBorderColor: '#ffffff', + pointHoverBorderWidth: 3, + fill: true, + tension: 0.4, + spanGaps: false + })) + }; + }); + // Salvar Atestado Médico async function salvarAtestadoMedico() { if ( @@ -1643,282 +1724,34 @@ {#if graficosQuery?.data} - {@const dados = graficosQuery.data.totalDiasPorTipo} - {@const maxDias = Math.max(...dados.map((d) => d.dias), 1)} - {@const chartWidth = 800} - {@const chartHeight = 350} - {@const padding = { top: 20, right: 40, bottom: 80, left: 70 }} - {@const barWidth = (chartWidth - padding.left - padding.right) / dados.length - 10} - {@const innerHeight = chartHeight - padding.top - padding.bottom} - {@const tendencias = graficosQuery.data.tendenciasMensais} - {@const tipos = [ - 'atestado_medico', - 'declaracao_comparecimento', - 'maternidade', - 'paternidade', - 'ferias' - ]} - {@const cores = ['#ef4444', '#f97316', '#ec4899', '#3b82f6', '#10b981']} - {@const nomes = ['Atestado Médico', 'Declaração', 'Maternidade', 'Paternidade', 'Férias']} - {@const maxValor = Math.max( - ...tendencias.flatMap((t) => tipos.map((tipo) => t[tipo as keyof typeof t] as number)), - 1 - )} - {@const chartWidth2 = 900} - {@const chartHeight2 = 400} - {@const padding2 = { top: 20, right: 40, bottom: 80, left: 70 }} - {@const innerWidth = chartWidth2 - padding2.left - padding2.right} - {@const innerHeight2 = chartHeight2 - padding2.top - padding2.bottom} - +

Total de Dias por Tipo

- - - {#each [0, 1, 2, 3, 4, 5] as t} - {@const val = Math.round((maxDias / 5) * t)} - {@const y = chartHeight - padding.bottom - (val / maxDias) * innerHeight} - - - {val} - - {/each} - - - - - - - {#each dados as item, i} - {@const x = padding.left + i * (barWidth + 10) + 5} - {@const height = (item.dias / maxDias) * innerHeight} - {@const y = chartHeight - padding.bottom - height} - {@const colors = ['#ef4444', '#f97316', '#ec4899', '#3b82f6', '#10b981']} - - - - - - - - - - - - - - {#if item.dias > 0} - - {item.dias} - - {/if} - - - -
- - {item.tipo} - -
-
- {/each} -
+ {#if chartDataTotalDiasPorTipo.labels.length === 0} +
+

Sem dados registrados até o momento.

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

Tendências Mensais (Últimos 6 Meses)

- - - {#each [0, 1, 2, 3, 4, 5] as t} - {@const val = Math.round((maxValor / 5) * t)} - {@const y = chartHeight2 - padding2.bottom - (val / maxValor) * innerHeight2} - - - {val} - - {/each} - - - - - - - {#each tipos as tipo, tipoIdx} - {@const cor = cores[tipoIdx]} - - - - - - - - - - {@const pontos = tendencias.map((t, i) => { - const x = padding2.left + (i / (tendencias.length - 1 || 1)) * innerWidth; - const valor = t[tipo as keyof typeof t] as number; - const y = chartHeight2 - padding2.bottom - (valor / maxValor) * innerHeight2; - return { x, y, valor }; - })} - - - {#if pontos.length > 0} - {@const pathArea = - `M ${pontos[0].x} ${chartHeight2 - padding2.bottom} ` + - pontos.map((p) => `L ${p.x} ${p.y}`).join(' ') + - ` L ${pontos[pontos.length - 1].x} ${chartHeight2 - padding2.bottom} Z`} - - {/if} - - - {#if pontos.length > 1} - `${p.x},${p.y}`).join(' ')} - fill="none" - stroke={cor} - stroke-width="3" - stroke-linecap="round" - stroke-linejoin="round" - /> - {/if} - - - {#each pontos as ponto, pontoIdx} - - - {nomes[tipoIdx]}: {ponto.valor} dias em {tendencias[pontoIdx]?.mes || - ''} - {/each} - {/each} - - - {#each tendencias as t, i} - {@const x = padding2.left + (i / (tendencias.length - 1 || 1)) * innerWidth} - -
- - {t.mes} - -
-
- {/each} -
- - -
- {#each tipos as tipo, idx} -
-
- {nomes[idx]} -
- {/each} -
+ {#if chartDataTendenciasMensais.labels.length === 0} +
+

Sem dados registrados até o momento.

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