refactor: enhance vacation management components and add status update functionality
- Improved the vacation request component with better loading states and error handling. - Added a new mutation to update the status of vacation requests, allowing transitions between different states. - Enhanced the calendar display for vacation periods and integrated a 3D bar chart for visualizing vacation data. - Refactored the code for better readability and maintainability, ensuring a smoother user experience.
This commit is contained in:
277
apps/web/src/lib/components/AlterarStatusFerias.svelte
Normal file
277
apps/web/src/lib/components/AlterarStatusFerias.svelte
Normal file
@@ -0,0 +1,277 @@
|
||||
<script lang="ts">
|
||||
import { useConvexClient } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import type { Id, Doc } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
|
||||
type SolicitacaoFerias = Doc<'solicitacoesFerias'> & {
|
||||
funcionario?: Doc<'funcionarios'> | null;
|
||||
gestor?: Doc<'usuarios'> | null;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
solicitacao: SolicitacaoFerias;
|
||||
usuarioId: Id<'usuarios'>;
|
||||
onSucesso?: () => void;
|
||||
onCancelar?: () => void;
|
||||
}
|
||||
|
||||
let { solicitacao, usuarioId, onSucesso, onCancelar }: Props = $props();
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
let processando = $state(false);
|
||||
let erro = $state('');
|
||||
|
||||
function getStatusBadge(status: string) {
|
||||
const badges: Record<string, string> = {
|
||||
aguardando_aprovacao: 'badge-warning',
|
||||
aprovado: 'badge-success',
|
||||
reprovado: 'badge-error',
|
||||
data_ajustada_aprovada: 'badge-info'
|
||||
};
|
||||
return badges[status] || 'badge-neutral';
|
||||
}
|
||||
|
||||
function getStatusTexto(status: string) {
|
||||
const textos: Record<string, string> = {
|
||||
aguardando_aprovacao: 'Aguardando Aprovação',
|
||||
aprovado: 'Aprovado',
|
||||
reprovado: 'Reprovado',
|
||||
data_ajustada_aprovada: 'Data Ajustada e Aprovada'
|
||||
};
|
||||
return textos[status] || status;
|
||||
}
|
||||
|
||||
async function voltarParaAguardando() {
|
||||
try {
|
||||
processando = true;
|
||||
erro = '';
|
||||
|
||||
await client.mutation(api.ferias.atualizarStatus, {
|
||||
solicitacaoId: solicitacao._id,
|
||||
novoStatus: 'aguardando_aprovacao',
|
||||
usuarioId: usuarioId
|
||||
});
|
||||
|
||||
if (onSucesso) onSucesso();
|
||||
} catch (e) {
|
||||
erro = e instanceof Error ? e.message : String(e);
|
||||
} finally {
|
||||
processando = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatarData(data: number) {
|
||||
return new Date(data).toLocaleString('pt-BR');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<div class="mb-4 flex items-start justify-between">
|
||||
<div>
|
||||
<h2 class="card-title text-2xl">
|
||||
{solicitacao.funcionario?.nome || 'Funcionário'}
|
||||
</h2>
|
||||
<p class="text-base-content/70 mt-1 text-sm">
|
||||
Ano de Referência: {solicitacao.anoReferencia}
|
||||
</p>
|
||||
</div>
|
||||
<div class={`badge ${getStatusBadge(solicitacao.status)} badge-lg`}>
|
||||
{getStatusTexto(solicitacao.status)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Períodos Solicitados -->
|
||||
<div class="mt-4">
|
||||
<h3 class="mb-3 text-lg font-semibold">Períodos Solicitados</h3>
|
||||
<div class="space-y-2">
|
||||
{#each solicitacao.periodos as periodo, index (index)}
|
||||
<div class="bg-base-200 flex items-center gap-4 rounded-lg p-3">
|
||||
<div class="badge badge-primary">{index + 1}</div>
|
||||
<div class="grid flex-1 grid-cols-3 gap-2 text-sm">
|
||||
<div>
|
||||
<span class="text-base-content/70">Início:</span>
|
||||
<span class="ml-1 font-semibold"
|
||||
>{new Date(periodo.dataInicio).toLocaleDateString('pt-BR')}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-base-content/70">Fim:</span>
|
||||
<span class="ml-1 font-semibold"
|
||||
>{new Date(periodo.dataFim).toLocaleDateString('pt-BR')}</span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-base-content/70">Dias:</span>
|
||||
<span class="text-primary ml-1 font-bold">{periodo.diasCorridos}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Observações -->
|
||||
{#if solicitacao.observacao}
|
||||
<div class="mt-4">
|
||||
<h3 class="mb-2 font-semibold">Observações</h3>
|
||||
<div class="bg-base-200 rounded-lg p-3 text-sm">
|
||||
{solicitacao.observacao}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Histórico -->
|
||||
{#if solicitacao.historicoAlteracoes && solicitacao.historicoAlteracoes.length > 0}
|
||||
<div class="mt-4">
|
||||
<h3 class="mb-2 font-semibold">Histórico</h3>
|
||||
<div class="space-y-1">
|
||||
{#each solicitacao.historicoAlteracoes as hist (hist.data)}
|
||||
<div class="text-base-content/70 flex items-center gap-2 text-xs">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-3 w-3"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{formatarData(hist.data)}</span>
|
||||
<span>-</span>
|
||||
<span>{hist.acao}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Ação: Voltar para Aguardando Aprovação -->
|
||||
{#if solicitacao.status !== 'aguardando_aprovacao'}
|
||||
<div class="divider mt-6"></div>
|
||||
<div class="alert alert-info">
|
||||
<svg
|
||||
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>
|
||||
<div>
|
||||
<h3 class="font-bold">Alterar Status</h3>
|
||||
<div class="text-sm">
|
||||
Ao voltar para "Aguardando Aprovação", a solicitação ficará disponível para aprovação ou
|
||||
reprovação pelo gestor.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-actions mt-4 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-warning gap-2"
|
||||
onclick={voltarParaAguardando}
|
||||
disabled={processando}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
Voltar para Aguardando Aprovação
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="divider mt-6"></div>
|
||||
<div class="alert">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-info h-6 w-6 shrink-0"
|
||||
>
|
||||
<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>Esta solicitação já está aguardando aprovação.</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Motivo Reprovação (se reprovado) -->
|
||||
{#if solicitacao.status === 'reprovado' && solicitacao.motivoReprovacao}
|
||||
<div class="alert alert-error mt-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<div>
|
||||
<div class="font-bold">Motivo da Reprovação:</div>
|
||||
<div class="text-sm">{solicitacao.motivoReprovacao}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Erro -->
|
||||
{#if erro}
|
||||
<div class="alert alert-error mt-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 shrink-0 stroke-current"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{erro}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Botão Fechar -->
|
||||
{#if onCancelar}
|
||||
<div class="card-actions mt-4 justify-end">
|
||||
<button type="button" class="btn" onclick={onCancelar} disabled={processando}>
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
372
apps/web/src/lib/components/ti/charts/BarChart3D.svelte
Normal file
372
apps/web/src/lib/components/ti/charts/BarChart3D.svelte
Normal file
@@ -0,0 +1,372 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
|
||||
Chart.register(...registerables);
|
||||
|
||||
type Props = {
|
||||
data: {
|
||||
labels: string[];
|
||||
datasets: Array<{
|
||||
label: string;
|
||||
data: number[];
|
||||
backgroundColor?: string | string[];
|
||||
borderColor?: string | string[];
|
||||
borderWidth?: number;
|
||||
}>;
|
||||
};
|
||||
title?: string;
|
||||
height?: number;
|
||||
stacked?: boolean;
|
||||
};
|
||||
|
||||
let { data, title = '', height = 400, stacked = false }: Props = $props();
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
let chart: Chart | null = null;
|
||||
|
||||
// Função para clarear cor
|
||||
function lightenColor(color: string, percent: number): string {
|
||||
const num = parseInt(color.replace('#', ''), 16);
|
||||
const amt = Math.round(2.55 * percent);
|
||||
const R = Math.min(255, (num >> 16) + amt);
|
||||
const G = Math.min(255, ((num >> 8) & 0x00ff) + amt);
|
||||
const B = Math.min(255, (num & 0x0000ff) + amt);
|
||||
return `#${(0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1)}`;
|
||||
}
|
||||
|
||||
// Função para escurecer cor
|
||||
function darkenColor(color: string, percent: number): string {
|
||||
const num = parseInt(color.replace('#', ''), 16);
|
||||
const amt = Math.round(2.55 * percent);
|
||||
const R = Math.max(0, (num >> 16) - amt);
|
||||
const G = Math.max(0, ((num >> 8) & 0x00ff) - amt);
|
||||
const B = Math.max(0, (num & 0x0000ff) - amt);
|
||||
return `#${(0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1)}`;
|
||||
}
|
||||
|
||||
// Criar gradientes 3D para cada cor
|
||||
function create3DGradientColors(colors: string[]): string[] {
|
||||
// Retornar cores com sombra 3D aplicada (usando cores mais claras e escuras)
|
||||
return colors.map((color) => {
|
||||
// Criar gradiente simulando 3D usando múltiplas cores
|
||||
return color; // Por enquanto retornar cor original, gradiente será aplicado via plugin
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
// Preparar dados com cores 3D
|
||||
const processedData = {
|
||||
labels: data.labels,
|
||||
datasets: data.datasets.map((dataset) => {
|
||||
// Processar cores de background
|
||||
let backgroundColor: string[];
|
||||
if (Array.isArray(dataset.backgroundColor)) {
|
||||
backgroundColor = dataset.backgroundColor;
|
||||
} else if (dataset.backgroundColor) {
|
||||
backgroundColor = data.labels.map(() => dataset.backgroundColor as string);
|
||||
} else {
|
||||
backgroundColor = data.labels.map(() => '#3b82f6');
|
||||
}
|
||||
|
||||
// Processar cores de borda
|
||||
let borderColor: string[];
|
||||
if (Array.isArray(dataset.borderColor)) {
|
||||
borderColor = dataset.borderColor;
|
||||
} else if (dataset.borderColor) {
|
||||
borderColor = data.labels.map(() => dataset.borderColor as string);
|
||||
} else {
|
||||
borderColor = backgroundColor.map((color) => darkenColor(color, 15));
|
||||
}
|
||||
|
||||
return {
|
||||
...dataset,
|
||||
backgroundColor,
|
||||
borderColor,
|
||||
borderWidth: dataset.borderWidth || 2,
|
||||
borderRadius: {
|
||||
topLeft: 10,
|
||||
topRight: 10,
|
||||
bottomLeft: 10,
|
||||
bottomRight: 10
|
||||
},
|
||||
borderSkipped: false,
|
||||
barThickness: 'flex',
|
||||
maxBarThickness: 60
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
chart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: processedData,
|
||||
options: {
|
||||
indexAxis: 'x',
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: {
|
||||
top: 15,
|
||||
right: 15,
|
||||
bottom: 15,
|
||||
left: 15
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top',
|
||||
labels: {
|
||||
color: '#374151', // Cinza escuro para melhor legibilidade
|
||||
font: {
|
||||
size: 13,
|
||||
family: "'Inter', sans-serif",
|
||||
weight: '600'
|
||||
},
|
||||
usePointStyle: false,
|
||||
padding: 18,
|
||||
boxWidth: 18,
|
||||
boxHeight: 14,
|
||||
generateLabels: function (chart: any) {
|
||||
const datasets = chart.data.datasets;
|
||||
return datasets.map((dataset: any, datasetIndex: number) => {
|
||||
// Priorizar cor da legenda se disponível, senão usar a cor do background
|
||||
let backgroundColor: string;
|
||||
|
||||
if (dataset.legendColor) {
|
||||
// Se há uma cor específica para a legenda, usar ela
|
||||
backgroundColor = dataset.legendColor;
|
||||
} else if (Array.isArray(dataset.backgroundColor)) {
|
||||
// Se todas as cores são iguais, usar a primeira
|
||||
const firstColor = dataset.backgroundColor[0];
|
||||
if (dataset.backgroundColor.every((c: string) => c === firstColor)) {
|
||||
backgroundColor = firstColor;
|
||||
} else {
|
||||
// Para múltiplas cores diferentes, usar a primeira como representativa
|
||||
backgroundColor = firstColor;
|
||||
}
|
||||
} else {
|
||||
backgroundColor = dataset.backgroundColor || '#3b82f6';
|
||||
}
|
||||
|
||||
// Cor da borda para a legenda
|
||||
let borderColor: string;
|
||||
if (Array.isArray(dataset.borderColor)) {
|
||||
borderColor = dataset.borderColor[0] || backgroundColor;
|
||||
} else {
|
||||
borderColor = dataset.borderColor || backgroundColor;
|
||||
}
|
||||
|
||||
return {
|
||||
text: dataset.label || `Dataset ${datasetIndex + 1}`,
|
||||
fillStyle: backgroundColor,
|
||||
strokeStyle: borderColor,
|
||||
lineWidth: dataset.borderWidth || 2,
|
||||
hidden: !chart.isDatasetVisible(datasetIndex),
|
||||
index: datasetIndex
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
title: {
|
||||
display: !!title,
|
||||
text: title,
|
||||
color: '#1f2937',
|
||||
font: {
|
||||
size: 18,
|
||||
weight: 'bold',
|
||||
family: "'Inter', sans-serif"
|
||||
},
|
||||
padding: {
|
||||
top: 10,
|
||||
bottom: 25
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
borderColor: '#3b82f6',
|
||||
borderWidth: 2,
|
||||
padding: 14,
|
||||
cornerRadius: 10,
|
||||
displayColors: true,
|
||||
titleFont: {
|
||||
size: 14,
|
||||
weight: 'bold',
|
||||
family: "'Inter', sans-serif"
|
||||
},
|
||||
bodyFont: {
|
||||
size: 13,
|
||||
family: "'Inter', sans-serif"
|
||||
},
|
||||
callbacks: {
|
||||
label: function (context: any) {
|
||||
let label = context.dataset.label || '';
|
||||
if (label) {
|
||||
label += ': ';
|
||||
}
|
||||
if (context.parsed.y !== null && context.parsed.y !== undefined) {
|
||||
label += context.parsed.y.toLocaleString('pt-BR');
|
||||
// Verificar se é número de solicitações ou dias
|
||||
if (label.includes('Solicitações')) {
|
||||
label += ' solicitação(ões)';
|
||||
} else {
|
||||
label += ' dia(s)';
|
||||
}
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: stacked,
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
color: '#6b7280',
|
||||
font: {
|
||||
size: 12,
|
||||
family: "'Inter', sans-serif",
|
||||
weight: '500'
|
||||
},
|
||||
maxRotation: 45,
|
||||
minRotation: 0
|
||||
},
|
||||
border: {
|
||||
display: true,
|
||||
color: '#e5e7eb',
|
||||
width: 2
|
||||
}
|
||||
},
|
||||
y: {
|
||||
stacked: stacked,
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.06)',
|
||||
lineWidth: 1,
|
||||
drawBorder: false
|
||||
},
|
||||
ticks: {
|
||||
color: '#6b7280',
|
||||
font: {
|
||||
size: 11,
|
||||
family: "'Inter', sans-serif",
|
||||
weight: '500'
|
||||
},
|
||||
callback: function (value: any) {
|
||||
if (typeof value === 'number') {
|
||||
return value.toLocaleString('pt-BR');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
},
|
||||
border: {
|
||||
display: true,
|
||||
color: '#e5e7eb',
|
||||
width: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
duration: 1200,
|
||||
easing: 'easeInOutQuart'
|
||||
},
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
// Plugin customizado para aplicar gradiente 3D
|
||||
onHover: (event: any, activeElements: any[]) => {
|
||||
if (event.native) {
|
||||
const target = event.native.target as HTMLElement;
|
||||
if (activeElements.length > 0) {
|
||||
target.style.cursor = 'pointer';
|
||||
} else {
|
||||
target.style.cursor = 'default';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
id: 'gradient3D',
|
||||
beforeDraw: (chart: any) => {
|
||||
const ctx = chart.ctx;
|
||||
const chartArea = chart.chartArea;
|
||||
|
||||
chart.data.datasets.forEach((dataset: any, datasetIndex: number) => {
|
||||
const meta = chart.getDatasetMeta(datasetIndex);
|
||||
if (!meta || !meta.data) return;
|
||||
|
||||
meta.data.forEach((bar: any, index: number) => {
|
||||
if (!bar || bar.hidden) return;
|
||||
|
||||
const backgroundColor = Array.isArray(dataset.backgroundColor)
|
||||
? dataset.backgroundColor[index]
|
||||
: dataset.backgroundColor;
|
||||
|
||||
if (!backgroundColor || typeof backgroundColor !== 'string') return;
|
||||
|
||||
// Criar gradiente 3D para a barra
|
||||
const gradient = ctx.createLinearGradient(
|
||||
bar.x - bar.width / 2,
|
||||
bar.y,
|
||||
bar.x + bar.width / 2,
|
||||
bar.base
|
||||
);
|
||||
|
||||
// Aplicar gradiente com efeito 3D
|
||||
const lightColor = lightenColor(backgroundColor, 25);
|
||||
const darkColor = darkenColor(backgroundColor, 15);
|
||||
|
||||
gradient.addColorStop(0, lightColor);
|
||||
gradient.addColorStop(0.3, backgroundColor);
|
||||
gradient.addColorStop(0.7, backgroundColor);
|
||||
gradient.addColorStop(1, darkColor);
|
||||
|
||||
// Redesenhar a barra com gradiente
|
||||
ctx.save();
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(
|
||||
bar.x - bar.width / 2,
|
||||
bar.y,
|
||||
bar.width,
|
||||
bar.base - bar.y
|
||||
);
|
||||
ctx.restore();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
} as any);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (chart && data) {
|
||||
// Atualizar dados do gráfico
|
||||
chart.data = data;
|
||||
chart.update('active');
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (chart) {
|
||||
chart.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div style="height: {height}px; position: relative;">
|
||||
<canvas bind:this={canvas}></canvas>
|
||||
</div>
|
||||
Reference in New Issue
Block a user