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:
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