Implemented by Ralph autonomous loop. Iteration: 10 Co-Authored-By: Claude <noreply@anthropic.com>
1287 lines
36 KiB
Vue
1287 lines
36 KiB
Vue
<template>
|
|
<main class="main-content">
|
|
<div class="app-container">
|
|
<!-- Dashboard Header -->
|
|
<div class="page-header">
|
|
<h1 class="page-title">Dashboard</h1>
|
|
</div>
|
|
|
|
<!-- Company selection removed - now handled in header only -->
|
|
|
|
<!-- Secțiune Carduri Noi - Adăugare -->
|
|
<div class="metrics-cards-section" v-if="!isLoading">
|
|
<!-- Mobile: Swipeable KPI Cards Carousel -->
|
|
<SwipeableCards v-if="isMobile" :totalCards="4" class="mobile-kpi-carousel">
|
|
<template #card-0>
|
|
<TreasuryDualCard
|
|
:casaTotal="treasuryData?.breakdown?.casa?.total || 0"
|
|
:bancaTotal="treasuryData?.breakdown?.banca?.total || 0"
|
|
:casaItems="treasuryData?.breakdown?.casa?.items || []"
|
|
:bancaItems="treasuryData?.breakdown?.banca?.items || []"
|
|
:casaTrend="casaTrend"
|
|
:bancaTrend="bancaTrend"
|
|
:casaSparklineData="casaSparkline"
|
|
:bancaSparklineData="bancaSparkline"
|
|
:casaPreviousSparklineData="casaPreviousSparkline"
|
|
:bancaPreviousSparklineData="bancaPreviousSparkline"
|
|
:sparklineLabels="sparklineLabels"
|
|
:previousSparklineLabels="previousSparklineLabels"
|
|
/>
|
|
</template>
|
|
<template #card-1>
|
|
<CashFlowMetricCard
|
|
:inflowsValue="monthlyInflows"
|
|
:outflowsValue="monthlyOutflows"
|
|
:inflowsTrend="inflowsTrend"
|
|
:outflowsTrend="outflowsTrend"
|
|
:inflowsSparkline="inflowsSparkline"
|
|
:outflowsSparkline="outflowsSparkline"
|
|
:inflowsPreviousSparkline="inflowsPreviousSparkline"
|
|
:outflowsPreviousSparkline="outflowsPreviousSparkline"
|
|
:sparklineLabels="sparklineLabels"
|
|
:previousSparklineLabels="previousSparklineLabels"
|
|
/>
|
|
</template>
|
|
<template #card-2>
|
|
<ClientiBalanceCard
|
|
:total="netBalanceData?.clienti_total || 0"
|
|
:trend="clientiTrend"
|
|
:sparklineData="clientiSparkline"
|
|
:previousSparklineData="clientiPreviousSparkline"
|
|
:sparklineLabels="sparklineLabels"
|
|
:previousSparklineLabels="previousSparklineLabels"
|
|
:breakdown="netBalanceData?.breakdown?.clienti"
|
|
/>
|
|
</template>
|
|
<template #card-3>
|
|
<FurnizoriBalanceCard
|
|
:total="netBalanceData?.furnizori_total || 0"
|
|
:trend="furnizoriTrend"
|
|
:sparklineData="furnizoriSparkline"
|
|
:previousSparklineData="furnizoriPreviousSparkline"
|
|
:sparklineLabels="sparklineLabels"
|
|
:previousSparklineLabels="previousSparklineLabels"
|
|
:breakdown="netBalanceData?.breakdown?.furnizori"
|
|
/>
|
|
</template>
|
|
</SwipeableCards>
|
|
|
|
<!-- Desktop: Grid layout -->
|
|
<div v-else class="metrics-row">
|
|
<TreasuryDualCard
|
|
:casaTotal="treasuryData?.breakdown?.casa?.total || 0"
|
|
:bancaTotal="treasuryData?.breakdown?.banca?.total || 0"
|
|
:casaItems="treasuryData?.breakdown?.casa?.items || []"
|
|
:bancaItems="treasuryData?.breakdown?.banca?.items || []"
|
|
:casaTrend="casaTrend"
|
|
:bancaTrend="bancaTrend"
|
|
:casaSparklineData="casaSparkline"
|
|
:bancaSparklineData="bancaSparkline"
|
|
:casaPreviousSparklineData="casaPreviousSparkline"
|
|
:bancaPreviousSparklineData="bancaPreviousSparkline"
|
|
:sparklineLabels="sparklineLabels"
|
|
:previousSparklineLabels="previousSparklineLabels"
|
|
/>
|
|
<CashFlowMetricCard
|
|
:inflowsValue="monthlyInflows"
|
|
:outflowsValue="monthlyOutflows"
|
|
:inflowsTrend="inflowsTrend"
|
|
:outflowsTrend="outflowsTrend"
|
|
:inflowsSparkline="inflowsSparkline"
|
|
:outflowsSparkline="outflowsSparkline"
|
|
:inflowsPreviousSparkline="inflowsPreviousSparkline"
|
|
:outflowsPreviousSparkline="outflowsPreviousSparkline"
|
|
:sparklineLabels="sparklineLabels"
|
|
:previousSparklineLabels="previousSparklineLabels"
|
|
/>
|
|
<ClientiBalanceCard
|
|
:total="netBalanceData?.clienti_total || 0"
|
|
:trend="clientiTrend"
|
|
:sparklineData="clientiSparkline"
|
|
:previousSparklineData="clientiPreviousSparkline"
|
|
:sparklineLabels="sparklineLabels"
|
|
:previousSparklineLabels="previousSparklineLabels"
|
|
:breakdown="netBalanceData?.breakdown?.clienti"
|
|
/>
|
|
<FurnizoriBalanceCard
|
|
:total="netBalanceData?.furnizori_total || 0"
|
|
:trend="furnizoriTrend"
|
|
:sparklineData="furnizoriSparkline"
|
|
:previousSparklineData="furnizoriPreviousSparkline"
|
|
:sparklineLabels="sparklineLabels"
|
|
:previousSparklineLabels="previousSparklineLabels"
|
|
:breakdown="netBalanceData?.breakdown?.furnizori"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Rând 2: Analiză comparativă și Date Detaliate (combinat) -->
|
|
<div class="comparison-row">
|
|
<MaturityAndDetailsCard
|
|
:companyId="companyStore.selectedCompany?.id_firma"
|
|
@periodChanged="handleMaturityPeriodChange"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dashboard Content -->
|
|
<div class="dashboard-content">
|
|
<!-- Componenta MaturityAndDetailsCard include acum și tabelul detaliat -->
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="isLoading" class="loading-state">
|
|
<div class="loading-spinner"></div>
|
|
<p>Se încarcă datele dashboard-ului...</p>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
|
import { useToast } from "primevue/usetoast";
|
|
// Import componente noi
|
|
import MetricCard from "@reports/components/dashboard/cards/MetricCard.vue";
|
|
import CashFlowMetricCard from "@reports/components/dashboard/cards/CashFlowMetricCard.vue";
|
|
import MaturityAndDetailsCard from "@reports/components/dashboard/cards/MaturityAndDetailsCard.vue";
|
|
import ClientiBalanceCard from "@reports/components/dashboard/cards/ClientiBalanceCard.vue";
|
|
import FurnizoriBalanceCard from "@reports/components/dashboard/cards/FurnizoriBalanceCard.vue";
|
|
import TreasuryDualCard from "@reports/components/dashboard/cards/TreasuryDualCard.vue";
|
|
// Mobile carousel component
|
|
import SwipeableCards from "@shared/components/mobile/SwipeableCards.vue";
|
|
import { useCompanyStore } from "@reports/stores/sharedStores";
|
|
import { useDashboardStore } from "@reports/stores/dashboard";
|
|
import { useAccountingPeriodStore } from "@reports/stores/sharedStores";
|
|
import api from "@reports/services/api";
|
|
import {
|
|
exportToExcel,
|
|
exportToPDF,
|
|
exportTrendData as prepareTrendData,
|
|
} from "@reports/utils/exportUtils";
|
|
|
|
const toast = useToast();
|
|
const companyStore = useCompanyStore();
|
|
const dashboardStore = useDashboardStore();
|
|
const periodStore = useAccountingPeriodStore();
|
|
|
|
// State
|
|
const filteredCompanies = ref([]);
|
|
const isLoading = ref(false);
|
|
|
|
// State pentru carduri
|
|
const monthlyInflows = ref(0);
|
|
const monthlyOutflows = ref(0);
|
|
const treasuryData = ref(null);
|
|
const netBalanceData = ref(null);
|
|
|
|
// New dashboard state
|
|
const selectedPeriod = ref("12m");
|
|
const selectedChartType = ref("line");
|
|
|
|
// Handlers pentru schimbare perioadă
|
|
const handleMaturityPeriodChange = (period) => {
|
|
console.log("Maturity period changed:", period);
|
|
// Trigger reload cu noua perioadă
|
|
};
|
|
|
|
// Calculare trend bazată pe date reale din trends.raw
|
|
const calculateTrend = (metric) => {
|
|
if (!dashboardStore.trends?.raw) return null;
|
|
|
|
const raw = dashboardStore.trends.raw;
|
|
let data = [];
|
|
|
|
// Selectează datele corecte pentru fiecare metric
|
|
switch (metric) {
|
|
case "clienti":
|
|
data = raw.clienti_sold || [];
|
|
break;
|
|
case "furnizori":
|
|
data = raw.furnizori_sold || [];
|
|
break;
|
|
case "treasury":
|
|
data = raw.trezorerie_sold || [];
|
|
break;
|
|
case "sold":
|
|
// Sold net = clienti_sold - furnizori_sold
|
|
if (raw.clienti_sold?.length && raw.furnizori_sold?.length) {
|
|
data = raw.clienti_sold.map(
|
|
(c, i) => Number(c || 0) - Number(raw.furnizori_sold[i] || 0),
|
|
);
|
|
}
|
|
break;
|
|
case "inflows":
|
|
data = raw.clienti_incasat || [];
|
|
break;
|
|
case "outflows":
|
|
data = raw.furnizori_achitat || [];
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
// Trebuie să avem cel puțin 2 puncte de date pentru a calcula trend
|
|
if (!data || data.length < 2) return null;
|
|
|
|
const current = Number(data[data.length - 1]) || 0;
|
|
const previous = Number(data[data.length - 2]) || 0;
|
|
|
|
// Handle edge case: division by zero
|
|
if (previous === 0) {
|
|
return current > 0
|
|
? { value: 100, direction: "up" }
|
|
: current < 0
|
|
? { value: 100, direction: "down" }
|
|
: { value: 0, direction: "neutral" };
|
|
}
|
|
|
|
// Calculate percentage change
|
|
const change = ((current - previous) / Math.abs(previous)) * 100;
|
|
const direction = change > 0.1 ? "up" : change < -0.1 ? "down" : "neutral";
|
|
|
|
return { value: Math.abs(change), direction };
|
|
};
|
|
|
|
// Obține date sparkline pentru mini grafice
|
|
const getSparklineData = (metric) => {
|
|
if (!dashboardStore.trends?.raw) {
|
|
return [];
|
|
}
|
|
|
|
const raw = dashboardStore.trends.raw;
|
|
let data = [];
|
|
|
|
switch (metric) {
|
|
case "clienti":
|
|
data = raw.clienti_sold || [];
|
|
break;
|
|
case "furnizori":
|
|
data = raw.furnizori_sold || [];
|
|
break;
|
|
case "treasury":
|
|
data = raw.trezorerie_sold || [];
|
|
break;
|
|
case "sold":
|
|
// Sold net = clienti_sold - furnizori_sold
|
|
if (raw.clienti_sold?.length && raw.furnizori_sold?.length) {
|
|
data = raw.clienti_sold.map(
|
|
(c, i) => Number(c || 0) - Number(raw.furnizori_sold[i] || 0),
|
|
);
|
|
}
|
|
break;
|
|
case "inflows":
|
|
data = raw.clienti_incasat || [];
|
|
break;
|
|
case "outflows":
|
|
data = raw.furnizori_achitat || [];
|
|
break;
|
|
default:
|
|
return [];
|
|
}
|
|
|
|
// Returnează ultimele 12 valori pentru sparkline
|
|
const sparklineData = data.slice(-12).map((v) => Number(v) || 0);
|
|
return sparklineData;
|
|
};
|
|
|
|
// Obține labels pentru sparkline (ultimele 12 perioade)
|
|
const getSparklineLabels = () => {
|
|
if (!dashboardStore.trends?.raw?.periods) {
|
|
return [];
|
|
}
|
|
|
|
const periods = dashboardStore.trends.raw.periods;
|
|
// Returnează ultimele 12 perioade, formatate scurt (MM/YY)
|
|
return periods.slice(-12).map((period) => {
|
|
const [year, month] = period.split("-");
|
|
return `${month}/${year.slice(-2)}`; // Format: MM/YY
|
|
});
|
|
};
|
|
|
|
// Obține date sparkline pentru perioada precedentă (year-over-year comparison)
|
|
const getPreviousSparklineData = (metric) => {
|
|
if (!dashboardStore.trends?.raw) {
|
|
return [];
|
|
}
|
|
|
|
const raw = dashboardStore.trends.raw;
|
|
let data = [];
|
|
|
|
switch (metric) {
|
|
case "clienti":
|
|
data = raw.clienti_sold_prev || [];
|
|
break;
|
|
case "furnizori":
|
|
data = raw.furnizori_sold_prev || [];
|
|
break;
|
|
case "treasury":
|
|
data = raw.trezorerie_sold_prev || [];
|
|
break;
|
|
case "inflows":
|
|
data = raw.clienti_incasat_prev || [];
|
|
break;
|
|
case "outflows":
|
|
data = raw.furnizori_achitat_prev || [];
|
|
break;
|
|
default:
|
|
return [];
|
|
}
|
|
|
|
// Returnează ultimele 12 valori pentru sparkline (aceeași perioadă, anul anterior)
|
|
const sparklineData = data.slice(-12).map((v) => Number(v) || 0);
|
|
return sparklineData;
|
|
};
|
|
|
|
// Obține labels pentru sparkline perioada precedentă
|
|
const getPreviousSparklineLabels = () => {
|
|
if (!dashboardStore.trends?.raw?.previous_periods) {
|
|
return [];
|
|
}
|
|
|
|
const periods = dashboardStore.trends.raw.previous_periods;
|
|
// Returnează ultimele 12 perioade precedente, formatate scurt (MM/YY)
|
|
return periods.slice(-12).map((period) => {
|
|
const [year, month] = period.split("-");
|
|
return `${month}/${year.slice(-2)}`; // Format: MM/YY
|
|
});
|
|
};
|
|
|
|
// Computed properties pentru carduri - REACTIVE!
|
|
const clientiTrend = computed(() => calculateTrend("clienti"));
|
|
const clientiSparkline = computed(() => getSparklineData("clienti"));
|
|
const clientiPreviousSparkline = computed(() =>
|
|
getPreviousSparklineData("clienti"),
|
|
);
|
|
|
|
const furnizoriTrend = computed(() => calculateTrend("furnizori"));
|
|
const furnizoriSparkline = computed(() => getSparklineData("furnizori"));
|
|
const furnizoriPreviousSparkline = computed(() =>
|
|
getPreviousSparklineData("furnizori"),
|
|
);
|
|
|
|
const soldTrend = computed(() => calculateTrend("sold"));
|
|
const soldSparkline = computed(() => getSparklineData("sold"));
|
|
const inflowsTrend = computed(() => calculateTrend("inflows"));
|
|
const inflowsSparkline = computed(() => getSparklineData("inflows"));
|
|
const inflowsPreviousSparkline = computed(() =>
|
|
getPreviousSparklineData("inflows"),
|
|
);
|
|
const outflowsTrend = computed(() => calculateTrend("outflows"));
|
|
const outflowsSparkline = computed(() => getSparklineData("outflows"));
|
|
const outflowsPreviousSparkline = computed(() =>
|
|
getPreviousSparklineData("outflows"),
|
|
);
|
|
const treasuryTrend = computed(() => calculateTrend("treasury"));
|
|
const treasurySparkline = computed(() => getSparklineData("treasury"));
|
|
const treasuryPreviousSparkline = computed(() =>
|
|
getPreviousSparklineData("treasury"),
|
|
);
|
|
const sparklineLabels = computed(() => getSparklineLabels());
|
|
const previousSparklineLabels = computed(() => getPreviousSparklineLabels());
|
|
|
|
// Casa and Bancă specific trends and sparklines
|
|
const casaTrend = computed(() => {
|
|
// Calculate trend based on Casa proportion of treasury
|
|
if (!treasuryData.value?.breakdown) return null;
|
|
|
|
const casaTotal = treasuryData.value.breakdown.casa?.total || 0;
|
|
const bancaTotal = treasuryData.value.breakdown.banca?.total || 0;
|
|
const totalTreasury = casaTotal + bancaTotal;
|
|
|
|
if (totalTreasury === 0) return null;
|
|
|
|
// Use treasury trend as base, since we don't have separate casa trend data
|
|
const tTrend = calculateTrend("treasury");
|
|
return tTrend ? { ...tTrend } : null;
|
|
});
|
|
|
|
const casaSparkline = computed(() => {
|
|
// Use treasury sparkline data scaled by casa proportion
|
|
if (!treasuryData.value?.breakdown) return [];
|
|
|
|
const sparklineData = getSparklineData("treasury");
|
|
if (!sparklineData.length) return [];
|
|
|
|
const casaTotal = treasuryData.value.breakdown.casa?.total || 0;
|
|
const bancaTotal = treasuryData.value.breakdown.banca?.total || 0;
|
|
const totalTreasury = casaTotal + bancaTotal;
|
|
|
|
if (totalTreasury === 0) return sparklineData.map(() => 0);
|
|
|
|
const casaProportion = casaTotal / totalTreasury;
|
|
return sparklineData.map((v) => v * casaProportion);
|
|
});
|
|
|
|
const bancaTrend = computed(() => {
|
|
// Calculate trend based on Bancă proportion of treasury
|
|
if (!treasuryData.value?.breakdown) return null;
|
|
|
|
const casaTotal = treasuryData.value.breakdown.casa?.total || 0;
|
|
const bancaTotal = treasuryData.value.breakdown.banca?.total || 0;
|
|
const totalTreasury = casaTotal + bancaTotal;
|
|
|
|
if (totalTreasury === 0) return null;
|
|
|
|
// Use treasury trend as base, since we don't have separate banca trend data
|
|
const tTrend = calculateTrend("treasury");
|
|
return tTrend ? { ...tTrend } : null;
|
|
});
|
|
|
|
const bancaSparkline = computed(() => {
|
|
// Use treasury sparkline data scaled by banca proportion
|
|
if (!treasuryData.value?.breakdown) return [];
|
|
|
|
const sparklineData = getSparklineData("treasury");
|
|
if (!sparklineData.length) return [];
|
|
|
|
const casaTotal = treasuryData.value.breakdown.casa?.total || 0;
|
|
const bancaTotal = treasuryData.value.breakdown.banca?.total || 0;
|
|
const totalTreasury = casaTotal + bancaTotal;
|
|
|
|
if (totalTreasury === 0) return sparklineData.map(() => 0);
|
|
|
|
const bancaProportion = bancaTotal / totalTreasury;
|
|
return sparklineData.map((v) => v * bancaProportion);
|
|
});
|
|
|
|
// Previous year sparklines for Casa and Bancă
|
|
const casaPreviousSparkline = computed(() => {
|
|
if (!treasuryData.value?.breakdown) return [];
|
|
|
|
const previousSparklineData = getPreviousSparklineData("treasury");
|
|
if (!previousSparklineData.length) return [];
|
|
|
|
const casaTotal = treasuryData.value.breakdown.casa?.total || 0;
|
|
const bancaTotal = treasuryData.value.breakdown.banca?.total || 0;
|
|
const totalTreasury = casaTotal + bancaTotal;
|
|
|
|
if (totalTreasury === 0) return previousSparklineData.map(() => 0);
|
|
|
|
const casaProportion = casaTotal / totalTreasury;
|
|
return previousSparklineData.map((v) => v * casaProportion);
|
|
});
|
|
|
|
const bancaPreviousSparkline = computed(() => {
|
|
if (!treasuryData.value?.breakdown) return [];
|
|
|
|
const previousSparklineData = getPreviousSparklineData("treasury");
|
|
if (!previousSparklineData.length) return [];
|
|
|
|
const casaTotal = treasuryData.value.breakdown.casa?.total || 0;
|
|
const bancaTotal = treasuryData.value.breakdown.banca?.total || 0;
|
|
const totalTreasury = casaTotal + bancaTotal;
|
|
|
|
if (totalTreasury === 0) return previousSparklineData.map(() => 0);
|
|
|
|
const bancaProportion = bancaTotal / totalTreasury;
|
|
return previousSparklineData.map((v) => v * bancaProportion);
|
|
});
|
|
|
|
// Detectare mobile - reactive with resize listener
|
|
const windowWidth = ref(window.innerWidth);
|
|
const isMobile = computed(() => windowWidth.value < 768);
|
|
|
|
// Handle window resize for mobile detection
|
|
const handleResize = () => {
|
|
windowWidth.value = window.innerWidth;
|
|
};
|
|
|
|
// Computed property pentru luna curentă - folosește perioada din period selector
|
|
const currentMonthLabel = computed(() => {
|
|
// Prioritate: period selector > dashboard current period > loading
|
|
if (periodStore.selectedPeriod) {
|
|
const { an, luna } = periodStore.selectedPeriod;
|
|
const date = new Date(an, luna - 1, 1);
|
|
return date.toLocaleDateString("ro-RO", { month: "long", year: "numeric" });
|
|
}
|
|
|
|
if (dashboardStore.currentPeriod) {
|
|
const { year, month } = dashboardStore.currentPeriod;
|
|
const date = new Date(year, month - 1, 1);
|
|
return date.toLocaleDateString("ro-RO", { month: "long", year: "numeric" });
|
|
}
|
|
|
|
return "Se încarcă...";
|
|
});
|
|
|
|
// Methods
|
|
const handleCompanyChanged = async (company) => {
|
|
if (company) {
|
|
companyStore.setSelectedCompany(company);
|
|
await loadDashboardData();
|
|
}
|
|
};
|
|
|
|
const formatCurrency = (amount) => {
|
|
if (!amount && amount !== 0) return "0,00 RON";
|
|
const numAmount = typeof amount === "string" ? parseFloat(amount) : amount;
|
|
if (isNaN(numAmount)) return "0,00 RON";
|
|
|
|
try {
|
|
return new Intl.NumberFormat("ro-RO", {
|
|
style: "currency",
|
|
currency: "RON",
|
|
}).format(numAmount);
|
|
} catch (error) {
|
|
return `${numAmount.toLocaleString("ro-RO", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} RON`;
|
|
}
|
|
};
|
|
|
|
const getBalanceClass = (amount) => {
|
|
if (!amount && amount !== 0) return "neutral";
|
|
const numAmount = typeof amount === "string" ? parseFloat(amount) : amount;
|
|
return numAmount > 0 ? "positive" : numAmount < 0 ? "negative" : "neutral";
|
|
};
|
|
|
|
// Trend methods
|
|
const loadTrendData = async () => {
|
|
if (!companyStore.selectedCompany) {
|
|
console.warn("No company selected for trend data loading");
|
|
return;
|
|
}
|
|
|
|
const luna = periodStore.selectedPeriod?.luna || null;
|
|
const an = periodStore.selectedPeriod?.an || null;
|
|
|
|
console.log(
|
|
"Loading trend data for company:",
|
|
companyStore.selectedCompany.id_firma,
|
|
"luna:", luna, "an:", an,
|
|
);
|
|
|
|
const result = await dashboardStore.loadTrendData(
|
|
companyStore.selectedCompany.id_firma,
|
|
selectedPeriod.value,
|
|
selectedChartType.value,
|
|
luna,
|
|
an,
|
|
);
|
|
|
|
if (result.success) {
|
|
console.log("Trend data loaded successfully:", result.data);
|
|
// Toast notification removed - it was showing on every dashboard refresh
|
|
} else {
|
|
console.error("Failed to load trend data:", result.error);
|
|
toast.add({
|
|
severity: "error",
|
|
summary: "Eroare la încărcarea datelor",
|
|
detail: result.error || "Nu s-au putut încărca datele de trend",
|
|
life: 4000,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Export Trend Data to Excel
|
|
const exportTrendExcel = () => {
|
|
const data = prepareTrendData(
|
|
dashboardStore.trends,
|
|
selectedPeriod.value,
|
|
selectedChartType.value,
|
|
);
|
|
const result = exportToExcel(data, "date_trend", "Date Trend");
|
|
|
|
if (result.success) {
|
|
toast.add({
|
|
severity: "success",
|
|
summary: "Export Reușit",
|
|
detail: "Fișier Excel generat cu succes",
|
|
life: 3000,
|
|
});
|
|
} else {
|
|
toast.add({
|
|
severity: "error",
|
|
summary: "Eroare Export",
|
|
detail: "Nu s-a putut genera fișierul Excel",
|
|
life: 3000,
|
|
});
|
|
}
|
|
};
|
|
|
|
// Export Trend Data to PDF
|
|
const exportTrendPDF = () => {
|
|
const data = prepareTrendData(
|
|
dashboardStore.trends,
|
|
selectedPeriod.value,
|
|
selectedChartType.value,
|
|
);
|
|
|
|
if (!data || data.length === 0) {
|
|
toast.add({
|
|
severity: "warn",
|
|
summary: "Nu există date",
|
|
detail: "Nu există date de trend pentru export",
|
|
life: 3000,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Generate columns based on data
|
|
const columns = Object.keys(data[0]).map((key) => ({
|
|
field: key,
|
|
header: key,
|
|
type: key === "Perioada" ? "text" : "currency",
|
|
}));
|
|
|
|
const result = exportToPDF(
|
|
data,
|
|
columns,
|
|
"date_trend",
|
|
`Date Trend (${selectedPeriod.value}) - ${companyStore.selectedCompany?.name || "ROA Reports"}`,
|
|
);
|
|
|
|
if (result.success) {
|
|
toast.add({
|
|
severity: "success",
|
|
summary: "Export Reușit",
|
|
detail: "Fișier PDF generat cu succes",
|
|
life: 3000,
|
|
});
|
|
}
|
|
};
|
|
|
|
const refreshTrendData = async () => {
|
|
await loadTrendData();
|
|
toast.add({
|
|
severity: "success",
|
|
summary: "Actualizat",
|
|
detail: "Datele de trend au fost actualizate",
|
|
life: 2000,
|
|
});
|
|
};
|
|
|
|
const getLastTrendValue = (datasetLabel) => {
|
|
if (!dashboardStore.trends?.datasets) {
|
|
console.warn("No trends data available for", datasetLabel);
|
|
return 0;
|
|
}
|
|
|
|
const dataset = dashboardStore.trends.datasets.find(
|
|
(d) => d.label === datasetLabel,
|
|
);
|
|
if (!dataset?.data?.length) {
|
|
console.warn("No dataset or data found for", datasetLabel);
|
|
return 0;
|
|
}
|
|
|
|
const lastValue = dataset.data[dataset.data.length - 1];
|
|
return typeof lastValue === "number" ? lastValue : 0;
|
|
};
|
|
|
|
const getTrendChange = (datasetLabel) => {
|
|
if (!dashboardStore.trends?.datasets) {
|
|
console.warn("No trends data available for trend change calculation");
|
|
return "0%";
|
|
}
|
|
|
|
const dataset = dashboardStore.trends.datasets.find(
|
|
(d) => d.label === datasetLabel,
|
|
);
|
|
if (!dataset?.data?.length || dataset.data.length < 2) {
|
|
console.warn(
|
|
"Insufficient data points for trend change calculation:",
|
|
datasetLabel,
|
|
);
|
|
return "0%";
|
|
}
|
|
|
|
const current = Number(dataset.data[dataset.data.length - 1]) || 0;
|
|
const previous = Number(dataset.data[dataset.data.length - 2]) || 0;
|
|
|
|
// Handle edge cases
|
|
if (previous === 0 && current === 0) return "0%";
|
|
if (previous === 0) return current > 0 ? "+∞%" : "-∞%";
|
|
|
|
const change = ((current - previous) / Math.abs(previous)) * 100;
|
|
|
|
// Handle very large changes
|
|
if (!isFinite(change)) return "0%";
|
|
|
|
const sign = change > 0 ? "+" : "";
|
|
return `${sign}${change.toFixed(1)}%`;
|
|
};
|
|
|
|
const getTrendChangeClass = (datasetLabel) => {
|
|
const changeStr = getTrendChange(datasetLabel);
|
|
|
|
// Handle infinite cases
|
|
if (changeStr.includes("∞")) return "neutral";
|
|
|
|
const change = parseFloat(changeStr.replace("%", ""));
|
|
|
|
// Handle NaN cases
|
|
if (isNaN(change)) return "neutral";
|
|
|
|
// For suppliers, negative change is good (less debt)
|
|
if (datasetLabel === "Furnizori - Sold Net") {
|
|
if (Math.abs(change) < 0.1) return "neutral"; // Very small changes
|
|
if (change > 0) return "negative"; // More debt = bad
|
|
if (change < 0) return "positive"; // Less debt = good
|
|
return "neutral";
|
|
}
|
|
|
|
// For clients and treasury, positive change is good
|
|
if (Math.abs(change) < 0.1) return "neutral"; // Very small changes
|
|
if (change > 0) return "positive";
|
|
if (change < 0) return "negative";
|
|
return "neutral";
|
|
};
|
|
|
|
const searchCompanies = (event) => {
|
|
// Handle undefined or null query
|
|
const query = (event?.query || "").toLowerCase();
|
|
|
|
// Ensure companyListFormatted exists and has valid data
|
|
if (
|
|
!companyStore.companyListFormatted ||
|
|
companyStore.companyListFormatted.length === 0
|
|
) {
|
|
filteredCompanies.value = [];
|
|
return;
|
|
}
|
|
|
|
filteredCompanies.value = companyStore.companyListFormatted.filter(
|
|
(company) => {
|
|
// Ensure displayName exists before using includes
|
|
const displayName = company?.displayName || "";
|
|
return displayName.toLowerCase().includes(query);
|
|
},
|
|
);
|
|
};
|
|
|
|
const handleCompanySelect = async (event) => {
|
|
const selectedCompany = event.value;
|
|
if (selectedCompany && selectedCompany.id_firma) {
|
|
const company = companyStore.getCompanyById(selectedCompany.id_firma);
|
|
if (company) {
|
|
companyStore.setSelectedCompany(company);
|
|
await loadDashboardData();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Fixed: Changed company_id to company parameter
|
|
// Updated: Added luna/an from period selector
|
|
const loadMonthlyFlows = async () => {
|
|
if (!companyStore.selectedCompany) return;
|
|
|
|
try {
|
|
const params = { company: companyStore.selectedCompany.id_firma };
|
|
if (periodStore.selectedPeriod) {
|
|
params.luna = periodStore.selectedPeriod.luna;
|
|
params.an = periodStore.selectedPeriod.an;
|
|
}
|
|
|
|
const response = await api.get("/dashboard/monthly-flows", { params });
|
|
monthlyInflows.value = response.data.inflows || 0;
|
|
monthlyOutflows.value = response.data.outflows || 0;
|
|
} catch (error) {
|
|
console.error("Failed to load monthly flows:", error);
|
|
}
|
|
};
|
|
|
|
const loadTreasuryBreakdown = async () => {
|
|
if (!companyStore.selectedCompany) return;
|
|
|
|
try {
|
|
const params = { company: companyStore.selectedCompany.id_firma };
|
|
if (periodStore.selectedPeriod) {
|
|
params.luna = periodStore.selectedPeriod.luna;
|
|
params.an = periodStore.selectedPeriod.an;
|
|
}
|
|
|
|
const response = await api.get("/dashboard/treasury-breakdown", { params });
|
|
treasuryData.value = response.data;
|
|
} catch (error) {
|
|
console.error("Failed to load treasury breakdown:", error);
|
|
}
|
|
};
|
|
|
|
const loadNetBalanceBreakdown = async () => {
|
|
if (!companyStore.selectedCompany) return;
|
|
|
|
try {
|
|
const params = { company: companyStore.selectedCompany.id_firma };
|
|
if (periodStore.selectedPeriod) {
|
|
params.luna = periodStore.selectedPeriod.luna;
|
|
params.an = periodStore.selectedPeriod.an;
|
|
}
|
|
|
|
const response = await api.get("/dashboard/net-balance-breakdown", { params });
|
|
|
|
// Folosește direct datele structurate de la backend
|
|
netBalanceData.value = {
|
|
clienti_total: response.data.clienti_total || 0,
|
|
furnizori_total: response.data.furnizori_total || 0,
|
|
breakdown: response.data.breakdown || {
|
|
clienti: {
|
|
total: 0,
|
|
in_termen: { total: 0 },
|
|
restant: { total: 0, perioade: {} },
|
|
},
|
|
furnizori: {
|
|
total: 0,
|
|
in_termen: { total: 0 },
|
|
restant: { total: 0, perioade: {} },
|
|
},
|
|
},
|
|
};
|
|
|
|
console.log("[NetBalance] Loaded balance data:", {
|
|
clienti_total: netBalanceData.value.clienti_total,
|
|
furnizori_total: netBalanceData.value.furnizori_total,
|
|
breakdown: netBalanceData.value.breakdown,
|
|
});
|
|
} catch (error) {
|
|
console.error("Failed to load net balance breakdown:", error);
|
|
}
|
|
};
|
|
|
|
const loadDashboardData = async () => {
|
|
if (!companyStore.selectedCompany) return;
|
|
isLoading.value = true;
|
|
|
|
const luna = periodStore.selectedPeriod?.luna || null;
|
|
const an = periodStore.selectedPeriod?.an || null;
|
|
|
|
try {
|
|
await Promise.all([
|
|
dashboardStore.loadDashboardSummary(
|
|
companyStore.selectedCompany.id_firma,
|
|
luna,
|
|
an,
|
|
),
|
|
dashboardStore.loadCurrentPeriod(companyStore.selectedCompany.id_firma),
|
|
loadTrendData(),
|
|
loadMonthlyFlows(),
|
|
loadTreasuryBreakdown(),
|
|
loadNetBalanceBreakdown(),
|
|
]);
|
|
} catch (error) {
|
|
console.error("Failed to load dashboard data:", error);
|
|
toast.add({
|
|
severity: "error",
|
|
summary: "Error",
|
|
detail: "Nu s-au putut încărca datele dashboard-ului",
|
|
life: 3000,
|
|
});
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const refreshData = async () => {
|
|
await loadDashboardData();
|
|
// Toast notification removed - it was covering the company and user selectors
|
|
};
|
|
|
|
const exportData = () => {
|
|
// Export functionality can be implemented for different sections
|
|
console.log("Export data triggered");
|
|
};
|
|
|
|
const searchData = () => {
|
|
// Focus on the detail filter input
|
|
const filterInput = document.querySelector(".detail-input");
|
|
if (filterInput) {
|
|
filterInput.focus();
|
|
}
|
|
};
|
|
|
|
// Watch for company changes - reload dashboard when company changes
|
|
watch(
|
|
() => companyStore.selectedCompany,
|
|
async (newCompany) => {
|
|
if (newCompany) {
|
|
await loadDashboardData();
|
|
}
|
|
},
|
|
);
|
|
|
|
// Watch for period selector changes - reload dashboard when period changes
|
|
watch(
|
|
() => periodStore.selectedPeriod,
|
|
async (newPeriod, oldPeriod) => {
|
|
// Only reload if period actually changed and we have a company selected
|
|
if (companyStore.selectedCompany && newPeriod &&
|
|
(newPeriod.luna !== oldPeriod?.luna || newPeriod.an !== oldPeriod?.an)) {
|
|
console.log("Period changed, reloading dashboard:", newPeriod);
|
|
await loadDashboardData();
|
|
}
|
|
},
|
|
{ deep: true }
|
|
);
|
|
|
|
// Lifecycle
|
|
onMounted(async () => {
|
|
// Add resize listener for mobile detection
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
// Load companies first
|
|
if (!companyStore.hasCompanies) {
|
|
await companyStore.loadCompanies();
|
|
}
|
|
|
|
filteredCompanies.value = companyStore.companyListFormatted;
|
|
|
|
// Check for saved company and verify it exists in loaded companies
|
|
if (companyStore.selectedCompany) {
|
|
const exists = companyStore.getCompanyById(
|
|
companyStore.selectedCompany.id_firma,
|
|
);
|
|
if (exists) {
|
|
// Update with fresh company data from API
|
|
companyStore.setSelectedCompany(exists);
|
|
await loadDashboardData();
|
|
} else {
|
|
// Saved company no longer exists, clear it
|
|
companyStore.clearSelectedCompany();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Cleanup resize listener
|
|
onUnmounted(() => {
|
|
window.removeEventListener('resize', handleResize);
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Dashboard Styles - Using Global Patterns */
|
|
|
|
/* Company Selection */
|
|
.company-selection {
|
|
max-width: 500px;
|
|
margin: 0 auto var(--space-xl) auto;
|
|
}
|
|
|
|
.company-input {
|
|
width: 100%;
|
|
}
|
|
|
|
.no-companies {
|
|
text-align: center;
|
|
color: var(--color-text-secondary);
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Dashboard Content */
|
|
.dashboard-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-xl);
|
|
}
|
|
|
|
/* Dashboard Sections */
|
|
.dashboard-section {
|
|
background: var(--color-bg);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--card-radius);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: var(--space-lg) var(--space-xl);
|
|
background: var(--color-bg-secondary);
|
|
border-bottom: 1px solid var(--color-border);
|
|
flex-wrap: wrap;
|
|
gap: var(--space-md);
|
|
}
|
|
|
|
.section-title {
|
|
font-size: var(--text-xl);
|
|
font-weight: var(--font-semibold);
|
|
color: var(--color-text);
|
|
margin: 0;
|
|
}
|
|
|
|
.section-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-md);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.control-group {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-sm);
|
|
}
|
|
|
|
.control-group label {
|
|
font-size: var(--text-sm);
|
|
font-weight: var(--font-medium);
|
|
color: var(--color-text-secondary);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.detail-select,
|
|
.detail-input,
|
|
.trend-select {
|
|
padding: var(--space-xs) var(--space-sm);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--text-sm);
|
|
min-width: 120px;
|
|
}
|
|
|
|
.detail-select:focus,
|
|
.detail-input:focus,
|
|
.trend-select:focus {
|
|
outline: none;
|
|
border-color: var(--color-primary);
|
|
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb), 0.2);
|
|
}
|
|
|
|
/* Tables - All table styles removed (tables are in card components) */
|
|
|
|
/* Trends Section */
|
|
.trends-container {
|
|
padding: var(--space-xl);
|
|
}
|
|
|
|
.trend-loading {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: var(--space-xl);
|
|
color: var(--color-text-secondary);
|
|
text-align: center;
|
|
}
|
|
|
|
.trend-error {
|
|
text-align: center;
|
|
padding: var(--space-xl);
|
|
background: var(--color-bg-secondary);
|
|
border-radius: var(--radius-lg);
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.error-icon {
|
|
font-size: 48px;
|
|
color: var(--color-warning);
|
|
margin-bottom: var(--space-lg);
|
|
}
|
|
|
|
.trend-error h3 {
|
|
font-size: var(--text-xl);
|
|
font-weight: var(--font-semibold);
|
|
color: var(--color-text);
|
|
margin: 0 0 var(--space-md) 0;
|
|
}
|
|
|
|
.trend-error p {
|
|
font-size: var(--text-base);
|
|
margin: 0 0 var(--space-lg) 0;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.trend-chart-wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-lg);
|
|
}
|
|
|
|
.trend-chart-component {
|
|
height: 300px;
|
|
background: var(--color-bg);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--space-md);
|
|
}
|
|
|
|
.trend-summary {
|
|
background: var(--color-bg-secondary);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--space-lg);
|
|
}
|
|
|
|
.trend-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: var(--space-lg);
|
|
}
|
|
|
|
.stat-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-xs);
|
|
padding: var(--space-md);
|
|
background: var(--color-bg);
|
|
border-radius: var(--radius-md);
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: var(--text-sm);
|
|
font-weight: var(--font-medium);
|
|
color: var(--color-text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: var(--text-lg);
|
|
font-weight: var(--font-semibold);
|
|
font-family: var(--font-mono, monospace);
|
|
}
|
|
|
|
.stat-item.positive .stat-value {
|
|
color: var(--color-success);
|
|
}
|
|
|
|
.stat-item.negative .stat-value {
|
|
color: var(--color-error);
|
|
}
|
|
|
|
.stat-item.neutral .stat-value {
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.stat-change {
|
|
font-size: var(--text-sm);
|
|
font-weight: var(--font-medium);
|
|
padding: var(--space-xs) var(--space-sm);
|
|
border-radius: var(--radius-full);
|
|
}
|
|
|
|
.stat-change.positive {
|
|
background: var(--color-success-bg);
|
|
color: var(--color-success);
|
|
}
|
|
|
|
.stat-change.negative {
|
|
background: var(--color-error-bg);
|
|
color: var(--color-error);
|
|
}
|
|
|
|
.stat-change.neutral {
|
|
background: var(--color-bg-muted);
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
/* Loading State - Uses global .loading-spinner from interactive.css */
|
|
.loading-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: var(--space-xl);
|
|
color: var(--color-text-secondary);
|
|
text-align: center;
|
|
}
|
|
|
|
/* Responsive Design - Consolidated (Component-specific only) */
|
|
@media (max-width: 768px) {
|
|
/* Trend section adjustments */
|
|
.trends-container {
|
|
padding: var(--space-lg);
|
|
}
|
|
|
|
.trend-chart-component {
|
|
height: 250px;
|
|
}
|
|
|
|
.trend-stats {
|
|
grid-template-columns: 1fr;
|
|
gap: var(--space-md);
|
|
}
|
|
|
|
.stat-item {
|
|
padding: var(--space-sm);
|
|
}
|
|
|
|
.error-icon {
|
|
font-size: 36px;
|
|
}
|
|
|
|
.trend-error h3 {
|
|
font-size: var(--text-lg);
|
|
}
|
|
}
|
|
|
|
/* Compact Layout Styles - Removed (tables are in card components) */
|
|
|
|
/* Print Styles - Simplified (card components handle their own print styles) */
|
|
@media print {
|
|
@page {
|
|
margin: 0.5in;
|
|
size: A4;
|
|
}
|
|
|
|
* {
|
|
-webkit-print-color-adjust: exact;
|
|
print-color-adjust: exact;
|
|
}
|
|
|
|
.trends-container,
|
|
.loading-state,
|
|
.no-print {
|
|
display: none !important;
|
|
}
|
|
}
|
|
|
|
/* Mobile Table Styles - Removed (tables are in card components) */
|
|
|
|
/* Secțiune Carduri Noi */
|
|
.metrics-cards-section {
|
|
margin-bottom: 2rem;
|
|
padding: 0 var(--space-md);
|
|
}
|
|
|
|
/* Metrics Cards Layout - Component-specific grid layouts */
|
|
.metrics-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.analysis-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.comparison-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
/* Responsive - All breakpoints consolidated */
|
|
@media (max-width: 1200px) {
|
|
.metrics-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.analysis-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.metrics-cards-section {
|
|
padding: 0 0.25rem;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.metrics-cards-section {
|
|
padding: 0;
|
|
margin-bottom: 1rem;
|
|
}
|
|
}
|
|
|
|
/* Mobile KPI Carousel Styles */
|
|
.mobile-kpi-carousel {
|
|
margin-bottom: var(--space-lg);
|
|
}
|
|
</style>
|