- 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.
373 lines
10 KiB
Svelte
373 lines
10 KiB
Svelte
<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>
|