feat: Implement unified Vue SPA with granular service control
Consolidate Reports and Data Entry apps into a single Vue.js SPA with: Architecture: - Module-based structure with lazy-loaded routes (@reports, @data-entry) - Error boundaries per module to prevent cascade failures - Dual API proxy in Vite for microservices (reports:8001, data-entry:8003) - Pinia store factories for shared auth, company, and period stores - Vite path aliases for clear module boundaries (@shared, @reports, @data-entry) Service Management: - Granular service control scripts (backend-reports.sh, backend-data-entry.sh, bot.sh, frontend.sh) - 87% faster frontend restart: 7s vs 53s full restart - 38% faster full startup: 33s vs 53s via parallel backend initialization - Enhanced start-dev.sh with proper service timeouts (OCR: 30s, Vite: 15s, Bot: 10s) - status.sh for comprehensive health checks Features: - Auto-select first company on login with period auto-load - Hamburger menu with feature toggle support - JWT token auto-injection via axios interceptors - Unified header with company/period selectors - IIS web.config for production deployment with multi-API routing UX Improvements: - Vue watchers for reactive company/period loading - Lazy store initialization with graceful error handling - Period persistence per user+company in localStorage - Feature flags for optional modules Deployment: - Single IIS site serves unified frontend with API proxy rules - Maintains separate backend processes for microservices - Windows line ending fixes (.env CRLF → LF conversion) Stats: 112 files changed, 38,342 insertions(+), 2,342 deletions(-) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,659 @@
|
||||
<template>
|
||||
<div class="metric-card cashflow-card">
|
||||
<!-- Main values section - Split layout (Încasări | Plăți) -->
|
||||
<div class="values-section">
|
||||
<!-- Încasări Section -->
|
||||
<div class="value-block inflows">
|
||||
<div class="metric-label">Încasări</div>
|
||||
<div class="metric-value text-success">
|
||||
{{ formatCurrency(inflowsValue) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- Plăți Section -->
|
||||
<div class="value-block outflows">
|
||||
<div class="metric-label">Plăți</div>
|
||||
<div class="metric-value text-error">
|
||||
{{ formatCurrency(outflowsValue) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dual sparkline charts - stacked vertical -->
|
||||
<div class="sparkline-dual-container" v-if="hasSparklineData">
|
||||
<!-- Grafic Încasări -->
|
||||
<div class="sparkline-wrapper">
|
||||
<div class="sparkline-title text-success">Încasări</div>
|
||||
<div class="sparkline-chart">
|
||||
<canvas ref="inflowsCanvas" class="sparkline-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grafic Plăți -->
|
||||
<div class="sparkline-wrapper">
|
||||
<div class="sparkline-title text-error">Plăți</div>
|
||||
<div class="sparkline-chart">
|
||||
<canvas ref="outflowsCanvas" class="sparkline-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
computed,
|
||||
watch,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
nextTick,
|
||||
} from "vue";
|
||||
import { Chart, registerables } from "chart.js";
|
||||
|
||||
Chart.register(...registerables);
|
||||
|
||||
const props = defineProps({
|
||||
inflowsValue: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
outflowsValue: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
inflowsTrend: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
outflowsTrend: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
inflowsSparkline: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
outflowsSparkline: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
inflowsPreviousSparkline: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
outflowsPreviousSparkline: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
sparklineLabels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
previousSparklineLabels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
// Refs pentru 2 canvas-uri separate
|
||||
const inflowsCanvas = ref(null);
|
||||
const outflowsCanvas = ref(null);
|
||||
let inflowsChartInstance = null;
|
||||
let outflowsChartInstance = null;
|
||||
|
||||
// Format currency
|
||||
const formatCurrency = (amount) => {
|
||||
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));
|
||||
};
|
||||
|
||||
// Check if sparkline data exists
|
||||
const hasSparklineData = computed(() => {
|
||||
return (
|
||||
props.inflowsSparkline.length > 0 && props.outflowsSparkline.length > 0
|
||||
);
|
||||
});
|
||||
|
||||
// Initialize Încasări chart
|
||||
const initializeInflowsChart = async () => {
|
||||
if (!inflowsCanvas.value || props.inflowsSparkline.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Destroy existing chart
|
||||
if (inflowsChartInstance) {
|
||||
inflowsChartInstance.destroy();
|
||||
inflowsChartInstance = null;
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
|
||||
const ctx = inflowsCanvas.value.getContext("2d");
|
||||
|
||||
// Generate labels
|
||||
const labels =
|
||||
props.sparklineLabels.length > 0
|
||||
? props.sparklineLabels
|
||||
: props.inflowsSparkline.map((_, i) => `L${i + 1}`);
|
||||
|
||||
// Prepare datasets
|
||||
const datasets = [
|
||||
{
|
||||
label: "Încasări (curent)",
|
||||
data: props.inflowsSparkline,
|
||||
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.inflowsPreviousSparkline &&
|
||||
props.inflowsPreviousSparkline.length > 0
|
||||
) {
|
||||
datasets.push({
|
||||
label: "Încasări (anul precedent)",
|
||||
data: props.inflowsPreviousSparkline,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate limits including both datasets
|
||||
const allDataPoints = [...props.inflowsSparkline];
|
||||
if (
|
||||
props.inflowsPreviousSparkline &&
|
||||
props.inflowsPreviousSparkline.length > 0
|
||||
) {
|
||||
allDataPoints.push(...props.inflowsPreviousSparkline);
|
||||
}
|
||||
const dataMin = Math.min(...allDataPoints);
|
||||
const dataMax = Math.max(...allDataPoints);
|
||||
const dataRange = dataMax - dataMin;
|
||||
const dataMean =
|
||||
allDataPoints.reduce((sum, val) => sum + val, 0) / allDataPoints.length;
|
||||
|
||||
// CORECT: Forțăm range minim SIMETRIC, apoi adăugăm padding
|
||||
const minVisibleRange = dataMean * 0.25; // 25% din medie = range minim vizibil
|
||||
const center = (dataMin + dataMax) / 2;
|
||||
const targetRange = Math.max(dataRange, minVisibleRange);
|
||||
|
||||
// Calculează limite simetric față de centru
|
||||
let calculatedMin = center - targetRange / 2;
|
||||
let calculatedMax = center + targetRange / 2;
|
||||
|
||||
// Adaugă padding PESTE range-ul asigurat (nu înăuntru!)
|
||||
const paddingAmount = targetRange * 0.1; // 10% padding suplimentar
|
||||
|
||||
// Aplică limitele finale (nu permite negative dacă toate datele sunt pozitive)
|
||||
const allPositive = dataMin >= 0;
|
||||
const yMin = allPositive
|
||||
? Math.max(0, calculatedMin - paddingAmount)
|
||||
: calculatedMin - paddingAmount;
|
||||
const yMax = calculatedMax + paddingAmount;
|
||||
|
||||
inflowsChartInstance = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: datasets,
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: "index",
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: datasets.length > 1,
|
||||
position: "top",
|
||||
align: "end",
|
||||
labels: {
|
||||
boxWidth: 12,
|
||||
boxHeight: 12,
|
||||
padding: 8,
|
||||
font: {
|
||||
size: 10,
|
||||
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
},
|
||||
color: "rgba(107, 114, 128, 0.9)",
|
||||
usePointStyle: true,
|
||||
pointStyle: "line",
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
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 || "",
|
||||
label: (context) => {
|
||||
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}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
grid: {
|
||||
display: false,
|
||||
drawBorder: false,
|
||||
},
|
||||
ticks: {
|
||||
color: "rgba(107, 114, 128, 0.7)",
|
||||
font: {
|
||||
size: 10,
|
||||
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
},
|
||||
maxRotation: 45,
|
||||
minRotation: 45,
|
||||
maxTicksLimit: 6,
|
||||
},
|
||||
border: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
min: yMin,
|
||||
max: yMax,
|
||||
grid: {
|
||||
color: "rgba(107, 114, 128, 0.1)",
|
||||
drawBorder: false,
|
||||
},
|
||||
ticks: {
|
||||
color: "#10b981",
|
||||
font: {
|
||||
size: 11,
|
||||
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
},
|
||||
maxTicksLimit: 3,
|
||||
callback: function (value) {
|
||||
if (value >= 1000000) {
|
||||
return (value / 1000000).toFixed(1) + "M";
|
||||
} else if (value >= 1000) {
|
||||
return (value / 1000).toFixed(0) + "k";
|
||||
}
|
||||
return value.toFixed(0);
|
||||
},
|
||||
},
|
||||
border: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize Plăți chart
|
||||
const initializeOutflowsChart = async () => {
|
||||
if (!outflowsCanvas.value || props.outflowsSparkline.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Destroy existing chart
|
||||
if (outflowsChartInstance) {
|
||||
outflowsChartInstance.destroy();
|
||||
outflowsChartInstance = null;
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
|
||||
const ctx = outflowsCanvas.value.getContext("2d");
|
||||
|
||||
// Generate labels
|
||||
const labels =
|
||||
props.sparklineLabels.length > 0
|
||||
? props.sparklineLabels
|
||||
: props.outflowsSparkline.map((_, i) => `L${i + 1}`);
|
||||
|
||||
// Prepare datasets
|
||||
const datasets = [
|
||||
{
|
||||
label: "Plăți (curent)",
|
||||
data: props.outflowsSparkline,
|
||||
borderColor: "#ef4444",
|
||||
backgroundColor: "rgba(239, 68, 68, 0.1)",
|
||||
borderWidth: 2,
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBackgroundColor: "#ef4444",
|
||||
pointHoverBorderColor: "#ffffff",
|
||||
pointHoverBorderWidth: 2,
|
||||
},
|
||||
];
|
||||
|
||||
// Add previous year dataset if available
|
||||
if (
|
||||
props.outflowsPreviousSparkline &&
|
||||
props.outflowsPreviousSparkline.length > 0
|
||||
) {
|
||||
datasets.push({
|
||||
label: "Plăți (anul precedent)",
|
||||
data: props.outflowsPreviousSparkline,
|
||||
borderColor: "rgba(239, 68, 68, 0.4)",
|
||||
backgroundColor: "rgba(239, 68, 68, 0.05)",
|
||||
borderWidth: 2,
|
||||
borderDash: [5, 5],
|
||||
fill: false,
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBackgroundColor: "rgba(239, 68, 68, 0.4)",
|
||||
pointHoverBorderColor: "#ffffff",
|
||||
pointHoverBorderWidth: 2,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate limits including both datasets
|
||||
const allDataPoints = [...props.outflowsSparkline];
|
||||
if (
|
||||
props.outflowsPreviousSparkline &&
|
||||
props.outflowsPreviousSparkline.length > 0
|
||||
) {
|
||||
allDataPoints.push(...props.outflowsPreviousSparkline);
|
||||
}
|
||||
const dataMin = Math.min(...allDataPoints);
|
||||
const dataMax = Math.max(...allDataPoints);
|
||||
const dataRange = dataMax - dataMin;
|
||||
const dataMean =
|
||||
allDataPoints.reduce((sum, val) => sum + val, 0) / allDataPoints.length;
|
||||
|
||||
// CORECT: Forțăm range minim SIMETRIC, apoi adăugăm padding
|
||||
const minVisibleRange = dataMean * 0.25; // 25% din medie = range minim vizibil
|
||||
const center = (dataMin + dataMax) / 2;
|
||||
const targetRange = Math.max(dataRange, minVisibleRange);
|
||||
|
||||
// Calculează limite simetric față de centru
|
||||
let calculatedMin = center - targetRange / 2;
|
||||
let calculatedMax = center + targetRange / 2;
|
||||
|
||||
// Adaugă padding PESTE range-ul asigurat (nu înăuntru!)
|
||||
const paddingAmount = targetRange * 0.1; // 10% padding suplimentar
|
||||
|
||||
// Aplică limitele finale (nu permite negative dacă toate datele sunt pozitive)
|
||||
const allPositive = dataMin >= 0;
|
||||
const yMin = allPositive
|
||||
? Math.max(0, calculatedMin - paddingAmount)
|
||||
: calculatedMin - paddingAmount;
|
||||
const yMax = calculatedMax + paddingAmount;
|
||||
|
||||
outflowsChartInstance = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: datasets,
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: "index",
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: datasets.length > 1,
|
||||
position: "top",
|
||||
align: "end",
|
||||
labels: {
|
||||
boxWidth: 12,
|
||||
boxHeight: 12,
|
||||
padding: 8,
|
||||
font: {
|
||||
size: 10,
|
||||
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
},
|
||||
color: "rgba(107, 114, 128, 0.9)",
|
||||
usePointStyle: true,
|
||||
pointStyle: "line",
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
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 || "",
|
||||
label: (context) => {
|
||||
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}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
grid: {
|
||||
display: false,
|
||||
drawBorder: false,
|
||||
},
|
||||
ticks: {
|
||||
color: "rgba(107, 114, 128, 0.7)",
|
||||
font: {
|
||||
size: 10,
|
||||
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
},
|
||||
maxRotation: 45,
|
||||
minRotation: 45,
|
||||
maxTicksLimit: 6,
|
||||
},
|
||||
border: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
display: true,
|
||||
min: yMin,
|
||||
max: yMax,
|
||||
grid: {
|
||||
color: "rgba(107, 114, 128, 0.1)",
|
||||
drawBorder: false,
|
||||
},
|
||||
ticks: {
|
||||
color: "#ef4444",
|
||||
font: {
|
||||
size: 11,
|
||||
family: "Inter, -apple-system, BlinkMacSystemFont, sans-serif",
|
||||
},
|
||||
maxTicksLimit: 3,
|
||||
callback: function (value) {
|
||||
if (value >= 1000000) {
|
||||
return (value / 1000000).toFixed(1) + "M";
|
||||
} else if (value >= 1000) {
|
||||
return (value / 1000).toFixed(0) + "k";
|
||||
}
|
||||
return value.toFixed(0);
|
||||
},
|
||||
},
|
||||
border: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Watch for data changes
|
||||
watch(
|
||||
() => [
|
||||
props.inflowsSparkline,
|
||||
props.outflowsSparkline,
|
||||
props.sparklineLabels,
|
||||
props.inflowsPreviousSparkline,
|
||||
props.outflowsPreviousSparkline,
|
||||
props.previousSparklineLabels,
|
||||
],
|
||||
async () => {
|
||||
await Promise.all([initializeInflowsChart(), initializeOutflowsChart()]);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(async () => {
|
||||
await Promise.all([initializeInflowsChart(), initializeOutflowsChart()]);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (inflowsChartInstance) {
|
||||
inflowsChartInstance.destroy();
|
||||
inflowsChartInstance = null;
|
||||
}
|
||||
if (outflowsChartInstance) {
|
||||
outflowsChartInstance.destroy();
|
||||
outflowsChartInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Component-specific: Dual-chart layout for CashFlowMetricCard */
|
||||
|
||||
/* Override min-height for dual chart layout */
|
||||
.cashflow-card {
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
/* Split layout: Încasări | Divider | Plăți */
|
||||
.values-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.value-block {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background: var(--color-border);
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
/* Dual sparkline container (unique to this card) */
|
||||
.sparkline-dual-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.sparkline-wrapper {
|
||||
width: 100%;
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.sparkline-chart {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Chart.js canvas sizing (required for proper rendering) */
|
||||
.sparkline-canvas {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Responsive: Stack vertically on mobile */
|
||||
@media (max-width: 768px) {
|
||||
.cashflow-card {
|
||||
min-height: 380px;
|
||||
}
|
||||
|
||||
.values-section {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
min-height: 1px;
|
||||
}
|
||||
|
||||
.sparkline-chart {
|
||||
height: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.sparkline-chart {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.sparkline-wrapper {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user