Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 58 additions & 17 deletions public/assets/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -614,36 +614,77 @@ header {

/* Footer */
footer {
text-align: center;
padding-top: 0.75rem;
border-top: 1px solid var(--border);
margin-top: auto;
display: flex;
justify-content: center;
align-items: center;
margin-top: 2rem;
padding: 1rem;
width: 100%;
}

#exportBtn {
background: none;
border: none;
color: var(--text);
.export-buttons {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: center;
background: var(--container);
padding: 0.5rem;
border-radius: 6px;
border: 1px solid var(--border);
margin: 0 auto;
width: fit-content;
}

.export-btn {
all: unset;
background: transparent;
border: 1px solid var(--border);
padding: 0.5rem 1rem;
cursor: pointer;
border-radius: 50%;
transition: all var(--transition);
color: var(--text);
border-radius: 4px;
transition: all 0.2s;
font-size: 0.9rem;
min-width: 60px;
text-align: center;
display: inline-flex;
align-items: center;
justify-content: center;
height: 32px;
box-sizing: border-box;
}

.export-btn:hover {
background-color: var(--hover-color);
border-color: var(--accent-color);
}

.export-btn:active {
transform: translateY(1px);
}

/* Remove these specific styles */
#exportBtn {
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
padding: 0.5rem;
background: none;
border: none;
cursor: pointer;
color: var(--text);
border-radius: 4px;
transition: background-color 0.2s;
}

#exportBtn:hover {
background: rgba(128, 128, 128, 0.1);
background-color: rgba(128, 128, 128, 0.1);
}

#exportBtn svg {
width: 20px;
height: 20px;
stroke: var(--text);
stroke-width: 2;
transition: stroke var(--transition);
width: 24px;
height: 24px;
stroke: currentColor;
}

/* Responsive adjustments */
Expand Down
15 changes: 7 additions & 8 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,10 @@ <h3>Transactions</h3>
</section>

<footer>
<button id="exportBtn" aria-label="Export CSV">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
</button>
<div class="export-buttons">
<button id="exportBtn" class="export-btn">CSV</button>
<button id="exportPdfBtn" class="export-btn">PDF</button>
</div>
</footer>
</div>
<div id="toast-container" class="toast-container"></div>
Expand Down Expand Up @@ -165,6 +162,8 @@ <h3>Transactions</h3>
</div>
</div>

<script src="script.js" type="module"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.31/jspdf.plugin.autotable.min.js"></script>
<script type="module" src="script.js"></script>
</body>
</html>
81 changes: 80 additions & 1 deletion public/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ async function handleFetchResponse(response) {

// Check content type
const contentType = response.headers.get('content-type');
if (!contentType || (!contentType.includes('application/json') && !contentType.includes('text/csv'))) {
if (!contentType || (!contentType.includes('application/json') && contentType.split(';')[0].trim() !== 'text/csv')) {
debugLog('Response is not JSON or CSV, session likely expired');
window.location.href = joinPath('login');
return null;
Expand Down Expand Up @@ -1000,6 +1000,85 @@ async function initMainPage() {
}
});

// Export to PDF
document.getElementById('exportPdfBtn').addEventListener('click', async () => {
try {
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;

// Get instance name from server
const configResponse = await fetch(joinPath('api/config'), fetchConfig);
await handleFetchResponse(configResponse);
const config = await configResponse.json();
const instanceName = config.instanceName || 'DumbBudget';

// Get the current totals
const response = await fetch(joinPath(`api/totals/range?start=${startDate}&end=${endDate}`), fetchConfig);
await handleFetchResponse(response);
const totals = await response.json();

// Get transactions
const transactionsResponse = await fetch(joinPath(`api/transactions/range?start=${startDate}&end=${endDate}`), fetchConfig);
await handleFetchResponse(transactionsResponse);
const transactions = await transactionsResponse.json();

// Create PDF
const { jsPDF } = window.jspdf;
const doc = new jsPDF();

// Set font
doc.setFont('helvetica');

// Add title
doc.setFontSize(20);
doc.text(instanceName, 20, 20);

// Add date range
doc.setFontSize(12);
doc.text(`Date Range: ${startDate} to ${endDate}`, 20, 30);

// Add totals section
doc.setFontSize(14);
doc.text('Summary', 20, 45);
doc.setFontSize(12);
doc.text(`Total Income: ${formatCurrency(totals.income)}`, 20, 55);
doc.text(`Total Expenses: ${formatCurrency(totals.expenses)}`, 20, 65);
doc.text(`Balance: ${formatCurrency(totals.balance)}`, 20, 75);

// Add transactions table
const tableData = transactions.map(t => [
t.date,
t.description,
t.category || '-',
formatCurrency(t.type === 'expense' ? -t.amount : t.amount),
t.type
]);

doc.autoTable({
startY: 85,
head: [['Date', 'Description', 'Category', 'Amount', 'Type']],
body: tableData,
theme: 'grid',
headStyles: { fillColor: [66, 66, 66] },
styles: { fontSize: 10 },
columnStyles: {
0: { cellWidth: 30 }, // Date
1: { cellWidth: 60 }, // Description
2: { cellWidth: 30 }, // Category
3: { cellWidth: 30 }, // Amount
4: { cellWidth: 20 } // Type
}
});

// Save the PDF
doc.save(`transactions-${startDate}-to-${endDate}.pdf`);

} catch (error) {
console.error('Error exporting to PDF:', error);
toastManager.show('Failed to export to PDF. Please try again.', 'error');
}
});

// Add filter button handlers
const filterButtons = document.querySelectorAll('.filter-btn');
filterButtons.forEach(btn => {
Expand Down
47 changes: 47 additions & 0 deletions public/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
footer {
display: flex;
justify-content: center;
margin-top: 2rem;
padding: 1rem;
text-align: center;
}

.export-buttons {
display: inline-flex;
gap: 0.5rem;
align-items: center;
justify-content: center;
background: var(--background);
padding: 0.5rem;
border-radius: 6px;
border: 1px solid var(--border);
margin: 0 auto;
}

.export-btn {
background: transparent;
border: 1px solid var(--border);
padding: 0.5rem 1rem;
cursor: pointer;
color: var(--text);
border-radius: 4px;
transition: all 0.2s;
font-size: 0.9rem;
min-width: 60px;
text-align: center;
display: inline-flex;
align-items: center;
justify-content: center;
height: 32px;
box-sizing: border-box;
margin: 0;
}

.export-btn:hover {
background-color: var(--hover-color);
border-color: var(--accent-color);
}

.export-btn:active {
transform: translateY(1px);
}
Loading