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:
2025-11-22 00:31:20 +02:00
parent bd41a3406e
commit f52aa27bdc
14 changed files with 1691 additions and 1144 deletions

View File

@@ -7,8 +7,9 @@ ORACLE_PASSWORD=ROMFASTSOFT
ORACLE_DSN=localhost:1526/roa ORACLE_DSN=localhost:1526/roa
# Test credentials for pytest (user exists in Oracle TEST) # Test credentials for pytest (user exists in Oracle TEST)
TEST_ORACLE_USER=MARIUS M # Quotes required for shell (source), dotenv strips them automatically
TEST_ORACLE_PASS=123 TEST_ORACLE_USER="MARIUS M"
TEST_ORACLE_PASS="123"
# Test company - MARIUSM_AUTO schema (only schema with full data in TEST) # Test company - MARIUSM_AUTO schema (only schema with full data in TEST)
# Other schemas (ACN, DANUBE, EMS) don't have required tables # Other schemas (ACN, DANUBE, EMS) don't have required tables

View File

@@ -67,8 +67,11 @@ export const useInvoicesStore = defineStore("invoices", () => {
// Convert Date object to YYYY-MM-DD string format (LOCAL date, not UTC) // Convert Date object to YYYY-MM-DD string format (LOCAL date, not UTC)
if (filters.value.dateFrom instanceof Date) { if (filters.value.dateFrom instanceof Date) {
const year = filters.value.dateFrom.getFullYear(); const year = filters.value.dateFrom.getFullYear();
const month = String(filters.value.dateFrom.getMonth() + 1).padStart(2, '0'); const month = String(filters.value.dateFrom.getMonth() + 1).padStart(
const day = String(filters.value.dateFrom.getDate()).padStart(2, '0'); 2,
"0",
);
const day = String(filters.value.dateFrom.getDate()).padStart(2, "0");
params.date_from = `${year}-${month}-${day}`; params.date_from = `${year}-${month}-${day}`;
} else { } else {
params.date_from = filters.value.dateFrom; 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) // Convert Date object to YYYY-MM-DD string format (LOCAL date, not UTC)
if (filters.value.dateTo instanceof Date) { if (filters.value.dateTo instanceof Date) {
const year = filters.value.dateTo.getFullYear(); const year = filters.value.dateTo.getFullYear();
const month = String(filters.value.dateTo.getMonth() + 1).padStart(2, '0'); const month = String(filters.value.dateTo.getMonth() + 1).padStart(
const day = String(filters.value.dateTo.getDate()).padStart(2, '0'); 2,
"0",
);
const day = String(filters.value.dateTo.getDate()).padStart(2, "0");
params.date_to = `${year}-${month}-${day}`; params.date_to = `${year}-${month}-${day}`;
} else { } else {
params.date_to = filters.value.dateTo; params.date_to = filters.value.dateTo;
@@ -93,8 +99,8 @@ export const useInvoicesStore = defineStore("invoices", () => {
const response = await apiService.get(`/invoices/`, { const response = await apiService.get(`/invoices/`, {
params: { params: {
company: companyCode, company: companyCode,
...params ...params,
} },
}); });
invoices.value = response.data.invoices || []; invoices.value = response.data.invoices || [];

View File

@@ -13,7 +13,7 @@ export const useTreasuryStore = defineStore("treasury", () => {
}); });
const totals = ref({ const totals = ref({
total_incasari: 0, total_incasari: 0,
total_plati: 0 total_plati: 0,
}); });
const loadBankCashRegister = async (companyId, filters = {}) => { const loadBankCashRegister = async (companyId, filters = {}) => {
@@ -25,18 +25,18 @@ export const useTreasuryStore = defineStore("treasury", () => {
company: companyId, company: companyId,
page: pagination.value.page + 1, page: pagination.value.page + 1,
page_size: pagination.value.rows, page_size: pagination.value.rows,
...filters ...filters,
}; };
const response = await apiService.get('/treasury/bank-cash-register', { const response = await apiService.get("/treasury/bank-cash-register", {
params params,
}); });
registers.value = response.data.registers || []; registers.value = response.data.registers || [];
pagination.value.totalRecords = response.data.total_count || 0; pagination.value.totalRecords = response.data.total_count || 0;
totals.value = { totals.value = {
total_incasari: response.data.total_incasari, total_incasari: response.data.total_incasari,
total_plati: response.data.total_plati total_plati: response.data.total_plati,
}; };
return { success: true }; return { success: true };
@@ -72,6 +72,6 @@ export const useTreasuryStore = defineStore("treasury", () => {
totals, totals,
loadBankCashRegister, loadBankCashRegister,
setPagination, setPagination,
reset reset,
}; };
}); });

View File

@@ -1,17 +1,17 @@
import * as XLSX from 'xlsx'; import * as XLSX from "xlsx";
import { jsPDF } from 'jspdf'; import { jsPDF } from "jspdf";
import autoTable from 'jspdf-autotable'; import autoTable from "jspdf-autotable";
/** /**
* Format currency values for export * Format currency values for export
*/ */
const formatCurrency = (value) => { const formatCurrency = (value) => {
if (value == null || value === '-') return '-'; if (value == null || value === "-") return "-";
return new Intl.NumberFormat('ro-RO', { return new Intl.NumberFormat("ro-RO", {
style: 'currency', style: "currency",
currency: 'RON', currency: "RON",
minimumFractionDigits: 0, minimumFractionDigits: 0,
maximumFractionDigits: 0 maximumFractionDigits: 0,
}).format(value); }).format(value);
}; };
@@ -21,15 +21,18 @@ const formatCurrency = (value) => {
* @param {String} filename - Name of the file (without extension) * @param {String} filename - Name of the file (without extension)
* @param {String} sheetName - Name of the Excel sheet * @param {String} sheetName - Name of the Excel sheet
*/ */
export const exportToExcel = (data, filename, sheetName = 'Sheet1') => { export const exportToExcel = (data, filename, sheetName = "Sheet1") => {
try { try {
const ws = XLSX.utils.json_to_sheet(data); const ws = XLSX.utils.json_to_sheet(data);
const wb = XLSX.utils.book_new(); const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, sheetName); 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 }; return { success: true };
} catch (error) { } catch (error) {
console.error('Excel export failed:', error); console.error("Excel export failed:", error);
return { success: false, error }; return { success: false, error };
} }
}; };
@@ -38,12 +41,12 @@ export const exportToExcel = (data, filename, sheetName = 'Sheet1') => {
* Format number for PDF export * Format number for PDF export
*/ */
const formatNumberForPDF = (value) => { const formatNumberForPDF = (value) => {
if (value == null || value === '' || value === '-') return '-'; if (value == null || value === "" || value === "-") return "-";
const num = parseFloat(value); const num = parseFloat(value);
if (isNaN(num)) return '-'; if (isNaN(num)) return "-";
return new Intl.NumberFormat('ro-RO', { return new Intl.NumberFormat("ro-RO", {
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 2 maximumFractionDigits: 2,
}).format(num); }).format(num);
}; };
@@ -58,17 +61,17 @@ export const exportToPDF = (data, columns, filename, header) => {
try { try {
// Check if data exists // Check if data exists
if (!data || data.length === 0) { if (!data || data.length === 0) {
console.error('No data to export'); console.error("No data to export");
return { success: false, error: 'No data available' }; return { success: false, error: "No data available" };
} }
// Check if jsPDF is properly imported // Check if jsPDF is properly imported
if (typeof jsPDF === 'undefined') { if (typeof jsPDF === "undefined") {
console.error('jsPDF not properly imported'); console.error("jsPDF not properly imported");
return { success: false, error: 'PDF library not available' }; 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 pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight(); const pageHeight = doc.internal.pageSize.getHeight();
const marginLeft = 8; const marginLeft = 8;
@@ -79,38 +82,38 @@ export const exportToPDF = (data, columns, filename, header) => {
const addHeader = () => { const addHeader = () => {
// Line 1: Company name (left aligned, bold, larger font) // Line 1: Company name (left aligned, bold, larger font)
doc.setFontSize(13); doc.setFontSize(13);
doc.setFont(undefined, 'bold'); doc.setFont(undefined, "bold");
const companyName = header.companyName || 'N/A'; const companyName = header.companyName || "N/A";
doc.text(companyName, marginLeft, 15); doc.text(companyName, marginLeft, 15);
// Line 2: Title "Balanta de Verificare" (centered) // Line 2: Title "Balanta de Verificare" (centered)
doc.setFontSize(14); doc.setFontSize(14);
doc.setFont(undefined, 'bold'); doc.setFont(undefined, "bold");
const titleWidth = doc.getTextWidth(header.title || ''); const titleWidth = doc.getTextWidth(header.title || "");
const titleX = marginLeft + (contentWidth - titleWidth) / 2; const titleX = marginLeft + (contentWidth - titleWidth) / 2;
doc.text(header.title || '', titleX, 24); doc.text(header.title || "", titleX, 24);
// Line 3: Period (centered, below title) // Line 3: Period (centered, below title)
doc.setFontSize(11); doc.setFontSize(11);
doc.setFont(undefined, 'normal'); doc.setFont(undefined, "normal");
const periodText = header.period || ''; const periodText = header.period || "";
const periodWidth = doc.getTextWidth(periodText); const periodWidth = doc.getTextWidth(periodText);
const periodX = marginLeft + (contentWidth - periodWidth) / 2; const periodX = marginLeft + (contentWidth - periodWidth) / 2;
doc.text(periodText, periodX, 32); doc.text(periodText, periodX, 32);
}; };
// Prepare table data // Prepare table data
const tableColumns = columns.map(col => col.header); const tableColumns = columns.map((col) => col.header);
const tableRows = data.map(row => const tableRows = data.map((row) =>
columns.map(col => { columns.map((col) => {
const value = row[col.field]; const value = row[col.field];
if (col.type === 'currency') { if (col.type === "currency") {
return formatCurrency(value); return formatCurrency(value);
} else if (col.type === 'number') { } else if (col.type === "number") {
return formatNumberForPDF(value); return formatNumberForPDF(value);
} }
return value || '-'; return value || "-";
}) }),
); );
// Function to add footer (called for each page) // Function to add footer (called for each page)
@@ -119,8 +122,12 @@ export const exportToPDF = (data, columns, filename, header) => {
// Left side: Generation date // Left side: Generation date
doc.setFontSize(8); doc.setFontSize(8);
doc.setFont(undefined, 'normal'); doc.setFont(undefined, "normal");
doc.text(`Generat: ${new Date().toLocaleString('ro-RO')}`, marginLeft, footerY); doc.text(
`Generat: ${new Date().toLocaleString("ro-RO")}`,
marginLeft,
footerY,
);
// Right side: Page numbers // Right side: Page numbers
const pageText = `Pagina ${pageNum} din ${totalPages}`; const pageText = `Pagina ${pageNum} din ${totalPages}`;
@@ -129,7 +136,7 @@ export const exportToPDF = (data, columns, filename, header) => {
}; };
// Check if autoTable is available // Check if autoTable is available
if (typeof autoTable === 'function') { if (typeof autoTable === "function") {
// Build column styles - jspdf-autotable uses numeric keys // Build column styles - jspdf-autotable uses numeric keys
const columnStyles = {}; const columnStyles = {};
@@ -142,37 +149,37 @@ export const exportToPDF = (data, columns, filename, header) => {
columns.forEach((col, index) => { columns.forEach((col, index) => {
// Use custom width if provided, otherwise auto // 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; widthAllocations[index] = totalWidth * col.width;
} else if (col.width === 'auto') { } else if (col.width === "auto") {
widthAllocations[index] = 'auto'; widthAllocations[index] = "auto";
} else { } else {
// Default width allocation for Trial Balance (8 columns) // Default width allocation for Trial Balance (8 columns)
const defaultWidths = { const defaultWidths = {
0: totalWidth * 0.07, // Cont: ~20mm 0: totalWidth * 0.07, // Cont: ~20mm
1: totalWidth * 0.33, // Denumire: ~93mm 1: totalWidth * 0.33, // Denumire: ~93mm
2: totalWidth * 0.10, // Sold Prec D: ~28mm 2: totalWidth * 0.1, // Sold Prec D: ~28mm
3: totalWidth * 0.10, // Sold Prec C: ~28mm 3: totalWidth * 0.1, // Sold Prec C: ~28mm
4: totalWidth * 0.10, // Rulaj D: ~28mm 4: totalWidth * 0.1, // Rulaj D: ~28mm
5: totalWidth * 0.10, // Rulaj C: ~28mm 5: totalWidth * 0.1, // Rulaj C: ~28mm
6: totalWidth * 0.10, // Sold Final D: ~28mm 6: totalWidth * 0.1, // Sold Final D: ~28mm
7: totalWidth * 0.10, // Sold Final C: ~28mm 7: totalWidth * 0.1, // Sold Final C: ~28mm
}; };
widthAllocations[index] = defaultWidths[index] || 'auto'; widthAllocations[index] = defaultWidths[index] || "auto";
} }
}); });
columns.forEach((col, index) => { columns.forEach((col, index) => {
columnStyles[index] = { columnStyles[index] = {
cellWidth: widthAllocations[index] cellWidth: widthAllocations[index],
}; };
// Set alignment based on type // Set alignment based on type
if (col.type === 'number' || col.type === 'currency') { if (col.type === "number" || col.type === "currency") {
columnStyles[index].halign = 'right'; columnStyles[index].halign = "right";
} else if (col.type === 'text') { } else if (col.type === "text") {
// All text columns aligned left (including Cont) // 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: { styles: {
fontSize: 9, fontSize: 9,
cellPadding: 2.5, cellPadding: 2.5,
valign: 'middle', valign: "middle",
lineColor: [200, 200, 200], lineColor: [200, 200, 200],
lineWidth: 0.1, lineWidth: 0.1,
overflow: 'linebreak' overflow: "linebreak",
}, },
headStyles: { headStyles: {
fillColor: [41, 128, 185], fillColor: [41, 128, 185],
textColor: 255, textColor: 255,
fontStyle: 'bold', fontStyle: "bold",
halign: 'center', halign: "center",
fontSize: 9, fontSize: 9,
cellPadding: 2.5 cellPadding: 2.5,
}, },
alternateRowStyles: { alternateRowStyles: {
fillColor: [248, 248, 248] fillColor: [248, 248, 248],
}, },
columnStyles: columnStyles, 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 tableWidth: pageWidth - marginLeft - marginRight, // Use full page width
theme: 'grid', theme: "grid",
didDrawPage: function(data) { didDrawPage: function (data) {
// Add header to each page // Add header to each page
addHeader(); addHeader();
}, },
didParseCell: function(data) { didParseCell: function (data) {
// Force alignment based on column type (body cells only) // Force alignment based on column type (body cells only)
if (data.section === 'body') { if (data.section === "body") {
const colIndex = data.column.index; const colIndex = data.column.index;
const column = columns[colIndex]; const column = columns[colIndex];
if (column) { if (column) {
if (column.type === 'number' || column.type === 'currency') { if (column.type === "number" || column.type === "currency") {
data.cell.styles.halign = 'right'; data.cell.styles.halign = "right";
} else if (column.type === 'text') { } else if (column.type === "text") {
if (colIndex === 0) { if (colIndex === 0) {
data.cell.styles.halign = 'center'; data.cell.styles.halign = "center";
} else { } else {
data.cell.styles.halign = 'left'; data.cell.styles.halign = "left";
} }
} }
} }
@@ -247,18 +259,18 @@ export const exportToPDF = (data, columns, filename, header) => {
// Draw headers // Draw headers
doc.setFontSize(8); doc.setFontSize(8);
doc.setFont(undefined, 'bold'); doc.setFont(undefined, "bold");
tableColumns.forEach((header, index) => { tableColumns.forEach((header, index) => {
doc.text(header, 14 + (index * 35), yPos); doc.text(header, 14 + index * 35, yPos);
}); });
// Draw rows // Draw rows
doc.setFont(undefined, 'normal'); doc.setFont(undefined, "normal");
doc.setFontSize(7); doc.setFontSize(7);
tableRows.forEach((row, rowIndex) => { tableRows.forEach((row, rowIndex) => {
yPos += 7; yPos += 7;
row.forEach((cell, cellIndex) => { 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 // 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 }; return { success: true };
} catch (error) { } catch (error) {
console.error('PDF export error details:', error); console.error("PDF export error details:", error);
return { success: false, error: error.message || 'PDF generation failed' }; return { success: false, error: error.message || "PDF generation failed" };
} }
}; };
@@ -281,31 +293,31 @@ export const exportToPDF = (data, columns, filename, header) => {
export const exportGeneralTotals = (summaryData) => { export const exportGeneralTotals = (summaryData) => {
const data = [ const data = [
{ {
Tip: 'Clienți', Tip: "Clienți",
'Total Facturat': summaryData?.clienti_total_facturat || 0, "Total Facturat": summaryData?.clienti_total_facturat || 0,
'Total Încasat': summaryData?.clienti_total_incasat || 0, "Total Încasat": summaryData?.clienti_total_incasat || 0,
'Sold Net': summaryData?.clienti_sold_total || 0, "Sold Net": summaryData?.clienti_sold_total || 0,
'Sold În Termen': summaryData?.clienti_sold_in_termen || 0, "Sold În Termen": summaryData?.clienti_sold_in_termen || 0,
'Sold Restant': summaryData?.clienti_sold_restant || 0 "Sold Restant": summaryData?.clienti_sold_restant || 0,
}, },
{ {
Tip: 'Furnizori', Tip: "Furnizori",
'Total Facturat': summaryData?.furnizori_total_facturat || 0, "Total Facturat": summaryData?.furnizori_total_facturat || 0,
'Total Achitat': summaryData?.furnizori_total_achitat || 0, "Total Achitat": summaryData?.furnizori_total_achitat || 0,
'Sold Net': summaryData?.furnizori_sold_total || 0, "Sold Net": summaryData?.furnizori_sold_total || 0,
'Sold În Termen': summaryData?.furnizori_sold_in_termen || 0, "Sold În Termen": summaryData?.furnizori_sold_in_termen || 0,
'Sold Restant': summaryData?.furnizori_sold_restant || 0 "Sold Restant": summaryData?.furnizori_sold_restant || 0,
}, },
{ {
Tip: 'Trezorerie', Tip: "Trezorerie",
'Total Facturat': '-', "Total Facturat": "-",
'Total Încasat/Achitat': '-', "Total Încasat/Achitat": "-",
'Sold Net': summaryData?.trezorerie_sold || 0, "Sold Net": summaryData?.trezorerie_sold || 0,
'Sold În Termen': '-', "Sold În Termen": "-",
'Sold Restant': '-' "Sold Restant": "-",
} },
]; ];
return data; return data;
}; };
@@ -315,27 +327,27 @@ export const exportGeneralTotals = (summaryData) => {
export const exportSoldNetBreakdown = (summaryData) => { export const exportSoldNetBreakdown = (summaryData) => {
const data = [ const data = [
{ {
Categorie: 'Clienți - Restant', Categorie: "Clienți - Restant",
'TOTAL': summaryData?.clienti_sold_restant || 0, TOTAL: summaryData?.clienti_sold_restant || 0,
'7 zile': summaryData?.clienti_restant_7 || 0, "7 zile": summaryData?.clienti_restant_7 || 0,
'14 zile': summaryData?.clienti_restant_14 || 0, "14 zile": summaryData?.clienti_restant_14 || 0,
'30 zile': summaryData?.clienti_restant_30 || 0, "30 zile": summaryData?.clienti_restant_30 || 0,
'60 zile': summaryData?.clienti_restant_60 || 0, "60 zile": summaryData?.clienti_restant_60 || 0,
'90 zile': summaryData?.clienti_restant_90 || 0, "90 zile": summaryData?.clienti_restant_90 || 0,
'90+ zile': summaryData?.clienti_restant_over_90 || 0 "90+ zile": summaryData?.clienti_restant_over_90 || 0,
}, },
{ {
Categorie: 'Furnizori - Restant', Categorie: "Furnizori - Restant",
'TOTAL': summaryData?.furnizori_sold_restant || 0, TOTAL: summaryData?.furnizori_sold_restant || 0,
'7 zile': summaryData?.furnizori_restant_7 || 0, "7 zile": summaryData?.furnizori_restant_7 || 0,
'14 zile': summaryData?.furnizori_restant_14 || 0, "14 zile": summaryData?.furnizori_restant_14 || 0,
'30 zile': summaryData?.furnizori_restant_30 || 0, "30 zile": summaryData?.furnizori_restant_30 || 0,
'60 zile': summaryData?.furnizori_restant_60 || 0, "60 zile": summaryData?.furnizori_restant_60 || 0,
'90 zile': summaryData?.furnizori_restant_90 || 0, "90 zile": summaryData?.furnizori_restant_90 || 0,
'90+ zile': summaryData?.furnizori_restant_over_90 || 0 "90+ zile": summaryData?.furnizori_restant_over_90 || 0,
} },
]; ];
return data; return data;
}; };
@@ -349,14 +361,14 @@ export const exportTrendData = (trendsData, period, chartType) => {
const data = trendsData.labels.map((label, index) => { const data = trendsData.labels.map((label, index) => {
const row = { Perioada: label }; const row = { Perioada: label };
trendsData.datasets.forEach(dataset => { trendsData.datasets.forEach((dataset) => {
const value = dataset.data[index]; const value = dataset.data[index];
row[dataset.label] = value || 0; row[dataset.label] = value || 0;
}); });
return row; return row;
}); });
return data; return data;
}; };

View File

@@ -26,19 +26,22 @@
</div> </div>
<div class="filter-item"> <div class="filter-item">
<label>Căutare partener</label> <label>Căutare partener</label>
<InputText v-model="filters.partnerName" placeholder="Nume partener..." /> <InputText
v-model="filters.partnerName"
placeholder="Nume partener..."
/>
</div> </div>
<div class="filter-actions"> <div class="filter-actions">
<Button <Button
label="Aplică Filtre" label="Aplică Filtre"
icon="pi pi-filter" icon="pi pi-filter"
@click="applyFilters" @click="applyFilters"
/> />
<Button <Button
label="Resetează" label="Resetează"
icon="pi pi-times" icon="pi pi-times"
class="p-button-secondary" class="p-button-secondary"
@click="resetFilters" @click="resetFilters"
/> />
</div> </div>
</div> </div>
@@ -54,7 +57,9 @@
<i class="pi pi-arrow-down"></i> <i class="pi pi-arrow-down"></i>
</div> </div>
<div class="stat-details"> <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> <p class="stat-label">Total Încasări</p>
</div> </div>
</div> </div>
@@ -67,7 +72,9 @@
<i class="pi pi-arrow-up"></i> <i class="pi pi-arrow-up"></i>
</div> </div>
<div class="stat-details"> <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> <p class="stat-label">Total Plăți</p>
</div> </div>
</div> </div>
@@ -99,29 +106,53 @@
<Column field="nume_cont_bancar" header="Cont" /> <Column field="nume_cont_bancar" header="Cont" />
<Column field="tip_registru" header="Tip"> <Column field="tip_registru" header="Tip">
<template #body="slotProps"> <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> </template>
</Column> </Column>
<Column field="incasari" header="Încasări"> <Column field="incasari" header="Încasări">
<template #body="slotProps"> <template #body="slotProps">
<span class="text-success font-semibold" v-if="slotProps.data.incasari > 0"> <span
{{ formatCurrency(slotProps.data.incasari, slotProps.data.valuta) }} class="text-success font-semibold"
v-if="slotProps.data.incasari > 0"
>
{{
formatCurrency(
slotProps.data.incasari,
slotProps.data.valuta,
)
}}
</span> </span>
<span v-else>-</span> <span v-else>-</span>
</template> </template>
</Column> </Column>
<Column field="plati" header="Plăți"> <Column field="plati" header="Plăți">
<template #body="slotProps"> <template #body="slotProps">
<span class="text-error font-semibold" v-if="slotProps.data.plati > 0"> <span
{{ formatCurrency(slotProps.data.plati, slotProps.data.valuta) }} class="text-error font-semibold"
v-if="slotProps.data.plati > 0"
>
{{
formatCurrency(slotProps.data.plati, slotProps.data.valuta)
}}
</span> </span>
<span v-else>-</span> <span v-else>-</span>
</template> </template>
</Column> </Column>
<Column field="sold" header="Sold"> <Column field="sold" header="Sold">
<template #body="slotProps"> <template #body="slotProps">
<span :class="slotProps.data.sold >= 0 ? 'text-success font-semibold' : 'text-error font-semibold'"> <span
{{ formatCurrency(slotProps.data.sold, slotProps.data.valuta) }} :class="
slotProps.data.sold >= 0
? 'text-success font-semibold'
: 'text-error font-semibold'
"
>
{{
formatCurrency(slotProps.data.sold, slotProps.data.valuta)
}}
</span> </span>
</template> </template>
</Column> </Column>
@@ -146,19 +177,19 @@ const companyStore = useCompanyStore();
const filters = ref({ const filters = ref({
dateFrom: null, dateFrom: null,
dateTo: null, dateTo: null,
partnerName: "" partnerName: "",
}); });
const pagination = ref({ const pagination = ref({
page: 0, page: 0,
rows: 50 rows: 50,
}); });
const formatCurrency = (amount, currency = 'RON') => { const formatCurrency = (amount, currency = "RON") => {
if (!amount) return "0,00 " + currency; if (!amount) return "0,00 " + currency;
return new Intl.NumberFormat("ro-RO", { return new Intl.NumberFormat("ro-RO", {
style: "currency", style: "currency",
currency: currency currency: currency,
}).format(amount); }).format(amount);
}; };
@@ -168,12 +199,12 @@ const formatDate = (dateString) => {
}; };
const getRowClass = (data) => { 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) => { const getRegisterSeverity = (type) => {
if (type.includes('BANCA')) return 'info'; if (type.includes("BANCA")) return "info";
if (type.includes('CASA')) return 'warning'; if (type.includes("CASA")) return "warning";
return null; return null;
}; };
@@ -191,7 +222,7 @@ const resetFilters = () => {
filters.value = { filters.value = {
dateFrom: null, dateFrom: null,
dateTo: null, dateTo: null,
partnerName: "" partnerName: "",
}; };
loadData(); loadData();
}; };
@@ -206,8 +237,8 @@ const loadData = async () => {
{ {
date_from: filters.value.dateFrom?.toISOString().split("T")[0], date_from: filters.value.dateFrom?.toISOString().split("T")[0],
date_to: filters.value.dateTo?.toISOString().split("T")[0], date_to: filters.value.dateTo?.toISOString().split("T")[0],
partner_name: filters.value.partnerName partner_name: filters.value.partnerName,
} },
); );
}; };
@@ -314,4 +345,4 @@ onMounted(() => {
flex: 1; flex: 1;
} }
} }
</style> </style>

View File

@@ -38,8 +38,11 @@
</div> </div>
<div class="status-item"> <div class="status-item">
<label>Your Setting:</label> <label>Your Setting:</label>
<InputSwitch v-model="userCacheEnabled" @change="toggleUserCache" /> <InputSwitch
<span>{{ userCacheEnabled ? 'ON' : 'OFF' }}</span> v-model="userCacheEnabled"
@change="toggleUserCache"
/>
<span>{{ userCacheEnabled ? "ON" : "OFF" }}</span>
</div> </div>
<div class="status-item"> <div class="status-item">
<label>Auto-Invalidation:</label> <label>Auto-Invalidation:</label>
@@ -58,7 +61,10 @@
<template #content> <template #content>
<div class="hit-rate"> <div class="hit-rate">
<h3>Hit Rate: {{ stats.hit_rate?.toFixed(1) }}%</h3> <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" /> <ProgressBar :value="stats.hit_rate" />
</div> </div>
</template> </template>
@@ -70,13 +76,23 @@
<template #content> <template #content>
<ul class="queries-list"> <ul class="queries-list">
<li> <li>
Today: <strong>{{ stats.queries_saved?.today?.toLocaleString() }}</strong> queries avoided Today:
<strong>{{
stats.queries_saved?.today?.toLocaleString()
}}</strong>
queries avoided
</li> </li>
<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>
<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> </li>
</ul> </ul>
</template> </template>
@@ -102,8 +118,9 @@
</DataTable> </DataTable>
<div v-if="overallAvg" class="average-row"> <div v-if="overallAvg" class="average-row">
<strong>Overall Average:</strong> <strong>Overall Average:</strong>
{{ overallAvg.cached }} ms vs {{ overallAvg.oracle }} ms {{ overallAvg.cached }} ms vs {{ overallAvg.oracle }} ms ({{
({{ overallAvg.improvement }}% faster) overallAvg.improvement
}}% faster)
</div> </div>
</template> </template>
</Card> </Card>
@@ -113,9 +130,17 @@
<template #title>Cache Details</template> <template #title>Cache Details</template>
<template #content> <template #content>
<ul class="details-list"> <ul class="details-list">
<li>Memory entries: <strong>{{ stats.cache_size?.memory?.toLocaleString() }}</strong></li> <li>
<li>SQLite entries: <strong>{{ stats.cache_size?.sqlite?.toLocaleString() }}</strong></li> Memory entries:
<li>Cache type: <strong>{{ stats.cache_type }}</strong></li> <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> </ul>
</template> </template>
</Card> </Card>
@@ -135,150 +160,162 @@
<label for="clear_all">All companies</label> <label for="clear_all">All companies</label>
</div> </div>
<div class="p-field-radiobutton"> <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> <label for="clear_current">Current company only</label>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<Button label="Cancel" text @click="showClearDialog = false" /> <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> </template>
</Dialog> </Dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from "vue";
import { useCacheStore } from '@/stores/cacheStore' import { useCacheStore } from "@/stores/cacheStore";
import { useCompanyStore } from '@/stores/companies' import { useCompanyStore } from "@/stores/companies";
import { useToast } from 'primevue/usetoast' import { useToast } from "primevue/usetoast";
import Button from 'primevue/button' import Button from "primevue/button";
import Card from 'primevue/card' import Card from "primevue/card";
import DataTable from 'primevue/datatable' import DataTable from "primevue/datatable";
import Column from 'primevue/column' import Column from "primevue/column";
import Tag from 'primevue/tag' import Tag from "primevue/tag";
import ProgressBar from 'primevue/progressbar' import ProgressBar from "primevue/progressbar";
import InputSwitch from 'primevue/inputswitch' import InputSwitch from "primevue/inputswitch";
import Dialog from 'primevue/dialog' import Dialog from "primevue/dialog";
import RadioButton from 'primevue/radiobutton' import RadioButton from "primevue/radiobutton";
import Message from 'primevue/message' import Message from "primevue/message";
const cacheStore = useCacheStore() const cacheStore = useCacheStore();
const companyStore = useCompanyStore() const companyStore = useCompanyStore();
const toast = useToast() const toast = useToast();
const loading = computed(() => cacheStore.isLoading) const loading = computed(() => cacheStore.isLoading);
const error = computed(() => cacheStore.error) const error = computed(() => cacheStore.error);
const stats = computed(() => cacheStore.stats) const stats = computed(() => cacheStore.stats);
const userCacheEnabled = ref(true) const userCacheEnabled = ref(true);
const showClearDialog = ref(false) const showClearDialog = ref(false);
const clearScope = ref('current') const clearScope = ref("current");
const responseTimesTable = computed(() => { 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]) => ({ return Object.entries(stats.value.response_times).map(([key, data]) => ({
endpoint: formatEndpointName(key), endpoint: formatEndpointName(key),
cached: data.cached, cached: data.cached,
oracle: data.oracle, oracle: data.oracle,
improvement: data.improvement improvement: data.improvement,
})) }));
}) });
const overallAvg = computed(() => { const overallAvg = computed(() => {
const times = Object.values(stats.value?.response_times || {}) const times = Object.values(stats.value?.response_times || {});
if (times.length === 0) return null if (times.length === 0) return null;
const avgCached = times.reduce((sum, t) => sum + t.cached, 0) / times.length 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 avgOracle = times.reduce((sum, t) => sum + t.oracle, 0) / times.length;
const improvement = ((avgOracle - avgCached) / avgOracle * 100).toFixed(0) const improvement = (((avgOracle - avgCached) / avgOracle) * 100).toFixed(0);
return { return {
cached: avgCached.toFixed(0), cached: avgCached.toFixed(0),
oracle: avgOracle.toFixed(0), oracle: avgOracle.toFixed(0),
improvement improvement,
} };
}) });
async function loadStats() { async function loadStats() {
try { try {
await cacheStore.getStats() await cacheStore.getStats();
userCacheEnabled.value = stats.value?.user_enabled ?? true userCacheEnabled.value = stats.value?.user_enabled ?? true;
} catch (error) { } catch (error) {
toast.add({ toast.add({
severity: 'error', severity: "error",
summary: 'Error', summary: "Error",
detail: 'Failed to load cache statistics', detail: "Failed to load cache statistics",
life: 3000 life: 3000,
}) });
} }
} }
async function toggleUserCache() { async function toggleUserCache() {
try { try {
await cacheStore.toggleUserCache(userCacheEnabled.value) await cacheStore.toggleUserCache(userCacheEnabled.value);
toast.add({ toast.add({
severity: 'success', severity: "success",
summary: 'Success', summary: "Success",
detail: `Cache ${userCacheEnabled.value ? 'enabled' : 'disabled'} for you`, detail: `Cache ${userCacheEnabled.value ? "enabled" : "disabled"} for you`,
life: 3000 life: 3000,
}) });
} catch (error) { } catch (error) {
toast.add({ toast.add({
severity: 'error', severity: "error",
summary: 'Error', summary: "Error",
detail: 'Failed to toggle cache', detail: "Failed to toggle cache",
life: 3000 life: 3000,
}) });
// Revert toggle // Revert toggle
userCacheEnabled.value = !userCacheEnabled.value userCacheEnabled.value = !userCacheEnabled.value;
} }
} }
async function clearCache() { async function clearCache() {
try { try {
const companyId = clearScope.value === 'current' ? companyStore.currentCompany?.id_firma : null const companyId =
await cacheStore.invalidateCache(companyId, null) clearScope.value === "current"
? companyStore.currentCompany?.id_firma
: null;
await cacheStore.invalidateCache(companyId, null);
toast.add({ toast.add({
severity: 'success', severity: "success",
summary: 'Success', summary: "Success",
detail: 'Cache cleared successfully', detail: "Cache cleared successfully",
life: 3000 life: 3000,
}) });
showClearDialog.value = false showClearDialog.value = false;
await loadStats() await loadStats();
} catch (error) { } catch (error) {
toast.add({ toast.add({
severity: 'error', severity: "error",
summary: 'Error', summary: "Error",
detail: 'Failed to clear cache', detail: "Failed to clear cache",
life: 3000 life: 3000,
}) });
} }
} }
function formatEndpointName(key) { function formatEndpointName(key) {
const names = { const names = {
'schema': 'Schema Lookup', schema: "Schema Lookup",
'dashboard_summary': 'Dashboard', dashboard_summary: "Dashboard",
'dashboard_trends': 'Dashboard Trends', dashboard_trends: "Dashboard Trends",
'companies': 'Companies List', companies: "Companies List",
'invoices': 'Invoices', invoices: "Invoices",
'treasury': 'Treasury' treasury: "Treasury",
} };
return names[key] || key return names[key] || key;
} }
function clearError() { function clearError() {
cacheStore.clearError() cacheStore.clearError();
} }
onMounted(() => { onMounted(() => {
loadStats() loadStats();
}) });
</script> </script>
<style scoped> <style scoped>

File diff suppressed because it is too large Load Diff

View File

@@ -189,7 +189,7 @@
<Column field="cont" header="Cont" sortable> <Column field="cont" header="Cont" sortable>
<template #body="slotProps"> <template #body="slotProps">
{{ slotProps.data.cont || '-' }} {{ slotProps.data.cont || "-" }}
</template> </template>
</Column> </Column>
@@ -241,10 +241,15 @@
</template> </template>
</Column> </Column>
<Column field="valuta" header="Valuta" sortable :style="{ width: '8%' }"> <Column
field="valuta"
header="Valuta"
sortable
:style="{ width: '8%' }"
>
<template #body="slotProps"> <template #body="slotProps">
<div class="text-center"> <div class="text-center">
{{ slotProps.data.valuta || 'RON' }} {{ slotProps.data.valuta || "RON" }}
</div> </div>
</template> </template>
</Column> </Column>
@@ -253,7 +258,9 @@
<!-- Total Sold --> <!-- Total Sold -->
<div v-if="invoicesStore.hasInvoices" class="total-sold"> <div v-if="invoicesStore.hasInvoices" class="total-sold">
<span class="total-sold-label">Total Sold:</span> <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> </div>
</template> </template>
</Card> </Card>
@@ -300,8 +307,18 @@ const totalSold = computed(() => {
const accountingPeriodText = computed(() => { const accountingPeriodText = computed(() => {
const months = [ const months = [
"Ianuarie", "Februarie", "Martie", "Aprilie", "Mai", "Iunie", "Ianuarie",
"Iulie", "August", "Septembrie", "Octombrie", "Noiembrie", "Decembrie" "Februarie",
"Martie",
"Aprilie",
"Mai",
"Iunie",
"Iulie",
"August",
"Septembrie",
"Octombrie",
"Noiembrie",
"Decembrie",
]; ];
const luna = invoicesStore.accountingPeriod.luna; const luna = invoicesStore.accountingPeriod.luna;
const an = invoicesStore.accountingPeriod.an; const an = invoicesStore.accountingPeriod.an;
@@ -405,33 +422,42 @@ const loadInvoices = async () => {
invoicesStore.setPagination(pagination.value); invoicesStore.setPagination(pagination.value);
const params = { const params = {
partner_type: filters.value.type, // FIX: Add partner_type filter partner_type: filters.value.type, // FIX: Add partner_type filter
page: pagination.value.page, page: pagination.value.page,
page_size: pagination.value.rows, page_size: pagination.value.rows,
only_unpaid: filters.value.paymentStatus === "neachitate", // Use filter value only_unpaid: filters.value.paymentStatus === "neachitate", // Use filter value
}; };
// Add optional filters (use LOCAL date, not UTC) // Add optional filters (use LOCAL date, not UTC)
if (filters.value.dateFrom) { if (filters.value.dateFrom) {
const year = filters.value.dateFrom.getFullYear(); const year = filters.value.dateFrom.getFullYear();
const month = String(filters.value.dateFrom.getMonth() + 1).padStart(2, '0'); const month = String(filters.value.dateFrom.getMonth() + 1).padStart(
const day = String(filters.value.dateFrom.getDate()).padStart(2, '0'); 2,
"0",
);
const day = String(filters.value.dateFrom.getDate()).padStart(2, "0");
params.date_from = `${year}-${month}-${day}`; params.date_from = `${year}-${month}-${day}`;
} }
if (filters.value.dateTo) { if (filters.value.dateTo) {
const year = filters.value.dateTo.getFullYear(); const year = filters.value.dateTo.getFullYear();
const month = String(filters.value.dateTo.getMonth() + 1).padStart(2, '0'); const month = String(filters.value.dateTo.getMonth() + 1).padStart(
const day = String(filters.value.dateTo.getDate()).padStart(2, '0'); 2,
"0",
);
const day = String(filters.value.dateTo.getDate()).padStart(2, "0");
params.date_to = `${year}-${month}-${day}`; params.date_to = `${year}-${month}-${day}`;
} }
if (filters.value.searchTerm) { if (filters.value.searchTerm) {
params.partner_name = filters.value.searchTerm; // FIX: Use partner_name not search params.partner_name = filters.value.searchTerm; // FIX: Use partner_name not search
} }
if (filters.value.cont) { if (filters.value.cont) {
params.cont = filters.value.cont; params.cont = filters.value.cont;
} }
await invoicesStore.loadInvoices(companyStore.selectedCompany.id_firma, params); await invoicesStore.loadInvoices(
companyStore.selectedCompany.id_firma,
params,
);
} catch (error) { } catch (error) {
console.error("Failed to load invoices:", error); console.error("Failed to load invoices:", error);
toast.add({ toast.add({
@@ -462,27 +488,33 @@ const fetchAllInvoicesData = async () => {
try { try {
const params = { const params = {
company: companyStore.selectedCompany.id_firma, company: companyStore.selectedCompany.id_firma,
partner_type: filters.value.type, // FIX: Correctly pass partner_type partner_type: filters.value.type, // FIX: Correctly pass partner_type
page: 1, page: 1,
page_size: 999999, // Get all data page_size: 999999, // Get all data
only_unpaid: filters.value.paymentStatus === "neachitate", // Use filter value for export only_unpaid: filters.value.paymentStatus === "neachitate", // Use filter value for export
}; };
// Add optional filters (use LOCAL date, not UTC) // Add optional filters (use LOCAL date, not UTC)
if (filters.value.dateFrom) { if (filters.value.dateFrom) {
const year = filters.value.dateFrom.getFullYear(); const year = filters.value.dateFrom.getFullYear();
const month = String(filters.value.dateFrom.getMonth() + 1).padStart(2, '0'); const month = String(filters.value.dateFrom.getMonth() + 1).padStart(
const day = String(filters.value.dateFrom.getDate()).padStart(2, '0'); 2,
"0",
);
const day = String(filters.value.dateFrom.getDate()).padStart(2, "0");
params.date_from = `${year}-${month}-${day}`; params.date_from = `${year}-${month}-${day}`;
} }
if (filters.value.dateTo) { if (filters.value.dateTo) {
const year = filters.value.dateTo.getFullYear(); const year = filters.value.dateTo.getFullYear();
const month = String(filters.value.dateTo.getMonth() + 1).padStart(2, '0'); const month = String(filters.value.dateTo.getMonth() + 1).padStart(
const day = String(filters.value.dateTo.getDate()).padStart(2, '0'); 2,
"0",
);
const day = String(filters.value.dateTo.getDate()).padStart(2, "0");
params.date_to = `${year}-${month}-${day}`; params.date_to = `${year}-${month}-${day}`;
} }
if (filters.value.searchTerm) { if (filters.value.searchTerm) {
params.partner_name = filters.value.searchTerm; // FIX: Use partner_name not search params.partner_name = filters.value.searchTerm; // FIX: Use partner_name not search
} }
if (filters.value.cont) { if (filters.value.cont) {
params.cont = filters.value.cont; params.cont = filters.value.cont;
@@ -531,22 +563,23 @@ const exportExcel = async () => {
// Prepare data for export - Format dates as strings for Excel // Prepare data for export - Format dates as strings for Excel
const exportData = allData.map((row) => ({ const exportData = allData.map((row) => ({
"Cont": row.cont || "", Cont: row.cont || "",
"Numar Doc.": row.nract, "Numar Doc.": row.nract,
"Data Doc.": row.dataact ? formatDate(row.dataact) : "", "Data Doc.": row.dataact ? formatDate(row.dataact) : "",
"Data Scadenta": row.datascad ? formatDate(row.datascad) : "", "Data Scadenta": row.datascad ? formatDate(row.datascad) : "",
"Partener": row.nume, Partener: row.nume,
"Facturat": parseFloat(row.totctva) || 0, Facturat: parseFloat(row.totctva) || 0,
"Achitat": parseFloat(row.achitat) || 0, Achitat: parseFloat(row.achitat) || 0,
"Sold": parseFloat(row.soldfinal) || 0, Sold: parseFloat(row.soldfinal) || 0,
"Valuta": row.valuta || "RON", Valuta: row.valuta || "RON",
})); }));
const invoiceType = filters.value.type === "CLIENTI" ? "Clienti" : "Furnizori"; const invoiceType =
filters.value.type === "CLIENTI" ? "Clienti" : "Furnizori";
const result = exportToExcel( const result = exportToExcel(
exportData, exportData,
`facturi_${invoiceType}_${companyStore.selectedCompany.name.replace(/\s+/g, "_")}`, `facturi_${invoiceType}_${companyStore.selectedCompany.name.replace(/\s+/g, "_")}`,
`Facturi ${invoiceType}` `Facturi ${invoiceType}`,
); );
if (result.success) { if (result.success) {
@@ -613,27 +646,34 @@ const exportPDF = async () => {
// Define columns for PDF with optimized widths (proportional percentages) // Define columns for PDF with optimized widths (proportional percentages)
// Compact numeric columns, more space for Partener (company names) // Compact numeric columns, more space for Partener (company names)
const columns = [ const columns = [
{ field: "cont", header: "Cont", type: "text", width: 0.06 }, // 6% - Compact account numbers { field: "cont", header: "Cont", type: "text", width: 0.06 }, // 6% - Compact account numbers
{ field: "nract", header: "Numar Doc.", type: "text", width: 0.08 }, // 8% - Document numbers { field: "nract", header: "Numar Doc.", type: "text", width: 0.08 }, // 8% - Document numbers
{ field: "dataact", header: "Data Doc.", type: "text", width: 0.08 }, // 8% - Dates { field: "dataact", header: "Data Doc.", type: "text", width: 0.08 }, // 8% - Dates
{ field: "datascad", header: "Data Scadenta", type: "text", width: 0.09 }, // 9% - Due dates { field: "datascad", header: "Data Scadenta", type: "text", width: 0.09 }, // 9% - Due dates
{ field: "nume", header: "Partener", type: "text", width: 0.37 }, // 37% - MORE SPACE for company names { field: "nume", header: "Partener", type: "text", width: 0.37 }, // 37% - MORE SPACE for company names
{ field: "totctva", header: "Facturat", type: "number", width: 0.09 }, // 9% - Compact numbers { field: "totctva", header: "Facturat", type: "number", width: 0.09 }, // 9% - Compact numbers
{ field: "achitat", header: "Achitat", type: "number", width: 0.09 }, // 9% - Compact numbers { field: "achitat", header: "Achitat", type: "number", width: 0.09 }, // 9% - Compact numbers
{ field: "soldfinal", header: "Sold", type: "number", width: 0.09 }, // 9% - Compact numbers { field: "soldfinal", header: "Sold", type: "number", width: 0.09 }, // 9% - Compact numbers
{ field: "valuta", header: "Valuta", type: "text", width: 0.05 }, // 5% - Very compact (just "RON") { 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) // Build period string - ALWAYS show accounting period (like Trial Balance)
let periodText = accountingPeriodText.value || ""; let periodText = accountingPeriodText.value || "";
// Optionally add date filter range if applied // Optionally add date filter range if applied
if (filters.value.dateFrom || filters.value.dateTo) { if (filters.value.dateFrom || filters.value.dateTo) {
const fromDate = filters.value.dateFrom ? formatDate(filters.value.dateFrom) : "început"; const fromDate = filters.value.dateFrom
const toDate = filters.value.dateTo ? formatDate(filters.value.dateTo) : "prezent"; ? formatDate(filters.value.dateFrom)
periodText += periodText ? ` | Filtru dată: ${fromDate} - ${toDate}` : `Filtru dată: ${fromDate} - ${toDate}`; : "început";
const toDate = filters.value.dateTo
? formatDate(filters.value.dateTo)
: "prezent";
periodText += periodText
? ` | Filtru dată: ${fromDate} - ${toDate}`
: `Filtru dată: ${fromDate} - ${toDate}`;
} }
const result = exportToPDF( const result = exportToPDF(
@@ -643,8 +683,8 @@ const exportPDF = async () => {
{ {
companyName: companyStore.selectedCompany?.name || "", companyName: companyStore.selectedCompany?.name || "",
title: `Facturi ${invoiceType}`, title: `Facturi ${invoiceType}`,
period: periodText period: periodText,
} },
); );
if (result.success) { if (result.success) {

View File

@@ -13,12 +13,14 @@
<template #content> <template #content>
<form @submit.prevent="handleLogin" class="login-form"> <form @submit.prevent="handleLogin" class="login-form">
<div class="form-group"> <div class="form-group">
<label for="username" class="form-label required">Utilizator</label> <label for="username" class="form-label required"
>Utilizator</label
>
<InputText <InputText
id="username" id="username"
v-model="credentials.username" v-model="credentials.username"
placeholder="Introduceți numele de utilizator" placeholder="Introduceți numele de utilizator"
:class="{ 'invalid': formErrors.username }" :class="{ invalid: formErrors.username }"
class="w-full" class="w-full"
autocomplete="username" autocomplete="username"
@blur="validateField('username')" @blur="validateField('username')"
@@ -34,7 +36,7 @@
id="password" id="password"
v-model="credentials.password" v-model="credentials.password"
placeholder="Introduceți parola" placeholder="Introduceți parola"
:class="{ 'invalid': formErrors.password }" :class="{ invalid: formErrors.password }"
class="w-full" class="w-full"
:feedback="false" :feedback="false"
toggle-mask toggle-mask
@@ -192,7 +194,11 @@ onUnmounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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; padding: 1rem;
} }
@@ -279,35 +285,35 @@ onUnmounted(() => {
.login-container { .login-container {
padding: 0.5rem; padding: 0.5rem;
} }
.login-wrapper { .login-wrapper {
max-width: 100%; max-width: 100%;
padding: 0 1rem; padding: 0 1rem;
} }
.login-card { .login-card {
border-radius: 8px; border-radius: 8px;
} }
.login-header { .login-header {
padding: 1.5rem 1rem; padding: 1.5rem 1rem;
} }
.login-title { .login-title {
font-size: 1.5rem; font-size: 1.5rem;
} }
.login-form { .login-form {
padding: 0 1rem 1.5rem 1rem; padding: 0 1rem 1.5rem 1rem;
} }
/* Ensure inputs are touch-friendly */ /* Ensure inputs are touch-friendly */
.p-inputtext, .p-inputtext,
.p-password input { .p-password input {
min-height: 44px; min-height: 44px;
font-size: 16px; /* Prevents zoom on iOS */ font-size: 16px; /* Prevents zoom on iOS */
} }
.login-footer { .login-footer {
padding: 1rem; padding: 1rem;
} }
@@ -329,7 +335,7 @@ onUnmounted(() => {
.login-title { .login-title {
font-size: 1.25rem; font-size: 1.25rem;
} }
.login-subtitle { .login-subtitle {
font-size: 0.875rem; font-size: 0.875rem;
} }
@@ -337,7 +343,7 @@ onUnmounted(() => {
.login-form { .login-form {
padding: 0 0.5rem 1rem 0.5rem; padding: 0 0.5rem 1rem 0.5rem;
} }
.login-footer { .login-footer {
padding: 0.75rem 0.5rem; padding: 0.75rem 0.5rem;
} }

View File

@@ -1,192 +1,189 @@
<template> <template>
<main class="main-content"> <main class="main-content">
<div class="app-container"> <div class="app-container">
<!-- Page Header -->
<!-- Page Header --> <div class="page-header">
<div class="page-header"> <h1 class="page-title">Telegram Bot</h1>
<h1 class="page-title">Telegram Bot</h1> <p class="page-subtitle">
<p class="page-subtitle">Conectează-ți contul pentru acces rapid din Telegram</p> Conectează-ți contul pentru acces rapid din Telegram
</div> </p>
<!-- Loading State -->
<div v-if="loading" class="loading-state">
<div class="loading-spinner"></div>
<p>Se generează codul...</p>
</div>
<!-- Main Card -->
<div v-else class="card">
<!-- Generate Button -->
<div class="generate-section">
<button
@click="generateCode"
:disabled="loading"
class="btn btn-primary btn-lg"
>
{{ 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">
<span class="code-label">Cod</span>
<span class="code-timer">{{ formatTime(timeRemaining) }}</span>
</div>
<div class="code-value">{{ linkingCode }}</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<a
:href="telegramDeepLink"
target="_blank"
rel="noopener noreferrer"
class="btn btn-primary action-btn"
>
Deschide Telegram
</a>
<Button
:label="showQR ? 'Ascunde QR' : 'Arată QR'"
@click="showQR = !showQR"
class="action-btn"
outlined
/>
<Button
label="Copiază Cod"
@click="copyCode"
class="action-btn"
outlined
/>
</div>
<!-- QR Code Display -->
<div v-if="showQR" class="qr-section">
<QRCodeVue :value="telegramDeepLink" :size="200" level="H" />
</div>
</div>
</div>
</div> </div>
<!-- Loading State -->
<div v-if="loading" class="loading-state">
<div class="loading-spinner"></div>
<p>Se generează codul...</p>
</div>
<!-- Main Card -->
<div v-else class="card">
<!-- Generate Button -->
<div class="generate-section">
<button
@click="generateCode"
:disabled="loading"
class="btn btn-primary btn-lg"
>
{{ 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">
<span class="code-label">Cod</span>
<span class="code-timer">{{ formatTime(timeRemaining) }}</span>
</div>
<div class="code-value">{{ linkingCode }}</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<a
:href="telegramDeepLink"
target="_blank"
rel="noopener noreferrer"
class="btn btn-primary action-btn"
>
Deschide Telegram
</a>
<Button
:label="showQR ? 'Ascunde QR' : 'Arată QR'"
@click="showQR = !showQR"
class="action-btn"
outlined
/>
<Button
label="Copiază Cod"
@click="copyCode"
class="action-btn"
outlined
/>
</div>
<!-- QR Code Display -->
<div v-if="showQR" class="qr-section">
<QRCodeVue :value="telegramDeepLink" :size="200" level="H" />
</div>
</div>
</div>
</div>
</main> </main>
</template> </template>
<script setup> <script setup>
import { ref, computed, onUnmounted } from 'vue' import { ref, computed, onUnmounted } from "vue";
import { useToast } from 'primevue/usetoast' import { useToast } from "primevue/usetoast";
import Button from 'primevue/button' import Button from "primevue/button";
import Toast from 'primevue/toast' import Toast from "primevue/toast";
import QRCodeVue from 'qrcode.vue' import QRCodeVue from "qrcode.vue";
import { apiService } from '../services/api' import { apiService } from "../services/api";
const toast = useToast() const toast = useToast();
// State // State
const linkingCode = ref('') const linkingCode = ref("");
const timeRemaining = ref(0) const timeRemaining = ref(0);
const loading = ref(false) const loading = ref(false);
const showQR = ref(false) const showQR = ref(false);
let countdownInterval = null let countdownInterval = null;
// Config // 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 // Computed
const telegramDeepLink = computed(() => { const telegramDeepLink = computed(() => {
if (!linkingCode.value) return '' if (!linkingCode.value) return "";
return `https://t.me/${BOT_USERNAME}?start=${linkingCode.value}` return `https://t.me/${BOT_USERNAME}?start=${linkingCode.value}`;
}) });
// Methods // Methods
const generateCode = async () => { const generateCode = async () => {
loading.value = true loading.value = true;
showQR.value = false showQR.value = false;
try { try {
const response = await apiService.post('/telegram/auth/generate-code') const response = await apiService.post("/telegram/auth/generate-code");
linkingCode.value = response.data.linking_code linkingCode.value = response.data.linking_code;
timeRemaining.value = response.data.expires_in_minutes * 60 timeRemaining.value = response.data.expires_in_minutes * 60;
toast.add({ toast.add({
severity: 'success', severity: "success",
summary: 'Cod Generat', summary: "Cod Generat",
detail: 'Alege o metodă de conectare', detail: "Alege o metodă de conectare",
life: 3000 life: 3000,
}) });
startCountdown() startCountdown();
} catch (error) { } catch (error) {
console.error('Error generating code:', error) console.error("Error generating code:", error);
toast.add({ toast.add({
severity: 'error', severity: "error",
summary: 'Eroare', summary: "Eroare",
detail: error.response?.data?.detail || 'Nu am putut genera codul', detail: error.response?.data?.detail || "Nu am putut genera codul",
life: 5000 life: 5000,
}) });
} finally { } finally {
loading.value = false loading.value = false;
} }
} };
const startCountdown = () => { const startCountdown = () => {
if (countdownInterval) clearInterval(countdownInterval) if (countdownInterval) clearInterval(countdownInterval);
countdownInterval = setInterval(() => { countdownInterval = setInterval(() => {
if (timeRemaining.value > 0) { if (timeRemaining.value > 0) {
timeRemaining.value-- timeRemaining.value--;
} else { } else {
clearInterval(countdownInterval) clearInterval(countdownInterval);
linkingCode.value = '' linkingCode.value = "";
toast.add({ toast.add({
severity: 'warn', severity: "warn",
summary: 'Cod Expirat', summary: "Cod Expirat",
detail: 'Generează un cod nou', detail: "Generează un cod nou",
life: 4000 life: 4000,
}) });
} }
}, 1000) }, 1000);
} };
const formatTime = (seconds) => { const formatTime = (seconds) => {
const minutes = Math.floor(seconds / 60) const minutes = Math.floor(seconds / 60);
const secs = seconds % 60 const secs = seconds % 60;
return `${minutes}:${secs.toString().padStart(2, '0')}` return `${minutes}:${secs.toString().padStart(2, "0")}`;
} };
const copyCode = async () => { const copyCode = async () => {
try { try {
await navigator.clipboard.writeText(linkingCode.value) await navigator.clipboard.writeText(linkingCode.value);
toast.add({ toast.add({
severity: 'success', severity: "success",
summary: 'Copiat', summary: "Copiat",
detail: 'Cod copiat în clipboard', detail: "Cod copiat în clipboard",
life: 2000 life: 2000,
}) });
} catch (error) { } catch (error) {
const tempInput = document.createElement('input') const tempInput = document.createElement("input");
tempInput.value = linkingCode.value tempInput.value = linkingCode.value;
document.body.appendChild(tempInput) document.body.appendChild(tempInput);
tempInput.select() tempInput.select();
document.execCommand('copy') document.execCommand("copy");
document.body.removeChild(tempInput) document.body.removeChild(tempInput);
toast.add({ toast.add({
severity: 'success', severity: "success",
summary: 'Copiat', summary: "Copiat",
life: 2000 life: 2000,
}) });
} }
} };
onUnmounted(() => { onUnmounted(() => {
if (countdownInterval) clearInterval(countdownInterval) if (countdownInterval) clearInterval(countdownInterval);
}) });
</script> </script>
<style scoped> <style scoped>
@@ -214,7 +211,11 @@ onUnmounted(() => {
/* Code Display */ /* Code Display */
.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: 2px solid var(--color-primary);
border-radius: var(--radius-md); border-radius: var(--radius-md);
padding: var(--space-md); padding: var(--space-md);
@@ -237,7 +238,7 @@ onUnmounted(() => {
.code-timer { .code-timer {
color: var(--color-primary); color: var(--color-primary);
font-weight: var(--font-bold); font-weight: var(--font-bold);
font-family: 'Courier New', monospace; font-family: "Courier New", monospace;
} }
.code-value { .code-value {
@@ -245,7 +246,7 @@ onUnmounted(() => {
font-weight: var(--font-bold); font-weight: var(--font-bold);
color: var(--color-primary); color: var(--color-primary);
letter-spacing: 0.3em; letter-spacing: 0.3em;
font-family: 'Courier New', monospace; font-family: "Courier New", monospace;
} }
/* Action Buttons - Use global .btn patterns */ /* Action Buttons - Use global .btn patterns */

View File

@@ -8,7 +8,8 @@
Balanță de Verificare Balanță de Verificare
</h1> </h1>
<p class="page-subtitle"> <p class="page-subtitle">
{{ currentPeriodText }} - {{ companyStore.selectedCompany?.name || "Selectați companie" }} {{ currentPeriodText }} -
{{ companyStore.selectedCompany?.name || "Selectați companie" }}
</p> </p>
</div> </div>
@@ -128,47 +129,99 @@
</div> </div>
</template> </template>
<Column field="cont" header="Cont" sortable :style="{ width: '8%' }"> <Column
field="cont"
header="Cont"
sortable
:style="{ width: '8%' }"
>
<template #body="slotProps"> <template #body="slotProps">
<strong>{{ slotProps.data.cont }}</strong> <strong>{{ slotProps.data.cont }}</strong>
</template> </template>
</Column> </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"> <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> </template>
</Column> </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"> <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> </template>
</Column> </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"> <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> </template>
</Column> </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"> <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> </template>
</Column> </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"> <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> </template>
</Column> </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"> <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> </template>
</Column> </Column>
</DataTable> </DataTable>
@@ -200,8 +253,18 @@ const localFilters = ref({
// Computed // Computed
const currentPeriodText = computed(() => { const currentPeriodText = computed(() => {
const months = [ const months = [
"Ianuarie", "Februarie", "Martie", "Aprilie", "Mai", "Iunie", "Ianuarie",
"Iulie", "August", "Septembrie", "Octombrie", "Noiembrie", "Decembrie" "Februarie",
"Martie",
"Aprilie",
"Mai",
"Iunie",
"Iulie",
"August",
"Septembrie",
"Octombrie",
"Noiembrie",
"Decembrie",
]; ];
const monthName = months[trialBalanceStore.filters.luna - 1] || ""; const monthName = months[trialBalanceStore.filters.luna - 1] || "";
return `${monthName} ${trialBalanceStore.filters.an}`; return `${monthName} ${trialBalanceStore.filters.an}`;
@@ -248,7 +311,7 @@ const applyFilters = async () => {
cont: localFilters.value.cont, cont: localFilters.value.cont,
denumire: localFilters.value.denumire, denumire: localFilters.value.denumire,
}, },
companyStore.selectedCompany.id_firma companyStore.selectedCompany.id_firma,
); );
}; };
@@ -275,7 +338,7 @@ const loadTrialBalance = async () => {
try { try {
await trialBalanceStore.fetchTrialBalance( await trialBalanceStore.fetchTrialBalance(
companyStore.selectedCompany.id_firma companyStore.selectedCompany.id_firma,
); );
} catch (error) { } catch (error) {
console.error("Failed to load trial balance:", error); console.error("Failed to load trial balance:", error);
@@ -293,7 +356,7 @@ const onPageChange = async (event) => {
await trialBalanceStore.changePage( await trialBalanceStore.changePage(
event.page + 1, event.page + 1,
companyStore.selectedCompany.id_firma companyStore.selectedCompany.id_firma,
); );
}; };
@@ -306,7 +369,7 @@ const onSort = async (event) => {
await trialBalanceStore.sort( await trialBalanceStore.sort(
sortBy, sortBy,
sortOrder, 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 // Prepare data for export - Use raw numbers (not formatted) so Excel treats them as numbers
const exportData = allData.map((row) => ({ const exportData = allData.map((row) => ({
"Cont": row.cont, Cont: row.cont,
"Denumire": row.denumire, Denumire: row.denumire,
"Sold Precedent D": parseFloat(row.sold_precedent_debit) || 0, "Sold Precedent D": parseFloat(row.sold_precedent_debit) || 0,
"Sold Precedent C": parseFloat(row.sold_precedent_credit) || 0, "Sold Precedent C": parseFloat(row.sold_precedent_credit) || 0,
"Rulaj Lunar D": parseFloat(row.rulaj_lunar_debit) || 0, "Rulaj Lunar D": parseFloat(row.rulaj_lunar_debit) || 0,
@@ -392,7 +455,7 @@ const exportExcel = async () => {
const result = exportToExcel( const result = exportToExcel(
exportData, exportData,
`balanta_verificare_${currentPeriodText.value.replace(/\s+/g, "_")}`, `balanta_verificare_${currentPeriodText.value.replace(/\s+/g, "_")}`,
"Balanță de Verificare" "Balanță de Verificare",
); );
if (result.success) { if (result.success) {
@@ -459,14 +522,44 @@ const exportPDF = async () => {
// A4 landscape width: ~297mm total, margins 8mm left+right = 281mm usable // A4 landscape width: ~297mm total, margins 8mm left+right = 281mm usable
// Use 'auto' width to fill entire page width // Use 'auto' width to fill entire page width
const columns = [ const columns = [
{ field: "cont", header: "Cont", type: "text", width: 'auto' }, { field: "cont", header: "Cont", type: "text", width: "auto" },
{ field: "denumire", header: "Denumire 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: "sold_precedent_debit",
{ field: "rulaj_lunar_debit", header: "Rulaj D", type: "number", width: 'auto' }, header: "Sold Prec. D",
{ field: "rulaj_lunar_credit", header: "Rulaj C", type: "number", width: 'auto' }, type: "number",
{ field: "sold_final_debit", header: "Sold Final D", type: "number", width: 'auto' }, width: "auto",
{ field: "sold_final_credit", header: "Sold Final C", 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( const result = exportToPDF(
@@ -476,8 +569,8 @@ const exportPDF = async () => {
{ {
companyName: companyStore.selectedCompany?.name || "", companyName: companyStore.selectedCompany?.name || "",
title: "Balanta de Verificare", title: "Balanta de Verificare",
period: currentPeriodText.value period: currentPeriodText.value,
} },
); );
if (result.success) { if (result.success) {
@@ -517,7 +610,7 @@ watch(
if (newCompany) { if (newCompany) {
await loadTrialBalance(); await loadTrialBalance();
} }
} },
); );
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="company-selector-mini"> <div class="company-selector-mini">
<div class="company-dropdown" ref="dropdown"> <div class="company-dropdown" ref="dropdown">
<button <button
class="company-trigger" class="company-trigger"
@click="toggleDropdown" @click="toggleDropdown"
:aria-expanded="dropdownOpen" :aria-expanded="dropdownOpen"
@@ -11,30 +11,33 @@
<span class="company-name">{{ selectedCompanyName }}</span> <span class="company-name">{{ selectedCompanyName }}</span>
<span class="company-code">{{ selectedCompanyCode }}</span> <span class="company-code">{{ selectedCompanyCode }}</span>
</div> </div>
<i class="pi pi-chevron-down" :class="{ 'rotate-180': dropdownOpen }"></i> <i
class="pi pi-chevron-down"
:class="{ 'rotate-180': dropdownOpen }"
></i>
</button> </button>
<div <div
v-show="dropdownOpen" v-show="dropdownOpen"
class="company-dropdown-panel" class="company-dropdown-panel"
:class="{ 'panel-open': dropdownOpen }" :class="{ 'panel-open': dropdownOpen }"
> >
<div class="dropdown-search"> <div class="dropdown-search">
<div class="search-wrapper"> <div class="search-wrapper">
<i class="pi pi-search search-icon"></i> <i class="pi pi-search search-icon"></i>
<input <input
type="text" type="text"
v-model="searchQuery" v-model="searchQuery"
placeholder="Search companies..." placeholder="Search companies..."
class="search-input" class="search-input"
@keydown.escape="closeDropdown" @keydown.escape="closeDropdown"
> />
</div> </div>
</div> </div>
<div class="company-list"> <div class="company-list">
<div <div
v-for="company in filteredCompanies" v-for="company in filteredCompanies"
:key="company.id" :key="company.id"
class="company-item" class="company-item"
:class="{ active: company.id === selectedCompany?.id }" :class="{ active: company.id === selectedCompany?.id }"
@@ -45,10 +48,15 @@
<div class="company-sub-info"> <div class="company-sub-info">
<span class="company-cui">CUI: {{ company.cui }}</span> <span class="company-cui">CUI: {{ company.cui }}</span>
<span class="company-separator"></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>
</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>
</div> </div>
@@ -62,87 +70,90 @@
</template> </template>
<script> <script>
import { ref, computed, onMounted, onUnmounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from "vue";
import { useCompanyStore } from '../../stores/companies' import { useCompanyStore } from "../../stores/companies";
export default { export default {
name: 'CompanySelectorMini', name: "CompanySelectorMini",
props: { props: {
modelValue: { modelValue: {
type: Object, type: Object,
default: null default: null,
} },
}, },
emits: ['update:modelValue', 'company-changed'], emits: ["update:modelValue", "company-changed"],
setup(props, { emit }) { setup(props, { emit }) {
const companiesStore = useCompanyStore() const companiesStore = useCompanyStore();
const dropdown = ref(null) const dropdown = ref(null);
const dropdownOpen = ref(false) const dropdownOpen = ref(false);
const searchQuery = ref('') const searchQuery = ref("");
const selectedCompany = computed({ const selectedCompany = computed({
get: () => props.modelValue || companiesStore.selectedCompany, get: () => props.modelValue || companiesStore.selectedCompany,
set: (value) => { set: (value) => {
emit('update:modelValue', value) emit("update:modelValue", value);
companiesStore.setSelectedCompany(value) companiesStore.setSelectedCompany(value);
} },
}) });
const selectedCompanyName = computed(() => { const selectedCompanyName = computed(() => {
return selectedCompany.value?.name || 'Select Company' return selectedCompany.value?.name || "Select Company";
}) });
const selectedCompanyCode = computed(() => { const selectedCompanyCode = computed(() => {
return selectedCompany.value?.cui ? `CUI: ${selectedCompany.value.cui}` : '' return selectedCompany.value?.cui
}) ? `CUI: ${selectedCompany.value.cui}`
: "";
});
const filteredCompanies = computed(() => { const filteredCompanies = computed(() => {
if (!searchQuery.value) { if (!searchQuery.value) {
return companiesStore.companies return companiesStore.companies;
} }
const query = searchQuery.value.toLowerCase() const query = searchQuery.value.toLowerCase();
return companiesStore.companies.filter(company => return companiesStore.companies.filter(
company.name.toLowerCase().includes(query) || (company) =>
company.cui.toLowerCase().includes(query) company.name.toLowerCase().includes(query) ||
) company.cui.toLowerCase().includes(query),
}) );
});
const toggleDropdown = () => { const toggleDropdown = () => {
dropdownOpen.value = !dropdownOpen.value dropdownOpen.value = !dropdownOpen.value;
if (dropdownOpen.value) { if (dropdownOpen.value) {
searchQuery.value = '' searchQuery.value = "";
} }
} };
const closeDropdown = () => { const closeDropdown = () => {
dropdownOpen.value = false dropdownOpen.value = false;
searchQuery.value = '' searchQuery.value = "";
} };
const selectCompany = (company) => { const selectCompany = (company) => {
selectedCompany.value = company selectedCompany.value = company;
emit('company-changed', company) emit("company-changed", company);
closeDropdown() closeDropdown();
} };
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
if (dropdown.value && !dropdown.value.contains(event.target)) { if (dropdown.value && !dropdown.value.contains(event.target)) {
closeDropdown() closeDropdown();
} }
} };
onMounted(() => { onMounted(() => {
document.addEventListener('click', handleClickOutside) document.addEventListener("click", handleClickOutside);
// Load companies if not already loaded // Load companies if not already loaded
if (companiesStore.companies.length === 0) { if (companiesStore.companies.length === 0) {
companiesStore.fetchCompanies() companiesStore.fetchCompanies();
} }
}) });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('click', handleClickOutside) document.removeEventListener("click", handleClickOutside);
}) });
return { return {
dropdown, dropdown,
@@ -154,10 +165,10 @@ export default {
filteredCompanies, filteredCompanies,
toggleDropdown, toggleDropdown,
closeDropdown, closeDropdown,
selectCompany selectCompany,
} };
} },
} };
</script> </script>
<style scoped> <style scoped>
@@ -364,15 +375,15 @@ export default {
max-width: none; max-width: none;
width: 100%; width: 100%;
} }
.company-trigger { .company-trigger {
min-width: auto; min-width: auto;
} }
.company-dropdown-panel { .company-dropdown-panel {
left: -16px; left: -16px;
right: -16px; right: -16px;
width: calc(100% + 32px); width: calc(100% + 32px);
} }
} }
</style> </style>

View File

@@ -3,8 +3,8 @@
<nav class="header-nav"> <nav class="header-nav">
<!-- Left side: Brand + Hamburger --> <!-- Left side: Brand + Hamburger -->
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<button <button
class="hamburger-btn" class="hamburger-btn"
:class="{ active: menuOpen }" :class="{ active: menuOpen }"
@click="toggleMenu" @click="toggleMenu"
aria-label="Toggle navigation menu" aria-label="Toggle navigation menu"
@@ -13,7 +13,7 @@
<span class="hamburger-line"></span> <span class="hamburger-line"></span>
<span class="hamburger-line"></span> <span class="hamburger-line"></span>
</button> </button>
<router-link to="/dashboard" class="header-brand"> <router-link to="/dashboard" class="header-brand">
<span>ROA2WEB</span> <span>ROA2WEB</span>
</router-link> </router-link>
@@ -21,11 +21,19 @@
<!-- Center: Quick Actions --> <!-- Center: Quick Actions -->
<div class="quick-actions desktop-only"> <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> <i class="pi pi-refresh"></i>
<span>Refresh</span> <span>Refresh</span>
</button> </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> <i class="pi pi-download"></i>
<span>Export</span> <span>Export</span>
</button> </button>
@@ -37,8 +45,8 @@
<!-- Right side: Company + User --> <!-- Right side: Company + User -->
<div class="header-actions"> <div class="header-actions">
<CompanySelectorMini <CompanySelectorMini
v-model="selectedCompany" v-model="selectedCompany"
@company-changed="onCompanyChanged" @company-changed="onCompanyChanged"
/> />
<div class="header-user" @click="toggleUserMenu"> <div class="header-user" @click="toggleUserMenu">
@@ -52,52 +60,52 @@
</template> </template>
<script> <script>
import { ref, computed } from 'vue' import { ref, computed } from "vue";
import { useRouter } from 'vue-router' import { useRouter } from "vue-router";
import CompanySelectorMini from '../dashboard/CompanySelectorMini.vue' import CompanySelectorMini from "../dashboard/CompanySelectorMini.vue";
import { useCompanyStore } from '../../stores/companies' import { useCompanyStore } from "../../stores/companies";
export default { export default {
name: 'DashboardHeader', name: "DashboardHeader",
components: { components: {
CompanySelectorMini CompanySelectorMini,
}, },
emits: ['menu-toggle', 'refresh', 'export', 'search', 'company-changed'], emits: ["menu-toggle", "refresh", "export", "search", "company-changed"],
setup(props, { emit }) { setup(props, { emit }) {
const router = useRouter() const router = useRouter();
const companiesStore = useCompanyStore() const companiesStore = useCompanyStore();
const menuOpen = ref(false) const menuOpen = ref(false);
const selectedCompany = computed({ const selectedCompany = computed({
get: () => companiesStore.selectedCompany, get: () => companiesStore.selectedCompany,
set: (value) => companiesStore.setSelectedCompany(value) set: (value) => companiesStore.setSelectedCompany(value),
}) });
const toggleMenu = () => { const toggleMenu = () => {
menuOpen.value = !menuOpen.value menuOpen.value = !menuOpen.value;
emit('menu-toggle', menuOpen.value) emit("menu-toggle", menuOpen.value);
} };
const refreshData = () => { const refreshData = () => {
emit('refresh') emit("refresh");
} };
const exportData = () => { const exportData = () => {
emit('export') emit("export");
} };
const searchData = () => { const searchData = () => {
emit('search') emit("search");
} };
const onCompanyChanged = (company) => { const onCompanyChanged = (company) => {
emit('company-changed', company) emit("company-changed", company);
} };
const toggleUserMenu = () => { const toggleUserMenu = () => {
// TODO: Implement user menu dropdown // TODO: Implement user menu dropdown
console.log('User menu clicked') console.log("User menu clicked");
} };
return { return {
menuOpen, menuOpen,
@@ -107,8 +115,8 @@ export default {
exportData, exportData,
searchData, searchData,
onCompanyChanged, onCompanyChanged,
toggleUserMenu toggleUserMenu,
} };
} },
} };
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<!-- Dashboard Header --> <!-- Dashboard Header -->
<DashboardHeader <DashboardHeader
@menu-toggle="handleMenuToggle" @menu-toggle="handleMenuToggle"
@refresh="refreshData" @refresh="refreshData"
@export="exportData" @export="exportData"
@@ -10,7 +10,7 @@
/> />
<!-- Hamburger Menu --> <!-- Hamburger Menu -->
<HamburgerMenu <HamburgerMenu
:is-open="menuOpen" :is-open="menuOpen"
@close="handleMenuClose" @close="handleMenuClose"
@refresh="refreshData" @refresh="refreshData"
@@ -21,10 +21,11 @@
<!-- Main Content --> <!-- Main Content -->
<main class="main-content"> <main class="main-content">
<div class="app-container"> <div class="app-container">
<!-- Spreadsheet Style Header --> <!-- Spreadsheet Style Header -->
<div class="v3-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> <p class="v3-subtitle">Data Tables Focus Spreadsheet Style</p>
</div> </div>
@@ -51,33 +52,38 @@
<!-- Data Tables Dashboard Content --> <!-- Data Tables Dashboard Content -->
<div v-if="companyStore.selectedCompany" class="v3-content"> <div v-if="companyStore.selectedCompany" class="v3-content">
<!-- Horizontal Summary Cards --> <!-- Horizontal Summary Cards -->
<div class="v3-summary-cards"> <div class="v3-summary-cards">
<div class="v3-summary-card"> <div class="v3-summary-card">
<span class="v3-summary-label">Total Facturat</span> <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>
<div class="v3-summary-card"> <div class="v3-summary-card">
<span class="v3-summary-label">Total Încasat</span> <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>
<div class="v3-summary-card"> <div class="v3-summary-card">
<span class="v3-summary-label">Sold Net</span> <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>
<div class="v3-summary-card"> <div class="v3-summary-card">
<span class="v3-summary-label">Trezorerie</span> <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>
</div> </div>
<!-- Main Data Layout: 70% table, 30% summary --> <!-- Main Data Layout: 70% table, 30% summary -->
<div class="v3-main-layout"> <div class="v3-main-layout">
<!-- Data Table Section (70%) --> <!-- Data Table Section (70%) -->
<div class="v3-table-section"> <div class="v3-table-section">
<!-- Table Controls --> <!-- Table Controls -->
<div class="v3-table-controls"> <div class="v3-table-controls">
<div class="v3-control-group"> <div class="v3-control-group">
@@ -88,17 +94,28 @@
<option value="treasury">Trezorerie</option> <option value="treasury">Trezorerie</option>
</select> </select>
</div> </div>
<div class="v3-control-group"> <div class="v3-control-group">
<label>Filtrare:</label> <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>
<div class="v3-control-group"> <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 <i class="pi pi-download"></i> Excel
</button> </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 <i class="pi pi-file-pdf"></i> PDF
</button> </button>
</div> </div>
@@ -109,35 +126,56 @@
<table class="v3-data-table"> <table class="v3-data-table">
<thead> <thead>
<tr> <tr>
<th v-for="column in getTableColumns()" :key="column.key" <th
:class="{ 'sortable': column.sortable, 'sorted': sortColumn === column.key }" v-for="column in getTableColumns()"
@click="column.sortable && handleSort(column.key)"> :key="column.key"
:class="{
sortable: column.sortable,
sorted: sortColumn === column.key,
}"
@click="column.sortable && handleSort(column.key)"
>
{{ column.label }} {{ column.label }}
<i v-if="column.sortable && sortColumn === column.key" <i
:class="sortDirection === 'asc' ? 'pi pi-sort-up' : 'pi pi-sort-down'" v-if="column.sortable && sortColumn === column.key"
class="sort-icon"></i> :class="
sortDirection === 'asc'
? 'pi pi-sort-up'
: 'pi pi-sort-down'
"
class="sort-icon"
></i>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(row, index) in getFilteredTableData()" :key="index" <tr
:class="{ 'row-editable': editableRow === index }" v-for="(row, index) in getFilteredTableData()"
@click="handleRowClick(index)"> :key="index"
<td v-for="column in getTableColumns()" :key="column.key" :class="{ 'row-editable': editableRow === index }"
:class="getCellClass(row, column)"> @click="handleRowClick(index)"
>
<td
v-for="column in getTableColumns()"
:key="column.key"
:class="getCellClass(row, column)"
>
<!-- Editable Cell --> <!-- Editable Cell -->
<span v-if="editableRow === index && column.editable" <span
class="editable-cell" v-if="editableRow === index && column.editable"
@click.stop> class="editable-cell"
<input v-model="row[column.key]" @click.stop
:type="column.type || 'text'" >
class="cell-input" <input
@blur="saveRowEdit(index)" v-model="row[column.key]"
@keydown.enter="saveRowEdit(index)" :type="column.type || 'text'"
@keydown.escape="cancelRowEdit()"> class="cell-input"
@blur="saveRowEdit(index)"
@keydown.enter="saveRowEdit(index)"
@keydown.escape="cancelRowEdit()"
/>
</span> </span>
<!-- Regular Cell --> <!-- Regular Cell -->
<span v-else> <span v-else>
{{ formatCellValue(row[column.key], column.type) }} {{ formatCellValue(row[column.key], column.type) }}
@@ -151,64 +189,93 @@
<!-- Pagination --> <!-- Pagination -->
<div class="v3-pagination"> <div class="v3-pagination">
<div class="pagination-info"> <div class="pagination-info">
Afișare {{ ((currentPage - 1) * rowsPerPage) + 1 }} - Afișare {{ (currentPage - 1) * rowsPerPage + 1 }} -
{{ Math.min(currentPage * rowsPerPage, getFilteredTableData().length) }} {{
Math.min(
currentPage * rowsPerPage,
getFilteredTableData().length,
)
}}
din {{ getFilteredTableData().length }} înregistrări din {{ getFilteredTableData().length }} înregistrări
</div> </div>
<div class="pagination-controls"> <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> <i class="pi pi-chevron-left"></i>
</button> </button>
<span class="page-info">Pagina {{ currentPage }} din {{ totalPages }}</span> <span class="page-info"
<button class="btn btn-sm" @click="goToPage(currentPage + 1)" :disabled="currentPage === totalPages"> >Pagina {{ currentPage }} din {{ totalPages }}</span
>
<button
class="btn btn-sm"
@click="goToPage(currentPage + 1)"
:disabled="currentPage === totalPages"
>
<i class="pi pi-chevron-right"></i> <i class="pi pi-chevron-right"></i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<!-- Summary Panel (30%) --> <!-- Summary Panel (30%) -->
<div class="v3-summary-section"> <div class="v3-summary-section">
<div class="v3-summary-panel"> <div class="v3-summary-panel">
<h3 class="v3-panel-title">Rezumat {{ getDataTypeLabel() }}</h3> <h3 class="v3-panel-title">Rezumat {{ getDataTypeLabel() }}</h3>
<div class="v3-summary-stats"> <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-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>
</div> </div>
<!-- Mini Chart Placeholder --> <!-- Mini Chart Placeholder -->
<div class="v3-mini-chart"> <div class="v3-mini-chart">
<div class="chart-title">Trend Ultimas 7 zile</div> <div class="chart-title">Trend Ultimas 7 zile</div>
<div class="chart-placeholder"> <div class="chart-placeholder">
<div class="chart-bar" v-for="i in 7" :key="i" <div
:style="{ height: Math.random() * 60 + 20 + 'px' }"></div> class="chart-bar"
v-for="i in 7"
:key="i"
:style="{ height: Math.random() * 60 + 20 + 'px' }"
></div>
</div> </div>
</div> </div>
<!-- Quick Actions --> <!-- Quick Actions -->
<div class="v3-quick-actions"> <div class="v3-quick-actions">
<h4>Acțiuni Rapide</h4> <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 <i class="pi pi-plus"></i> Adaugă Înregistrare
</button> </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ă <i class="pi pi-refresh"></i> Actualizează
</button> </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 <i class="pi pi-external-link"></i> Raport Complet
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Loading State --> <!-- Loading State -->
@@ -216,7 +283,6 @@
<div class="v3-loading-spinner"></div> <div class="v3-loading-spinner"></div>
<p>Se încarcă datele...</p> <p>Se încarcă datele...</p>
</div> </div>
</div> </div>
</main> </main>
</div> </div>
@@ -243,10 +309,10 @@ const menuOpen = ref(false);
const selectedCompanyId = ref(companyStore.selectedCompany?.id_firma || null); const selectedCompanyId = ref(companyStore.selectedCompany?.id_firma || null);
const filteredCompanies = ref([]); const filteredCompanies = ref([]);
const isLoading = ref(false); const isLoading = ref(false);
const selectedDataType = ref('clients'); const selectedDataType = ref("clients");
const tableFilter = ref(''); const tableFilter = ref("");
const sortColumn = ref(null); const sortColumn = ref(null);
const sortDirection = ref('asc'); const sortDirection = ref("asc");
const editableRow = ref(-1); const editableRow = ref(-1);
const currentPage = ref(1); const currentPage = ref(1);
const rowsPerPage = ref(15); const rowsPerPage = ref(15);
@@ -273,10 +339,10 @@ const handleCompanyChanged = async (company) => {
}; };
const formatCurrency = (amount) => { const formatCurrency = (amount) => {
if (!amount) return '0,00 RON'; if (!amount) return "0,00 RON";
const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount; const numAmount = typeof amount === "string" ? parseFloat(amount) : amount;
if (isNaN(numAmount)) return '0,00 RON'; if (isNaN(numAmount)) return "0,00 RON";
try { try {
return new Intl.NumberFormat("ro-RO", { return new Intl.NumberFormat("ro-RO", {
style: "currency", style: "currency",
@@ -289,7 +355,8 @@ const formatCurrency = (amount) => {
const getTotalInvoiced = () => { const getTotalInvoiced = () => {
const clientInvoiced = dashboardStore.summary?.clienti_total_facturat || 0; 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); return parseFloat(clientInvoiced) + parseFloat(supplierInvoiced);
}; };
@@ -307,7 +374,7 @@ const getNetBalance = () => {
const getNetBalanceClass = () => { const getNetBalanceClass = () => {
const balance = getNetBalance(); const balance = getNetBalance();
return balance > 0 ? 'positive' : balance < 0 ? 'negative' : 'neutral'; return balance > 0 ? "positive" : balance < 0 ? "negative" : "neutral";
}; };
const getTreasuryTotal = () => { const getTreasuryTotal = () => {
@@ -318,32 +385,86 @@ const getTreasuryTotal = () => {
const getTableColumns = () => { const getTableColumns = () => {
switch (selectedDataType.value) { switch (selectedDataType.value) {
case 'clients': case "clients":
return [ return [
{ key: 'client', label: 'Client', 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: "facturat",
{ key: 'sold', label: 'Sold', sortable: true, editable: false, type: 'currency' }, label: "Facturat",
{ key: 'restant', label: 'Restant', sortable: true, editable: false, type: 'currency' }, sortable: true,
{ key: 'status', label: 'Status', sortable: true, editable: false } 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 [ return [
{ key: 'furnizor', label: 'Furnizor', 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: "facturat",
{ key: 'sold', label: 'Sold', sortable: true, editable: false, type: 'currency' }, label: "Facturat",
{ key: 'restant', label: 'Restant', sortable: true, editable: false, type: 'currency' }, sortable: true,
{ key: 'status', label: 'Status', sortable: true, editable: false } 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 [ return [
{ key: 'cont', label: 'Cont', sortable: true, editable: false }, { key: "cont", label: "Cont", sortable: true, editable: false },
{ key: 'nume', label: 'Nume 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: "sold",
{ key: 'tip', label: 'Tip', sortable: true, editable: false }, label: "Sold",
{ key: 'status', label: 'Status', sortable: true, editable: false } 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: default:
return []; return [];
@@ -352,62 +473,153 @@ const getTableColumns = () => {
const getFilteredTableData = () => { const getFilteredTableData = () => {
let data = getTableData(); let data = getTableData();
// Apply filter // Apply filter
if (tableFilter.value) { if (tableFilter.value) {
const filter = tableFilter.value.toLowerCase(); const filter = tableFilter.value.toLowerCase();
data = data.filter(row => data = data.filter((row) =>
Object.values(row).some(value => Object.values(row).some((value) =>
String(value).toLowerCase().includes(filter) String(value).toLowerCase().includes(filter),
) ),
); );
} }
// Apply sorting // Apply sorting
if (sortColumn.value) { if (sortColumn.value) {
data.sort((a, b) => { data.sort((a, b) => {
const aVal = a[sortColumn.value]; const aVal = a[sortColumn.value];
const bVal = b[sortColumn.value]; const bVal = b[sortColumn.value];
let comparison = 0; let comparison = 0;
if (aVal > bVal) comparison = 1; if (aVal > bVal) comparison = 1;
if (aVal < bVal) comparison = -1; if (aVal < bVal) comparison = -1;
return sortDirection.value === 'asc' ? comparison : -comparison; return sortDirection.value === "asc" ? comparison : -comparison;
}); });
} }
// Apply pagination // Apply pagination
const start = (currentPage.value - 1) * rowsPerPage.value; const start = (currentPage.value - 1) * rowsPerPage.value;
const end = start + rowsPerPage.value; const end = start + rowsPerPage.value;
return data.slice(start, end); return data.slice(start, end);
}; };
const getTableData = () => { const getTableData = () => {
// Mock data - in real implementation, this would come from the store // Mock data - in real implementation, this would come from the store
switch (selectedDataType.value) { switch (selectedDataType.value) {
case 'clients': case "clients":
return [ 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 A",
{ client: 'Client C', facturat: 200000, incasat: 150000, sold: 50000, restant: 15000, status: 'Restant' }, facturat: 150000,
{ client: 'Client D', facturat: 90000, incasat: 90000, sold: 0, restant: 0, status: 'Activ' }, incasat: 120000,
{ client: 'Client E', facturat: 180000, incasat: 140000, sold: 40000, restant: 8000, status: 'Activ' }, 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 [ 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 A",
{ furnizor: 'Furnizor C', facturat: 95000, achitat: 95000, sold: 0, restant: 0, status: 'Activ' }, facturat: 80000,
{ furnizor: 'Furnizor D', facturat: 65000, achitat: 50000, sold: 15000, restant: 2000, status: 'Restant' }, 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 [ 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: "5121",
{ cont: '5311', nume: 'Casa RON', sold: 5000, valuta: 'RON', tip: 'Casă', status: 'Activ' }, nume: "BCR - Cont Principal",
{ cont: '5314', nume: 'Casa EUR', sold: 1000, valuta: 'EUR', tip: 'Casă', status: 'Activ' }, 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: default:
return []; return [];
@@ -416,37 +628,83 @@ const getTableData = () => {
const getDataTypeLabel = () => { const getDataTypeLabel = () => {
switch (selectedDataType.value) { switch (selectedDataType.value) {
case 'clients': return 'Clienți'; case "clients":
case 'suppliers': return 'Furnizori'; return "Clienți";
case 'treasury': return 'Trezorerie'; case "suppliers":
default: return ''; return "Furnizori";
case "treasury":
return "Trezorerie";
default:
return "";
} }
}; };
const getSummaryStats = () => { const getSummaryStats = () => {
const data = getTableData(); const data = getTableData();
switch (selectedDataType.value) { switch (selectedDataType.value) {
case 'clients': case "clients":
return [ return [
{ label: 'Total Clienți', value: 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: "Clienți Activi",
{ label: 'Sold Mediu', value: formatCurrency(data.reduce((sum, c) => sum + c.sold, 0) / data.length), class: 'neutral' } 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 [ return [
{ label: 'Total Furnizori', value: 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: "Furnizori Activi",
{ label: 'Sold Mediu', value: formatCurrency(data.reduce((sum, s) => sum + s.sold, 0) / data.length), class: 'neutral' } 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 [ return [
{ label: 'Total Conturi', value: 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: "Conturi Bancă",
{ label: 'Sold Mediu', value: formatCurrency(data.reduce((sum, t) => sum + t.sold, 0) / data.length), class: 'neutral' } 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: default:
return []; return [];
@@ -454,30 +712,30 @@ const getSummaryStats = () => {
}; };
const formatCellValue = (value, type) => { const formatCellValue = (value, type) => {
if (type === 'currency') { if (type === "currency") {
return formatCurrency(value); return formatCurrency(value);
} }
return value; return value;
}; };
const getCellClass = (row, column) => { const getCellClass = (row, column) => {
if (column.type === 'currency') { if (column.type === "currency") {
const value = parseFloat(row[column.key]); const value = parseFloat(row[column.key]);
if (value > 0) return 'cell-positive'; if (value > 0) return "cell-positive";
if (value < 0) return 'cell-negative'; if (value < 0) return "cell-negative";
} }
if (column.key === 'status') { if (column.key === "status") {
return row.status === 'Activ' ? 'cell-active' : 'cell-inactive'; return row.status === "Activ" ? "cell-active" : "cell-inactive";
} }
return ''; return "";
}; };
const handleSort = (column) => { const handleSort = (column) => {
if (sortColumn.value === column) { if (sortColumn.value === column) {
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc'; sortDirection.value = sortDirection.value === "asc" ? "desc" : "asc";
} else { } else {
sortColumn.value = column; sortColumn.value = column;
sortDirection.value = 'asc'; sortDirection.value = "asc";
} }
}; };
@@ -539,22 +797,22 @@ const refreshCurrentData = () => {
const viewFullReport = () => { const viewFullReport = () => {
switch (selectedDataType.value) { switch (selectedDataType.value) {
case 'clients': case "clients":
router.push('/invoices'); router.push("/invoices");
break; break;
case 'suppliers': case "suppliers":
router.push('/invoices?type=suppliers'); router.push("/invoices?type=suppliers");
break; break;
case 'treasury': case "treasury":
router.push('/bank-cash-register'); router.push("/bank-cash-register");
break; break;
} }
}; };
const searchCompanies = (event) => { const searchCompanies = (event) => {
const query = event.query.toLowerCase(); const query = event.query.toLowerCase();
filteredCompanies.value = companyStore.companyListFormatted.filter(company => filteredCompanies.value = companyStore.companyListFormatted.filter(
company.displayName.toLowerCase().includes(query) (company) => company.displayName.toLowerCase().includes(query),
); );
}; };
@@ -574,7 +832,9 @@ const loadDashboardData = async () => {
isLoading.value = true; isLoading.value = true;
try { try {
await dashboardStore.loadDashboardSummary(companyStore.selectedCompany.id_firma); await dashboardStore.loadDashboardSummary(
companyStore.selectedCompany.id_firma,
);
} catch (error) { } catch (error) {
console.error("Failed to load dashboard data:", error); console.error("Failed to load dashboard data:", error);
toast.add({ toast.add({
@@ -604,7 +864,7 @@ const exportData = () => {
const searchData = () => { const searchData = () => {
// Focus on the table filter input // Focus on the table filter input
const filterInput = document.querySelector('.v3-input'); const filterInput = document.querySelector(".v3-input");
if (filterInput) { if (filterInput) {
filterInput.focus(); filterInput.focus();
} }
@@ -613,7 +873,7 @@ const searchData = () => {
// Watchers // Watchers
watch(selectedDataType, () => { watch(selectedDataType, () => {
currentPage.value = 1; currentPage.value = 1;
tableFilter.value = ''; tableFilter.value = "";
sortColumn.value = null; sortColumn.value = null;
editableRow.value = -1; editableRow.value = -1;
}); });
@@ -623,9 +883,9 @@ onMounted(async () => {
if (!companyStore.hasCompanies) { if (!companyStore.hasCompanies) {
await companyStore.loadCompanies(); await companyStore.loadCompanies();
} }
filteredCompanies.value = companyStore.companyListFormatted; filteredCompanies.value = companyStore.companyListFormatted;
if (companyStore.selectedCompany) { if (companyStore.selectedCompany) {
await loadDashboardData(); await loadDashboardData();
} }
@@ -981,7 +1241,7 @@ onMounted(async () => {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: var(--space-lg); gap: var(--space-lg);
} }
.v3-summary-cards { .v3-summary-cards {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
@@ -991,25 +1251,25 @@ onMounted(async () => {
.v3-summary-cards { .v3-summary-cards {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.v3-table-controls { .v3-table-controls {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
.v3-control-group { .v3-control-group {
justify-content: space-between; justify-content: space-between;
} }
.v3-data-table { .v3-data-table {
font-size: var(--text-xs); font-size: var(--text-xs);
} }
.v3-data-table th, .v3-data-table th,
.v3-data-table td { .v3-data-table td {
padding: var(--space-xs) var(--space-sm); padding: var(--space-xs) var(--space-sm);
} }
.v3-pagination { .v3-pagination {
flex-direction: column; flex-direction: column;
gap: var(--space-sm); gap: var(--space-sm);
@@ -1023,13 +1283,13 @@ onMounted(async () => {
.v3-summary-section { .v3-summary-section {
display: none; display: none;
} }
.v3-main-layout { .v3-main-layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.v3-data-table { .v3-data-table {
font-size: 12px; font-size: 12px;
} }
} }
</style> </style>