Skip to content

Commit 4b1a47b

Browse files
committed
feat: add Excel export functionality for session data and enhance export button styling
1 parent eecfedd commit 4b1a47b

File tree

8 files changed

+300
-26
lines changed

8 files changed

+300
-26
lines changed

package-lock.json

Lines changed: 105 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@tauri-apps/plugin-notification": "^2",
2222
"@tauri-apps/plugin-opener": "^2",
2323
"@tauri-apps/plugin-process": "^2",
24-
"@tauri-apps/plugin-updater": "^2.7.1"
24+
"@tauri-apps/plugin-updater": "^2.7.1",
25+
"xlsx": "^0.18.5"
2526
}
2627
}

src-tauri/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ serde = { version = "1", features = ["derive"], default-features = false }
4444
serde_json = { version = "1", default-features = false }
4545
chrono = { version = "0.4", features = ["serde"], default-features = false }
4646
dotenv = "0.15"
47+
base64 = "0.21"
4748

4849
[target.'cfg(target_os = "macos")'.dependencies]
4950
core-graphics = "0.23"

src-tauri/src/lib.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::fs;
44
use std::sync::{Arc, LazyLock, Mutex};
55
use std::thread;
66
use std::time::{Duration, Instant};
7+
use base64::{Engine as _, engine::general_purpose};
78
use tauri::menu::{Menu, MenuItem};
89
use tauri::tray::{TrayIconBuilder, TrayIconEvent};
910
use tauri::{Emitter, Manager};
@@ -908,7 +909,8 @@ pub fn run() {
908909
load_session_tags,
909910
save_session_tags,
910911
add_session_tag,
911-
get_env_var
912+
get_env_var,
913+
write_excel_file
912914
])
913915
.setup(|app| {
914916
// Track app started event (if enabled)
@@ -1273,3 +1275,17 @@ async fn update_tray_menu(
12731275

12741276
Ok(())
12751277
}
1278+
1279+
#[tauri::command]
1280+
async fn write_excel_file(path: String, data: String) -> Result<(), String> {
1281+
// Decode base64 data
1282+
let decoded_data = general_purpose::STANDARD
1283+
.decode(data)
1284+
.map_err(|e| format!("Failed to decode base64 data: {}", e))?;
1285+
1286+
// Write the binary data to file
1287+
fs::write(&path, decoded_data)
1288+
.map_err(|e| format!("Failed to write Excel file to {}: {}", path, e))?;
1289+
1290+
Ok(())
1291+
}

src/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,16 @@ <h3>Session History</h3>
300300
<option value="month">This Month</option>
301301
<option value="all">All Time</option>
302302
</select>
303+
<button id="export-sessions-btn" class="export-btn" title="Export to Excel">
304+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
305+
<path d="M14 2H6C5.46957 2 4.96086 2.21071 4.58579 2.58579C4.21071 2.96086 4 3.46957 4 4V20C4 20.5304 4.21071 21.0391 4.58579 21.4142C4.96086 21.7893 5.46957 22 6 22H18C18.5304 22 19.0391 21.7893 19.4142 21.4142C19.7893 21.0391 20 20.5304 20 20V8L14 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
306+
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
307+
<line x1="16" y1="13" x2="8" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
308+
<line x1="16" y1="17" x2="8" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
309+
<polyline points="10,9 9,9 8,9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
310+
</svg>
311+
Export
312+
</button>
303313
</div>
304314
</div>
305315
<div class="sessions-table-container">
@@ -959,6 +969,9 @@ <h3 id="session-modal-title">Add Session</h3>
959969
</div>
960970
</div>
961971

972+
<!-- XLSX Library for Excel Export -->
973+
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
974+
962975
</body>
963976

964977
</html>

src/managers/navigation-manager.js

Lines changed: 132 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,6 +1472,13 @@ export class NavigationManager {
14721472
await this.populateSessionsTable(e.target.value);
14731473
});
14741474
}
1475+
1476+
const exportBtn = document.getElementById('export-sessions-btn');
1477+
if (exportBtn) {
1478+
exportBtn.addEventListener('click', () => {
1479+
this.exportSessionsToExcel();
1480+
});
1481+
}
14751482
}
14761483

14771484
async populateSessionsTable(period = 'today') {
@@ -1629,34 +1636,136 @@ export class NavigationManager {
16291636
async deleteSessionFromTable(sessionId) {
16301637
if (!window.sessionManager || !sessionId) return;
16311638

1632-
if (confirm('Are you sure you want to delete this session? This action cannot be undone.')) {
1633-
try {
1634-
// Find and delete the session
1635-
for (const [dateString, sessions] of Object.entries(window.sessionManager.sessions)) {
1636-
const sessionIndex = sessions.findIndex(s => s.id === sessionId);
1637-
if (sessionIndex !== -1) {
1638-
sessions.splice(sessionIndex, 1);
1639-
await window.sessionManager.saveSessionsToStorage();
1640-
1641-
// Refresh the table
1642-
const filterSelect = document.getElementById('sessions-filter-period');
1643-
const currentPeriod = filterSelect ? filterSelect.value : 'today';
1644-
await this.populateSessionsTable(currentPeriod);
1639+
try {
1640+
// Find and delete the session
1641+
for (const [dateString, sessions] of Object.entries(window.sessionManager.sessions)) {
1642+
const sessionIndex = sessions.findIndex(s => s.id === sessionId);
1643+
if (sessionIndex !== -1) {
1644+
sessions.splice(sessionIndex, 1);
1645+
await window.sessionManager.saveSessionsToStorage();
1646+
1647+
// Refresh the table
1648+
const filterSelect = document.getElementById('sessions-filter-period');
1649+
const currentPeriod = filterSelect ? filterSelect.value : 'today';
1650+
await this.populateSessionsTable(currentPeriod);
1651+
1652+
// Refresh other views
1653+
await this.updateDailyChart();
1654+
await this.updateFocusSummary();
1655+
await this.updateWeeklySessionsChart();
1656+
await this.updateTimelineForDate(new Date());
1657+
1658+
console.log('Session deleted successfully:', sessionId);
1659+
break;
1660+
}
1661+
}
1662+
} catch (error) {
1663+
console.error('Error deleting session:', error);
1664+
alert('Failed to delete session. Please try again.');
1665+
}
1666+
}
1667+
1668+
async exportSessionsToExcel() {
1669+
try {
1670+
const filterSelect = document.getElementById('sessions-filter-period');
1671+
const currentPeriod = filterSelect ? filterSelect.value : 'today';
1672+
const sessions = this.getSessionsForPeriod(currentPeriod);
1673+
1674+
if (sessions.length === 0) {
1675+
alert('No sessions to export for the selected period.');
1676+
return;
1677+
}
1678+
1679+
const XLSX = window.XLSX;
1680+
if (!XLSX) {
1681+
console.error('XLSX library not found');
1682+
alert('Excel export functionality is not available.');
1683+
return;
1684+
}
1685+
1686+
// Prepare export data
1687+
const exportData = [];
1688+
for (const session of sessions) {
1689+
const sessionDate = new Date(session.created_at);
1690+
const formattedDate = sessionDate.toLocaleDateString('en-US', {
1691+
year: 'numeric',
1692+
month: '2-digit',
1693+
day: '2-digit'
1694+
});
1695+
1696+
const tags = await this.getSessionTags(session.id);
1697+
const tagNames = tags.map(tag => tag.name).join(', ');
1698+
1699+
exportData.push({
1700+
'Date': formattedDate,
1701+
'Start Time': session.start_time,
1702+
'End Time': session.end_time,
1703+
'Duration (minutes)': session.duration,
1704+
'Type': session.session_type,
1705+
'Tags': tagNames || '-',
1706+
'Notes': session.notes || '-'
1707+
});
1708+
}
1709+
1710+
exportData.sort((a, b) => {
1711+
const dateComparison = new Date(b.Date).getTime() - new Date(a.Date).getTime();
1712+
if (dateComparison !== 0) return dateComparison;
1713+
return b['Start Time'].localeCompare(a['Start Time']);
1714+
});
1715+
1716+
// Create Excel workbook
1717+
const ws = XLSX.utils.json_to_sheet(exportData);
1718+
const wb = XLSX.utils.book_new();
1719+
XLSX.utils.book_append_sheet(wb, ws, 'Session History');
1720+
1721+
// Generate default filename
1722+
const today = new Date();
1723+
const dateStr = today.toISOString().split('T')[0];
1724+
const defaultFilename = `presto-session-history-${currentPeriod}-${dateStr}.xlsx`;
1725+
1726+
// Check if we're in Tauri environment
1727+
if (window.__TAURI__) {
1728+
try {
1729+
// Use Tauri's save dialog
1730+
const filePath = await window.__TAURI__.dialog.save({
1731+
defaultPath: defaultFilename,
1732+
filters: [{
1733+
name: 'Excel files',
1734+
extensions: ['xlsx']
1735+
}]
1736+
});
1737+
1738+
if (filePath) {
1739+
// Convert workbook to base64 for Tauri
1740+
const wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'base64' });
16451741

1646-
// Refresh other views
1647-
await this.updateDailyChart();
1648-
await this.updateFocusSummary();
1649-
await this.updateWeeklySessionsChart();
1650-
await this.updateTimelineForDate(new Date());
1742+
// Use invoke to call Rust backend to save file
1743+
await window.__TAURI__.core.invoke('write_excel_file', {
1744+
path: filePath,
1745+
data: wbout
1746+
});
16511747

1652-
console.log('Session deleted successfully:', sessionId);
1653-
break;
1748+
console.log(`Exported ${sessions.length} sessions to ${filePath}`);
1749+
alert(`Sessions exported successfully to:\n${filePath}`);
1750+
} else {
1751+
console.log('Export cancelled by user');
16541752
}
1753+
} catch (tauriError) {
1754+
console.error('Tauri save error:', tauriError);
1755+
// Fallback to direct download
1756+
XLSX.writeFile(wb, defaultFilename);
1757+
console.log(`Tauri save failed, using fallback download: ${defaultFilename}`);
1758+
alert(`File saved to Downloads folder as: ${defaultFilename}`);
16551759
}
1656-
} catch (error) {
1657-
console.error('Error deleting session:', error);
1658-
alert('Failed to delete session. Please try again.');
1760+
} else {
1761+
// Fallback for web environment - direct download
1762+
XLSX.writeFile(wb, defaultFilename);
1763+
console.log(`Exported ${sessions.length} sessions to ${defaultFilename}`);
16591764
}
1765+
1766+
} catch (error) {
1767+
console.error('Error exporting sessions:', error);
1768+
alert('Failed to export sessions. Please try again.');
16601769
}
16611770
}
16621771
}

0 commit comments

Comments
 (0)