Files
roa2web-service-auto/src/modules/reports/views/DashboardView.vue
Claude Agent 6d7613a82e feat(unified-mobile-desktop-ui): Complete US-507 - Selecție Companie/Perioadă în MobileDrawerMenu
Implemented by Ralph autonomous loop.
Iteration: 7

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 22:42:54 +00:00

1329 lines
37 KiB
Vue

<template>
<!-- Mobile Top Bar -->
<MobileTopBar
v-if="isMobile"
title="Dashboard"
:show-menu="true"
@menu-click="showDrawer = true"
/>
<!-- Mobile Drawer Menu (replaces old Sidebar) -->
<MobileDrawerMenu
v-model="showDrawer"
:user="authStore.user"
:companies-store="companyStore"
:period-store="periodStore"
@logout="handleLogout"
/>
<main class="main-content" :class="{ 'mobile-layout': isMobile }">
<div class="app-container">
<!-- Dashboard Header - only on desktop -->
<div v-if="!isMobile" 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>
<!-- Desktop: Rând 2: Analiză comparativă și Date Detaliate (combinat) -->
<div v-if="!isMobile" 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>
<!-- Mobile Bottom Nav - US-307: Using default nav items -->
<MobileBottomNav v-if="isMobile" />
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { useRouter } from "vue-router";
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 components
import SwipeableCards from "@shared/components/mobile/SwipeableCards.vue";
import MobileTopBar from "@shared/components/mobile/MobileTopBar.vue";
import MobileBottomNav from "@shared/components/mobile/MobileBottomNav.vue";
import MobileDrawerMenu from "@shared/components/mobile/MobileDrawerMenu.vue";
import { useCompanyStore, useAuthStore } 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 router = useRouter();
const companyStore = useCompanyStore();
const dashboardStore = useDashboardStore();
const periodStore = useAccountingPeriodStore();
const authStore = useAuthStore();
// State
const filteredCompanies = ref([]);
const isLoading = ref(false);
const showDrawer = 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;
};
// US-307: Removed custom mobileNavItems - using MobileBottomNav defaults
// Dashboard is now at /dashboard with its own nav item
// Handle logout from drawer menu
const handleLogout = async () => {
await authStore.logout();
router.push('/login');
};
// 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 */
/* Mobile Layout - Padding for fixed top/bottom bars */
.main-content.mobile-layout {
padding-top: 56px; /* MobileTopBar height */
padding-bottom: 56px; /* MobileBottomNav height */
}
/* 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>