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>
814 lines
18 KiB
Vue
814 lines
18 KiB
Vue
<template>
|
|
<div class="maturity-card">
|
|
<div class="card-header">
|
|
<h3>Analiză Comparativă Scadențe</h3>
|
|
<select
|
|
v-model="selectedPeriod"
|
|
@change="handlePeriodChange"
|
|
class="period-selector"
|
|
:disabled="isLoading"
|
|
>
|
|
<option value="7d">7 zile</option>
|
|
<option value="1m">1 lună</option>
|
|
<option value="3m">3 luni</option>
|
|
<option value="6m">6 luni</option>
|
|
<option value="12m">12 luni</option>
|
|
<option value="all">Toate</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div v-if="isLoading" class="loading-state">
|
|
<div class="loading-spinner"></div>
|
|
<p>Se încarcă analiza scadențelor...</p>
|
|
</div>
|
|
|
|
<div v-else-if="error" class="error-state">
|
|
<div class="error-icon">!</div>
|
|
<p>{{ error }}</p>
|
|
<button @click="loadData" class="retry-btn">Încearcă din nou</button>
|
|
</div>
|
|
|
|
<div v-else class="maturity-comparison">
|
|
<!-- Clients Side -->
|
|
<div class="clients-side">
|
|
<h4 class="side-title clients-title">
|
|
Clienți - De încasat
|
|
<span class="total-amount">{{ formatCurrency(clientsTotal) }}</span>
|
|
</h4>
|
|
<div class="maturity-list">
|
|
<div
|
|
v-for="(client, index) in clientsData"
|
|
:key="`client-${index}`"
|
|
class="maturity-item"
|
|
:class="{
|
|
overdue: client.daysOverdue > 0,
|
|
critical: client.daysOverdue > 30,
|
|
}"
|
|
>
|
|
<div class="item-info">
|
|
<span class="client-name">{{ client.name }}</span>
|
|
<span class="due-info">
|
|
<span v-if="client.daysOverdue > 0" class="overdue-days">
|
|
Restant {{ client.daysOverdue }} zile
|
|
</span>
|
|
<span v-else class="due-date">
|
|
Scadent în {{ Math.abs(client.daysOverdue) }} zile
|
|
</span>
|
|
</span>
|
|
</div>
|
|
<div class="amount-bar">
|
|
<div class="bar-container">
|
|
<div
|
|
class="bar-fill clients-bar"
|
|
:style="{
|
|
width: getBarWidth(client.amount, maxClientAmount) + '%',
|
|
}"
|
|
></div>
|
|
</div>
|
|
<span class="amount-value">{{
|
|
formatCurrency(client.amount)
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
<div v-if="clientsData.length === 0" class="empty-state">
|
|
<p>Nu există facturi de încasat pentru această perioadă</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Divider -->
|
|
<div class="comparison-divider"></div>
|
|
|
|
<!-- Suppliers Side -->
|
|
<div class="suppliers-side">
|
|
<h4 class="side-title suppliers-title">
|
|
Furnizori - De plătit
|
|
<span class="total-amount">{{ formatCurrency(suppliersTotal) }}</span>
|
|
</h4>
|
|
<div class="maturity-list">
|
|
<div
|
|
v-for="(supplier, index) in suppliersData"
|
|
:key="`supplier-${index}`"
|
|
class="maturity-item"
|
|
:class="{
|
|
overdue: supplier.daysOverdue > 0,
|
|
critical: supplier.daysOverdue > 30,
|
|
}"
|
|
>
|
|
<div class="item-info">
|
|
<span class="supplier-name">{{ supplier.name }}</span>
|
|
<span class="due-info">
|
|
<span v-if="supplier.daysOverdue > 0" class="overdue-days">
|
|
Restant {{ supplier.daysOverdue }} zile
|
|
</span>
|
|
<span v-else class="due-date">
|
|
Scadent în {{ Math.abs(supplier.daysOverdue) }} zile
|
|
</span>
|
|
</span>
|
|
</div>
|
|
<div class="amount-bar">
|
|
<div class="bar-container">
|
|
<div
|
|
class="bar-fill suppliers-bar"
|
|
:style="{
|
|
width:
|
|
getBarWidth(supplier.amount, maxSupplierAmount) + '%',
|
|
}"
|
|
></div>
|
|
</div>
|
|
<span class="amount-value">{{
|
|
formatCurrency(supplier.amount)
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
<div v-if="suppliersData.length === 0" class="empty-state">
|
|
<p>Nu există facturi de plătit pentru această perioadă</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Balance Indicator -->
|
|
<div v-if="!isLoading && !error" class="balance-indicator">
|
|
<div class="balance-content">
|
|
<div class="balance-text">
|
|
<span class="balance-label">{{ balanceLabel }}</span>
|
|
<span class="balance-amount" :class="balanceClass">
|
|
{{ formatCurrency(Math.abs(balance)) }}
|
|
</span>
|
|
</div>
|
|
<div v-if="recommendations.length > 0" class="recommendations">
|
|
<details>
|
|
<summary>Recomandări</summary>
|
|
<ul>
|
|
<li v-for="(rec, index) in recommendations" :key="index">
|
|
{{ rec }}
|
|
</li>
|
|
</ul>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer with period info -->
|
|
<div v-if="!isLoading && !error" class="card-footer">
|
|
<div class="period-info">
|
|
<span class="period-label">Perioada analizată:</span>
|
|
<span class="period-value">{{ getPeriodLabel(selectedPeriod) }}</span>
|
|
</div>
|
|
<div class="last-updated">
|
|
<span class="update-label">Actualizat:</span>
|
|
<span class="update-time">{{ formatLastUpdated(lastUpdated) }}</span>
|
|
<button
|
|
@click="refreshData"
|
|
class="refresh-btn"
|
|
:disabled="isLoading"
|
|
title="Reîmprospătează datele"
|
|
>
|
|
<i
|
|
class="pi pi-refresh refresh-icon"
|
|
:class="{ spinning: isLoading }"
|
|
></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, watch } from "vue";
|
|
import { useDashboardStore } from "@reports/stores/dashboard";
|
|
|
|
// Props
|
|
const props = defineProps({
|
|
companyId: {
|
|
type: [Number, String],
|
|
required: true,
|
|
},
|
|
});
|
|
|
|
// Emits
|
|
const emit = defineEmits(["periodChanged"]);
|
|
|
|
// Store
|
|
const dashboardStore = useDashboardStore();
|
|
|
|
// Reactive state
|
|
const selectedPeriod = ref("1m");
|
|
const isLoading = ref(false);
|
|
const error = ref(null);
|
|
const lastUpdated = ref(null);
|
|
|
|
// Mock data structure - in production this would come from API
|
|
const maturityData = ref({
|
|
clients: [],
|
|
suppliers: [],
|
|
balance: 0,
|
|
recommendations: [],
|
|
});
|
|
|
|
// Romanian currency formatter
|
|
const formatCurrency = (value) => {
|
|
if (value === null || value === undefined) return "0,00 RON";
|
|
return new Intl.NumberFormat("ro-RO", {
|
|
style: "currency",
|
|
currency: "RON",
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}).format(value);
|
|
};
|
|
|
|
// Computed properties
|
|
const clientsData = computed(() => maturityData.value.clients || []);
|
|
const suppliersData = computed(() => maturityData.value.suppliers || []);
|
|
const recommendations = computed(
|
|
() => maturityData.value.recommendations || [],
|
|
);
|
|
|
|
const clientsTotal = computed(() =>
|
|
clientsData.value.reduce((sum, client) => sum + (client.amount || 0), 0),
|
|
);
|
|
|
|
const suppliersTotal = computed(() =>
|
|
suppliersData.value.reduce(
|
|
(sum, supplier) => sum + (supplier.amount || 0),
|
|
0,
|
|
),
|
|
);
|
|
|
|
const balance = computed(() => clientsTotal.value - suppliersTotal.value);
|
|
|
|
const balanceClass = computed(() =>
|
|
balance.value < 0 ? "deficit" : "surplus",
|
|
);
|
|
|
|
const balanceIcon = computed(() => (balance.value < 0 ? "📉" : "📈"));
|
|
|
|
const balanceLabel = computed(() =>
|
|
balance.value < 0 ? "Deficit estimat:" : "Surplus estimat:",
|
|
);
|
|
|
|
const maxClientAmount = computed(() =>
|
|
Math.max(...clientsData.value.map((c) => c.amount || 0), 1),
|
|
);
|
|
|
|
const maxSupplierAmount = computed(() =>
|
|
Math.max(...suppliersData.value.map((s) => s.amount || 0), 1),
|
|
);
|
|
|
|
// Methods
|
|
const getBarWidth = (amount, maxAmount) => {
|
|
return maxAmount > 0 ? Math.min((amount / maxAmount) * 100, 100) : 0;
|
|
};
|
|
|
|
const getPeriodLabel = (period) => {
|
|
const labels = {
|
|
"7d": "Toate restanțele + următoarele 7 zile",
|
|
"1m": "Toate restanțele + următoarea lună",
|
|
"3m": "Toate restanțele + următoarele 3 luni",
|
|
"6m": "Toate restanțele + următoarele 6 luni",
|
|
"12m": "Toate restanțele + următorul an",
|
|
all: "Toate soldurile (fără filtru)",
|
|
};
|
|
return labels[period] || period;
|
|
};
|
|
|
|
const formatLastUpdated = (timestamp) => {
|
|
if (!timestamp) return "Necunoscut";
|
|
return new Date(timestamp).toLocaleString("ro-RO");
|
|
};
|
|
|
|
const handlePeriodChange = () => {
|
|
emit("periodChanged", selectedPeriod.value);
|
|
loadData();
|
|
};
|
|
|
|
const refreshData = () => {
|
|
loadData(true);
|
|
};
|
|
|
|
const loadData = async (forceRefresh = false) => {
|
|
if (!props.companyId) {
|
|
error.value = "ID firmă necunoscut";
|
|
return;
|
|
}
|
|
|
|
isLoading.value = true;
|
|
error.value = null;
|
|
|
|
try {
|
|
// Apelăm API-ul real pentru a obține datele de scadențe
|
|
const response = await dashboardStore.loadMaturityData(
|
|
props.companyId,
|
|
selectedPeriod.value,
|
|
);
|
|
|
|
if (response && response.success) {
|
|
maturityData.value = response.data;
|
|
lastUpdated.value = new Date();
|
|
} else {
|
|
throw new Error(response?.error || "Eroare la încărcarea datelor");
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to load maturity data:", err);
|
|
error.value =
|
|
err.message ||
|
|
"Eroare la încărcarea datelor. Vă rugăm încercați din nou.";
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
// Watchers
|
|
watch(
|
|
() => props.companyId,
|
|
(newCompanyId) => {
|
|
if (newCompanyId) {
|
|
loadData();
|
|
}
|
|
},
|
|
{ immediate: false },
|
|
);
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
if (props.companyId) {
|
|
loadData();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Base Card Styles */
|
|
.maturity-card {
|
|
background: var(--color-bg);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--card-radius, 8px);
|
|
padding: 0;
|
|
box-shadow: var(--shadow-sm);
|
|
transition: all var(--transition-fast, 0.3s ease);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.maturity-card:hover {
|
|
box-shadow: var(--shadow-md);
|
|
border-color: var(--color-primary);
|
|
}
|
|
|
|
/* Card Header */
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: var(--space-lg, 1rem);
|
|
border-bottom: 1px solid var(--color-border);
|
|
background: transparent;
|
|
}
|
|
|
|
.card-header h3 {
|
|
margin: 0;
|
|
font-size: var(--text-lg, 1.125rem);
|
|
font-weight: var(--font-semibold, 600);
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.period-selector {
|
|
padding: 0.5rem 0.75rem;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-sm, 4px);
|
|
background: var(--color-bg);
|
|
color: var(--color-text);
|
|
font-size: var(--text-sm, 0.875rem);
|
|
cursor: pointer;
|
|
transition: border-color 0.2s ease;
|
|
}
|
|
|
|
.period-selector:hover {
|
|
border-color: var(--color-primary);
|
|
}
|
|
|
|
.period-selector:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* Loading and Error States */
|
|
.loading-state,
|
|
.error-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: var(--space-xl, 2rem);
|
|
text-align: center;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 32px;
|
|
height: 32px;
|
|
border: 3px solid var(--color-border);
|
|
border-top: 3px solid var(--color-primary);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin-bottom: var(--space-md, 1rem);
|
|
}
|
|
|
|
.error-icon {
|
|
font-size: 2rem;
|
|
margin-bottom: var(--space-sm, 0.5rem);
|
|
}
|
|
|
|
.retry-btn {
|
|
margin-top: var(--space-md, 1rem);
|
|
padding: 0.5rem 1rem;
|
|
background: var(--color-primary);
|
|
color: white;
|
|
border: none;
|
|
border-radius: var(--radius-sm, 4px);
|
|
cursor: pointer;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.retry-btn:hover {
|
|
background: var(--color-primary-dark);
|
|
}
|
|
|
|
/* Comparison Layout */
|
|
.maturity-comparison {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1px 1fr;
|
|
gap: var(--space-lg, 1rem);
|
|
padding: var(--space-lg, 1rem);
|
|
min-height: 300px;
|
|
}
|
|
|
|
.comparison-divider {
|
|
background: var(--color-border);
|
|
margin: var(--space-md, 1rem) 0;
|
|
}
|
|
|
|
/* Side Headers */
|
|
.side-title {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin: 0 0 var(--space-md, 1rem) 0;
|
|
font-size: var(--text-base, 1rem);
|
|
font-weight: var(--font-semibold, 600);
|
|
padding-bottom: var(--space-sm, 0.5rem);
|
|
border-bottom: 1px solid var(--color-border);
|
|
}
|
|
|
|
.clients-title {
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.suppliers-title {
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.total-amount {
|
|
font-size: var(--text-sm, 0.875rem);
|
|
font-weight: var(--font-bold, 700);
|
|
padding: 0.25rem 0.5rem;
|
|
background: var(--color-bg-secondary, #f8f9fa);
|
|
border-radius: var(--radius-sm, 4px);
|
|
}
|
|
|
|
/* Maturity Lists */
|
|
.maturity-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-sm, 0.5rem);
|
|
max-height: 250px;
|
|
overflow-y: auto;
|
|
padding-right: var(--space-xs, 0.25rem);
|
|
}
|
|
|
|
.maturity-list::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
|
|
.maturity-list::-webkit-scrollbar-track {
|
|
background: var(--color-bg-secondary, #f8f9fa);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.maturity-list::-webkit-scrollbar-thumb {
|
|
background: var(--color-border);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* Maturity Items */
|
|
.maturity-item {
|
|
padding: var(--space-sm, 0.5rem);
|
|
background: var(--color-bg);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-sm, 4px);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.maturity-item:hover {
|
|
background: var(--color-bg-secondary, #f8f9fa);
|
|
border-color: var(--color-primary);
|
|
}
|
|
|
|
.maturity-item.overdue {
|
|
border: 1px solid var(--color-border);
|
|
}
|
|
|
|
.maturity-item.critical {
|
|
border: 1px solid var(--color-border);
|
|
}
|
|
|
|
.item-info {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--space-xs, 0.25rem);
|
|
}
|
|
|
|
.client-name,
|
|
.supplier-name {
|
|
font-weight: var(--font-medium, 500);
|
|
color: var(--color-text);
|
|
font-size: var(--text-sm, 0.875rem);
|
|
}
|
|
|
|
.due-info {
|
|
font-size: var(--text-xs, 0.75rem);
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.overdue-days {
|
|
color: var(--color-text);
|
|
font-weight: var(--font-medium, 500);
|
|
}
|
|
|
|
.due-date {
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
/* Amount Bars */
|
|
.amount-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-sm, 0.5rem);
|
|
}
|
|
|
|
.bar-container {
|
|
flex: 1;
|
|
height: 8px;
|
|
background: var(--color-bg-secondary, #f8f9fa);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.bar-fill {
|
|
height: 100%;
|
|
border-radius: 4px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.clients-bar {
|
|
background: var(--color-primary);
|
|
}
|
|
|
|
.suppliers-bar {
|
|
background: var(--color-secondary, #6b7280);
|
|
}
|
|
|
|
.amount-value {
|
|
font-size: var(--text-xs, 0.75rem);
|
|
font-weight: var(--font-bold, 700);
|
|
color: var(--color-text);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Empty State */
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: var(--space-xl, 2rem);
|
|
text-align: center;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
/* Balance Indicator */
|
|
.balance-indicator {
|
|
padding: var(--space-lg, 1rem) 0;
|
|
border-top: 1px solid var(--color-border);
|
|
background: transparent;
|
|
}
|
|
|
|
.balance-content {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-md, 1rem);
|
|
}
|
|
|
|
.balance-text {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-xs, 0.25rem);
|
|
}
|
|
|
|
.balance-label {
|
|
font-size: var(--text-sm, 0.875rem);
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.balance-amount {
|
|
font-size: var(--text-lg, 1.125rem);
|
|
font-weight: var(--font-bold, 700);
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.balance-amount.surplus {
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.balance-amount.deficit {
|
|
color: var(--color-text);
|
|
}
|
|
|
|
/* Recommendations */
|
|
.recommendations {
|
|
margin-left: auto;
|
|
}
|
|
|
|
.recommendations details {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.recommendations summary {
|
|
font-size: var(--text-sm, 0.875rem);
|
|
color: var(--color-primary);
|
|
list-style: none;
|
|
padding: 0.5rem;
|
|
border-radius: var(--radius-sm, 4px);
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.recommendations summary:hover {
|
|
background: rgba(59, 130, 246, 0.1);
|
|
}
|
|
|
|
.recommendations ul {
|
|
margin: var(--space-sm, 0.5rem) 0 0 0;
|
|
padding-left: var(--space-lg, 1rem);
|
|
}
|
|
|
|
.recommendations li {
|
|
font-size: var(--text-sm, 0.875rem);
|
|
color: var(--color-text-secondary);
|
|
margin-bottom: var(--space-xs, 0.25rem);
|
|
}
|
|
|
|
/* Card Footer */
|
|
.card-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: var(--space-md, 0.75rem) var(--space-lg, 1rem);
|
|
border-top: 1px solid var(--color-border);
|
|
background: var(--color-bg);
|
|
}
|
|
|
|
.period-info,
|
|
.last-updated {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-xs, 0.25rem);
|
|
font-size: var(--text-xs, 0.75rem);
|
|
}
|
|
|
|
.period-label,
|
|
.update-label {
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.period-value,
|
|
.update-time {
|
|
color: var(--color-text);
|
|
font-weight: var(--font-medium, 500);
|
|
}
|
|
|
|
.refresh-btn {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 0.25rem;
|
|
margin-left: var(--space-sm, 0.5rem);
|
|
border-radius: var(--radius-sm, 4px);
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.refresh-btn:hover {
|
|
background: var(--color-bg-secondary, #f8f9fa);
|
|
}
|
|
|
|
.refresh-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.refresh-icon {
|
|
display: inline-block;
|
|
font-size: var(--text-sm, 0.875rem);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.refresh-icon.spinning {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
/* Animations */
|
|
@keyframes spin {
|
|
from {
|
|
transform: rotate(0deg);
|
|
}
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 1024px) {
|
|
.maturity-comparison {
|
|
grid-template-columns: 1fr;
|
|
gap: var(--space-md, 1rem);
|
|
}
|
|
|
|
.comparison-divider {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.card-header {
|
|
flex-direction: column;
|
|
gap: var(--space-sm, 0.5rem);
|
|
align-items: stretch;
|
|
}
|
|
|
|
.card-header h3 {
|
|
text-align: center;
|
|
font-size: var(--text-base, 1rem);
|
|
}
|
|
|
|
.period-selector {
|
|
width: 100%;
|
|
}
|
|
|
|
.maturity-comparison {
|
|
padding: var(--space-md, 0.75rem);
|
|
}
|
|
|
|
.balance-content {
|
|
flex-direction: column;
|
|
text-align: center;
|
|
}
|
|
|
|
.recommendations {
|
|
margin-left: 0;
|
|
width: 100%;
|
|
}
|
|
|
|
.card-footer {
|
|
flex-direction: column;
|
|
gap: var(--space-sm, 0.5rem);
|
|
align-items: center;
|
|
}
|
|
|
|
.side-title {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: var(--space-xs, 0.25rem);
|
|
}
|
|
|
|
.total-amount {
|
|
align-self: flex-end;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.maturity-card {
|
|
margin: 0 -var(--space-sm, 0.5rem);
|
|
}
|
|
|
|
.card-header,
|
|
.maturity-comparison,
|
|
.balance-indicator,
|
|
.card-footer {
|
|
padding: var(--space-md, 0.75rem);
|
|
}
|
|
|
|
.maturity-list {
|
|
max-height: 200px;
|
|
}
|
|
}
|
|
</style>
|