Frontend: - Refactored CSS architecture with new utility classes - Updated dashboard components styling - Improved responsive grid system - Enhanced typography and variables - Updated E2E and integration tests Added: - Claude Code slash commands for validation - SSH tunnel and start test scripts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
509 lines
14 KiB
JavaScript
509 lines
14 KiB
JavaScript
import { defineStore } from "pinia";
|
|
import { ref } from "vue";
|
|
import { apiService } from "../services/api";
|
|
|
|
export const useDashboardStore = defineStore("dashboard", () => {
|
|
// State existent
|
|
const summary = ref(null);
|
|
const trends = ref(null);
|
|
const isLoading = ref(false);
|
|
const error = ref(null);
|
|
|
|
// State nou pentru carduri
|
|
const performanceData = ref({});
|
|
const cashflowData = ref({});
|
|
const maturityData = ref({});
|
|
const currentPeriod = ref(null);
|
|
|
|
// State pentru detailed data pagination
|
|
const detailedDataTotal = ref(0);
|
|
|
|
// Cache pentru date
|
|
const dataCache = new Map();
|
|
|
|
const loadDashboardSummary = async (companyId) => {
|
|
isLoading.value = true;
|
|
error.value = null;
|
|
|
|
try {
|
|
const response = await apiService.get("/dashboard/summary", {
|
|
params: { company: companyId },
|
|
});
|
|
summary.value = response.data;
|
|
return { success: true };
|
|
} catch (err) {
|
|
error.value = err.response?.data?.detail || "Failed to load dashboard";
|
|
console.error("Failed to load dashboard:", err);
|
|
return { success: false, error: error.value };
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
const loadTrendData = async (
|
|
companyId,
|
|
period = "12m",
|
|
chartType = "line",
|
|
) => {
|
|
isLoading.value = true;
|
|
error.value = null;
|
|
|
|
try {
|
|
console.log(
|
|
`Loading trend data for company ${companyId}, period: ${period}`,
|
|
);
|
|
|
|
const response = await apiService.get("/dashboard/trends", {
|
|
params: {
|
|
company: companyId,
|
|
period: period,
|
|
},
|
|
});
|
|
|
|
// Validate response structure
|
|
if (!response.data) {
|
|
throw new Error("Empty response from trends API");
|
|
}
|
|
|
|
console.log("Raw trends response:", response.data);
|
|
|
|
// Transform backend response to Chart.js format
|
|
const backendData = response.data;
|
|
const transformedData = transformTrendsData(backendData);
|
|
|
|
if (!transformedData) {
|
|
throw new Error("Failed to transform trends data - invalid format");
|
|
}
|
|
|
|
trends.value = transformedData;
|
|
console.log("Transformed trends data:", transformedData);
|
|
|
|
return { success: true, data: transformedData };
|
|
} catch (err) {
|
|
const errorMessage =
|
|
err.response?.data?.detail ||
|
|
err.message ||
|
|
"Failed to load trend data";
|
|
error.value = errorMessage;
|
|
console.error("Failed to load trend data:", err);
|
|
console.error("Error details:", {
|
|
status: err.response?.status,
|
|
statusText: err.response?.statusText,
|
|
data: err.response?.data,
|
|
});
|
|
|
|
// Clear trends data and return error - no more mock data
|
|
trends.value = null;
|
|
return { success: false, error: error.value };
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
// Transform backend trends data to Chart.js format AND preserve raw data
|
|
const transformTrendsData = (backendData) => {
|
|
if (
|
|
!backendData ||
|
|
!backendData.periods ||
|
|
!Array.isArray(backendData.periods) ||
|
|
backendData.periods.length === 0
|
|
) {
|
|
console.warn("Invalid trends data received:", backendData);
|
|
return null;
|
|
}
|
|
|
|
// Validate that we have all required data
|
|
const requiredFields = [
|
|
"trezorerie_sold",
|
|
"clienti_sold",
|
|
"furnizori_sold",
|
|
"clienti_incasat",
|
|
"furnizori_achitat",
|
|
];
|
|
for (const field of requiredFields) {
|
|
if (!backendData[field] || !Array.isArray(backendData[field])) {
|
|
console.warn(`Missing ${field} data`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Data is already in ASC order from backend
|
|
const periods = [...backendData.periods];
|
|
|
|
// Format labels for monthly data (YYYY-MM -> MM/YYYY)
|
|
const formattedPeriods = periods.map((period) => {
|
|
const [year, month] = period.split("-");
|
|
const date = new Date(year, month - 1);
|
|
return date.toLocaleDateString("ro-RO", {
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
});
|
|
});
|
|
|
|
// Preserve all raw data from backend for card calculations
|
|
return {
|
|
labels: formattedPeriods,
|
|
raw: {
|
|
// Current period data
|
|
periods: backendData.periods,
|
|
clienti_facturat: backendData.clienti_facturat || [],
|
|
clienti_incasat: backendData.clienti_incasat || [],
|
|
clienti_sold: backendData.clienti_sold || [],
|
|
furnizori_facturat: backendData.furnizori_facturat || [],
|
|
furnizori_achitat: backendData.furnizori_achitat || [],
|
|
furnizori_sold: backendData.furnizori_sold || [],
|
|
trezorerie_sold: backendData.trezorerie_sold || [],
|
|
|
|
// Previous period data (year-over-year comparison)
|
|
previous_periods: backendData.previous_periods || [],
|
|
clienti_facturat_prev: backendData.clienti_facturat_prev || [],
|
|
clienti_incasat_prev: backendData.clienti_incasat_prev || [],
|
|
clienti_sold_prev: backendData.clienti_sold_prev || [],
|
|
furnizori_facturat_prev: backendData.furnizori_facturat_prev || [],
|
|
furnizori_achitat_prev: backendData.furnizori_achitat_prev || [],
|
|
furnizori_sold_prev: backendData.furnizori_sold_prev || [],
|
|
trezorerie_sold_prev: backendData.trezorerie_sold_prev || [],
|
|
},
|
|
datasets: [
|
|
{
|
|
label: "Trezorerie - Sold Net",
|
|
data: [...backendData.trezorerie_sold].map((val) => Number(val) || 0),
|
|
borderColor: "rgb(59, 130, 246)",
|
|
backgroundColor: "rgba(59, 130, 246, 0.1)",
|
|
tension: 0.4,
|
|
fill: false,
|
|
pointBackgroundColor: "rgb(59, 130, 246)",
|
|
pointBorderColor: "#ffffff",
|
|
pointBorderWidth: 2,
|
|
pointRadius: 4,
|
|
pointHoverRadius: 6,
|
|
},
|
|
],
|
|
};
|
|
};
|
|
|
|
const loadDetailedData = async (
|
|
dataType,
|
|
companyId,
|
|
page = 1,
|
|
pageSize = 25,
|
|
search = "",
|
|
) => {
|
|
isLoading.value = true;
|
|
error.value = null;
|
|
|
|
try {
|
|
const response = await apiService.get("/dashboard/detailed-data", {
|
|
params: {
|
|
company: companyId,
|
|
data_type: dataType,
|
|
page: page,
|
|
page_size: pageSize,
|
|
search: search,
|
|
},
|
|
});
|
|
|
|
// Store total for pagination
|
|
detailedDataTotal.value = response.data.total || 0;
|
|
|
|
return {
|
|
success: true,
|
|
data: response.data.data || [], // Backend returns 'data' not 'items'
|
|
total: response.data.total || 0,
|
|
page: response.data.page || 1,
|
|
};
|
|
} catch (err) {
|
|
error.value =
|
|
err.response?.data?.detail || "Failed to load detailed data";
|
|
console.error("Failed to load detailed data:", err);
|
|
|
|
// Return mock data structure for testing
|
|
const mockData = generateMockDetailedData(dataType);
|
|
detailedDataTotal.value = mockData.length;
|
|
return {
|
|
success: false,
|
|
error: error.value,
|
|
data: mockData,
|
|
total: mockData.length,
|
|
page: 1,
|
|
};
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
};
|
|
|
|
// Generate mock data for testing until backend endpoint is implemented
|
|
const generateMockDetailedData = (dataType) => {
|
|
switch (dataType) {
|
|
case "clients":
|
|
return [
|
|
{
|
|
id: 1,
|
|
client: "SC ALPHA SRL",
|
|
facturat: 15000,
|
|
incasat: 12000,
|
|
sold: 3000,
|
|
status: "Activ",
|
|
},
|
|
{
|
|
id: 2,
|
|
client: "SC BETA SRL",
|
|
facturat: 8500,
|
|
incasat: 8500,
|
|
sold: 0,
|
|
status: "Activ",
|
|
},
|
|
{
|
|
id: 3,
|
|
client: "SC GAMMA SRL",
|
|
facturat: 22000,
|
|
incasat: 15000,
|
|
sold: 7000,
|
|
status: "Activ",
|
|
},
|
|
{
|
|
id: 4,
|
|
client: "SC DELTA SRL",
|
|
facturat: 5500,
|
|
incasat: 2000,
|
|
sold: 3500,
|
|
status: "Întârziere",
|
|
},
|
|
{
|
|
id: 5,
|
|
client: "SC EPSILON SRL",
|
|
facturat: 18000,
|
|
incasat: 18000,
|
|
sold: 0,
|
|
status: "Activ",
|
|
},
|
|
];
|
|
case "suppliers":
|
|
return [
|
|
{
|
|
id: 1,
|
|
furnizor: "SC SUPPLIER A SRL",
|
|
facturat: 12000,
|
|
achitat: 10000,
|
|
sold: 2000,
|
|
status: "Activ",
|
|
},
|
|
{
|
|
id: 2,
|
|
furnizor: "SC SUPPLIER B SRL",
|
|
facturat: 7500,
|
|
achitat: 7500,
|
|
sold: 0,
|
|
status: "Activ",
|
|
},
|
|
{
|
|
id: 3,
|
|
furnizor: "SC SUPPLIER C SRL",
|
|
facturat: 19000,
|
|
achitat: 12000,
|
|
sold: 7000,
|
|
status: "Pendente",
|
|
},
|
|
{
|
|
id: 4,
|
|
furnizor: "SC SUPPLIER D SRL",
|
|
facturat: 4200,
|
|
achitat: 4200,
|
|
sold: 0,
|
|
status: "Activ",
|
|
},
|
|
{
|
|
id: 5,
|
|
furnizor: "SC SUPPLIER E SRL",
|
|
facturat: 16800,
|
|
achitat: 8000,
|
|
sold: 8800,
|
|
status: "Pendente",
|
|
},
|
|
];
|
|
case "treasury":
|
|
return [
|
|
{
|
|
id: 1,
|
|
cont: "5121",
|
|
nume_cont: "Cont curent BCR",
|
|
sold: 45000,
|
|
valuta: "RON",
|
|
tip: "Bancă",
|
|
},
|
|
{
|
|
id: 2,
|
|
cont: "5311",
|
|
nume_cont: "Casa RON",
|
|
sold: 2500,
|
|
valuta: "RON",
|
|
tip: "Numerar",
|
|
},
|
|
{
|
|
id: 3,
|
|
cont: "5124",
|
|
nume_cont: "Cont curent BRD EUR",
|
|
sold: 8500,
|
|
valuta: "EUR",
|
|
tip: "Bancă",
|
|
},
|
|
{
|
|
id: 4,
|
|
cont: "5125",
|
|
nume_cont: "Cont economii ING",
|
|
sold: 125000,
|
|
valuta: "RON",
|
|
tip: "Economii",
|
|
},
|
|
{
|
|
id: 5,
|
|
cont: "5312",
|
|
nume_cont: "Casa valută",
|
|
sold: 500,
|
|
valuta: "EUR",
|
|
tip: "Numerar",
|
|
},
|
|
];
|
|
default:
|
|
return [];
|
|
}
|
|
};
|
|
|
|
// Funcții noi pentru carduri
|
|
const loadPerformanceData = async (companyId, period = "7d") => {
|
|
const cacheKey = `performance-${companyId}-${period}`;
|
|
|
|
// Check cache
|
|
if (dataCache.has(cacheKey)) {
|
|
performanceData.value[period] = dataCache.get(cacheKey);
|
|
return { success: true, data: dataCache.get(cacheKey) };
|
|
}
|
|
|
|
try {
|
|
const response = await apiService.get("/dashboard/performance", {
|
|
params: { company: companyId, period },
|
|
});
|
|
|
|
performanceData.value[period] = response.data;
|
|
dataCache.set(cacheKey, response.data);
|
|
|
|
return { success: true, data: response.data };
|
|
} catch (err) {
|
|
console.error("Failed to load performance data:", err);
|
|
return { success: false, error: err.message };
|
|
}
|
|
};
|
|
|
|
const loadCashFlowData = async (companyId, period = "7d") => {
|
|
const cacheKey = `cashflow-${companyId}-${period}`;
|
|
|
|
if (dataCache.has(cacheKey)) {
|
|
cashflowData.value[period] = dataCache.get(cacheKey);
|
|
return { success: true, data: dataCache.get(cacheKey) };
|
|
}
|
|
|
|
try {
|
|
const response = await apiService.get("/dashboard/cashflow", {
|
|
params: { company: companyId, period },
|
|
});
|
|
|
|
cashflowData.value[period] = response.data;
|
|
dataCache.set(cacheKey, response.data);
|
|
|
|
return { success: true, data: response.data };
|
|
} catch (err) {
|
|
console.error("Failed to load cashflow data:", err);
|
|
return { success: false, error: err.message };
|
|
}
|
|
};
|
|
|
|
const loadMaturityData = async (companyId, period = "7d") => {
|
|
const cacheKey = `maturity-${companyId}-${period}`;
|
|
|
|
if (dataCache.has(cacheKey)) {
|
|
maturityData.value[period] = dataCache.get(cacheKey);
|
|
return { success: true, data: dataCache.get(cacheKey) };
|
|
}
|
|
|
|
try {
|
|
const response = await apiService.get("/dashboard/maturity", {
|
|
params: { company: companyId, period },
|
|
});
|
|
|
|
maturityData.value[period] = response.data;
|
|
dataCache.set(cacheKey, response.data);
|
|
|
|
return { success: true, data: response.data };
|
|
} catch (err) {
|
|
console.error("Failed to load maturity data:", err);
|
|
return { success: false, error: err.message };
|
|
}
|
|
};
|
|
|
|
const loadCurrentPeriod = async (companyId) => {
|
|
try {
|
|
const response = await apiService.get("/dashboard/current-period", {
|
|
params: { company: companyId },
|
|
});
|
|
|
|
currentPeriod.value = response.data;
|
|
return { success: true, data: response.data };
|
|
} catch (err) {
|
|
console.error("Failed to load current period:", err);
|
|
// Fallback to current date if API fails
|
|
const now = new Date();
|
|
const fallbackPeriod = {
|
|
year: now.getFullYear(),
|
|
month: now.getMonth() + 1,
|
|
period: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`,
|
|
};
|
|
currentPeriod.value = fallbackPeriod;
|
|
return { success: false, error: err.message, data: fallbackPeriod };
|
|
}
|
|
};
|
|
|
|
// Clear cache
|
|
const clearCache = () => {
|
|
dataCache.clear();
|
|
};
|
|
|
|
const reset = () => {
|
|
summary.value = null;
|
|
trends.value = null;
|
|
isLoading.value = false;
|
|
error.value = null;
|
|
// Clear new data as well
|
|
performanceData.value = {};
|
|
cashflowData.value = {};
|
|
maturityData.value = {};
|
|
currentPeriod.value = null;
|
|
clearCache();
|
|
};
|
|
|
|
return {
|
|
// Existing
|
|
summary,
|
|
trends,
|
|
isLoading,
|
|
error,
|
|
loadDashboardSummary,
|
|
loadTrendData,
|
|
loadDetailedData,
|
|
reset,
|
|
|
|
// New
|
|
performanceData,
|
|
cashflowData,
|
|
maturityData,
|
|
currentPeriod,
|
|
loadPerformanceData,
|
|
loadCashFlowData,
|
|
loadMaturityData,
|
|
loadCurrentPeriod,
|
|
clearCache,
|
|
|
|
// Detailed data pagination
|
|
detailedDataTotal,
|
|
};
|
|
});
|