feat: Frontend CSS refactoring and test improvements

Frontend:
- Refactored CSS architecture with new utility classes
- Updated dashboard components styling
- Improved responsive grid system
- Enhanced typography and variables
- Updated E2E and integration tests

Added:
- Claude Code slash commands for validation
- SSH tunnel and start test scripts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-21 21:08:47 +02:00
parent 05fc705fe5
commit 12ac2b671e
58 changed files with 7783 additions and 3539 deletions

View File

@@ -42,12 +42,18 @@
</div>
<!-- Breakdown section -->
<div class="breakdown-section" v-if="casaItems.length > 0 || bancaItems.length > 0">
<div
class="breakdown-section"
v-if="casaItems.length > 0 || bancaItems.length > 0"
>
<!-- Casa Breakdown -->
<div class="breakdown-group" v-if="casaItems.length > 0">
<div class="breakdown-header" @click="toggleCasaExpanded">
<div class="breakdown-header-left">
<i class="pi pi-chevron-right breakdown-toggle" :class="{ expanded: isCasaExpanded }"></i>
<i
class="pi pi-chevron-right breakdown-toggle"
:class="{ expanded: isCasaExpanded }"
></i>
<span class="breakdown-label">Casa</span>
</div>
<span class="breakdown-value">{{ formatCurrency(casaTotal) }}</span>
@@ -55,12 +61,20 @@
<!-- Casa Sub-items -->
<div v-show="isCasaExpanded" class="breakdown-subitems slide-down">
<div v-for="(item, idx) in casaItems" :key="idx" class="breakdown-subitem">
<div
v-for="(item, idx) in casaItems"
:key="idx"
class="breakdown-subitem"
>
<span class="breakdown-sublabel">
{{ item.nume || `Cont ${item.cont}` }}
<span v-if="item.cont" class="breakdown-cont">({{ item.cont }})</span>
<span v-if="item.cont" class="breakdown-cont"
>({{ item.cont }})</span
>
</span>
<span class="breakdown-subvalue">{{ formatCurrency(item.sold) }}</span>
<span class="breakdown-subvalue">{{
formatCurrency(item.sold)
}}</span>
</div>
</div>
</div>
@@ -69,7 +83,10 @@
<div class="breakdown-group" v-if="bancaItems.length > 0">
<div class="breakdown-header" @click="toggleBancaExpanded">
<div class="breakdown-header-left">
<i class="pi pi-chevron-right breakdown-toggle" :class="{ expanded: isBancaExpanded }"></i>
<i
class="pi pi-chevron-right breakdown-toggle"
:class="{ expanded: isBancaExpanded }"
></i>
<span class="breakdown-label">Bancă</span>
</div>
<span class="breakdown-value">{{ formatCurrency(bancaTotal) }}</span>
@@ -77,12 +94,20 @@
<!-- Bancă Sub-items -->
<div v-show="isBancaExpanded" class="breakdown-subitems slide-down">
<div v-for="(item, idx) in bancaItems" :key="idx" class="breakdown-subitem">
<div
v-for="(item, idx) in bancaItems"
:key="idx"
class="breakdown-subitem"
>
<span class="breakdown-sublabel">
{{ item.nume || `Cont ${item.cont}` }}
<span v-if="item.cont" class="breakdown-cont">({{ item.cont }})</span>
<span v-if="item.cont" class="breakdown-cont"
>({{ item.cont }})</span
>
</span>
<span class="breakdown-subvalue">{{ formatCurrency(item.sold) }}</span>
<span class="breakdown-subvalue">{{
formatCurrency(item.sold)
}}</span>
</div>
</div>
</div>
@@ -91,474 +116,499 @@
</template>
<script setup>
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { Chart, registerables } from 'chart.js'
import {
ref,
computed,
watch,
onMounted,
onBeforeUnmount,
nextTick,
} from "vue";
import { Chart, registerables } from "chart.js";
Chart.register(...registerables)
Chart.register(...registerables);
const props = defineProps({
casaTotal: {
type: Number,
default: 0
default: 0,
},
bancaTotal: {
type: Number,
default: 0
default: 0,
},
casaItems: {
type: Array,
default: () => []
default: () => [],
},
bancaItems: {
type: Array,
default: () => []
default: () => [],
},
casaSparklineData: {
type: Array,
default: () => []
default: () => [],
},
bancaSparklineData: {
type: Array,
default: () => []
default: () => [],
},
casaPreviousSparklineData: {
type: Array,
default: () => []
default: () => [],
},
bancaPreviousSparklineData: {
type: Array,
default: () => []
default: () => [],
},
sparklineLabels: {
type: Array,
default: () => []
default: () => [],
},
previousSparklineLabels: {
type: Array,
default: () => []
default: () => [],
},
trend: {
type: Object,
default: null
}
})
default: null,
},
});
// Refs pentru 2 canvas-uri separate
const casaCanvas = ref(null)
const bancaCanvas = ref(null)
let casaChartInstance = null
let bancaChartInstance = null
const isCasaExpanded = ref(false)
const isBancaExpanded = ref(false)
const casaCanvas = ref(null);
const bancaCanvas = ref(null);
let casaChartInstance = null;
let bancaChartInstance = null;
const isCasaExpanded = ref(false);
const isBancaExpanded = ref(false);
// Toggle functions
const toggleCasaExpanded = () => {
isCasaExpanded.value = !isCasaExpanded.value
}
isCasaExpanded.value = !isCasaExpanded.value;
};
const toggleBancaExpanded = () => {
isBancaExpanded.value = !isBancaExpanded.value
}
isBancaExpanded.value = !isBancaExpanded.value;
};
// Format currency
const formatCurrency = (amount) => {
if (!amount && amount !== 0) return '0 RON'
return new Intl.NumberFormat('ro-RO', {
style: 'currency',
currency: 'RON',
if (!amount && amount !== 0) return "0 RON";
return new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: "RON",
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(Math.abs(amount))
}
maximumFractionDigits: 0,
}).format(Math.abs(amount));
};
// Check if sparkline data exists
const hasSparklineData = computed(() => {
return props.casaSparklineData.length > 0 && props.bancaSparklineData.length > 0
})
return (
props.casaSparklineData.length > 0 && props.bancaSparklineData.length > 0
);
});
// Initialize Casa chart
const initializeCasaChart = async () => {
if (!casaCanvas.value || props.casaSparklineData.length === 0) {
return
return;
}
// Destroy existing chart
if (casaChartInstance) {
casaChartInstance.destroy()
casaChartInstance = null
casaChartInstance.destroy();
casaChartInstance = null;
}
await nextTick()
await nextTick();
const ctx = casaCanvas.value.getContext('2d')
const ctx = casaCanvas.value.getContext("2d");
// Generate labels
const labels = props.sparklineLabels.length > 0
? props.sparklineLabels
: props.casaSparklineData.map((_, i) => `L${i + 1}`)
const labels =
props.sparklineLabels.length > 0
? props.sparklineLabels
: props.casaSparklineData.map((_, i) => `L${i + 1}`);
// Prepare datasets
const datasets = [{
label: 'Casa (curent)',
data: props.casaSparklineData,
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: '#10b981',
pointHoverBorderColor: '#ffffff',
pointHoverBorderWidth: 2
}]
const datasets = [
{
label: "Casa (curent)",
data: props.casaSparklineData,
borderColor: "#10b981",
backgroundColor: "rgba(16, 185, 129, 0.1)",
borderWidth: 2,
fill: true,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: "#10b981",
pointHoverBorderColor: "#ffffff",
pointHoverBorderWidth: 2,
},
];
// Add previous year dataset if available
if (props.casaPreviousSparklineData && props.casaPreviousSparklineData.length > 0) {
if (
props.casaPreviousSparklineData &&
props.casaPreviousSparklineData.length > 0
) {
datasets.push({
label: 'Casa (anul precedent)',
label: "Casa (anul precedent)",
data: props.casaPreviousSparklineData,
borderColor: 'rgba(16, 185, 129, 0.4)',
backgroundColor: 'rgba(16, 185, 129, 0.05)',
borderColor: "rgba(16, 185, 129, 0.4)",
backgroundColor: "rgba(16, 185, 129, 0.05)",
borderWidth: 2,
borderDash: [5, 5],
fill: false,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: 'rgba(16, 185, 129, 0.4)',
pointHoverBorderColor: '#ffffff',
pointHoverBorderWidth: 2
})
pointHoverBackgroundColor: "rgba(16, 185, 129, 0.4)",
pointHoverBorderColor: "#ffffff",
pointHoverBorderWidth: 2,
});
}
// Calculate limits including both datasets
const allDataPoints = [...props.casaSparklineData]
if (props.casaPreviousSparklineData && props.casaPreviousSparklineData.length > 0) {
allDataPoints.push(...props.casaPreviousSparklineData)
const allDataPoints = [...props.casaSparklineData];
if (
props.casaPreviousSparklineData &&
props.casaPreviousSparklineData.length > 0
) {
allDataPoints.push(...props.casaPreviousSparklineData);
}
const dataMin = Math.min(...allDataPoints)
const dataMax = Math.max(...allDataPoints)
const dataRange = dataMax - dataMin
const dataPadding = dataRange * 0.05
const dataMin = Math.min(...allDataPoints);
const dataMax = Math.max(...allDataPoints);
const dataRange = dataMax - dataMin;
const dataPadding = dataRange * 0.05;
casaChartInstance = new Chart(ctx, {
type: 'line',
type: "line",
data: {
labels: labels,
datasets: datasets
datasets: datasets,
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
mode: "index",
},
plugins: {
legend: {
display: datasets.length > 1,
position: 'top',
align: 'end',
position: "top",
align: "end",
labels: {
boxWidth: 12,
boxHeight: 12,
padding: 8,
font: {
size: 10,
family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif'
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
},
color: 'rgba(107, 114, 128, 0.9)',
color: "rgba(107, 114, 128, 0.9)",
usePointStyle: true,
pointStyle: 'line'
}
pointStyle: "line",
},
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: '#ffffff',
bodyColor: '#ffffff',
borderColor: 'rgba(255, 255, 255, 0.2)',
backgroundColor: "rgba(0, 0, 0, 0.8)",
titleColor: "#ffffff",
bodyColor: "#ffffff",
borderColor: "rgba(255, 255, 255, 0.2)",
borderWidth: 1,
cornerRadius: 6,
displayColors: true,
callbacks: {
title: (context) => context[0].label || '',
title: (context) => context[0].label || "",
label: (context) => {
const value = context.parsed.y
const label = context.dataset.label || ''
const formattedValue = new Intl.NumberFormat('ro-RO', {
style: 'currency',
currency: 'RON',
const value = context.parsed.y;
const label = context.dataset.label || "";
const formattedValue = new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: "RON",
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(value)
return `${label}: ${formattedValue}`
}
}
}
maximumFractionDigits: 0,
}).format(value);
return `${label}: ${formattedValue}`;
},
},
},
},
scales: {
x: {
display: true,
grid: {
display: false,
drawBorder: false
drawBorder: false,
},
ticks: {
color: 'rgba(107, 114, 128, 0.7)',
color: "rgba(107, 114, 128, 0.7)",
font: {
size: 10,
family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif'
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
},
maxRotation: 45,
minRotation: 45,
maxTicksLimit: 6
maxTicksLimit: 6,
},
border: {
display: false
}
display: false,
},
},
y: {
display: true,
min: dataMin - dataPadding,
max: dataMax + dataPadding,
grid: {
color: 'rgba(107, 114, 128, 0.1)',
drawBorder: false
color: "rgba(107, 114, 128, 0.1)",
drawBorder: false,
},
ticks: {
color: '#10b981',
color: "#10b981",
font: {
size: 11,
family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif'
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
},
maxTicksLimit: 3,
callback: function(value) {
callback: function (value) {
if (value >= 1000000) {
return (value / 1000000).toFixed(1) + 'M'
return (value / 1000000).toFixed(1) + "M";
} else if (value >= 1000) {
return (value / 1000).toFixed(0) + 'k'
return (value / 1000).toFixed(0) + "k";
}
return value.toFixed(0)
}
return value.toFixed(0);
},
},
border: {
display: false
}
}
}
}
})
}
display: false,
},
},
},
},
});
};
// Initialize Bancă chart
const initializeBancaChart = async () => {
if (!bancaCanvas.value || props.bancaSparklineData.length === 0) {
return
return;
}
// Destroy existing chart
if (bancaChartInstance) {
bancaChartInstance.destroy()
bancaChartInstance = null
bancaChartInstance.destroy();
bancaChartInstance = null;
}
await nextTick()
await nextTick();
const ctx = bancaCanvas.value.getContext('2d')
const ctx = bancaCanvas.value.getContext("2d");
// Generate labels
const labels = props.sparklineLabels.length > 0
? props.sparklineLabels
: props.bancaSparklineData.map((_, i) => `L${i + 1}`)
const labels =
props.sparklineLabels.length > 0
? props.sparklineLabels
: props.bancaSparklineData.map((_, i) => `L${i + 1}`);
// Prepare datasets
const datasets = [{
label: 'Bancă (curent)',
data: props.bancaSparklineData,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: '#3b82f6',
pointHoverBorderColor: '#ffffff',
pointHoverBorderWidth: 2
}]
const datasets = [
{
label: "Bancă (curent)",
data: props.bancaSparklineData,
borderColor: "#3b82f6",
backgroundColor: "rgba(59, 130, 246, 0.1)",
borderWidth: 2,
fill: true,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: "#3b82f6",
pointHoverBorderColor: "#ffffff",
pointHoverBorderWidth: 2,
},
];
// Add previous year dataset if available
if (props.bancaPreviousSparklineData && props.bancaPreviousSparklineData.length > 0) {
if (
props.bancaPreviousSparklineData &&
props.bancaPreviousSparklineData.length > 0
) {
datasets.push({
label: 'Bancă (anul precedent)',
label: "Bancă (anul precedent)",
data: props.bancaPreviousSparklineData,
borderColor: 'rgba(59, 130, 246, 0.4)',
backgroundColor: 'rgba(59, 130, 246, 0.05)',
borderColor: "rgba(59, 130, 246, 0.4)",
backgroundColor: "rgba(59, 130, 246, 0.05)",
borderWidth: 2,
borderDash: [5, 5],
fill: false,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBackgroundColor: 'rgba(59, 130, 246, 0.4)',
pointHoverBorderColor: '#ffffff',
pointHoverBorderWidth: 2
})
pointHoverBackgroundColor: "rgba(59, 130, 246, 0.4)",
pointHoverBorderColor: "#ffffff",
pointHoverBorderWidth: 2,
});
}
// Calculate limits including both datasets
const allDataPoints = [...props.bancaSparklineData]
if (props.bancaPreviousSparklineData && props.bancaPreviousSparklineData.length > 0) {
allDataPoints.push(...props.bancaPreviousSparklineData)
const allDataPoints = [...props.bancaSparklineData];
if (
props.bancaPreviousSparklineData &&
props.bancaPreviousSparklineData.length > 0
) {
allDataPoints.push(...props.bancaPreviousSparklineData);
}
const dataMin = Math.min(...allDataPoints)
const dataMax = Math.max(...allDataPoints)
const dataRange = dataMax - dataMin
const dataPadding = dataRange * 0.05
const dataMin = Math.min(...allDataPoints);
const dataMax = Math.max(...allDataPoints);
const dataRange = dataMax - dataMin;
const dataPadding = dataRange * 0.05;
bancaChartInstance = new Chart(ctx, {
type: 'line',
type: "line",
data: {
labels: labels,
datasets: datasets
datasets: datasets,
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
mode: "index",
},
plugins: {
legend: {
display: datasets.length > 1,
position: 'top',
align: 'end',
position: "top",
align: "end",
labels: {
boxWidth: 12,
boxHeight: 12,
padding: 8,
font: {
size: 10,
family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif'
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
},
color: 'rgba(107, 114, 128, 0.9)',
color: "rgba(107, 114, 128, 0.9)",
usePointStyle: true,
pointStyle: 'line'
}
pointStyle: "line",
},
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: '#ffffff',
bodyColor: '#ffffff',
borderColor: 'rgba(255, 255, 255, 0.2)',
backgroundColor: "rgba(0, 0, 0, 0.8)",
titleColor: "#ffffff",
bodyColor: "#ffffff",
borderColor: "rgba(255, 255, 255, 0.2)",
borderWidth: 1,
cornerRadius: 6,
displayColors: true,
callbacks: {
title: (context) => context[0].label || '',
title: (context) => context[0].label || "",
label: (context) => {
const value = context.parsed.y
const label = context.dataset.label || ''
const formattedValue = new Intl.NumberFormat('ro-RO', {
style: 'currency',
currency: 'RON',
const value = context.parsed.y;
const label = context.dataset.label || "";
const formattedValue = new Intl.NumberFormat("ro-RO", {
style: "currency",
currency: "RON",
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(value)
return `${label}: ${formattedValue}`
}
}
}
maximumFractionDigits: 0,
}).format(value);
return `${label}: ${formattedValue}`;
},
},
},
},
scales: {
x: {
display: true,
grid: {
display: false,
drawBorder: false
drawBorder: false,
},
ticks: {
color: 'rgba(107, 114, 128, 0.7)',
color: "rgba(107, 114, 128, 0.7)",
font: {
size: 10,
family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif'
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
},
maxRotation: 45,
minRotation: 45,
maxTicksLimit: 6
maxTicksLimit: 6,
},
border: {
display: false
}
display: false,
},
},
y: {
display: true,
min: dataMin - dataPadding,
max: dataMax + dataPadding,
grid: {
color: 'rgba(107, 114, 128, 0.1)',
drawBorder: false
color: "rgba(107, 114, 128, 0.1)",
drawBorder: false,
},
ticks: {
color: '#3b82f6',
color: "#3b82f6",
font: {
size: 11,
family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif'
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
},
maxTicksLimit: 3,
callback: function(value) {
callback: function (value) {
if (value >= 1000000) {
return (value / 1000000).toFixed(1) + 'M'
return (value / 1000000).toFixed(1) + "M";
} else if (value >= 1000) {
return (value / 1000).toFixed(0) + 'k'
return (value / 1000).toFixed(0) + "k";
}
return value.toFixed(0)
}
return value.toFixed(0);
},
},
border: {
display: false
}
}
}
}
})
}
display: false,
},
},
},
},
});
};
// Watch for data changes
watch(() => [
props.casaSparklineData,
props.bancaSparklineData,
props.sparklineLabels,
props.casaPreviousSparklineData,
props.bancaPreviousSparklineData,
props.previousSparklineLabels
], async () => {
await Promise.all([
initializeCasaChart(),
initializeBancaChart()
])
}, { deep: true })
watch(
() => [
props.casaSparklineData,
props.bancaSparklineData,
props.sparklineLabels,
props.casaPreviousSparklineData,
props.bancaPreviousSparklineData,
props.previousSparklineLabels,
],
async () => {
await Promise.all([initializeCasaChart(), initializeBancaChart()]);
},
{ deep: true },
);
// Lifecycle hooks
onMounted(async () => {
await Promise.all([
initializeCasaChart(),
initializeBancaChart()
])
})
await Promise.all([initializeCasaChart(), initializeBancaChart()]);
});
onBeforeUnmount(() => {
if (casaChartInstance) {
casaChartInstance.destroy()
casaChartInstance = null
casaChartInstance.destroy();
casaChartInstance = null;
}
if (bancaChartInstance) {
bancaChartInstance.destroy()
bancaChartInstance = null
bancaChartInstance.destroy();
bancaChartInstance = null;
}
})
});
</script>
<style scoped>