chore: Fix .env.test quotes and format frontend code
- Fix TEST_ORACLE_USER quotes in .env.test for shell source compatibility - Format 14 frontend files with Prettier (stores, views, utils) - All 122 tests passing (77 telegram + 35 backend + 10 E2E) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,9 @@ ORACLE_PASSWORD=ROMFASTSOFT
|
||||
ORACLE_DSN=localhost:1526/roa
|
||||
|
||||
# Test credentials for pytest (user exists in Oracle TEST)
|
||||
TEST_ORACLE_USER=MARIUS M
|
||||
TEST_ORACLE_PASS=123
|
||||
# Quotes required for shell (source), dotenv strips them automatically
|
||||
TEST_ORACLE_USER="MARIUS M"
|
||||
TEST_ORACLE_PASS="123"
|
||||
|
||||
# Test company - MARIUSM_AUTO schema (only schema with full data in TEST)
|
||||
# Other schemas (ACN, DANUBE, EMS) don't have required tables
|
||||
|
||||
@@ -67,8 +67,11 @@ export const useInvoicesStore = defineStore("invoices", () => {
|
||||
// Convert Date object to YYYY-MM-DD string format (LOCAL date, not UTC)
|
||||
if (filters.value.dateFrom instanceof Date) {
|
||||
const year = filters.value.dateFrom.getFullYear();
|
||||
const month = String(filters.value.dateFrom.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(filters.value.dateFrom.getDate()).padStart(2, '0');
|
||||
const month = String(filters.value.dateFrom.getMonth() + 1).padStart(
|
||||
2,
|
||||
"0",
|
||||
);
|
||||
const day = String(filters.value.dateFrom.getDate()).padStart(2, "0");
|
||||
params.date_from = `${year}-${month}-${day}`;
|
||||
} else {
|
||||
params.date_from = filters.value.dateFrom;
|
||||
@@ -78,8 +81,11 @@ export const useInvoicesStore = defineStore("invoices", () => {
|
||||
// Convert Date object to YYYY-MM-DD string format (LOCAL date, not UTC)
|
||||
if (filters.value.dateTo instanceof Date) {
|
||||
const year = filters.value.dateTo.getFullYear();
|
||||
const month = String(filters.value.dateTo.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(filters.value.dateTo.getDate()).padStart(2, '0');
|
||||
const month = String(filters.value.dateTo.getMonth() + 1).padStart(
|
||||
2,
|
||||
"0",
|
||||
);
|
||||
const day = String(filters.value.dateTo.getDate()).padStart(2, "0");
|
||||
params.date_to = `${year}-${month}-${day}`;
|
||||
} else {
|
||||
params.date_to = filters.value.dateTo;
|
||||
@@ -93,8 +99,8 @@ export const useInvoicesStore = defineStore("invoices", () => {
|
||||
const response = await apiService.get(`/invoices/`, {
|
||||
params: {
|
||||
company: companyCode,
|
||||
...params
|
||||
}
|
||||
...params,
|
||||
},
|
||||
});
|
||||
|
||||
invoices.value = response.data.invoices || [];
|
||||
|
||||
@@ -13,7 +13,7 @@ export const useTreasuryStore = defineStore("treasury", () => {
|
||||
});
|
||||
const totals = ref({
|
||||
total_incasari: 0,
|
||||
total_plati: 0
|
||||
total_plati: 0,
|
||||
});
|
||||
|
||||
const loadBankCashRegister = async (companyId, filters = {}) => {
|
||||
@@ -25,18 +25,18 @@ export const useTreasuryStore = defineStore("treasury", () => {
|
||||
company: companyId,
|
||||
page: pagination.value.page + 1,
|
||||
page_size: pagination.value.rows,
|
||||
...filters
|
||||
...filters,
|
||||
};
|
||||
|
||||
const response = await apiService.get('/treasury/bank-cash-register', {
|
||||
params
|
||||
const response = await apiService.get("/treasury/bank-cash-register", {
|
||||
params,
|
||||
});
|
||||
|
||||
registers.value = response.data.registers || [];
|
||||
pagination.value.totalRecords = response.data.total_count || 0;
|
||||
totals.value = {
|
||||
total_incasari: response.data.total_incasari,
|
||||
total_plati: response.data.total_plati
|
||||
total_plati: response.data.total_plati,
|
||||
};
|
||||
|
||||
return { success: true };
|
||||
@@ -72,6 +72,6 @@ export const useTreasuryStore = defineStore("treasury", () => {
|
||||
totals,
|
||||
loadBankCashRegister,
|
||||
setPagination,
|
||||
reset
|
||||
reset,
|
||||
};
|
||||
});
|
||||
@@ -1,17 +1,17 @@
|
||||
import * as XLSX from 'xlsx';
|
||||
import { jsPDF } from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import * as XLSX from "xlsx";
|
||||
import { jsPDF } from "jspdf";
|
||||
import autoTable from "jspdf-autotable";
|
||||
|
||||
/**
|
||||
* Format currency values for export
|
||||
*/
|
||||
const formatCurrency = (value) => {
|
||||
if (value == null || value === '-') return '-';
|
||||
return new Intl.NumberFormat('ro-RO', {
|
||||
style: 'currency',
|
||||
currency: 'RON',
|
||||
if (value == null || value === "-") return "-";
|
||||
return new Intl.NumberFormat("ro-RO", {
|
||||
style: "currency",
|
||||
currency: "RON",
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
@@ -21,15 +21,18 @@ const formatCurrency = (value) => {
|
||||
* @param {String} filename - Name of the file (without extension)
|
||||
* @param {String} sheetName - Name of the Excel sheet
|
||||
*/
|
||||
export const exportToExcel = (data, filename, sheetName = 'Sheet1') => {
|
||||
export const exportToExcel = (data, filename, sheetName = "Sheet1") => {
|
||||
try {
|
||||
const ws = XLSX.utils.json_to_sheet(data);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, sheetName);
|
||||
XLSX.writeFile(wb, `${filename}_${new Date().toISOString().split('T')[0]}.xlsx`);
|
||||
XLSX.writeFile(
|
||||
wb,
|
||||
`${filename}_${new Date().toISOString().split("T")[0]}.xlsx`,
|
||||
);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Excel export failed:', error);
|
||||
console.error("Excel export failed:", error);
|
||||
return { success: false, error };
|
||||
}
|
||||
};
|
||||
@@ -38,12 +41,12 @@ export const exportToExcel = (data, filename, sheetName = 'Sheet1') => {
|
||||
* Format number for PDF export
|
||||
*/
|
||||
const formatNumberForPDF = (value) => {
|
||||
if (value == null || value === '' || value === '-') return '-';
|
||||
if (value == null || value === "" || value === "-") return "-";
|
||||
const num = parseFloat(value);
|
||||
if (isNaN(num)) return '-';
|
||||
return new Intl.NumberFormat('ro-RO', {
|
||||
if (isNaN(num)) return "-";
|
||||
return new Intl.NumberFormat("ro-RO", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
maximumFractionDigits: 2,
|
||||
}).format(num);
|
||||
};
|
||||
|
||||
@@ -58,17 +61,17 @@ export const exportToPDF = (data, columns, filename, header) => {
|
||||
try {
|
||||
// Check if data exists
|
||||
if (!data || data.length === 0) {
|
||||
console.error('No data to export');
|
||||
return { success: false, error: 'No data available' };
|
||||
console.error("No data to export");
|
||||
return { success: false, error: "No data available" };
|
||||
}
|
||||
|
||||
// Check if jsPDF is properly imported
|
||||
if (typeof jsPDF === 'undefined') {
|
||||
console.error('jsPDF not properly imported');
|
||||
return { success: false, error: 'PDF library not available' };
|
||||
if (typeof jsPDF === "undefined") {
|
||||
console.error("jsPDF not properly imported");
|
||||
return { success: false, error: "PDF library not available" };
|
||||
}
|
||||
|
||||
const doc = new jsPDF('landscape', 'mm', 'a4');
|
||||
const doc = new jsPDF("landscape", "mm", "a4");
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
const pageHeight = doc.internal.pageSize.getHeight();
|
||||
const marginLeft = 8;
|
||||
@@ -79,38 +82,38 @@ export const exportToPDF = (data, columns, filename, header) => {
|
||||
const addHeader = () => {
|
||||
// Line 1: Company name (left aligned, bold, larger font)
|
||||
doc.setFontSize(13);
|
||||
doc.setFont(undefined, 'bold');
|
||||
const companyName = header.companyName || 'N/A';
|
||||
doc.setFont(undefined, "bold");
|
||||
const companyName = header.companyName || "N/A";
|
||||
doc.text(companyName, marginLeft, 15);
|
||||
|
||||
// Line 2: Title "Balanta de Verificare" (centered)
|
||||
doc.setFontSize(14);
|
||||
doc.setFont(undefined, 'bold');
|
||||
const titleWidth = doc.getTextWidth(header.title || '');
|
||||
doc.setFont(undefined, "bold");
|
||||
const titleWidth = doc.getTextWidth(header.title || "");
|
||||
const titleX = marginLeft + (contentWidth - titleWidth) / 2;
|
||||
doc.text(header.title || '', titleX, 24);
|
||||
doc.text(header.title || "", titleX, 24);
|
||||
|
||||
// Line 3: Period (centered, below title)
|
||||
doc.setFontSize(11);
|
||||
doc.setFont(undefined, 'normal');
|
||||
const periodText = header.period || '';
|
||||
doc.setFont(undefined, "normal");
|
||||
const periodText = header.period || "";
|
||||
const periodWidth = doc.getTextWidth(periodText);
|
||||
const periodX = marginLeft + (contentWidth - periodWidth) / 2;
|
||||
doc.text(periodText, periodX, 32);
|
||||
};
|
||||
|
||||
// Prepare table data
|
||||
const tableColumns = columns.map(col => col.header);
|
||||
const tableRows = data.map(row =>
|
||||
columns.map(col => {
|
||||
const tableColumns = columns.map((col) => col.header);
|
||||
const tableRows = data.map((row) =>
|
||||
columns.map((col) => {
|
||||
const value = row[col.field];
|
||||
if (col.type === 'currency') {
|
||||
if (col.type === "currency") {
|
||||
return formatCurrency(value);
|
||||
} else if (col.type === 'number') {
|
||||
} else if (col.type === "number") {
|
||||
return formatNumberForPDF(value);
|
||||
}
|
||||
return value || '-';
|
||||
})
|
||||
return value || "-";
|
||||
}),
|
||||
);
|
||||
|
||||
// Function to add footer (called for each page)
|
||||
@@ -119,8 +122,12 @@ export const exportToPDF = (data, columns, filename, header) => {
|
||||
|
||||
// Left side: Generation date
|
||||
doc.setFontSize(8);
|
||||
doc.setFont(undefined, 'normal');
|
||||
doc.text(`Generat: ${new Date().toLocaleString('ro-RO')}`, marginLeft, footerY);
|
||||
doc.setFont(undefined, "normal");
|
||||
doc.text(
|
||||
`Generat: ${new Date().toLocaleString("ro-RO")}`,
|
||||
marginLeft,
|
||||
footerY,
|
||||
);
|
||||
|
||||
// Right side: Page numbers
|
||||
const pageText = `Pagina ${pageNum} din ${totalPages}`;
|
||||
@@ -129,7 +136,7 @@ export const exportToPDF = (data, columns, filename, header) => {
|
||||
};
|
||||
|
||||
// Check if autoTable is available
|
||||
if (typeof autoTable === 'function') {
|
||||
if (typeof autoTable === "function") {
|
||||
// Build column styles - jspdf-autotable uses numeric keys
|
||||
const columnStyles = {};
|
||||
|
||||
@@ -142,37 +149,37 @@ export const exportToPDF = (data, columns, filename, header) => {
|
||||
|
||||
columns.forEach((col, index) => {
|
||||
// Use custom width if provided, otherwise auto
|
||||
if (col.width && typeof col.width === 'number') {
|
||||
if (col.width && typeof col.width === "number") {
|
||||
widthAllocations[index] = totalWidth * col.width;
|
||||
} else if (col.width === 'auto') {
|
||||
widthAllocations[index] = 'auto';
|
||||
} else if (col.width === "auto") {
|
||||
widthAllocations[index] = "auto";
|
||||
} else {
|
||||
// Default width allocation for Trial Balance (8 columns)
|
||||
const defaultWidths = {
|
||||
0: totalWidth * 0.07, // Cont: ~20mm
|
||||
1: totalWidth * 0.33, // Denumire: ~93mm
|
||||
2: totalWidth * 0.10, // Sold Prec D: ~28mm
|
||||
3: totalWidth * 0.10, // Sold Prec C: ~28mm
|
||||
4: totalWidth * 0.10, // Rulaj D: ~28mm
|
||||
5: totalWidth * 0.10, // Rulaj C: ~28mm
|
||||
6: totalWidth * 0.10, // Sold Final D: ~28mm
|
||||
7: totalWidth * 0.10, // Sold Final C: ~28mm
|
||||
2: totalWidth * 0.1, // Sold Prec D: ~28mm
|
||||
3: totalWidth * 0.1, // Sold Prec C: ~28mm
|
||||
4: totalWidth * 0.1, // Rulaj D: ~28mm
|
||||
5: totalWidth * 0.1, // Rulaj C: ~28mm
|
||||
6: totalWidth * 0.1, // Sold Final D: ~28mm
|
||||
7: totalWidth * 0.1, // Sold Final C: ~28mm
|
||||
};
|
||||
widthAllocations[index] = defaultWidths[index] || 'auto';
|
||||
widthAllocations[index] = defaultWidths[index] || "auto";
|
||||
}
|
||||
});
|
||||
|
||||
columns.forEach((col, index) => {
|
||||
columnStyles[index] = {
|
||||
cellWidth: widthAllocations[index]
|
||||
cellWidth: widthAllocations[index],
|
||||
};
|
||||
|
||||
// Set alignment based on type
|
||||
if (col.type === 'number' || col.type === 'currency') {
|
||||
columnStyles[index].halign = 'right';
|
||||
} else if (col.type === 'text') {
|
||||
if (col.type === "number" || col.type === "currency") {
|
||||
columnStyles[index].halign = "right";
|
||||
} else if (col.type === "text") {
|
||||
// All text columns aligned left (including Cont)
|
||||
columnStyles[index].halign = 'left';
|
||||
columnStyles[index].halign = "left";
|
||||
}
|
||||
});
|
||||
|
||||
@@ -186,44 +193,49 @@ export const exportToPDF = (data, columns, filename, header) => {
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: 2.5,
|
||||
valign: 'middle',
|
||||
valign: "middle",
|
||||
lineColor: [200, 200, 200],
|
||||
lineWidth: 0.1,
|
||||
overflow: 'linebreak'
|
||||
overflow: "linebreak",
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [41, 128, 185],
|
||||
textColor: 255,
|
||||
fontStyle: 'bold',
|
||||
halign: 'center',
|
||||
fontStyle: "bold",
|
||||
halign: "center",
|
||||
fontSize: 9,
|
||||
cellPadding: 2.5
|
||||
cellPadding: 2.5,
|
||||
},
|
||||
alternateRowStyles: {
|
||||
fillColor: [248, 248, 248]
|
||||
fillColor: [248, 248, 248],
|
||||
},
|
||||
columnStyles: columnStyles,
|
||||
margin: { left: marginLeft, right: marginRight, top: tableStartY, bottom: 15 },
|
||||
margin: {
|
||||
left: marginLeft,
|
||||
right: marginRight,
|
||||
top: tableStartY,
|
||||
bottom: 15,
|
||||
},
|
||||
tableWidth: pageWidth - marginLeft - marginRight, // Use full page width
|
||||
theme: 'grid',
|
||||
theme: "grid",
|
||||
didDrawPage: function (data) {
|
||||
// Add header to each page
|
||||
addHeader();
|
||||
},
|
||||
didParseCell: function (data) {
|
||||
// Force alignment based on column type (body cells only)
|
||||
if (data.section === 'body') {
|
||||
if (data.section === "body") {
|
||||
const colIndex = data.column.index;
|
||||
const column = columns[colIndex];
|
||||
|
||||
if (column) {
|
||||
if (column.type === 'number' || column.type === 'currency') {
|
||||
data.cell.styles.halign = 'right';
|
||||
} else if (column.type === 'text') {
|
||||
if (column.type === "number" || column.type === "currency") {
|
||||
data.cell.styles.halign = "right";
|
||||
} else if (column.type === "text") {
|
||||
if (colIndex === 0) {
|
||||
data.cell.styles.halign = 'center';
|
||||
data.cell.styles.halign = "center";
|
||||
} else {
|
||||
data.cell.styles.halign = 'left';
|
||||
data.cell.styles.halign = "left";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,18 +259,18 @@ export const exportToPDF = (data, columns, filename, header) => {
|
||||
|
||||
// Draw headers
|
||||
doc.setFontSize(8);
|
||||
doc.setFont(undefined, 'bold');
|
||||
doc.setFont(undefined, "bold");
|
||||
tableColumns.forEach((header, index) => {
|
||||
doc.text(header, 14 + (index * 35), yPos);
|
||||
doc.text(header, 14 + index * 35, yPos);
|
||||
});
|
||||
|
||||
// Draw rows
|
||||
doc.setFont(undefined, 'normal');
|
||||
doc.setFont(undefined, "normal");
|
||||
doc.setFontSize(7);
|
||||
tableRows.forEach((row, rowIndex) => {
|
||||
yPos += 7;
|
||||
row.forEach((cell, cellIndex) => {
|
||||
doc.text(String(cell), 14 + (cellIndex * 35), yPos);
|
||||
doc.text(String(cell), 14 + cellIndex * 35, yPos);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -267,11 +279,11 @@ export const exportToPDF = (data, columns, filename, header) => {
|
||||
}
|
||||
|
||||
// Save PDF
|
||||
doc.save(`${filename}_${new Date().toISOString().split('T')[0]}.pdf`);
|
||||
doc.save(`${filename}_${new Date().toISOString().split("T")[0]}.pdf`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('PDF export error details:', error);
|
||||
return { success: false, error: error.message || 'PDF generation failed' };
|
||||
console.error("PDF export error details:", error);
|
||||
return { success: false, error: error.message || "PDF generation failed" };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -281,29 +293,29 @@ export const exportToPDF = (data, columns, filename, header) => {
|
||||
export const exportGeneralTotals = (summaryData) => {
|
||||
const data = [
|
||||
{
|
||||
Tip: 'Clienți',
|
||||
'Total Facturat': summaryData?.clienti_total_facturat || 0,
|
||||
'Total Încasat': summaryData?.clienti_total_incasat || 0,
|
||||
'Sold Net': summaryData?.clienti_sold_total || 0,
|
||||
'Sold În Termen': summaryData?.clienti_sold_in_termen || 0,
|
||||
'Sold Restant': summaryData?.clienti_sold_restant || 0
|
||||
Tip: "Clienți",
|
||||
"Total Facturat": summaryData?.clienti_total_facturat || 0,
|
||||
"Total Încasat": summaryData?.clienti_total_incasat || 0,
|
||||
"Sold Net": summaryData?.clienti_sold_total || 0,
|
||||
"Sold În Termen": summaryData?.clienti_sold_in_termen || 0,
|
||||
"Sold Restant": summaryData?.clienti_sold_restant || 0,
|
||||
},
|
||||
{
|
||||
Tip: 'Furnizori',
|
||||
'Total Facturat': summaryData?.furnizori_total_facturat || 0,
|
||||
'Total Achitat': summaryData?.furnizori_total_achitat || 0,
|
||||
'Sold Net': summaryData?.furnizori_sold_total || 0,
|
||||
'Sold În Termen': summaryData?.furnizori_sold_in_termen || 0,
|
||||
'Sold Restant': summaryData?.furnizori_sold_restant || 0
|
||||
Tip: "Furnizori",
|
||||
"Total Facturat": summaryData?.furnizori_total_facturat || 0,
|
||||
"Total Achitat": summaryData?.furnizori_total_achitat || 0,
|
||||
"Sold Net": summaryData?.furnizori_sold_total || 0,
|
||||
"Sold În Termen": summaryData?.furnizori_sold_in_termen || 0,
|
||||
"Sold Restant": summaryData?.furnizori_sold_restant || 0,
|
||||
},
|
||||
{
|
||||
Tip: 'Trezorerie',
|
||||
'Total Facturat': '-',
|
||||
'Total Încasat/Achitat': '-',
|
||||
'Sold Net': summaryData?.trezorerie_sold || 0,
|
||||
'Sold În Termen': '-',
|
||||
'Sold Restant': '-'
|
||||
}
|
||||
Tip: "Trezorerie",
|
||||
"Total Facturat": "-",
|
||||
"Total Încasat/Achitat": "-",
|
||||
"Sold Net": summaryData?.trezorerie_sold || 0,
|
||||
"Sold În Termen": "-",
|
||||
"Sold Restant": "-",
|
||||
},
|
||||
];
|
||||
|
||||
return data;
|
||||
@@ -315,25 +327,25 @@ export const exportGeneralTotals = (summaryData) => {
|
||||
export const exportSoldNetBreakdown = (summaryData) => {
|
||||
const data = [
|
||||
{
|
||||
Categorie: 'Clienți - Restant',
|
||||
'TOTAL': summaryData?.clienti_sold_restant || 0,
|
||||
'7 zile': summaryData?.clienti_restant_7 || 0,
|
||||
'14 zile': summaryData?.clienti_restant_14 || 0,
|
||||
'30 zile': summaryData?.clienti_restant_30 || 0,
|
||||
'60 zile': summaryData?.clienti_restant_60 || 0,
|
||||
'90 zile': summaryData?.clienti_restant_90 || 0,
|
||||
'90+ zile': summaryData?.clienti_restant_over_90 || 0
|
||||
Categorie: "Clienți - Restant",
|
||||
TOTAL: summaryData?.clienti_sold_restant || 0,
|
||||
"7 zile": summaryData?.clienti_restant_7 || 0,
|
||||
"14 zile": summaryData?.clienti_restant_14 || 0,
|
||||
"30 zile": summaryData?.clienti_restant_30 || 0,
|
||||
"60 zile": summaryData?.clienti_restant_60 || 0,
|
||||
"90 zile": summaryData?.clienti_restant_90 || 0,
|
||||
"90+ zile": summaryData?.clienti_restant_over_90 || 0,
|
||||
},
|
||||
{
|
||||
Categorie: 'Furnizori - Restant',
|
||||
'TOTAL': summaryData?.furnizori_sold_restant || 0,
|
||||
'7 zile': summaryData?.furnizori_restant_7 || 0,
|
||||
'14 zile': summaryData?.furnizori_restant_14 || 0,
|
||||
'30 zile': summaryData?.furnizori_restant_30 || 0,
|
||||
'60 zile': summaryData?.furnizori_restant_60 || 0,
|
||||
'90 zile': summaryData?.furnizori_restant_90 || 0,
|
||||
'90+ zile': summaryData?.furnizori_restant_over_90 || 0
|
||||
}
|
||||
Categorie: "Furnizori - Restant",
|
||||
TOTAL: summaryData?.furnizori_sold_restant || 0,
|
||||
"7 zile": summaryData?.furnizori_restant_7 || 0,
|
||||
"14 zile": summaryData?.furnizori_restant_14 || 0,
|
||||
"30 zile": summaryData?.furnizori_restant_30 || 0,
|
||||
"60 zile": summaryData?.furnizori_restant_60 || 0,
|
||||
"90 zile": summaryData?.furnizori_restant_90 || 0,
|
||||
"90+ zile": summaryData?.furnizori_restant_over_90 || 0,
|
||||
},
|
||||
];
|
||||
|
||||
return data;
|
||||
@@ -350,7 +362,7 @@ export const exportTrendData = (trendsData, period, chartType) => {
|
||||
const data = trendsData.labels.map((label, index) => {
|
||||
const row = { Perioada: label };
|
||||
|
||||
trendsData.datasets.forEach(dataset => {
|
||||
trendsData.datasets.forEach((dataset) => {
|
||||
const value = dataset.data[index];
|
||||
row[dataset.label] = value || 0;
|
||||
});
|
||||
|
||||
@@ -26,7 +26,10 @@
|
||||
</div>
|
||||
<div class="filter-item">
|
||||
<label>Căutare partener</label>
|
||||
<InputText v-model="filters.partnerName" placeholder="Nume partener..." />
|
||||
<InputText
|
||||
v-model="filters.partnerName"
|
||||
placeholder="Nume partener..."
|
||||
/>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<Button
|
||||
@@ -54,7 +57,9 @@
|
||||
<i class="pi pi-arrow-down"></i>
|
||||
</div>
|
||||
<div class="stat-details">
|
||||
<h3 class="stat-value">{{ formatCurrency(treasuryStore.totals.total_incasari) }}</h3>
|
||||
<h3 class="stat-value">
|
||||
{{ formatCurrency(treasuryStore.totals.total_incasari) }}
|
||||
</h3>
|
||||
<p class="stat-label">Total Încasări</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,7 +72,9 @@
|
||||
<i class="pi pi-arrow-up"></i>
|
||||
</div>
|
||||
<div class="stat-details">
|
||||
<h3 class="stat-value">{{ formatCurrency(treasuryStore.totals.total_plati) }}</h3>
|
||||
<h3 class="stat-value">
|
||||
{{ formatCurrency(treasuryStore.totals.total_plati) }}
|
||||
</h3>
|
||||
<p class="stat-label">Total Plăți</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,29 +106,53 @@
|
||||
<Column field="nume_cont_bancar" header="Cont" />
|
||||
<Column field="tip_registru" header="Tip">
|
||||
<template #body="slotProps">
|
||||
<Tag :value="slotProps.data.tip_registru" :severity="getRegisterSeverity(slotProps.data.tip_registru)" />
|
||||
<Tag
|
||||
:value="slotProps.data.tip_registru"
|
||||
:severity="getRegisterSeverity(slotProps.data.tip_registru)"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="incasari" header="Încasări">
|
||||
<template #body="slotProps">
|
||||
<span class="text-success font-semibold" v-if="slotProps.data.incasari > 0">
|
||||
{{ formatCurrency(slotProps.data.incasari, slotProps.data.valuta) }}
|
||||
<span
|
||||
class="text-success font-semibold"
|
||||
v-if="slotProps.data.incasari > 0"
|
||||
>
|
||||
{{
|
||||
formatCurrency(
|
||||
slotProps.data.incasari,
|
||||
slotProps.data.valuta,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="plati" header="Plăți">
|
||||
<template #body="slotProps">
|
||||
<span class="text-error font-semibold" v-if="slotProps.data.plati > 0">
|
||||
{{ formatCurrency(slotProps.data.plati, slotProps.data.valuta) }}
|
||||
<span
|
||||
class="text-error font-semibold"
|
||||
v-if="slotProps.data.plati > 0"
|
||||
>
|
||||
{{
|
||||
formatCurrency(slotProps.data.plati, slotProps.data.valuta)
|
||||
}}
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="sold" header="Sold">
|
||||
<template #body="slotProps">
|
||||
<span :class="slotProps.data.sold >= 0 ? 'text-success font-semibold' : 'text-error font-semibold'">
|
||||
{{ formatCurrency(slotProps.data.sold, slotProps.data.valuta) }}
|
||||
<span
|
||||
:class="
|
||||
slotProps.data.sold >= 0
|
||||
? 'text-success font-semibold'
|
||||
: 'text-error font-semibold'
|
||||
"
|
||||
>
|
||||
{{
|
||||
formatCurrency(slotProps.data.sold, slotProps.data.valuta)
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
@@ -146,19 +177,19 @@ const companyStore = useCompanyStore();
|
||||
const filters = ref({
|
||||
dateFrom: null,
|
||||
dateTo: null,
|
||||
partnerName: ""
|
||||
partnerName: "",
|
||||
});
|
||||
|
||||
const pagination = ref({
|
||||
page: 0,
|
||||
rows: 50
|
||||
rows: 50,
|
||||
});
|
||||
|
||||
const formatCurrency = (amount, currency = 'RON') => {
|
||||
const formatCurrency = (amount, currency = "RON") => {
|
||||
if (!amount) return "0,00 " + currency;
|
||||
return new Intl.NumberFormat("ro-RO", {
|
||||
style: "currency",
|
||||
currency: currency
|
||||
currency: currency,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
@@ -168,12 +199,12 @@ const formatDate = (dateString) => {
|
||||
};
|
||||
|
||||
const getRowClass = (data) => {
|
||||
return data.tip_registru.includes('BANCA') ? 'bank-row' : 'cash-row';
|
||||
return data.tip_registru.includes("BANCA") ? "bank-row" : "cash-row";
|
||||
};
|
||||
|
||||
const getRegisterSeverity = (type) => {
|
||||
if (type.includes('BANCA')) return 'info';
|
||||
if (type.includes('CASA')) return 'warning';
|
||||
if (type.includes("BANCA")) return "info";
|
||||
if (type.includes("CASA")) return "warning";
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -191,7 +222,7 @@ const resetFilters = () => {
|
||||
filters.value = {
|
||||
dateFrom: null,
|
||||
dateTo: null,
|
||||
partnerName: ""
|
||||
partnerName: "",
|
||||
};
|
||||
loadData();
|
||||
};
|
||||
@@ -206,8 +237,8 @@ const loadData = async () => {
|
||||
{
|
||||
date_from: filters.value.dateFrom?.toISOString().split("T")[0],
|
||||
date_to: filters.value.dateTo?.toISOString().split("T")[0],
|
||||
partner_name: filters.value.partnerName
|
||||
}
|
||||
partner_name: filters.value.partnerName,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -38,8 +38,11 @@
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<label>Your Setting:</label>
|
||||
<InputSwitch v-model="userCacheEnabled" @change="toggleUserCache" />
|
||||
<span>{{ userCacheEnabled ? 'ON' : 'OFF' }}</span>
|
||||
<InputSwitch
|
||||
v-model="userCacheEnabled"
|
||||
@change="toggleUserCache"
|
||||
/>
|
||||
<span>{{ userCacheEnabled ? "ON" : "OFF" }}</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<label>Auto-Invalidation:</label>
|
||||
@@ -58,7 +61,10 @@
|
||||
<template #content>
|
||||
<div class="hit-rate">
|
||||
<h3>Hit Rate: {{ stats.hit_rate?.toFixed(1) }}%</h3>
|
||||
<p>{{ stats.total_hits }} hits / {{ stats.total_hits + stats.total_misses }} total requests</p>
|
||||
<p>
|
||||
{{ stats.total_hits }} hits /
|
||||
{{ stats.total_hits + stats.total_misses }} total requests
|
||||
</p>
|
||||
<ProgressBar :value="stats.hit_rate" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -70,13 +76,23 @@
|
||||
<template #content>
|
||||
<ul class="queries-list">
|
||||
<li>
|
||||
Today: <strong>{{ stats.queries_saved?.today?.toLocaleString() }}</strong> queries avoided
|
||||
Today:
|
||||
<strong>{{
|
||||
stats.queries_saved?.today?.toLocaleString()
|
||||
}}</strong>
|
||||
queries avoided
|
||||
</li>
|
||||
<li>
|
||||
This week: <strong>{{ stats.queries_saved?.week?.toLocaleString() }}</strong> queries avoided
|
||||
This week:
|
||||
<strong>{{ stats.queries_saved?.week?.toLocaleString() }}</strong>
|
||||
queries avoided
|
||||
</li>
|
||||
<li>
|
||||
All time: <strong>{{ stats.queries_saved?.total?.toLocaleString() }}</strong> queries avoided
|
||||
All time:
|
||||
<strong>{{
|
||||
stats.queries_saved?.total?.toLocaleString()
|
||||
}}</strong>
|
||||
queries avoided
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
@@ -102,8 +118,9 @@
|
||||
</DataTable>
|
||||
<div v-if="overallAvg" class="average-row">
|
||||
<strong>Overall Average:</strong>
|
||||
{{ overallAvg.cached }} ms vs {{ overallAvg.oracle }} ms
|
||||
({{ overallAvg.improvement }}% faster)
|
||||
{{ overallAvg.cached }} ms vs {{ overallAvg.oracle }} ms ({{
|
||||
overallAvg.improvement
|
||||
}}% faster)
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
@@ -113,9 +130,17 @@
|
||||
<template #title>Cache Details</template>
|
||||
<template #content>
|
||||
<ul class="details-list">
|
||||
<li>Memory entries: <strong>{{ stats.cache_size?.memory?.toLocaleString() }}</strong></li>
|
||||
<li>SQLite entries: <strong>{{ stats.cache_size?.sqlite?.toLocaleString() }}</strong></li>
|
||||
<li>Cache type: <strong>{{ stats.cache_type }}</strong></li>
|
||||
<li>
|
||||
Memory entries:
|
||||
<strong>{{ stats.cache_size?.memory?.toLocaleString() }}</strong>
|
||||
</li>
|
||||
<li>
|
||||
SQLite entries:
|
||||
<strong>{{ stats.cache_size?.sqlite?.toLocaleString() }}</strong>
|
||||
</li>
|
||||
<li>
|
||||
Cache type: <strong>{{ stats.cache_type }}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</Card>
|
||||
@@ -135,150 +160,162 @@
|
||||
<label for="clear_all">All companies</label>
|
||||
</div>
|
||||
<div class="p-field-radiobutton">
|
||||
<RadioButton id="clear_current" v-model="clearScope" value="current" />
|
||||
<RadioButton
|
||||
id="clear_current"
|
||||
v-model="clearScope"
|
||||
value="current"
|
||||
/>
|
||||
<label for="clear_current">Current company only</label>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="Cancel" text @click="showClearDialog = false" />
|
||||
<Button label="Clear" severity="danger" @click="clearCache" :loading="loading" />
|
||||
<Button
|
||||
label="Clear"
|
||||
severity="danger"
|
||||
@click="clearCache"
|
||||
:loading="loading"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useCacheStore } from '@/stores/cacheStore'
|
||||
import { useCompanyStore } from '@/stores/companies'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import Button from 'primevue/button'
|
||||
import Card from 'primevue/card'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Column from 'primevue/column'
|
||||
import Tag from 'primevue/tag'
|
||||
import ProgressBar from 'primevue/progressbar'
|
||||
import InputSwitch from 'primevue/inputswitch'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import RadioButton from 'primevue/radiobutton'
|
||||
import Message from 'primevue/message'
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useCacheStore } from "@/stores/cacheStore";
|
||||
import { useCompanyStore } from "@/stores/companies";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import Button from "primevue/button";
|
||||
import Card from "primevue/card";
|
||||
import DataTable from "primevue/datatable";
|
||||
import Column from "primevue/column";
|
||||
import Tag from "primevue/tag";
|
||||
import ProgressBar from "primevue/progressbar";
|
||||
import InputSwitch from "primevue/inputswitch";
|
||||
import Dialog from "primevue/dialog";
|
||||
import RadioButton from "primevue/radiobutton";
|
||||
import Message from "primevue/message";
|
||||
|
||||
const cacheStore = useCacheStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const toast = useToast()
|
||||
const cacheStore = useCacheStore();
|
||||
const companyStore = useCompanyStore();
|
||||
const toast = useToast();
|
||||
|
||||
const loading = computed(() => cacheStore.isLoading)
|
||||
const error = computed(() => cacheStore.error)
|
||||
const stats = computed(() => cacheStore.stats)
|
||||
const loading = computed(() => cacheStore.isLoading);
|
||||
const error = computed(() => cacheStore.error);
|
||||
const stats = computed(() => cacheStore.stats);
|
||||
|
||||
const userCacheEnabled = ref(true)
|
||||
const showClearDialog = ref(false)
|
||||
const clearScope = ref('current')
|
||||
const userCacheEnabled = ref(true);
|
||||
const showClearDialog = ref(false);
|
||||
const clearScope = ref("current");
|
||||
|
||||
const responseTimesTable = computed(() => {
|
||||
if (!stats.value?.response_times) return []
|
||||
if (!stats.value?.response_times) return [];
|
||||
|
||||
return Object.entries(stats.value.response_times).map(([key, data]) => ({
|
||||
endpoint: formatEndpointName(key),
|
||||
cached: data.cached,
|
||||
oracle: data.oracle,
|
||||
improvement: data.improvement
|
||||
}))
|
||||
})
|
||||
improvement: data.improvement,
|
||||
}));
|
||||
});
|
||||
|
||||
const overallAvg = computed(() => {
|
||||
const times = Object.values(stats.value?.response_times || {})
|
||||
if (times.length === 0) return null
|
||||
const times = Object.values(stats.value?.response_times || {});
|
||||
if (times.length === 0) return null;
|
||||
|
||||
const avgCached = times.reduce((sum, t) => sum + t.cached, 0) / times.length
|
||||
const avgOracle = times.reduce((sum, t) => sum + t.oracle, 0) / times.length
|
||||
const improvement = ((avgOracle - avgCached) / avgOracle * 100).toFixed(0)
|
||||
const avgCached = times.reduce((sum, t) => sum + t.cached, 0) / times.length;
|
||||
const avgOracle = times.reduce((sum, t) => sum + t.oracle, 0) / times.length;
|
||||
const improvement = (((avgOracle - avgCached) / avgOracle) * 100).toFixed(0);
|
||||
|
||||
return {
|
||||
cached: avgCached.toFixed(0),
|
||||
oracle: avgOracle.toFixed(0),
|
||||
improvement
|
||||
}
|
||||
})
|
||||
improvement,
|
||||
};
|
||||
});
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
await cacheStore.getStats()
|
||||
userCacheEnabled.value = stats.value?.user_enabled ?? true
|
||||
await cacheStore.getStats();
|
||||
userCacheEnabled.value = stats.value?.user_enabled ?? true;
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to load cache statistics',
|
||||
life: 3000
|
||||
})
|
||||
severity: "error",
|
||||
summary: "Error",
|
||||
detail: "Failed to load cache statistics",
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleUserCache() {
|
||||
try {
|
||||
await cacheStore.toggleUserCache(userCacheEnabled.value)
|
||||
await cacheStore.toggleUserCache(userCacheEnabled.value);
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Success',
|
||||
detail: `Cache ${userCacheEnabled.value ? 'enabled' : 'disabled'} for you`,
|
||||
life: 3000
|
||||
})
|
||||
severity: "success",
|
||||
summary: "Success",
|
||||
detail: `Cache ${userCacheEnabled.value ? "enabled" : "disabled"} for you`,
|
||||
life: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to toggle cache',
|
||||
life: 3000
|
||||
})
|
||||
severity: "error",
|
||||
summary: "Error",
|
||||
detail: "Failed to toggle cache",
|
||||
life: 3000,
|
||||
});
|
||||
// Revert toggle
|
||||
userCacheEnabled.value = !userCacheEnabled.value
|
||||
userCacheEnabled.value = !userCacheEnabled.value;
|
||||
}
|
||||
}
|
||||
|
||||
async function clearCache() {
|
||||
try {
|
||||
const companyId = clearScope.value === 'current' ? companyStore.currentCompany?.id_firma : null
|
||||
await cacheStore.invalidateCache(companyId, null)
|
||||
const companyId =
|
||||
clearScope.value === "current"
|
||||
? companyStore.currentCompany?.id_firma
|
||||
: null;
|
||||
await cacheStore.invalidateCache(companyId, null);
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Success',
|
||||
detail: 'Cache cleared successfully',
|
||||
life: 3000
|
||||
})
|
||||
severity: "success",
|
||||
summary: "Success",
|
||||
detail: "Cache cleared successfully",
|
||||
life: 3000,
|
||||
});
|
||||
|
||||
showClearDialog.value = false
|
||||
await loadStats()
|
||||
showClearDialog.value = false;
|
||||
await loadStats();
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Failed to clear cache',
|
||||
life: 3000
|
||||
})
|
||||
severity: "error",
|
||||
summary: "Error",
|
||||
detail: "Failed to clear cache",
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function formatEndpointName(key) {
|
||||
const names = {
|
||||
'schema': 'Schema Lookup',
|
||||
'dashboard_summary': 'Dashboard',
|
||||
'dashboard_trends': 'Dashboard Trends',
|
||||
'companies': 'Companies List',
|
||||
'invoices': 'Invoices',
|
||||
'treasury': 'Treasury'
|
||||
}
|
||||
return names[key] || key
|
||||
schema: "Schema Lookup",
|
||||
dashboard_summary: "Dashboard",
|
||||
dashboard_trends: "Dashboard Trends",
|
||||
companies: "Companies List",
|
||||
invoices: "Invoices",
|
||||
treasury: "Treasury",
|
||||
};
|
||||
return names[key] || key;
|
||||
}
|
||||
|
||||
function clearError() {
|
||||
cacheStore.clearError()
|
||||
cacheStore.clearError();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
})
|
||||
loadStats();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<main class="main-content">
|
||||
<div class="app-container">
|
||||
|
||||
<!-- Dashboard Header -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">{{ companyStore.selectedCompany?.name || 'Dashboard Principal' }}</h1>
|
||||
<h1 class="page-title">
|
||||
{{ companyStore.selectedCompany?.name || "Dashboard Principal" }}
|
||||
</h1>
|
||||
<p class="page-subtitle">{{ currentMonthLabel }}</p>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +13,6 @@
|
||||
|
||||
<!-- Secțiune Carduri Noi - Adăugare -->
|
||||
<div class="metrics-cards-section" v-if="!isLoading">
|
||||
|
||||
<!-- Rând 1: Metrici principale -->
|
||||
<div class="metrics-row">
|
||||
<TreasuryDualCard
|
||||
@@ -68,7 +68,6 @@
|
||||
@periodChanged="handleMaturityPeriodChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Content -->
|
||||
@@ -81,7 +80,6 @@
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Se încarcă datele dashboard-ului...</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
@@ -90,19 +88,19 @@
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
// Import componente noi
|
||||
import MetricCard from '../components/dashboard/cards/MetricCard.vue'
|
||||
import CashFlowMetricCard from '../components/dashboard/cards/CashFlowMetricCard.vue'
|
||||
import MaturityAndDetailsCard from '../components/dashboard/cards/MaturityAndDetailsCard.vue'
|
||||
import ClientiBalanceCard from '../components/dashboard/cards/ClientiBalanceCard.vue'
|
||||
import FurnizoriBalanceCard from '../components/dashboard/cards/FurnizoriBalanceCard.vue'
|
||||
import TreasuryDualCard from '../components/dashboard/cards/TreasuryDualCard.vue'
|
||||
import MetricCard from "../components/dashboard/cards/MetricCard.vue";
|
||||
import CashFlowMetricCard from "../components/dashboard/cards/CashFlowMetricCard.vue";
|
||||
import MaturityAndDetailsCard from "../components/dashboard/cards/MaturityAndDetailsCard.vue";
|
||||
import ClientiBalanceCard from "../components/dashboard/cards/ClientiBalanceCard.vue";
|
||||
import FurnizoriBalanceCard from "../components/dashboard/cards/FurnizoriBalanceCard.vue";
|
||||
import TreasuryDualCard from "../components/dashboard/cards/TreasuryDualCard.vue";
|
||||
import { useCompanyStore } from "../stores/companies";
|
||||
import { useDashboardStore } from "../stores/dashboard";
|
||||
import { apiService } from "../services/api";
|
||||
import {
|
||||
exportToExcel,
|
||||
exportToPDF,
|
||||
exportTrendData as prepareTrendData
|
||||
exportTrendData as prepareTrendData,
|
||||
} from "../utils/exportUtils";
|
||||
|
||||
const toast = useToast();
|
||||
@@ -114,315 +112,331 @@ 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)
|
||||
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');
|
||||
const selectedPeriod = ref("12m");
|
||||
const selectedChartType = ref("line");
|
||||
|
||||
// Handlers pentru schimbare perioadă
|
||||
const handleMaturityPeriodChange = (period) => {
|
||||
console.log('Maturity period changed:', 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
|
||||
if (!dashboardStore.trends?.raw) return null;
|
||||
|
||||
const raw = dashboardStore.trends.raw
|
||||
let data = []
|
||||
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':
|
||||
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))
|
||||
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
|
||||
break;
|
||||
case "inflows":
|
||||
data = raw.clienti_incasat || [];
|
||||
break;
|
||||
case "outflows":
|
||||
data = raw.furnizori_achitat || [];
|
||||
break;
|
||||
default:
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
// Trebuie să avem cel puțin 2 puncte de date pentru a calcula trend
|
||||
if (!data || data.length < 2) return null
|
||||
if (!data || data.length < 2) return null;
|
||||
|
||||
const current = Number(data[data.length - 1]) || 0
|
||||
const previous = Number(data[data.length - 2]) || 0
|
||||
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' }
|
||||
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'
|
||||
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 }
|
||||
}
|
||||
return { value: Math.abs(change), direction };
|
||||
};
|
||||
|
||||
// Obține date sparkline pentru mini grafice
|
||||
const getSparklineData = (metric) => {
|
||||
if (!dashboardStore.trends?.raw) {
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
|
||||
const raw = dashboardStore.trends.raw
|
||||
let data = []
|
||||
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':
|
||||
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))
|
||||
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
|
||||
break;
|
||||
case "inflows":
|
||||
data = raw.clienti_incasat || [];
|
||||
break;
|
||||
case "outflows":
|
||||
data = raw.furnizori_achitat || [];
|
||||
break;
|
||||
default:
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
|
||||
// Returnează ultimele 12 valori pentru sparkline
|
||||
const sparklineData = data.slice(-12).map(v => Number(v) || 0)
|
||||
return sparklineData
|
||||
}
|
||||
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 []
|
||||
return [];
|
||||
}
|
||||
|
||||
const periods = dashboardStore.trends.raw.periods
|
||||
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
|
||||
})
|
||||
}
|
||||
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 []
|
||||
return [];
|
||||
}
|
||||
|
||||
const raw = dashboardStore.trends.raw
|
||||
let data = []
|
||||
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
|
||||
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 []
|
||||
return [];
|
||||
}
|
||||
|
||||
// Returnează ultimele 12 valori pentru sparkline (aceeași perioadă, anul anterior)
|
||||
const sparklineData = data.slice(-12).map(v => Number(v) || 0)
|
||||
return sparklineData
|
||||
}
|
||||
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 []
|
||||
return [];
|
||||
}
|
||||
|
||||
const periods = dashboardStore.trends.raw.previous_periods
|
||||
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
|
||||
})
|
||||
}
|
||||
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 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 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())
|
||||
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
|
||||
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
|
||||
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
|
||||
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 tTrend = calculateTrend("treasury");
|
||||
return tTrend ? { ...tTrend } : null;
|
||||
});
|
||||
|
||||
const casaSparkline = computed(() => {
|
||||
// Use treasury sparkline data scaled by casa proportion
|
||||
if (!treasuryData.value?.breakdown) return []
|
||||
if (!treasuryData.value?.breakdown) return [];
|
||||
|
||||
const sparklineData = getSparklineData('treasury')
|
||||
if (!sparklineData.length) 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
|
||||
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)
|
||||
if (totalTreasury === 0) return sparklineData.map(() => 0);
|
||||
|
||||
const casaProportion = casaTotal / totalTreasury
|
||||
return sparklineData.map(v => v * casaProportion)
|
||||
})
|
||||
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
|
||||
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
|
||||
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
|
||||
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 tTrend = calculateTrend("treasury");
|
||||
return tTrend ? { ...tTrend } : null;
|
||||
});
|
||||
|
||||
const bancaSparkline = computed(() => {
|
||||
// Use treasury sparkline data scaled by banca proportion
|
||||
if (!treasuryData.value?.breakdown) return []
|
||||
if (!treasuryData.value?.breakdown) return [];
|
||||
|
||||
const sparklineData = getSparklineData('treasury')
|
||||
if (!sparklineData.length) 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
|
||||
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)
|
||||
if (totalTreasury === 0) return sparklineData.map(() => 0);
|
||||
|
||||
const bancaProportion = bancaTotal / totalTreasury
|
||||
return sparklineData.map(v => v * bancaProportion)
|
||||
})
|
||||
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 []
|
||||
if (!treasuryData.value?.breakdown) return [];
|
||||
|
||||
const previousSparklineData = getPreviousSparklineData('treasury')
|
||||
if (!previousSparklineData.length) 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
|
||||
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)
|
||||
if (totalTreasury === 0) return previousSparklineData.map(() => 0);
|
||||
|
||||
const casaProportion = casaTotal / totalTreasury
|
||||
return previousSparklineData.map(v => v * casaProportion)
|
||||
})
|
||||
const casaProportion = casaTotal / totalTreasury;
|
||||
return previousSparklineData.map((v) => v * casaProportion);
|
||||
});
|
||||
|
||||
const bancaPreviousSparkline = computed(() => {
|
||||
if (!treasuryData.value?.breakdown) return []
|
||||
if (!treasuryData.value?.breakdown) return [];
|
||||
|
||||
const previousSparklineData = getPreviousSparklineData('treasury')
|
||||
if (!previousSparklineData.length) 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
|
||||
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)
|
||||
if (totalTreasury === 0) return previousSparklineData.map(() => 0);
|
||||
|
||||
const bancaProportion = bancaTotal / totalTreasury
|
||||
return previousSparklineData.map(v => v * bancaProportion)
|
||||
})
|
||||
const bancaProportion = bancaTotal / totalTreasury;
|
||||
return previousSparklineData.map((v) => v * bancaProportion);
|
||||
});
|
||||
|
||||
// Detectare mobile
|
||||
const isMobile = computed(() => window.innerWidth < 768)
|
||||
const isMobile = computed(() => window.innerWidth < 768);
|
||||
|
||||
// Computed property pentru luna curentă Oracle formatată în română
|
||||
const currentMonthLabel = computed(() => {
|
||||
if (!dashboardStore.currentPeriod) {
|
||||
return 'Se încarcă...'
|
||||
return "Se încarcă...";
|
||||
}
|
||||
|
||||
const { year, month } = dashboardStore.currentPeriod
|
||||
const { year, month } = dashboardStore.currentPeriod;
|
||||
|
||||
// Crează un obiect Date pentru a formata luna în română
|
||||
const date = new Date(year, month - 1, 1)
|
||||
const date = new Date(year, month - 1, 1);
|
||||
|
||||
// Formatează luna în română: "Octombrie 2025"
|
||||
return date.toLocaleDateString('ro-RO', { month: 'long', year: 'numeric' })
|
||||
})
|
||||
return date.toLocaleDateString("ro-RO", { month: "long", year: "numeric" });
|
||||
});
|
||||
|
||||
// Methods
|
||||
const handleCompanyChanged = async (company) => {
|
||||
@@ -433,9 +447,9 @@ const handleCompanyChanged = async (company) => {
|
||||
};
|
||||
|
||||
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';
|
||||
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", {
|
||||
@@ -448,31 +462,34 @@ const formatCurrency = (amount) => {
|
||||
};
|
||||
|
||||
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';
|
||||
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');
|
||||
console.warn("No company selected for trend data loading");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Loading trend data for company:', companyStore.selectedCompany.id_firma);
|
||||
console.log(
|
||||
"Loading trend data for company:",
|
||||
companyStore.selectedCompany.id_firma,
|
||||
);
|
||||
|
||||
const result = await dashboardStore.loadTrendData(
|
||||
companyStore.selectedCompany.id_firma,
|
||||
selectedPeriod.value,
|
||||
selectedChartType.value
|
||||
selectedChartType.value,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
console.log('Trend data loaded successfully:', result.data);
|
||||
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);
|
||||
console.error("Failed to load trend data:", result.error);
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "Eroare la încărcarea datelor",
|
||||
@@ -484,60 +501,68 @@ const loadTrendData = async () => {
|
||||
|
||||
// Export Trend Data to Excel
|
||||
const exportTrendExcel = () => {
|
||||
const data = prepareTrendData(dashboardStore.trends, selectedPeriod.value, selectedChartType.value);
|
||||
const result = exportToExcel(data, 'date_trend', 'Date Trend');
|
||||
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
|
||||
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
|
||||
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);
|
||||
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
|
||||
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 => ({
|
||||
const columns = Object.keys(data[0]).map((key) => ({
|
||||
field: key,
|
||||
header: key,
|
||||
type: key === 'Perioada' ? 'text' : 'currency'
|
||||
type: key === "Perioada" ? "text" : "currency",
|
||||
}));
|
||||
|
||||
const result = exportToPDF(
|
||||
data,
|
||||
columns,
|
||||
'date_trend',
|
||||
`Date Trend (${selectedPeriod.value}) - ${companyStore.selectedCompany?.name || 'ROA Reports'}`
|
||||
"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
|
||||
severity: "success",
|
||||
summary: "Export Reușit",
|
||||
detail: "Fișier PDF generat cu succes",
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -554,45 +579,52 @@ const refreshTrendData = async () => {
|
||||
|
||||
const getLastTrendValue = (datasetLabel) => {
|
||||
if (!dashboardStore.trends?.datasets) {
|
||||
console.warn('No trends data available for', datasetLabel);
|
||||
console.warn("No trends data available for", datasetLabel);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const dataset = dashboardStore.trends.datasets.find(d => d.label === datasetLabel);
|
||||
const dataset = dashboardStore.trends.datasets.find(
|
||||
(d) => d.label === datasetLabel,
|
||||
);
|
||||
if (!dataset?.data?.length) {
|
||||
console.warn('No dataset or data found for', datasetLabel);
|
||||
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;
|
||||
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%';
|
||||
console.warn("No trends data available for trend change calculation");
|
||||
return "0%";
|
||||
}
|
||||
|
||||
const dataset = dashboardStore.trends.datasets.find(d => d.label === datasetLabel);
|
||||
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%';
|
||||
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 ? '+∞%' : '-∞%';
|
||||
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%';
|
||||
if (!isFinite(change)) return "0%";
|
||||
|
||||
const sign = change > 0 ? '+' : '';
|
||||
const sign = change > 0 ? "+" : "";
|
||||
return `${sign}${change.toFixed(1)}%`;
|
||||
};
|
||||
|
||||
@@ -600,43 +632,48 @@ const getTrendChangeClass = (datasetLabel) => {
|
||||
const changeStr = getTrendChange(datasetLabel);
|
||||
|
||||
// Handle infinite cases
|
||||
if (changeStr.includes('∞')) return 'neutral';
|
||||
if (changeStr.includes("∞")) return "neutral";
|
||||
|
||||
const change = parseFloat(changeStr.replace('%', ''));
|
||||
const change = parseFloat(changeStr.replace("%", ""));
|
||||
|
||||
// Handle NaN cases
|
||||
if (isNaN(change)) return 'neutral';
|
||||
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';
|
||||
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';
|
||||
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();
|
||||
const query = (event?.query || "").toLowerCase();
|
||||
|
||||
// Ensure companyListFormatted exists and has valid data
|
||||
if (!companyStore.companyListFormatted || companyStore.companyListFormatted.length === 0) {
|
||||
if (
|
||||
!companyStore.companyListFormatted ||
|
||||
companyStore.companyListFormatted.length === 0
|
||||
) {
|
||||
filteredCompanies.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
filteredCompanies.value = companyStore.companyListFormatted.filter(company => {
|
||||
filteredCompanies.value = companyStore.companyListFormatted.filter(
|
||||
(company) => {
|
||||
// Ensure displayName exists before using includes
|
||||
const displayName = company?.displayName || '';
|
||||
const displayName = company?.displayName || "";
|
||||
return displayName.toLowerCase().includes(query);
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleCompanySelect = async (event) => {
|
||||
@@ -655,13 +692,13 @@ const loadMonthlyFlows = async () => {
|
||||
if (!companyStore.selectedCompany) return;
|
||||
|
||||
try {
|
||||
const response = await apiService.get('/dashboard/monthly-flows', {
|
||||
params: { company: companyStore.selectedCompany.id_firma }
|
||||
const response = await apiService.get("/dashboard/monthly-flows", {
|
||||
params: { company: companyStore.selectedCompany.id_firma },
|
||||
});
|
||||
monthlyInflows.value = response.data.inflows || 0;
|
||||
monthlyOutflows.value = response.data.outflows || 0;
|
||||
} catch (error) {
|
||||
console.error('Failed to load monthly flows:', error);
|
||||
console.error("Failed to load monthly flows:", error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -669,12 +706,12 @@ const loadTreasuryBreakdown = async () => {
|
||||
if (!companyStore.selectedCompany) return;
|
||||
|
||||
try {
|
||||
const response = await apiService.get('/dashboard/treasury-breakdown', {
|
||||
params: { company: companyStore.selectedCompany.id_firma }
|
||||
const response = await apiService.get("/dashboard/treasury-breakdown", {
|
||||
params: { company: companyStore.selectedCompany.id_firma },
|
||||
});
|
||||
treasuryData.value = response.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to load treasury breakdown:', error);
|
||||
console.error("Failed to load treasury breakdown:", error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -682,8 +719,8 @@ const loadNetBalanceBreakdown = async () => {
|
||||
if (!companyStore.selectedCompany) return;
|
||||
|
||||
try {
|
||||
const response = await apiService.get('/dashboard/net-balance-breakdown', {
|
||||
params: { company: companyStore.selectedCompany.id_firma }
|
||||
const response = await apiService.get("/dashboard/net-balance-breakdown", {
|
||||
params: { company: companyStore.selectedCompany.id_firma },
|
||||
});
|
||||
|
||||
// Folosește direct datele structurate de la backend
|
||||
@@ -694,23 +731,23 @@ const loadNetBalanceBreakdown = async () => {
|
||||
clienti: {
|
||||
total: 0,
|
||||
in_termen: { total: 0 },
|
||||
restant: { total: 0, perioade: {} }
|
||||
restant: { total: 0, perioade: {} },
|
||||
},
|
||||
furnizori: {
|
||||
total: 0,
|
||||
in_termen: { total: 0 },
|
||||
restant: { total: 0, perioade: {} }
|
||||
}
|
||||
}
|
||||
restant: { total: 0, perioade: {} },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
console.log('[NetBalance] Loaded balance data:', {
|
||||
console.log("[NetBalance] Loaded balance data:", {
|
||||
clienti_total: netBalanceData.value.clienti_total,
|
||||
furnizori_total: netBalanceData.value.furnizori_total,
|
||||
breakdown: netBalanceData.value.breakdown
|
||||
breakdown: netBalanceData.value.breakdown,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load net balance breakdown:', error);
|
||||
console.error("Failed to load net balance breakdown:", error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -720,12 +757,14 @@ const loadDashboardData = async () => {
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
dashboardStore.loadDashboardSummary(companyStore.selectedCompany.id_firma),
|
||||
dashboardStore.loadDashboardSummary(
|
||||
companyStore.selectedCompany.id_firma,
|
||||
),
|
||||
dashboardStore.loadCurrentPeriod(companyStore.selectedCompany.id_firma),
|
||||
loadTrendData(),
|
||||
loadMonthlyFlows(),
|
||||
loadTreasuryBreakdown(),
|
||||
loadNetBalanceBreakdown()
|
||||
loadNetBalanceBreakdown(),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Failed to load dashboard data:", error);
|
||||
@@ -747,12 +786,12 @@ const refreshData = async () => {
|
||||
|
||||
const exportData = () => {
|
||||
// Export functionality can be implemented for different sections
|
||||
console.log('Export data triggered');
|
||||
console.log("Export data triggered");
|
||||
};
|
||||
|
||||
const searchData = () => {
|
||||
// Focus on the detail filter input
|
||||
const filterInput = document.querySelector('.detail-input');
|
||||
const filterInput = document.querySelector(".detail-input");
|
||||
if (filterInput) {
|
||||
filterInput.focus();
|
||||
}
|
||||
@@ -771,7 +810,9 @@ onMounted(async () => {
|
||||
|
||||
// Check for saved company and verify it exists in loaded companies
|
||||
if (companyStore.selectedCompany) {
|
||||
const exists = companyStore.getCompanyById(companyStore.selectedCompany.id_firma);
|
||||
const exists = companyStore.getCompanyById(
|
||||
companyStore.selectedCompany.id_firma,
|
||||
);
|
||||
if (exists) {
|
||||
// Update with fresh company data from API
|
||||
companyStore.setSelectedCompany(exists);
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
|
||||
<Column field="cont" header="Cont" sortable>
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.cont || '-' }}
|
||||
{{ slotProps.data.cont || "-" }}
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
@@ -241,10 +241,15 @@
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="valuta" header="Valuta" sortable :style="{ width: '8%' }">
|
||||
<Column
|
||||
field="valuta"
|
||||
header="Valuta"
|
||||
sortable
|
||||
:style="{ width: '8%' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<div class="text-center">
|
||||
{{ slotProps.data.valuta || 'RON' }}
|
||||
{{ slotProps.data.valuta || "RON" }}
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
@@ -253,7 +258,9 @@
|
||||
<!-- Total Sold -->
|
||||
<div v-if="invoicesStore.hasInvoices" class="total-sold">
|
||||
<span class="total-sold-label">Total Sold:</span>
|
||||
<span class="total-sold-value">{{ formatCurrency(totalSold) }}</span>
|
||||
<span class="total-sold-value">{{
|
||||
formatCurrency(totalSold)
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
@@ -300,8 +307,18 @@ const totalSold = computed(() => {
|
||||
|
||||
const accountingPeriodText = computed(() => {
|
||||
const months = [
|
||||
"Ianuarie", "Februarie", "Martie", "Aprilie", "Mai", "Iunie",
|
||||
"Iulie", "August", "Septembrie", "Octombrie", "Noiembrie", "Decembrie"
|
||||
"Ianuarie",
|
||||
"Februarie",
|
||||
"Martie",
|
||||
"Aprilie",
|
||||
"Mai",
|
||||
"Iunie",
|
||||
"Iulie",
|
||||
"August",
|
||||
"Septembrie",
|
||||
"Octombrie",
|
||||
"Noiembrie",
|
||||
"Decembrie",
|
||||
];
|
||||
const luna = invoicesStore.accountingPeriod.luna;
|
||||
const an = invoicesStore.accountingPeriod.an;
|
||||
@@ -414,14 +431,20 @@ const loadInvoices = async () => {
|
||||
// Add optional filters (use LOCAL date, not UTC)
|
||||
if (filters.value.dateFrom) {
|
||||
const year = filters.value.dateFrom.getFullYear();
|
||||
const month = String(filters.value.dateFrom.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(filters.value.dateFrom.getDate()).padStart(2, '0');
|
||||
const month = String(filters.value.dateFrom.getMonth() + 1).padStart(
|
||||
2,
|
||||
"0",
|
||||
);
|
||||
const day = String(filters.value.dateFrom.getDate()).padStart(2, "0");
|
||||
params.date_from = `${year}-${month}-${day}`;
|
||||
}
|
||||
if (filters.value.dateTo) {
|
||||
const year = filters.value.dateTo.getFullYear();
|
||||
const month = String(filters.value.dateTo.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(filters.value.dateTo.getDate()).padStart(2, '0');
|
||||
const month = String(filters.value.dateTo.getMonth() + 1).padStart(
|
||||
2,
|
||||
"0",
|
||||
);
|
||||
const day = String(filters.value.dateTo.getDate()).padStart(2, "0");
|
||||
params.date_to = `${year}-${month}-${day}`;
|
||||
}
|
||||
if (filters.value.searchTerm) {
|
||||
@@ -431,7 +454,10 @@ const loadInvoices = async () => {
|
||||
params.cont = filters.value.cont;
|
||||
}
|
||||
|
||||
await invoicesStore.loadInvoices(companyStore.selectedCompany.id_firma, params);
|
||||
await invoicesStore.loadInvoices(
|
||||
companyStore.selectedCompany.id_firma,
|
||||
params,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to load invoices:", error);
|
||||
toast.add({
|
||||
@@ -471,14 +497,20 @@ const fetchAllInvoicesData = async () => {
|
||||
// Add optional filters (use LOCAL date, not UTC)
|
||||
if (filters.value.dateFrom) {
|
||||
const year = filters.value.dateFrom.getFullYear();
|
||||
const month = String(filters.value.dateFrom.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(filters.value.dateFrom.getDate()).padStart(2, '0');
|
||||
const month = String(filters.value.dateFrom.getMonth() + 1).padStart(
|
||||
2,
|
||||
"0",
|
||||
);
|
||||
const day = String(filters.value.dateFrom.getDate()).padStart(2, "0");
|
||||
params.date_from = `${year}-${month}-${day}`;
|
||||
}
|
||||
if (filters.value.dateTo) {
|
||||
const year = filters.value.dateTo.getFullYear();
|
||||
const month = String(filters.value.dateTo.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(filters.value.dateTo.getDate()).padStart(2, '0');
|
||||
const month = String(filters.value.dateTo.getMonth() + 1).padStart(
|
||||
2,
|
||||
"0",
|
||||
);
|
||||
const day = String(filters.value.dateTo.getDate()).padStart(2, "0");
|
||||
params.date_to = `${year}-${month}-${day}`;
|
||||
}
|
||||
if (filters.value.searchTerm) {
|
||||
@@ -531,22 +563,23 @@ const exportExcel = async () => {
|
||||
|
||||
// Prepare data for export - Format dates as strings for Excel
|
||||
const exportData = allData.map((row) => ({
|
||||
"Cont": row.cont || "",
|
||||
Cont: row.cont || "",
|
||||
"Numar Doc.": row.nract,
|
||||
"Data Doc.": row.dataact ? formatDate(row.dataact) : "",
|
||||
"Data Scadenta": row.datascad ? formatDate(row.datascad) : "",
|
||||
"Partener": row.nume,
|
||||
"Facturat": parseFloat(row.totctva) || 0,
|
||||
"Achitat": parseFloat(row.achitat) || 0,
|
||||
"Sold": parseFloat(row.soldfinal) || 0,
|
||||
"Valuta": row.valuta || "RON",
|
||||
Partener: row.nume,
|
||||
Facturat: parseFloat(row.totctva) || 0,
|
||||
Achitat: parseFloat(row.achitat) || 0,
|
||||
Sold: parseFloat(row.soldfinal) || 0,
|
||||
Valuta: row.valuta || "RON",
|
||||
}));
|
||||
|
||||
const invoiceType = filters.value.type === "CLIENTI" ? "Clienti" : "Furnizori";
|
||||
const invoiceType =
|
||||
filters.value.type === "CLIENTI" ? "Clienti" : "Furnizori";
|
||||
const result = exportToExcel(
|
||||
exportData,
|
||||
`facturi_${invoiceType}_${companyStore.selectedCompany.name.replace(/\s+/g, "_")}`,
|
||||
`Facturi ${invoiceType}`
|
||||
`Facturi ${invoiceType}`,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
@@ -624,16 +657,23 @@ const exportPDF = async () => {
|
||||
{ field: "valuta", header: "Valuta", type: "text", width: 0.05 }, // 5% - Very compact (just "RON")
|
||||
];
|
||||
|
||||
const invoiceType = filters.value.type === "CLIENTI" ? "Clienti" : "Furnizori";
|
||||
const invoiceType =
|
||||
filters.value.type === "CLIENTI" ? "Clienti" : "Furnizori";
|
||||
|
||||
// Build period string - ALWAYS show accounting period (like Trial Balance)
|
||||
let periodText = accountingPeriodText.value || "";
|
||||
|
||||
// Optionally add date filter range if applied
|
||||
if (filters.value.dateFrom || filters.value.dateTo) {
|
||||
const fromDate = filters.value.dateFrom ? formatDate(filters.value.dateFrom) : "început";
|
||||
const toDate = filters.value.dateTo ? formatDate(filters.value.dateTo) : "prezent";
|
||||
periodText += periodText ? ` | Filtru dată: ${fromDate} - ${toDate}` : `Filtru dată: ${fromDate} - ${toDate}`;
|
||||
const fromDate = filters.value.dateFrom
|
||||
? formatDate(filters.value.dateFrom)
|
||||
: "început";
|
||||
const toDate = filters.value.dateTo
|
||||
? formatDate(filters.value.dateTo)
|
||||
: "prezent";
|
||||
periodText += periodText
|
||||
? ` | Filtru dată: ${fromDate} - ${toDate}`
|
||||
: `Filtru dată: ${fromDate} - ${toDate}`;
|
||||
}
|
||||
|
||||
const result = exportToPDF(
|
||||
@@ -643,8 +683,8 @@ const exportPDF = async () => {
|
||||
{
|
||||
companyName: companyStore.selectedCompany?.name || "",
|
||||
title: `Facturi ${invoiceType}`,
|
||||
period: periodText
|
||||
}
|
||||
period: periodText,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
|
||||
@@ -13,12 +13,14 @@
|
||||
<template #content>
|
||||
<form @submit.prevent="handleLogin" class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="username" class="form-label required">Utilizator</label>
|
||||
<label for="username" class="form-label required"
|
||||
>Utilizator</label
|
||||
>
|
||||
<InputText
|
||||
id="username"
|
||||
v-model="credentials.username"
|
||||
placeholder="Introduceți numele de utilizator"
|
||||
:class="{ 'invalid': formErrors.username }"
|
||||
:class="{ invalid: formErrors.username }"
|
||||
class="w-full"
|
||||
autocomplete="username"
|
||||
@blur="validateField('username')"
|
||||
@@ -34,7 +36,7 @@
|
||||
id="password"
|
||||
v-model="credentials.password"
|
||||
placeholder="Introduceți parola"
|
||||
:class="{ 'invalid': formErrors.password }"
|
||||
:class="{ invalid: formErrors.password }"
|
||||
class="w-full"
|
||||
:feedback="false"
|
||||
toggle-mask
|
||||
@@ -192,7 +194,11 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--color-primary-light) 0%, var(--color-primary) 100%);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--color-primary-light) 0%,
|
||||
var(--color-primary) 100%
|
||||
);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<main class="main-content">
|
||||
<div class="app-container">
|
||||
|
||||
<!-- Page Header -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Telegram Bot</h1>
|
||||
<p class="page-subtitle">Conectează-ți contul pentru acces rapid din Telegram</p>
|
||||
<p class="page-subtitle">
|
||||
Conectează-ți contul pentru acces rapid din Telegram
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
@@ -16,7 +17,6 @@
|
||||
|
||||
<!-- Main Card -->
|
||||
<div v-else class="card">
|
||||
|
||||
<!-- Generate Button -->
|
||||
<div class="generate-section">
|
||||
<button
|
||||
@@ -24,13 +24,12 @@
|
||||
:disabled="loading"
|
||||
class="btn btn-primary btn-lg"
|
||||
>
|
||||
{{ loading ? 'Se generează...' : 'Generează Cod' }}
|
||||
{{ loading ? "Se generează..." : "Generează Cod" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Code Display & Actions -->
|
||||
<div v-if="linkingCode" class="code-section">
|
||||
|
||||
<!-- Code Display -->
|
||||
<div class="code-display">
|
||||
<div class="code-header">
|
||||
@@ -68,125 +67,123 @@
|
||||
<div v-if="showQR" class="qr-section">
|
||||
<QRCodeVue :value="telegramDeepLink" :size="200" level="H" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import Button from 'primevue/button'
|
||||
import Toast from 'primevue/toast'
|
||||
import QRCodeVue from 'qrcode.vue'
|
||||
import { apiService } from '../services/api'
|
||||
import { ref, computed, onUnmounted } from "vue";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import Button from "primevue/button";
|
||||
import Toast from "primevue/toast";
|
||||
import QRCodeVue from "qrcode.vue";
|
||||
import { apiService } from "../services/api";
|
||||
|
||||
const toast = useToast()
|
||||
const toast = useToast();
|
||||
|
||||
// State
|
||||
const linkingCode = ref('')
|
||||
const timeRemaining = ref(0)
|
||||
const loading = ref(false)
|
||||
const showQR = ref(false)
|
||||
const linkingCode = ref("");
|
||||
const timeRemaining = ref(0);
|
||||
const loading = ref(false);
|
||||
const showQR = ref(false);
|
||||
|
||||
let countdownInterval = null
|
||||
let countdownInterval = null;
|
||||
|
||||
// Config
|
||||
const BOT_USERNAME = import.meta.env.VITE_TELEGRAM_BOT_USERNAME || 'roa2web_bot'
|
||||
const BOT_USERNAME =
|
||||
import.meta.env.VITE_TELEGRAM_BOT_USERNAME || "roa2web_bot";
|
||||
|
||||
// Computed
|
||||
const telegramDeepLink = computed(() => {
|
||||
if (!linkingCode.value) return ''
|
||||
return `https://t.me/${BOT_USERNAME}?start=${linkingCode.value}`
|
||||
})
|
||||
if (!linkingCode.value) return "";
|
||||
return `https://t.me/${BOT_USERNAME}?start=${linkingCode.value}`;
|
||||
});
|
||||
|
||||
// Methods
|
||||
const generateCode = async () => {
|
||||
loading.value = true
|
||||
showQR.value = false
|
||||
loading.value = true;
|
||||
showQR.value = false;
|
||||
|
||||
try {
|
||||
const response = await apiService.post('/telegram/auth/generate-code')
|
||||
linkingCode.value = response.data.linking_code
|
||||
timeRemaining.value = response.data.expires_in_minutes * 60
|
||||
const response = await apiService.post("/telegram/auth/generate-code");
|
||||
linkingCode.value = response.data.linking_code;
|
||||
timeRemaining.value = response.data.expires_in_minutes * 60;
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Cod Generat',
|
||||
detail: 'Alege o metodă de conectare',
|
||||
life: 3000
|
||||
})
|
||||
severity: "success",
|
||||
summary: "Cod Generat",
|
||||
detail: "Alege o metodă de conectare",
|
||||
life: 3000,
|
||||
});
|
||||
|
||||
startCountdown()
|
||||
startCountdown();
|
||||
} catch (error) {
|
||||
console.error('Error generating code:', error)
|
||||
console.error("Error generating code:", error);
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Eroare',
|
||||
detail: error.response?.data?.detail || 'Nu am putut genera codul',
|
||||
life: 5000
|
||||
})
|
||||
severity: "error",
|
||||
summary: "Eroare",
|
||||
detail: error.response?.data?.detail || "Nu am putut genera codul",
|
||||
life: 5000,
|
||||
});
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startCountdown = () => {
|
||||
if (countdownInterval) clearInterval(countdownInterval)
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
|
||||
countdownInterval = setInterval(() => {
|
||||
if (timeRemaining.value > 0) {
|
||||
timeRemaining.value--
|
||||
timeRemaining.value--;
|
||||
} else {
|
||||
clearInterval(countdownInterval)
|
||||
linkingCode.value = ''
|
||||
clearInterval(countdownInterval);
|
||||
linkingCode.value = "";
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: 'Cod Expirat',
|
||||
detail: 'Generează un cod nou',
|
||||
life: 4000
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
severity: "warn",
|
||||
summary: "Cod Expirat",
|
||||
detail: "Generează un cod nou",
|
||||
life: 4000,
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const formatTime = (seconds) => {
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${minutes}:${secs.toString().padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
const copyCode = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(linkingCode.value)
|
||||
await navigator.clipboard.writeText(linkingCode.value);
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Copiat',
|
||||
detail: 'Cod copiat în clipboard',
|
||||
life: 2000
|
||||
})
|
||||
severity: "success",
|
||||
summary: "Copiat",
|
||||
detail: "Cod copiat în clipboard",
|
||||
life: 2000,
|
||||
});
|
||||
} catch (error) {
|
||||
const tempInput = document.createElement('input')
|
||||
tempInput.value = linkingCode.value
|
||||
document.body.appendChild(tempInput)
|
||||
tempInput.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(tempInput)
|
||||
const tempInput = document.createElement("input");
|
||||
tempInput.value = linkingCode.value;
|
||||
document.body.appendChild(tempInput);
|
||||
tempInput.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(tempInput);
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Copiat',
|
||||
life: 2000
|
||||
})
|
||||
}
|
||||
severity: "success",
|
||||
summary: "Copiat",
|
||||
life: 2000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
if (countdownInterval) clearInterval(countdownInterval)
|
||||
})
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -214,7 +211,11 @@ onUnmounted(() => {
|
||||
|
||||
/* Code Display */
|
||||
.code-display {
|
||||
background: linear-gradient(135deg, rgba(67, 97, 238, 0.08), rgba(67, 97, 238, 0.02));
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(67, 97, 238, 0.08),
|
||||
rgba(67, 97, 238, 0.02)
|
||||
);
|
||||
border: 2px solid var(--color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-md);
|
||||
@@ -237,7 +238,7 @@ onUnmounted(() => {
|
||||
.code-timer {
|
||||
color: var(--color-primary);
|
||||
font-weight: var(--font-bold);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-family: "Courier New", monospace;
|
||||
}
|
||||
|
||||
.code-value {
|
||||
@@ -245,7 +246,7 @@ onUnmounted(() => {
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--color-primary);
|
||||
letter-spacing: 0.3em;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-family: "Courier New", monospace;
|
||||
}
|
||||
|
||||
/* Action Buttons - Use global .btn patterns */
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
Balanță de Verificare
|
||||
</h1>
|
||||
<p class="page-subtitle">
|
||||
{{ currentPeriodText }} - {{ companyStore.selectedCompany?.name || "Selectați companie" }}
|
||||
{{ currentPeriodText }} -
|
||||
{{ companyStore.selectedCompany?.name || "Selectați companie" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -128,47 +129,99 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column field="cont" header="Cont" sortable :style="{ width: '8%' }">
|
||||
<Column
|
||||
field="cont"
|
||||
header="Cont"
|
||||
sortable
|
||||
:style="{ width: '8%' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<strong>{{ slotProps.data.cont }}</strong>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="denumire" header="Denumire Cont" sortable :style="{ width: '20%' }" />
|
||||
<Column
|
||||
field="denumire"
|
||||
header="Denumire Cont"
|
||||
sortable
|
||||
:style="{ width: '20%' }"
|
||||
/>
|
||||
|
||||
<Column field="sold_precedent_debit" header="Sold Prec. D" sortable :style="{ width: '10%' }">
|
||||
<Column
|
||||
field="sold_precedent_debit"
|
||||
header="Sold Prec. D"
|
||||
sortable
|
||||
:style="{ width: '10%' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<div class="text-right">{{ formatCurrency(slotProps.data.sold_precedent_debit) }}</div>
|
||||
<div class="text-right">
|
||||
{{ formatCurrency(slotProps.data.sold_precedent_debit) }}
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="sold_precedent_credit" header="Sold Prec. C" sortable :style="{ width: '10%' }">
|
||||
<Column
|
||||
field="sold_precedent_credit"
|
||||
header="Sold Prec. C"
|
||||
sortable
|
||||
:style="{ width: '10%' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<div class="text-right">{{ formatCurrency(slotProps.data.sold_precedent_credit) }}</div>
|
||||
<div class="text-right">
|
||||
{{ formatCurrency(slotProps.data.sold_precedent_credit) }}
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="rulaj_lunar_debit" header="Rulaj D" sortable :style="{ width: '10%' }">
|
||||
<Column
|
||||
field="rulaj_lunar_debit"
|
||||
header="Rulaj D"
|
||||
sortable
|
||||
:style="{ width: '10%' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<div class="text-right">{{ formatCurrency(slotProps.data.rulaj_lunar_debit) }}</div>
|
||||
<div class="text-right">
|
||||
{{ formatCurrency(slotProps.data.rulaj_lunar_debit) }}
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="rulaj_lunar_credit" header="Rulaj C" sortable :style="{ width: '10%' }">
|
||||
<Column
|
||||
field="rulaj_lunar_credit"
|
||||
header="Rulaj C"
|
||||
sortable
|
||||
:style="{ width: '10%' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<div class="text-right">{{ formatCurrency(slotProps.data.rulaj_lunar_credit) }}</div>
|
||||
<div class="text-right">
|
||||
{{ formatCurrency(slotProps.data.rulaj_lunar_credit) }}
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="sold_final_debit" header="Sold Final D" sortable :style="{ width: '11%' }">
|
||||
<Column
|
||||
field="sold_final_debit"
|
||||
header="Sold Final D"
|
||||
sortable
|
||||
:style="{ width: '11%' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<div class="text-right">{{ formatCurrency(slotProps.data.sold_final_debit) }}</div>
|
||||
<div class="text-right">
|
||||
{{ formatCurrency(slotProps.data.sold_final_debit) }}
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="sold_final_credit" header="Sold Final C" sortable :style="{ width: '11%' }">
|
||||
<Column
|
||||
field="sold_final_credit"
|
||||
header="Sold Final C"
|
||||
sortable
|
||||
:style="{ width: '11%' }"
|
||||
>
|
||||
<template #body="slotProps">
|
||||
<div class="text-right">{{ formatCurrency(slotProps.data.sold_final_credit) }}</div>
|
||||
<div class="text-right">
|
||||
{{ formatCurrency(slotProps.data.sold_final_credit) }}
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
@@ -200,8 +253,18 @@ const localFilters = ref({
|
||||
// Computed
|
||||
const currentPeriodText = computed(() => {
|
||||
const months = [
|
||||
"Ianuarie", "Februarie", "Martie", "Aprilie", "Mai", "Iunie",
|
||||
"Iulie", "August", "Septembrie", "Octombrie", "Noiembrie", "Decembrie"
|
||||
"Ianuarie",
|
||||
"Februarie",
|
||||
"Martie",
|
||||
"Aprilie",
|
||||
"Mai",
|
||||
"Iunie",
|
||||
"Iulie",
|
||||
"August",
|
||||
"Septembrie",
|
||||
"Octombrie",
|
||||
"Noiembrie",
|
||||
"Decembrie",
|
||||
];
|
||||
const monthName = months[trialBalanceStore.filters.luna - 1] || "";
|
||||
return `${monthName} ${trialBalanceStore.filters.an}`;
|
||||
@@ -248,7 +311,7 @@ const applyFilters = async () => {
|
||||
cont: localFilters.value.cont,
|
||||
denumire: localFilters.value.denumire,
|
||||
},
|
||||
companyStore.selectedCompany.id_firma
|
||||
companyStore.selectedCompany.id_firma,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -275,7 +338,7 @@ const loadTrialBalance = async () => {
|
||||
|
||||
try {
|
||||
await trialBalanceStore.fetchTrialBalance(
|
||||
companyStore.selectedCompany.id_firma
|
||||
companyStore.selectedCompany.id_firma,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to load trial balance:", error);
|
||||
@@ -293,7 +356,7 @@ const onPageChange = async (event) => {
|
||||
|
||||
await trialBalanceStore.changePage(
|
||||
event.page + 1,
|
||||
companyStore.selectedCompany.id_firma
|
||||
companyStore.selectedCompany.id_firma,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -306,7 +369,7 @@ const onSort = async (event) => {
|
||||
await trialBalanceStore.sort(
|
||||
sortBy,
|
||||
sortOrder,
|
||||
companyStore.selectedCompany.id_firma
|
||||
companyStore.selectedCompany.id_firma,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -379,8 +442,8 @@ const exportExcel = async () => {
|
||||
|
||||
// Prepare data for export - Use raw numbers (not formatted) so Excel treats them as numbers
|
||||
const exportData = allData.map((row) => ({
|
||||
"Cont": row.cont,
|
||||
"Denumire": row.denumire,
|
||||
Cont: row.cont,
|
||||
Denumire: row.denumire,
|
||||
"Sold Precedent D": parseFloat(row.sold_precedent_debit) || 0,
|
||||
"Sold Precedent C": parseFloat(row.sold_precedent_credit) || 0,
|
||||
"Rulaj Lunar D": parseFloat(row.rulaj_lunar_debit) || 0,
|
||||
@@ -392,7 +455,7 @@ const exportExcel = async () => {
|
||||
const result = exportToExcel(
|
||||
exportData,
|
||||
`balanta_verificare_${currentPeriodText.value.replace(/\s+/g, "_")}`,
|
||||
"Balanță de Verificare"
|
||||
"Balanță de Verificare",
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
@@ -459,14 +522,44 @@ const exportPDF = async () => {
|
||||
// A4 landscape width: ~297mm total, margins 8mm left+right = 281mm usable
|
||||
// Use 'auto' width to fill entire page width
|
||||
const columns = [
|
||||
{ field: "cont", header: "Cont", type: "text", width: 'auto' },
|
||||
{ field: "denumire", header: "Denumire Cont", type: "text", width: 'auto' },
|
||||
{ field: "sold_precedent_debit", header: "Sold Prec. D", type: "number", width: 'auto' },
|
||||
{ field: "sold_precedent_credit", header: "Sold Prec. C", type: "number", width: 'auto' },
|
||||
{ field: "rulaj_lunar_debit", header: "Rulaj D", type: "number", width: 'auto' },
|
||||
{ field: "rulaj_lunar_credit", header: "Rulaj C", type: "number", width: 'auto' },
|
||||
{ field: "sold_final_debit", header: "Sold Final D", type: "number", width: 'auto' },
|
||||
{ field: "sold_final_credit", header: "Sold Final C", type: "number", width: 'auto' },
|
||||
{ field: "cont", header: "Cont", type: "text", width: "auto" },
|
||||
{ field: "denumire", header: "Denumire Cont", type: "text", width: "auto" },
|
||||
{
|
||||
field: "sold_precedent_debit",
|
||||
header: "Sold Prec. D",
|
||||
type: "number",
|
||||
width: "auto",
|
||||
},
|
||||
{
|
||||
field: "sold_precedent_credit",
|
||||
header: "Sold Prec. C",
|
||||
type: "number",
|
||||
width: "auto",
|
||||
},
|
||||
{
|
||||
field: "rulaj_lunar_debit",
|
||||
header: "Rulaj D",
|
||||
type: "number",
|
||||
width: "auto",
|
||||
},
|
||||
{
|
||||
field: "rulaj_lunar_credit",
|
||||
header: "Rulaj C",
|
||||
type: "number",
|
||||
width: "auto",
|
||||
},
|
||||
{
|
||||
field: "sold_final_debit",
|
||||
header: "Sold Final D",
|
||||
type: "number",
|
||||
width: "auto",
|
||||
},
|
||||
{
|
||||
field: "sold_final_credit",
|
||||
header: "Sold Final C",
|
||||
type: "number",
|
||||
width: "auto",
|
||||
},
|
||||
];
|
||||
|
||||
const result = exportToPDF(
|
||||
@@ -476,8 +569,8 @@ const exportPDF = async () => {
|
||||
{
|
||||
companyName: companyStore.selectedCompany?.name || "",
|
||||
title: "Balanta de Verificare",
|
||||
period: currentPeriodText.value
|
||||
}
|
||||
period: currentPeriodText.value,
|
||||
},
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
@@ -517,7 +610,7 @@ watch(
|
||||
if (newCompany) {
|
||||
await loadTrialBalance();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
<span class="company-name">{{ selectedCompanyName }}</span>
|
||||
<span class="company-code">{{ selectedCompanyCode }}</span>
|
||||
</div>
|
||||
<i class="pi pi-chevron-down" :class="{ 'rotate-180': dropdownOpen }"></i>
|
||||
<i
|
||||
class="pi pi-chevron-down"
|
||||
:class="{ 'rotate-180': dropdownOpen }"
|
||||
></i>
|
||||
</button>
|
||||
|
||||
<div
|
||||
@@ -28,7 +31,7 @@
|
||||
placeholder="Search companies..."
|
||||
class="search-input"
|
||||
@keydown.escape="closeDropdown"
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -45,10 +48,15 @@
|
||||
<div class="company-sub-info">
|
||||
<span class="company-cui">CUI: {{ company.cui }}</span>
|
||||
<span class="company-separator">•</span>
|
||||
<span class="company-status" :class="company.status">{{ company.status }}</span>
|
||||
<span class="company-status" :class="company.status">{{
|
||||
company.status
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<i v-if="company.id === selectedCompany?.id" class="pi pi-check company-selected-icon"></i>
|
||||
<i
|
||||
v-if="company.id === selectedCompany?.id"
|
||||
class="pi pi-check company-selected-icon"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,87 +70,90 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useCompanyStore } from '../../stores/companies'
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { useCompanyStore } from "../../stores/companies";
|
||||
|
||||
export default {
|
||||
name: 'CompanySelectorMini',
|
||||
name: "CompanySelectorMini",
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
emits: ['update:modelValue', 'company-changed'],
|
||||
},
|
||||
emits: ["update:modelValue", "company-changed"],
|
||||
setup(props, { emit }) {
|
||||
const companiesStore = useCompanyStore()
|
||||
const dropdown = ref(null)
|
||||
const dropdownOpen = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const companiesStore = useCompanyStore();
|
||||
const dropdown = ref(null);
|
||||
const dropdownOpen = ref(false);
|
||||
const searchQuery = ref("");
|
||||
|
||||
const selectedCompany = computed({
|
||||
get: () => props.modelValue || companiesStore.selectedCompany,
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
companiesStore.setSelectedCompany(value)
|
||||
}
|
||||
})
|
||||
emit("update:modelValue", value);
|
||||
companiesStore.setSelectedCompany(value);
|
||||
},
|
||||
});
|
||||
|
||||
const selectedCompanyName = computed(() => {
|
||||
return selectedCompany.value?.name || 'Select Company'
|
||||
})
|
||||
return selectedCompany.value?.name || "Select Company";
|
||||
});
|
||||
|
||||
const selectedCompanyCode = computed(() => {
|
||||
return selectedCompany.value?.cui ? `CUI: ${selectedCompany.value.cui}` : ''
|
||||
})
|
||||
return selectedCompany.value?.cui
|
||||
? `CUI: ${selectedCompany.value.cui}`
|
||||
: "";
|
||||
});
|
||||
|
||||
const filteredCompanies = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return companiesStore.companies
|
||||
return companiesStore.companies;
|
||||
}
|
||||
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
return companiesStore.companies.filter(company =>
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return companiesStore.companies.filter(
|
||||
(company) =>
|
||||
company.name.toLowerCase().includes(query) ||
|
||||
company.cui.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
company.cui.toLowerCase().includes(query),
|
||||
);
|
||||
});
|
||||
|
||||
const toggleDropdown = () => {
|
||||
dropdownOpen.value = !dropdownOpen.value
|
||||
dropdownOpen.value = !dropdownOpen.value;
|
||||
if (dropdownOpen.value) {
|
||||
searchQuery.value = ''
|
||||
}
|
||||
searchQuery.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const closeDropdown = () => {
|
||||
dropdownOpen.value = false
|
||||
searchQuery.value = ''
|
||||
}
|
||||
dropdownOpen.value = false;
|
||||
searchQuery.value = "";
|
||||
};
|
||||
|
||||
const selectCompany = (company) => {
|
||||
selectedCompany.value = company
|
||||
emit('company-changed', company)
|
||||
closeDropdown()
|
||||
}
|
||||
selectedCompany.value = company;
|
||||
emit("company-changed", company);
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdown.value && !dropdown.value.contains(event.target)) {
|
||||
closeDropdown()
|
||||
}
|
||||
closeDropdown();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
// Load companies if not already loaded
|
||||
if (companiesStore.companies.length === 0) {
|
||||
companiesStore.fetchCompanies()
|
||||
companiesStore.fetchCompanies();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
});
|
||||
|
||||
return {
|
||||
dropdown,
|
||||
@@ -154,10 +165,10 @@ export default {
|
||||
filteredCompanies,
|
||||
toggleDropdown,
|
||||
closeDropdown,
|
||||
selectCompany
|
||||
}
|
||||
}
|
||||
}
|
||||
selectCompany,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -21,11 +21,19 @@
|
||||
|
||||
<!-- Center: Quick Actions -->
|
||||
<div class="quick-actions desktop-only">
|
||||
<button class="quick-action-btn" @click="refreshData" title="Refresh Data">
|
||||
<button
|
||||
class="quick-action-btn"
|
||||
@click="refreshData"
|
||||
title="Refresh Data"
|
||||
>
|
||||
<i class="pi pi-refresh"></i>
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
<button class="quick-action-btn" @click="exportData" title="Export Data">
|
||||
<button
|
||||
class="quick-action-btn"
|
||||
@click="exportData"
|
||||
title="Export Data"
|
||||
>
|
||||
<i class="pi pi-download"></i>
|
||||
<span>Export</span>
|
||||
</button>
|
||||
@@ -52,52 +60,52 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CompanySelectorMini from '../dashboard/CompanySelectorMini.vue'
|
||||
import { useCompanyStore } from '../../stores/companies'
|
||||
import { ref, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import CompanySelectorMini from "../dashboard/CompanySelectorMini.vue";
|
||||
import { useCompanyStore } from "../../stores/companies";
|
||||
|
||||
export default {
|
||||
name: 'DashboardHeader',
|
||||
name: "DashboardHeader",
|
||||
components: {
|
||||
CompanySelectorMini
|
||||
CompanySelectorMini,
|
||||
},
|
||||
emits: ['menu-toggle', 'refresh', 'export', 'search', 'company-changed'],
|
||||
emits: ["menu-toggle", "refresh", "export", "search", "company-changed"],
|
||||
setup(props, { emit }) {
|
||||
const router = useRouter()
|
||||
const companiesStore = useCompanyStore()
|
||||
const router = useRouter();
|
||||
const companiesStore = useCompanyStore();
|
||||
|
||||
const menuOpen = ref(false)
|
||||
const menuOpen = ref(false);
|
||||
const selectedCompany = computed({
|
||||
get: () => companiesStore.selectedCompany,
|
||||
set: (value) => companiesStore.setSelectedCompany(value)
|
||||
})
|
||||
set: (value) => companiesStore.setSelectedCompany(value),
|
||||
});
|
||||
|
||||
const toggleMenu = () => {
|
||||
menuOpen.value = !menuOpen.value
|
||||
emit('menu-toggle', menuOpen.value)
|
||||
}
|
||||
menuOpen.value = !menuOpen.value;
|
||||
emit("menu-toggle", menuOpen.value);
|
||||
};
|
||||
|
||||
const refreshData = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
emit("refresh");
|
||||
};
|
||||
|
||||
const exportData = () => {
|
||||
emit('export')
|
||||
}
|
||||
emit("export");
|
||||
};
|
||||
|
||||
const searchData = () => {
|
||||
emit('search')
|
||||
}
|
||||
emit("search");
|
||||
};
|
||||
|
||||
const onCompanyChanged = (company) => {
|
||||
emit('company-changed', company)
|
||||
}
|
||||
emit("company-changed", company);
|
||||
};
|
||||
|
||||
const toggleUserMenu = () => {
|
||||
// TODO: Implement user menu dropdown
|
||||
console.log('User menu clicked')
|
||||
}
|
||||
console.log("User menu clicked");
|
||||
};
|
||||
|
||||
return {
|
||||
menuOpen,
|
||||
@@ -107,8 +115,8 @@ export default {
|
||||
exportData,
|
||||
searchData,
|
||||
onCompanyChanged,
|
||||
toggleUserMenu
|
||||
}
|
||||
}
|
||||
}
|
||||
toggleUserMenu,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -21,10 +21,11 @@
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<div class="app-container">
|
||||
|
||||
<!-- Spreadsheet Style Header -->
|
||||
<div class="v3-header">
|
||||
<h1 class="v3-title">{{ companyStore.selectedCompany?.name || 'Dashboard' }}</h1>
|
||||
<h1 class="v3-title">
|
||||
{{ companyStore.selectedCompany?.name || "Dashboard" }}
|
||||
</h1>
|
||||
<p class="v3-subtitle">Data Tables Focus • Spreadsheet Style</p>
|
||||
</div>
|
||||
|
||||
@@ -51,33 +52,38 @@
|
||||
|
||||
<!-- Data Tables Dashboard Content -->
|
||||
<div v-if="companyStore.selectedCompany" class="v3-content">
|
||||
|
||||
<!-- Horizontal Summary Cards -->
|
||||
<div class="v3-summary-cards">
|
||||
<div class="v3-summary-card">
|
||||
<span class="v3-summary-label">Total Facturat</span>
|
||||
<span class="v3-summary-value">{{ formatCurrency(getTotalInvoiced()) }}</span>
|
||||
<span class="v3-summary-value">{{
|
||||
formatCurrency(getTotalInvoiced())
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="v3-summary-card">
|
||||
<span class="v3-summary-label">Total Încasat</span>
|
||||
<span class="v3-summary-value">{{ formatCurrency(getTotalReceived()) }}</span>
|
||||
<span class="v3-summary-value">{{
|
||||
formatCurrency(getTotalReceived())
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="v3-summary-card">
|
||||
<span class="v3-summary-label">Sold Net</span>
|
||||
<span class="v3-summary-value" :class="getNetBalanceClass()">{{ formatCurrency(getNetBalance()) }}</span>
|
||||
<span class="v3-summary-value" :class="getNetBalanceClass()">{{
|
||||
formatCurrency(getNetBalance())
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="v3-summary-card">
|
||||
<span class="v3-summary-label">Trezorerie</span>
|
||||
<span class="v3-summary-value">{{ formatCurrency(getTreasuryTotal()) }}</span>
|
||||
<span class="v3-summary-value">{{
|
||||
formatCurrency(getTreasuryTotal())
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Data Layout: 70% table, 30% summary -->
|
||||
<div class="v3-main-layout">
|
||||
|
||||
<!-- Data Table Section (70%) -->
|
||||
<div class="v3-table-section">
|
||||
|
||||
<!-- Table Controls -->
|
||||
<div class="v3-table-controls">
|
||||
<div class="v3-control-group">
|
||||
@@ -91,14 +97,25 @@
|
||||
|
||||
<div class="v3-control-group">
|
||||
<label>Filtrare:</label>
|
||||
<input v-model="tableFilter" type="text" placeholder="Caută..." class="v3-input">
|
||||
<input
|
||||
v-model="tableFilter"
|
||||
type="text"
|
||||
placeholder="Caută..."
|
||||
class="v3-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="v3-control-group">
|
||||
<button class="btn btn-sm btn-outline" @click="exportTableData">
|
||||
<button
|
||||
class="btn btn-sm btn-outline"
|
||||
@click="exportTableData"
|
||||
>
|
||||
<i class="pi pi-download"></i> Excel
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline" @click="exportTableToPDF">
|
||||
<button
|
||||
class="btn btn-sm btn-outline"
|
||||
@click="exportTableToPDF"
|
||||
>
|
||||
<i class="pi pi-file-pdf"></i> PDF
|
||||
</button>
|
||||
</div>
|
||||
@@ -109,33 +126,54 @@
|
||||
<table class="v3-data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="column in getTableColumns()" :key="column.key"
|
||||
:class="{ 'sortable': column.sortable, 'sorted': sortColumn === column.key }"
|
||||
@click="column.sortable && handleSort(column.key)">
|
||||
<th
|
||||
v-for="column in getTableColumns()"
|
||||
:key="column.key"
|
||||
:class="{
|
||||
sortable: column.sortable,
|
||||
sorted: sortColumn === column.key,
|
||||
}"
|
||||
@click="column.sortable && handleSort(column.key)"
|
||||
>
|
||||
{{ column.label }}
|
||||
<i v-if="column.sortable && sortColumn === column.key"
|
||||
:class="sortDirection === 'asc' ? 'pi pi-sort-up' : 'pi pi-sort-down'"
|
||||
class="sort-icon"></i>
|
||||
<i
|
||||
v-if="column.sortable && sortColumn === column.key"
|
||||
:class="
|
||||
sortDirection === 'asc'
|
||||
? 'pi pi-sort-up'
|
||||
: 'pi pi-sort-down'
|
||||
"
|
||||
class="sort-icon"
|
||||
></i>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in getFilteredTableData()" :key="index"
|
||||
<tr
|
||||
v-for="(row, index) in getFilteredTableData()"
|
||||
:key="index"
|
||||
:class="{ 'row-editable': editableRow === index }"
|
||||
@click="handleRowClick(index)">
|
||||
<td v-for="column in getTableColumns()" :key="column.key"
|
||||
:class="getCellClass(row, column)">
|
||||
|
||||
@click="handleRowClick(index)"
|
||||
>
|
||||
<td
|
||||
v-for="column in getTableColumns()"
|
||||
:key="column.key"
|
||||
:class="getCellClass(row, column)"
|
||||
>
|
||||
<!-- Editable Cell -->
|
||||
<span v-if="editableRow === index && column.editable"
|
||||
<span
|
||||
v-if="editableRow === index && column.editable"
|
||||
class="editable-cell"
|
||||
@click.stop>
|
||||
<input v-model="row[column.key]"
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
v-model="row[column.key]"
|
||||
:type="column.type || 'text'"
|
||||
class="cell-input"
|
||||
@blur="saveRowEdit(index)"
|
||||
@keydown.enter="saveRowEdit(index)"
|
||||
@keydown.escape="cancelRowEdit()">
|
||||
@keydown.escape="cancelRowEdit()"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<!-- Regular Cell -->
|
||||
@@ -151,33 +189,52 @@
|
||||
<!-- Pagination -->
|
||||
<div class="v3-pagination">
|
||||
<div class="pagination-info">
|
||||
Afișare {{ ((currentPage - 1) * rowsPerPage) + 1 }} -
|
||||
{{ Math.min(currentPage * rowsPerPage, getFilteredTableData().length) }}
|
||||
Afișare {{ (currentPage - 1) * rowsPerPage + 1 }} -
|
||||
{{
|
||||
Math.min(
|
||||
currentPage * rowsPerPage,
|
||||
getFilteredTableData().length,
|
||||
)
|
||||
}}
|
||||
din {{ getFilteredTableData().length }} înregistrări
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button class="btn btn-sm" @click="goToPage(currentPage - 1)" :disabled="currentPage === 1">
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
@click="goToPage(currentPage - 1)"
|
||||
:disabled="currentPage === 1"
|
||||
>
|
||||
<i class="pi pi-chevron-left"></i>
|
||||
</button>
|
||||
<span class="page-info">Pagina {{ currentPage }} din {{ totalPages }}</span>
|
||||
<button class="btn btn-sm" @click="goToPage(currentPage + 1)" :disabled="currentPage === totalPages">
|
||||
<span class="page-info"
|
||||
>Pagina {{ currentPage }} din {{ totalPages }}</span
|
||||
>
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
@click="goToPage(currentPage + 1)"
|
||||
:disabled="currentPage === totalPages"
|
||||
>
|
||||
<i class="pi pi-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Summary Panel (30%) -->
|
||||
<div class="v3-summary-section">
|
||||
|
||||
<div class="v3-summary-panel">
|
||||
<h3 class="v3-panel-title">Rezumat {{ getDataTypeLabel() }}</h3>
|
||||
|
||||
<div class="v3-summary-stats">
|
||||
<div v-for="stat in getSummaryStats()" :key="stat.label" class="v3-stat-item">
|
||||
<div
|
||||
v-for="stat in getSummaryStats()"
|
||||
:key="stat.label"
|
||||
class="v3-stat-item"
|
||||
>
|
||||
<span class="stat-label">{{ stat.label }}</span>
|
||||
<span class="stat-value" :class="stat.class">{{ stat.value }}</span>
|
||||
<span class="stat-value" :class="stat.class">{{
|
||||
stat.value
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -185,30 +242,40 @@
|
||||
<div class="v3-mini-chart">
|
||||
<div class="chart-title">Trend Ultimas 7 zile</div>
|
||||
<div class="chart-placeholder">
|
||||
<div class="chart-bar" v-for="i in 7" :key="i"
|
||||
:style="{ height: Math.random() * 60 + 20 + 'px' }"></div>
|
||||
<div
|
||||
class="chart-bar"
|
||||
v-for="i in 7"
|
||||
:key="i"
|
||||
:style="{ height: Math.random() * 60 + 20 + 'px' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="v3-quick-actions">
|
||||
<h4>Acțiuni Rapide</h4>
|
||||
<button class="btn btn-sm btn-primary btn-full-width mb-2" @click="addNewRecord">
|
||||
<button
|
||||
class="btn btn-sm btn-primary btn-full-width mb-2"
|
||||
@click="addNewRecord"
|
||||
>
|
||||
<i class="pi pi-plus"></i> Adaugă Înregistrare
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline btn-full-width mb-2" @click="refreshCurrentData">
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-full-width mb-2"
|
||||
@click="refreshCurrentData"
|
||||
>
|
||||
<i class="pi pi-refresh"></i> Actualizează
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline btn-full-width" @click="viewFullReport">
|
||||
<button
|
||||
class="btn btn-sm btn-outline btn-full-width"
|
||||
@click="viewFullReport"
|
||||
>
|
||||
<i class="pi pi-external-link"></i> Raport Complet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
@@ -216,7 +283,6 @@
|
||||
<div class="v3-loading-spinner"></div>
|
||||
<p>Se încarcă datele...</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -243,10 +309,10 @@ const menuOpen = ref(false);
|
||||
const selectedCompanyId = ref(companyStore.selectedCompany?.id_firma || null);
|
||||
const filteredCompanies = ref([]);
|
||||
const isLoading = ref(false);
|
||||
const selectedDataType = ref('clients');
|
||||
const tableFilter = ref('');
|
||||
const selectedDataType = ref("clients");
|
||||
const tableFilter = ref("");
|
||||
const sortColumn = ref(null);
|
||||
const sortDirection = ref('asc');
|
||||
const sortDirection = ref("asc");
|
||||
const editableRow = ref(-1);
|
||||
const currentPage = ref(1);
|
||||
const rowsPerPage = ref(15);
|
||||
@@ -273,9 +339,9 @@ const handleCompanyChanged = async (company) => {
|
||||
};
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
if (!amount) return '0,00 RON';
|
||||
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount;
|
||||
if (isNaN(numAmount)) return '0,00 RON';
|
||||
if (!amount) 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", {
|
||||
@@ -289,7 +355,8 @@ const formatCurrency = (amount) => {
|
||||
|
||||
const getTotalInvoiced = () => {
|
||||
const clientInvoiced = dashboardStore.summary?.clienti_total_facturat || 0;
|
||||
const supplierInvoiced = dashboardStore.summary?.furnizori_total_facturat || 0;
|
||||
const supplierInvoiced =
|
||||
dashboardStore.summary?.furnizori_total_facturat || 0;
|
||||
return parseFloat(clientInvoiced) + parseFloat(supplierInvoiced);
|
||||
};
|
||||
|
||||
@@ -307,7 +374,7 @@ const getNetBalance = () => {
|
||||
|
||||
const getNetBalanceClass = () => {
|
||||
const balance = getNetBalance();
|
||||
return balance > 0 ? 'positive' : balance < 0 ? 'negative' : 'neutral';
|
||||
return balance > 0 ? "positive" : balance < 0 ? "negative" : "neutral";
|
||||
};
|
||||
|
||||
const getTreasuryTotal = () => {
|
||||
@@ -318,32 +385,86 @@ const getTreasuryTotal = () => {
|
||||
|
||||
const getTableColumns = () => {
|
||||
switch (selectedDataType.value) {
|
||||
case 'clients':
|
||||
case "clients":
|
||||
return [
|
||||
{ key: 'client', label: 'Client', sortable: true, editable: false },
|
||||
{ key: 'facturat', label: 'Facturat', sortable: true, editable: true, type: 'currency' },
|
||||
{ key: 'incasat', label: 'Încasat', sortable: true, editable: true, type: 'currency' },
|
||||
{ key: 'sold', label: 'Sold', sortable: true, editable: false, type: 'currency' },
|
||||
{ key: 'restant', label: 'Restant', sortable: true, editable: false, type: 'currency' },
|
||||
{ key: 'status', label: 'Status', sortable: true, editable: false }
|
||||
{ key: "client", label: "Client", sortable: true, editable: false },
|
||||
{
|
||||
key: "facturat",
|
||||
label: "Facturat",
|
||||
sortable: true,
|
||||
editable: true,
|
||||
type: "currency",
|
||||
},
|
||||
{
|
||||
key: "incasat",
|
||||
label: "Încasat",
|
||||
sortable: true,
|
||||
editable: true,
|
||||
type: "currency",
|
||||
},
|
||||
{
|
||||
key: "sold",
|
||||
label: "Sold",
|
||||
sortable: true,
|
||||
editable: false,
|
||||
type: "currency",
|
||||
},
|
||||
{
|
||||
key: "restant",
|
||||
label: "Restant",
|
||||
sortable: true,
|
||||
editable: false,
|
||||
type: "currency",
|
||||
},
|
||||
{ key: "status", label: "Status", sortable: true, editable: false },
|
||||
];
|
||||
case 'suppliers':
|
||||
case "suppliers":
|
||||
return [
|
||||
{ key: 'furnizor', label: 'Furnizor', sortable: true, editable: false },
|
||||
{ key: 'facturat', label: 'Facturat', sortable: true, editable: true, type: 'currency' },
|
||||
{ key: 'achitat', label: 'Achitat', sortable: true, editable: true, type: 'currency' },
|
||||
{ key: 'sold', label: 'Sold', sortable: true, editable: false, type: 'currency' },
|
||||
{ key: 'restant', label: 'Restant', sortable: true, editable: false, type: 'currency' },
|
||||
{ key: 'status', label: 'Status', sortable: true, editable: false }
|
||||
{ key: "furnizor", label: "Furnizor", sortable: true, editable: false },
|
||||
{
|
||||
key: "facturat",
|
||||
label: "Facturat",
|
||||
sortable: true,
|
||||
editable: true,
|
||||
type: "currency",
|
||||
},
|
||||
{
|
||||
key: "achitat",
|
||||
label: "Achitat",
|
||||
sortable: true,
|
||||
editable: true,
|
||||
type: "currency",
|
||||
},
|
||||
{
|
||||
key: "sold",
|
||||
label: "Sold",
|
||||
sortable: true,
|
||||
editable: false,
|
||||
type: "currency",
|
||||
},
|
||||
{
|
||||
key: "restant",
|
||||
label: "Restant",
|
||||
sortable: true,
|
||||
editable: false,
|
||||
type: "currency",
|
||||
},
|
||||
{ key: "status", label: "Status", sortable: true, editable: false },
|
||||
];
|
||||
case 'treasury':
|
||||
case "treasury":
|
||||
return [
|
||||
{ key: 'cont', label: 'Cont', sortable: true, editable: false },
|
||||
{ key: 'nume', label: 'Nume Cont', sortable: true, editable: false },
|
||||
{ key: 'sold', label: 'Sold', sortable: true, editable: true, type: 'currency' },
|
||||
{ key: 'valuta', label: 'Valută', sortable: true, editable: false },
|
||||
{ key: 'tip', label: 'Tip', sortable: true, editable: false },
|
||||
{ key: 'status', label: 'Status', sortable: true, editable: false }
|
||||
{ key: "cont", label: "Cont", sortable: true, editable: false },
|
||||
{ key: "nume", label: "Nume Cont", sortable: true, editable: false },
|
||||
{
|
||||
key: "sold",
|
||||
label: "Sold",
|
||||
sortable: true,
|
||||
editable: true,
|
||||
type: "currency",
|
||||
},
|
||||
{ key: "valuta", label: "Valută", sortable: true, editable: false },
|
||||
{ key: "tip", label: "Tip", sortable: true, editable: false },
|
||||
{ key: "status", label: "Status", sortable: true, editable: false },
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
@@ -356,10 +477,10 @@ const getFilteredTableData = () => {
|
||||
// Apply filter
|
||||
if (tableFilter.value) {
|
||||
const filter = tableFilter.value.toLowerCase();
|
||||
data = data.filter(row =>
|
||||
Object.values(row).some(value =>
|
||||
String(value).toLowerCase().includes(filter)
|
||||
)
|
||||
data = data.filter((row) =>
|
||||
Object.values(row).some((value) =>
|
||||
String(value).toLowerCase().includes(filter),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -373,7 +494,7 @@ const getFilteredTableData = () => {
|
||||
if (aVal > bVal) comparison = 1;
|
||||
if (aVal < bVal) comparison = -1;
|
||||
|
||||
return sortDirection.value === 'asc' ? comparison : -comparison;
|
||||
return sortDirection.value === "asc" ? comparison : -comparison;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -387,27 +508,118 @@ const getFilteredTableData = () => {
|
||||
const getTableData = () => {
|
||||
// Mock data - in real implementation, this would come from the store
|
||||
switch (selectedDataType.value) {
|
||||
case 'clients':
|
||||
case "clients":
|
||||
return [
|
||||
{ client: 'Client A', facturat: 150000, incasat: 120000, sold: 30000, restant: 5000, status: 'Activ' },
|
||||
{ client: 'Client B', facturat: 75000, incasat: 75000, sold: 0, restant: 0, status: 'Activ' },
|
||||
{ client: 'Client C', facturat: 200000, incasat: 150000, sold: 50000, restant: 15000, status: 'Restant' },
|
||||
{ client: 'Client D', facturat: 90000, incasat: 90000, sold: 0, restant: 0, status: 'Activ' },
|
||||
{ client: 'Client E', facturat: 180000, incasat: 140000, sold: 40000, restant: 8000, status: 'Activ' },
|
||||
{
|
||||
client: "Client A",
|
||||
facturat: 150000,
|
||||
incasat: 120000,
|
||||
sold: 30000,
|
||||
restant: 5000,
|
||||
status: "Activ",
|
||||
},
|
||||
{
|
||||
client: "Client B",
|
||||
facturat: 75000,
|
||||
incasat: 75000,
|
||||
sold: 0,
|
||||
restant: 0,
|
||||
status: "Activ",
|
||||
},
|
||||
{
|
||||
client: "Client C",
|
||||
facturat: 200000,
|
||||
incasat: 150000,
|
||||
sold: 50000,
|
||||
restant: 15000,
|
||||
status: "Restant",
|
||||
},
|
||||
{
|
||||
client: "Client D",
|
||||
facturat: 90000,
|
||||
incasat: 90000,
|
||||
sold: 0,
|
||||
restant: 0,
|
||||
status: "Activ",
|
||||
},
|
||||
{
|
||||
client: "Client E",
|
||||
facturat: 180000,
|
||||
incasat: 140000,
|
||||
sold: 40000,
|
||||
restant: 8000,
|
||||
status: "Activ",
|
||||
},
|
||||
];
|
||||
case 'suppliers':
|
||||
case "suppliers":
|
||||
return [
|
||||
{ furnizor: 'Furnizor A', facturat: 80000, achitat: 80000, sold: 0, restant: 0, status: 'Activ' },
|
||||
{ furnizor: 'Furnizor B', facturat: 120000, achitat: 100000, sold: 20000, restant: 3000, status: 'Restant' },
|
||||
{ furnizor: 'Furnizor C', facturat: 95000, achitat: 95000, sold: 0, restant: 0, status: 'Activ' },
|
||||
{ furnizor: 'Furnizor D', facturat: 65000, achitat: 50000, sold: 15000, restant: 2000, status: 'Restant' },
|
||||
{
|
||||
furnizor: "Furnizor A",
|
||||
facturat: 80000,
|
||||
achitat: 80000,
|
||||
sold: 0,
|
||||
restant: 0,
|
||||
status: "Activ",
|
||||
},
|
||||
{
|
||||
furnizor: "Furnizor B",
|
||||
facturat: 120000,
|
||||
achitat: 100000,
|
||||
sold: 20000,
|
||||
restant: 3000,
|
||||
status: "Restant",
|
||||
},
|
||||
{
|
||||
furnizor: "Furnizor C",
|
||||
facturat: 95000,
|
||||
achitat: 95000,
|
||||
sold: 0,
|
||||
restant: 0,
|
||||
status: "Activ",
|
||||
},
|
||||
{
|
||||
furnizor: "Furnizor D",
|
||||
facturat: 65000,
|
||||
achitat: 50000,
|
||||
sold: 15000,
|
||||
restant: 2000,
|
||||
status: "Restant",
|
||||
},
|
||||
];
|
||||
case 'treasury':
|
||||
case "treasury":
|
||||
return [
|
||||
{ cont: '5121', nume: 'BCR - Cont Principal', sold: 125000, valuta: 'RON', tip: 'Bancă', status: 'Activ' },
|
||||
{ cont: '5124', nume: 'BRD - Cont Euro', sold: 25000, valuta: 'EUR', tip: 'Bancă', status: 'Activ' },
|
||||
{ cont: '5311', nume: 'Casa RON', sold: 5000, valuta: 'RON', tip: 'Casă', status: 'Activ' },
|
||||
{ cont: '5314', nume: 'Casa EUR', sold: 1000, valuta: 'EUR', tip: 'Casă', status: 'Activ' },
|
||||
{
|
||||
cont: "5121",
|
||||
nume: "BCR - Cont Principal",
|
||||
sold: 125000,
|
||||
valuta: "RON",
|
||||
tip: "Bancă",
|
||||
status: "Activ",
|
||||
},
|
||||
{
|
||||
cont: "5124",
|
||||
nume: "BRD - Cont Euro",
|
||||
sold: 25000,
|
||||
valuta: "EUR",
|
||||
tip: "Bancă",
|
||||
status: "Activ",
|
||||
},
|
||||
{
|
||||
cont: "5311",
|
||||
nume: "Casa RON",
|
||||
sold: 5000,
|
||||
valuta: "RON",
|
||||
tip: "Casă",
|
||||
status: "Activ",
|
||||
},
|
||||
{
|
||||
cont: "5314",
|
||||
nume: "Casa EUR",
|
||||
sold: 1000,
|
||||
valuta: "EUR",
|
||||
tip: "Casă",
|
||||
status: "Activ",
|
||||
},
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
@@ -416,10 +628,14 @@ const getTableData = () => {
|
||||
|
||||
const getDataTypeLabel = () => {
|
||||
switch (selectedDataType.value) {
|
||||
case 'clients': return 'Clienți';
|
||||
case 'suppliers': return 'Furnizori';
|
||||
case 'treasury': return 'Trezorerie';
|
||||
default: return '';
|
||||
case "clients":
|
||||
return "Clienți";
|
||||
case "suppliers":
|
||||
return "Furnizori";
|
||||
case "treasury":
|
||||
return "Trezorerie";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -427,26 +643,68 @@ const getSummaryStats = () => {
|
||||
const data = getTableData();
|
||||
|
||||
switch (selectedDataType.value) {
|
||||
case 'clients':
|
||||
case "clients":
|
||||
return [
|
||||
{ label: 'Total Clienți', value: data.length, class: 'neutral' },
|
||||
{ label: 'Clienți Activi', value: data.filter(c => c.status === 'Activ').length, class: 'positive' },
|
||||
{ label: 'Cu Restante', value: data.filter(c => c.restant > 0).length, class: 'negative' },
|
||||
{ label: 'Sold Mediu', value: formatCurrency(data.reduce((sum, c) => sum + c.sold, 0) / data.length), class: 'neutral' }
|
||||
{ label: "Total Clienți", value: data.length, class: "neutral" },
|
||||
{
|
||||
label: "Clienți Activi",
|
||||
value: data.filter((c) => c.status === "Activ").length,
|
||||
class: "positive",
|
||||
},
|
||||
{
|
||||
label: "Cu Restante",
|
||||
value: data.filter((c) => c.restant > 0).length,
|
||||
class: "negative",
|
||||
},
|
||||
{
|
||||
label: "Sold Mediu",
|
||||
value: formatCurrency(
|
||||
data.reduce((sum, c) => sum + c.sold, 0) / data.length,
|
||||
),
|
||||
class: "neutral",
|
||||
},
|
||||
];
|
||||
case 'suppliers':
|
||||
case "suppliers":
|
||||
return [
|
||||
{ label: 'Total Furnizori', value: data.length, class: 'neutral' },
|
||||
{ label: 'Furnizori Activi', value: data.filter(s => s.status === 'Activ').length, class: 'positive' },
|
||||
{ label: 'Cu Restante', value: data.filter(s => s.restant > 0).length, class: 'negative' },
|
||||
{ label: 'Sold Mediu', value: formatCurrency(data.reduce((sum, s) => sum + s.sold, 0) / data.length), class: 'neutral' }
|
||||
{ label: "Total Furnizori", value: data.length, class: "neutral" },
|
||||
{
|
||||
label: "Furnizori Activi",
|
||||
value: data.filter((s) => s.status === "Activ").length,
|
||||
class: "positive",
|
||||
},
|
||||
{
|
||||
label: "Cu Restante",
|
||||
value: data.filter((s) => s.restant > 0).length,
|
||||
class: "negative",
|
||||
},
|
||||
{
|
||||
label: "Sold Mediu",
|
||||
value: formatCurrency(
|
||||
data.reduce((sum, s) => sum + s.sold, 0) / data.length,
|
||||
),
|
||||
class: "neutral",
|
||||
},
|
||||
];
|
||||
case 'treasury':
|
||||
case "treasury":
|
||||
return [
|
||||
{ label: 'Total Conturi', value: data.length, class: 'neutral' },
|
||||
{ label: 'Conturi Bancă', value: data.filter(t => t.tip === 'Bancă').length, class: 'positive' },
|
||||
{ label: 'Conturi Casă', value: data.filter(t => t.tip === 'Casă').length, class: 'positive' },
|
||||
{ label: 'Sold Mediu', value: formatCurrency(data.reduce((sum, t) => sum + t.sold, 0) / data.length), class: 'neutral' }
|
||||
{ label: "Total Conturi", value: data.length, class: "neutral" },
|
||||
{
|
||||
label: "Conturi Bancă",
|
||||
value: data.filter((t) => t.tip === "Bancă").length,
|
||||
class: "positive",
|
||||
},
|
||||
{
|
||||
label: "Conturi Casă",
|
||||
value: data.filter((t) => t.tip === "Casă").length,
|
||||
class: "positive",
|
||||
},
|
||||
{
|
||||
label: "Sold Mediu",
|
||||
value: formatCurrency(
|
||||
data.reduce((sum, t) => sum + t.sold, 0) / data.length,
|
||||
),
|
||||
class: "neutral",
|
||||
},
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
@@ -454,30 +712,30 @@ const getSummaryStats = () => {
|
||||
};
|
||||
|
||||
const formatCellValue = (value, type) => {
|
||||
if (type === 'currency') {
|
||||
if (type === "currency") {
|
||||
return formatCurrency(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const getCellClass = (row, column) => {
|
||||
if (column.type === 'currency') {
|
||||
if (column.type === "currency") {
|
||||
const value = parseFloat(row[column.key]);
|
||||
if (value > 0) return 'cell-positive';
|
||||
if (value < 0) return 'cell-negative';
|
||||
if (value > 0) return "cell-positive";
|
||||
if (value < 0) return "cell-negative";
|
||||
}
|
||||
if (column.key === 'status') {
|
||||
return row.status === 'Activ' ? 'cell-active' : 'cell-inactive';
|
||||
if (column.key === "status") {
|
||||
return row.status === "Activ" ? "cell-active" : "cell-inactive";
|
||||
}
|
||||
return '';
|
||||
return "";
|
||||
};
|
||||
|
||||
const handleSort = (column) => {
|
||||
if (sortColumn.value === column) {
|
||||
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc';
|
||||
sortDirection.value = sortDirection.value === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
sortColumn.value = column;
|
||||
sortDirection.value = 'asc';
|
||||
sortDirection.value = "asc";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -539,22 +797,22 @@ const refreshCurrentData = () => {
|
||||
|
||||
const viewFullReport = () => {
|
||||
switch (selectedDataType.value) {
|
||||
case 'clients':
|
||||
router.push('/invoices');
|
||||
case "clients":
|
||||
router.push("/invoices");
|
||||
break;
|
||||
case 'suppliers':
|
||||
router.push('/invoices?type=suppliers');
|
||||
case "suppliers":
|
||||
router.push("/invoices?type=suppliers");
|
||||
break;
|
||||
case 'treasury':
|
||||
router.push('/bank-cash-register');
|
||||
case "treasury":
|
||||
router.push("/bank-cash-register");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const searchCompanies = (event) => {
|
||||
const query = event.query.toLowerCase();
|
||||
filteredCompanies.value = companyStore.companyListFormatted.filter(company =>
|
||||
company.displayName.toLowerCase().includes(query)
|
||||
filteredCompanies.value = companyStore.companyListFormatted.filter(
|
||||
(company) => company.displayName.toLowerCase().includes(query),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -574,7 +832,9 @@ const loadDashboardData = async () => {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
await dashboardStore.loadDashboardSummary(companyStore.selectedCompany.id_firma);
|
||||
await dashboardStore.loadDashboardSummary(
|
||||
companyStore.selectedCompany.id_firma,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to load dashboard data:", error);
|
||||
toast.add({
|
||||
@@ -604,7 +864,7 @@ const exportData = () => {
|
||||
|
||||
const searchData = () => {
|
||||
// Focus on the table filter input
|
||||
const filterInput = document.querySelector('.v3-input');
|
||||
const filterInput = document.querySelector(".v3-input");
|
||||
if (filterInput) {
|
||||
filterInput.focus();
|
||||
}
|
||||
@@ -613,7 +873,7 @@ const searchData = () => {
|
||||
// Watchers
|
||||
watch(selectedDataType, () => {
|
||||
currentPage.value = 1;
|
||||
tableFilter.value = '';
|
||||
tableFilter.value = "";
|
||||
sortColumn.value = null;
|
||||
editableRow.value = -1;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user