Modern ERP Reports Application with microservices architecture Tech Stack: - Backend: FastAPI + python-oracledb (Oracle DB integration) - Frontend: Vue.js 3 + PrimeVue + Vite - Telegram Bot: python-telegram-bot + SQLite - Infrastructure: Shared database pool, JWT authentication, SSH tunnel Features: - FastAPI backend with async Oracle connection pool - Vue.js 3 responsive frontend with PrimeVue components - Telegram bot alternative interface - Microservices architecture with shared components - Complete deployment support (Linux Docker + Windows IIS) - Comprehensive testing (Playwright E2E + pytest) Repository Structure: - reports-app/ - Main application (backend, frontend, telegram-bot) - shared/ - Shared components (database pool, auth, utils) - deployment/ - Deployment scripts (Linux & Windows) - docs/ - Project documentation - security/ - Security scanning and git hooks
859 lines
21 KiB
Vue
859 lines
21 KiB
Vue
<template>
|
|
<div class="treasury-dual-card">
|
|
<!-- Main values section - Split layout (Casa | Bancă) -->
|
|
<div class="values-section">
|
|
<!-- Casa Section -->
|
|
<div class="value-block casa">
|
|
<div class="value-label">Casa</div>
|
|
<div class="value-amount positive">
|
|
{{ formatCurrency(casaTotal) }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Divider -->
|
|
<div class="divider"></div>
|
|
|
|
<!-- Bancă Section -->
|
|
<div class="value-block banca">
|
|
<div class="value-label">Bancă</div>
|
|
<div class="value-amount positive">
|
|
{{ formatCurrency(bancaTotal) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dual sparkline charts - stacked vertical -->
|
|
<div class="sparkline-dual-container" v-if="hasSparklineData">
|
|
<!-- Grafic Casa -->
|
|
<div class="sparkline-wrapper">
|
|
<div class="sparkline-label">Casa</div>
|
|
<div class="sparkline-chart">
|
|
<canvas ref="casaCanvas" class="sparkline-canvas"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Grafic Bancă -->
|
|
<div class="sparkline-wrapper">
|
|
<div class="sparkline-label">Bancă</div>
|
|
<div class="sparkline-chart">
|
|
<canvas ref="bancaCanvas" class="sparkline-canvas"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Breakdown section -->
|
|
<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">
|
|
<span class="collapse-icon">{{ isCasaExpanded ? '▼' : '▶' }}</span>
|
|
<span class="breakdown-label">Casa</span>
|
|
</div>
|
|
<span class="breakdown-value">{{ formatCurrency(casaTotal) }}</span>
|
|
</div>
|
|
|
|
<!-- Casa Sub-items -->
|
|
<div v-show="isCasaExpanded" class="breakdown-subitems">
|
|
<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>
|
|
<span class="breakdown-subvalue">{{ formatCurrency(item.sold) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bancă Breakdown -->
|
|
<div class="breakdown-group" v-if="bancaItems.length > 0">
|
|
<div class="breakdown-header" @click="toggleBancaExpanded">
|
|
<div class="breakdown-header-left">
|
|
<span class="collapse-icon">{{ isBancaExpanded ? '▼' : '▶' }}</span>
|
|
<span class="breakdown-label">Bancă</span>
|
|
</div>
|
|
<span class="breakdown-value">{{ formatCurrency(bancaTotal) }}</span>
|
|
</div>
|
|
|
|
<!-- Bancă Sub-items -->
|
|
<div v-show="isBancaExpanded" class="breakdown-subitems">
|
|
<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>
|
|
<span class="breakdown-subvalue">{{ formatCurrency(item.sold) }}</span>
|
|
</div>
|
|
</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({
|
|
casaTotal: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
bancaTotal: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
casaItems: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
bancaItems: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
casaSparklineData: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
bancaSparklineData: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
casaPreviousSparklineData: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
bancaPreviousSparklineData: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
sparklineLabels: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
previousSparklineLabels: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
trend: {
|
|
type: Object,
|
|
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)
|
|
|
|
// Toggle functions
|
|
const toggleCasaExpanded = () => {
|
|
isCasaExpanded.value = !isCasaExpanded.value
|
|
}
|
|
|
|
const toggleBancaExpanded = () => {
|
|
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',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0
|
|
}).format(Math.abs(amount))
|
|
}
|
|
|
|
// Check if sparkline data exists
|
|
const hasSparklineData = computed(() => {
|
|
return props.casaSparklineData.length > 0 && props.bancaSparklineData.length > 0
|
|
})
|
|
|
|
// Initialize Casa chart
|
|
const initializeCasaChart = async () => {
|
|
if (!casaCanvas.value || props.casaSparklineData.length === 0) {
|
|
return
|
|
}
|
|
|
|
// Destroy existing chart
|
|
if (casaChartInstance) {
|
|
casaChartInstance.destroy()
|
|
casaChartInstance = null
|
|
}
|
|
|
|
await nextTick()
|
|
|
|
const ctx = casaCanvas.value.getContext('2d')
|
|
|
|
// Generate labels
|
|
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
|
|
}]
|
|
|
|
// Add previous year dataset if available
|
|
if (props.casaPreviousSparklineData && props.casaPreviousSparklineData.length > 0) {
|
|
datasets.push({
|
|
label: 'Casa (anul precedent)',
|
|
data: props.casaPreviousSparklineData,
|
|
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.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
|
|
|
|
casaChartInstance = 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: dataMin - dataPadding,
|
|
max: dataMax + dataPadding,
|
|
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 Bancă chart
|
|
const initializeBancaChart = async () => {
|
|
if (!bancaCanvas.value || props.bancaSparklineData.length === 0) {
|
|
return
|
|
}
|
|
|
|
// Destroy existing chart
|
|
if (bancaChartInstance) {
|
|
bancaChartInstance.destroy()
|
|
bancaChartInstance = null
|
|
}
|
|
|
|
await nextTick()
|
|
|
|
const ctx = bancaCanvas.value.getContext('2d')
|
|
|
|
// Generate labels
|
|
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
|
|
}]
|
|
|
|
// Add previous year dataset if available
|
|
if (props.bancaPreviousSparklineData && props.bancaPreviousSparklineData.length > 0) {
|
|
datasets.push({
|
|
label: 'Bancă (anul precedent)',
|
|
data: props.bancaPreviousSparklineData,
|
|
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
|
|
})
|
|
}
|
|
|
|
// Calculate limits including both datasets
|
|
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
|
|
|
|
bancaChartInstance = 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: dataMin - dataPadding,
|
|
max: dataMax + dataPadding,
|
|
grid: {
|
|
color: 'rgba(107, 114, 128, 0.1)',
|
|
drawBorder: false
|
|
},
|
|
ticks: {
|
|
color: '#3b82f6',
|
|
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.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()
|
|
])
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (casaChartInstance) {
|
|
casaChartInstance.destroy()
|
|
casaChartInstance = null
|
|
}
|
|
if (bancaChartInstance) {
|
|
bancaChartInstance.destroy()
|
|
bancaChartInstance = null
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* === TYPOGRAPHY TOKENS === */
|
|
:root {
|
|
--card-label-size: 0.875rem;
|
|
--card-value-size: 1.5rem;
|
|
--card-trend-size: 0.75rem;
|
|
--breakdown-label-size: 0.875rem;
|
|
--breakdown-value-size: 0.9375rem;
|
|
--breakdown-sub-label-size: 0.8125rem;
|
|
--breakdown-sub-value-size: 0.8125rem;
|
|
|
|
--font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
|
|
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
}
|
|
|
|
.treasury-dual-card {
|
|
background: var(--color-bg, #ffffff);
|
|
border: 1px solid var(--color-border, #e5e7eb);
|
|
border-radius: var(--card-radius, 8px);
|
|
padding: var(--space-lg, 1.5rem);
|
|
transition: all 0.3s ease;
|
|
position: relative;
|
|
min-height: 420px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.treasury-dual-card:hover {
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
transform: translateY(-2px);
|
|
border-color: var(--color-primary, #3b82f6);
|
|
}
|
|
|
|
/* Values section - Split layout */
|
|
.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;
|
|
}
|
|
|
|
.value-label {
|
|
font-size: var(--card-label-size);
|
|
font-weight: 500;
|
|
color: var(--color-text-secondary, #6b7280);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
font-family: var(--font-sans);
|
|
}
|
|
|
|
.value-amount {
|
|
font-size: var(--card-value-size);
|
|
font-weight: 700;
|
|
line-height: 1.2;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.value-amount.positive {
|
|
color: var(--color-success, #10b981);
|
|
}
|
|
|
|
.value-amount.negative {
|
|
color: var(--color-danger, #ef4444);
|
|
}
|
|
|
|
.value-amount.neutral {
|
|
color: var(--color-text, #111827);
|
|
}
|
|
|
|
.divider {
|
|
width: 1px;
|
|
height: 100%;
|
|
background: var(--color-border, #e5e7eb);
|
|
min-height: 60px;
|
|
}
|
|
|
|
/* Dual sparkline container - stack vertical */
|
|
.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, #f8fafc);
|
|
border: 1px solid var(--color-border, #e5e7eb);
|
|
border-radius: 4px;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.sparkline-label {
|
|
font-size: var(--card-label-size);
|
|
font-weight: 600;
|
|
color: var(--color-text-secondary, #6b7280);
|
|
margin-bottom: 0.25rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
font-family: var(--font-sans);
|
|
}
|
|
|
|
/* Culori distinctive pentru label-uri */
|
|
.sparkline-wrapper:first-child .sparkline-label {
|
|
color: #10b981; /* Verde pentru Casa */
|
|
}
|
|
|
|
.sparkline-wrapper:last-child .sparkline-label {
|
|
color: #3b82f6; /* Albastru pentru Bancă */
|
|
}
|
|
|
|
.sparkline-chart {
|
|
width: 100%;
|
|
height: 150px;
|
|
position: relative;
|
|
}
|
|
|
|
.sparkline-canvas {
|
|
width: 100% !important;
|
|
height: 100% !important;
|
|
display: block;
|
|
}
|
|
|
|
/* Breakdown section */
|
|
.breakdown-section {
|
|
margin-top: 0.5rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid var(--color-border, #e5e7eb);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.breakdown-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.breakdown-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
padding: 0.375rem 0.5rem;
|
|
border-radius: var(--radius-sm, 4px);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.breakdown-header:hover {
|
|
background: var(--color-bg-secondary, #f8fafc);
|
|
}
|
|
|
|
.breakdown-header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.collapse-icon {
|
|
font-size: 0.625rem;
|
|
color: var(--color-text-secondary, #6b7280);
|
|
transition: transform 0.2s ease;
|
|
display: inline-block;
|
|
width: 0.875rem;
|
|
}
|
|
|
|
.breakdown-label {
|
|
font-size: var(--breakdown-label-size);
|
|
color: var(--color-text, #111827);
|
|
font-weight: 500;
|
|
font-family: var(--font-sans);
|
|
}
|
|
|
|
.breakdown-value {
|
|
font-size: var(--breakdown-value-size);
|
|
font-weight: 600;
|
|
font-family: var(--font-mono);
|
|
color: var(--color-text, #111827);
|
|
}
|
|
|
|
.breakdown-subitems {
|
|
padding-left: 0;
|
|
animation: slideDown 0.2s ease-out;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
@keyframes slideDown {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-10px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.breakdown-subitem {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.25rem 0.5rem 0.25rem 1.5rem;
|
|
}
|
|
|
|
.breakdown-sublabel {
|
|
color: var(--color-text-secondary, #6b7280);
|
|
font-size: var(--breakdown-sub-label-size);
|
|
font-weight: 400;
|
|
font-family: var(--font-sans);
|
|
}
|
|
|
|
.breakdown-cont {
|
|
font-size: var(--breakdown-sub-label-size);
|
|
opacity: 0.7;
|
|
margin-left: 0.25rem;
|
|
}
|
|
|
|
.breakdown-subvalue {
|
|
font-weight: 500;
|
|
font-family: var(--font-mono);
|
|
font-size: var(--breakdown-sub-value-size);
|
|
color: var(--color-text, #111827);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.treasury-dual-card {
|
|
min-height: 380px;
|
|
padding: var(--space-md, 1rem);
|
|
}
|
|
|
|
.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) {
|
|
.treasury-dual-card {
|
|
min-height: 340px;
|
|
padding: 0.5rem 0.25rem;
|
|
gap: 0.5rem;
|
|
border-radius: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.sparkline-chart {
|
|
height: 120px;
|
|
}
|
|
|
|
.sparkline-wrapper {
|
|
padding: 0;
|
|
border: none;
|
|
}
|
|
|
|
.values-section {
|
|
gap: 0.5rem;
|
|
}
|
|
}
|
|
|
|
/* Dark mode support */
|
|
@media (prefers-color-scheme: dark) {
|
|
.treasury-dual-card {
|
|
--color-bg: #1f2937;
|
|
--color-bg-secondary: #374151;
|
|
--color-border: #4b5563;
|
|
--color-text: #f9fafb;
|
|
--color-text-secondary: #d1d5db;
|
|
}
|
|
}
|
|
</style>
|