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:
2025-11-13 05:51:55 -03:00
parent 0b7f1ad621
commit ebde59c6d2
6 changed files with 1503 additions and 721 deletions

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