
Building a Java Desktop App to Track Job Applications
A Journey with NetBeans, Embedded H2, and AI Assistance.
Over the past month or so, I started a project to develop a Java desktop application to track my job applications. I used Apache NetBeans as the IDE and assistance from AI to work out the bugs and to help with the development of the app. I created a Java app with multiple features that not only records job application details but also provides dynamic reporting capabilities. In this blog post, I’ll walk through the development process, culminating in a fat JAR distribution that uses an embedded H2 database as a single source of truth.
My intention with this post is to cover my development of a production-ready desktop application with an embedded database, this post will provide a guide to replicating my solution.
Project Overview
Key Features:
- Job Application Tracking: Easily add, edit, delete, and search job applications.
- Dynamic Reporting: Generate historical reports filtered by time frame, status, and company.
- Embedded Database: Uses H2 as an embedded database for portability.
- Fat JAR Packaging: Bundles all dependencies into a single executable JAR file for production.
- Modern UI: Developed using Java Swing with a custom navigation toolbar and a calendar popup for date selection.
Development Tools and Libraries:
- IDE: Apache NetBeans
- Build Tool: Ant
- Database: H2 Embedded Database
- Charting Library: JFreeChart
- Calendar Component: JCalendar (for the JDateChooser)
- AI Assistance: ChatGPT, Used to troubleshoot, optimize, and guide the development process
Architecture and Design Decisions
Single Source of Truth:
To ensure that both the development (IDE) and production (fat JAR file) versions of the app use the same data, I configured the app to connect to an embedded H2 database stored at a fixed, absolute location (e.g., C:\ProgramData\JobTrackerApp\job_tracker.mv.db). This ensures portability and consistency across all environments.
Fat JAR Packaging:
Using Ant and a custom build.xml target, I bundled the entire application, including all dependencies, into one “fat” JAR file. This makes hypothetical distribution simple since potential end-users need to only run a single JAR file.
Dynamic Reporting and UI:
The reports section includes multiple filters (time frame, status, and company) and uses JFreeChart to generate dynamic bar charts. I also enhanced the UI with a navigation toolbar using simple icon buttons for a modern look.
Application Screenshots and Structure





Source Code Overview
Below is a description of what each of the source code files do for the application. These eight Java source files go into a project package (the package name I chose is: com.myjobtracker.app).
The source files and their description are:
- DatabaseConnection.java
- This file encapsulates the logic for connecting to the embedded H2 database.
- JobApplication.java
- This is the model (or domain) class that represents a single job application.
- JobApplicationDAO.java
- This Data Access Object (DAO) class handles all CRUD operations (Create, Read, Update, Delete) for job applications in the H2 database.
- AddEditPanel.java
- This Swing panel provides the user interface for adding new job applications or editing existing ones.
- DashboardPanel.java
- This panel displays a table view of all job applications.
- SearchPanel.java
- The Search panel allows users to search for job applications based on keywords.
- ReportsPanel.java
- This panel generates dynamic reports based on the job application data.
- MainApp.java
- This is the main entry point of the application, orchestrating the overall UI and navigation.
How to Replicate the App
- Set up a new Java project in NetBeans and copy all the source files into a package com.myjobtracker.app
- Include any required external libraries (JCalendar, JFreeChart, and H2) in your project’s lib folder and add them to your project’s classpath.
- Set Up the Database, Create the folder C:\ProgramData\JobTrackerApp on your machine.
- Use the H2 Console to run the SQL command below to initialize the schema and create the table:
CREATE TABLE IF NOT EXISTS job_applications (
id INT AUTO_INCREMENT PRIMARY KEY,
company_name VARCHAR(255) NOT NULL,
job_title VARCHAR(255) NOT NULL,
application_date DATE NOT NULL,
status VARCHAR(50),
notes TEXT
);
- In NetBeans, run the app to ensure that it connects to the H2 database and that you can add, search, and view job applications.
- Export the fat JAR using the provided build.xml target (PowerShell command: ant clean fat-jar).
Full Source Code
DatabaseConnection.java
package com.myjobtracker.app;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DatabaseConnection {
// Absolute path for the embedded H2 database.
// For example, using C:\ProgramData\JobTrackerApp\job_tracker
private static final String URL = "jdbc:h2:file:C:/ProgramData/JobTrackerApp/job_tracker;AUTO_SERVER=TRUE";
private static final String USER = "sa";
private static final String PASSWORD = "";
public static Connection connect() {
Connection conn = null;
try {
Class.forName("org.h2.Driver");
conn = DriverManager.getConnection(URL, USER, PASSWORD);
System.out.println("Connected to embedded H2 database at " + URL);
} catch (SQLException | ClassNotFoundException e) {
System.out.println("Connection failed: " + e.getMessage());
}
return conn;
}
}
JobApplication.java
package com.myjobtracker.app;
import java.time.LocalDate;
public class JobApplication {
private int id;
private String company;
private String jobTitle;
private LocalDate applicationDate; // Using LocalDate for date values
private String status;
private String notes;
public JobApplication() {
}
public JobApplication(String company, String jobTitle, LocalDate applicationDate, String status, String notes) {
this.company = company;
this.jobTitle = jobTitle;
this.applicationDate = applicationDate;
this.status = status;
this.notes = notes;
}
// Getters and setters
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getCompany() {
return company;
}
public void setCompany(String company) {
this.company = company;
}
public String getJobTitle() {
return jobTitle;
}
public void setJobTitle(String jobTitle) {
this.jobTitle = jobTitle;
}
public LocalDate getApplicationDate() {
return applicationDate;
}
public void setApplicationDate(LocalDate applicationDate) {
this.applicationDate = applicationDate;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getNotes() {
return notes;
}
public void setNotes(String notes) {
this.notes = notes;
}
}
JobApplicationDAO.java
package com.myjobtracker.app;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class JobApplicationDAO {
// CREATE: Add a new job application
public static boolean addJobApplication(JobApplication app) {
String sql = "INSERT INTO job_applications (company_name, job_title, application_date, status, notes) VALUES (?, ?, ?, ?, ?)";
try (Connection conn = DatabaseConnection.connect();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, app.getCompany());
pstmt.setString(2, app.getJobTitle());
// Convert LocalDate to SQL Date
pstmt.setDate(3, java.sql.Date.valueOf(app.getApplicationDate()));
pstmt.setString(4, app.getStatus());
pstmt.setString(5, app.getNotes());
int affectedRows = pstmt.executeUpdate();
return affectedRows > 0;
} catch (SQLException e) {
System.out.println("Error inserting job application: " + e.getMessage());
return false;
}
}
// READ: Retrieve all job applications
public static List<JobApplication> getJobApplications() {
List<JobApplication> apps = new ArrayList<>();
String sql = "SELECT * FROM job_applications ORDER BY application_date DESC";
try (Connection conn = DatabaseConnection.connect();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
JobApplication app = new JobApplication();
app.setId(rs.getInt("id"));
app.setCompany(rs.getString("company_name"));
app.setJobTitle(rs.getString("job_title"));
// Convert SQL Date to LocalDate
app.setApplicationDate(rs.getDate("application_date").toLocalDate());
app.setStatus(rs.getString("status"));
app.setNotes(rs.getString("notes"));
apps.add(app);
}
} catch (SQLException e) {
System.out.println("Error retrieving job applications: " + e.getMessage());
}
return apps;
}
// UPDATE: Update an existing job application
public static boolean updateJobApplication(JobApplication app) {
String sql = "UPDATE job_applications SET company_name = ?, job_title = ?, application_date = ?, status = ?, notes = ? WHERE id = ?";
try (Connection conn = DatabaseConnection.connect();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, app.getCompany());
pstmt.setString(2, app.getJobTitle());
pstmt.setDate(3, java.sql.Date.valueOf(app.getApplicationDate()));
pstmt.setString(4, app.getStatus());
pstmt.setString(5, app.getNotes());
pstmt.setInt(6, app.getId());
int affectedRows = pstmt.executeUpdate();
return affectedRows > 0;
} catch (SQLException e) {
System.out.println("Error updating job application: " + e.getMessage());
return false;
}
}
// DELETE: Delete a job application by its ID
public static boolean deleteJobApplication(int id) {
String sql = "DELETE FROM job_applications WHERE id = ?";
try (Connection conn = DatabaseConnection.connect();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, id);
int affectedRows = pstmt.executeUpdate();
return affectedRows > 0;
} catch (SQLException e) {
System.out.println("Error deleting job application: " + e.getMessage());
return false;
}
}
// SEARCH: Retrieve job applications matching a query in company_name or job_title
public static List<JobApplication> searchJobApplications(String query) {
List<JobApplication> apps = new ArrayList<>();
String sql = "SELECT * FROM job_applications WHERE company_name ILIKE ? OR job_title ILIKE ? ORDER BY application_date DESC";
try (Connection conn = DatabaseConnection.connect();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
String searchPattern = "%" + query + "%";
pstmt.setString(1, searchPattern);
pstmt.setString(2, searchPattern);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
JobApplication app = new JobApplication();
app.setId(rs.getInt("id"));
app.setCompany(rs.getString("company_name"));
app.setJobTitle(rs.getString("job_title"));
app.setApplicationDate(rs.getDate("application_date").toLocalDate());
app.setStatus(rs.getString("status"));
app.setNotes(rs.getString("notes"));
apps.add(app);
}
}
} catch (SQLException e) {
System.out.println("Error searching job applications: " + e.getMessage());
}
return apps;
}
// NEW: Retrieve a single job application by its ID
public static JobApplication getJobApplicationById(int id) {
String sql = "SELECT * FROM job_applications WHERE id = ?";
try (Connection conn = DatabaseConnection.connect();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, id);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
JobApplication app = new JobApplication();
app.setId(rs.getInt("id"));
app.setCompany(rs.getString("company_name"));
app.setJobTitle(rs.getString("job_title"));
app.setApplicationDate(rs.getDate("application_date").toLocalDate());
app.setStatus(rs.getString("status"));
app.setNotes(rs.getString("notes"));
return app;
}
}
} catch (SQLException e) {
System.out.println("Error retrieving job application by ID: " + e.getMessage());
}
return null;
}
}
AddEditPanel.java
package com.myjobtracker.app;
import com.toedter.calendar.JDateChooser;
import javax.swing.*;
import java.awt.*;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class AddEditPanel extends JPanel {
private JTextField txtCompany;
private JTextField txtJobTitle;
private JDateChooser dateChooser; // Using JDateChooser for date selection
private JComboBox<String> comboStatus;
private JTextArea txtNotes;
private JButton btnSave;
// Holds the ID of the job application being edited (null for new entries)
private Integer currentId = null;
// Formatter for dates in MM-DD-YYYY format.
public static final DateTimeFormatter DISPLAY_FORMATTER = DateTimeFormatter.ofPattern("MM-dd-yyyy");
public AddEditPanel() {
setLayout(new BorderLayout(10, 10));
// Create a panel for the one-line fields using GridBagLayout.
JPanel topPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5); // Padding around components
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.anchor = GridBagConstraints.WEST;
gbc.weightx = 1.0;
// Row 0: Company Name
gbc.gridx = 0;
gbc.gridy = 0;
topPanel.add(new JLabel("Company Name:"), gbc);
txtCompany = new JTextField();
txtCompany.setPreferredSize(new Dimension(200, 25));
gbc.gridx = 1;
topPanel.add(txtCompany, gbc);
// Row 1: Job Title
gbc.gridx = 0;
gbc.gridy = 1;
topPanel.add(new JLabel("Job Title:"), gbc);
txtJobTitle = new JTextField();
txtJobTitle.setPreferredSize(new Dimension(200, 25));
gbc.gridx = 1;
topPanel.add(txtJobTitle, gbc);
// Row 2: Application Date using JDateChooser
gbc.gridx = 0;
gbc.gridy = 2;
topPanel.add(new JLabel("Application Date:"), gbc);
dateChooser = new JDateChooser();
dateChooser.setDateFormatString("MM-dd-yyyy");
dateChooser.setPreferredSize(new Dimension(200, 25));
gbc.gridx = 1;
topPanel.add(dateChooser, gbc);
// Row 3: Status
gbc.gridx = 0;
gbc.gridy = 3;
topPanel.add(new JLabel("Status:"), gbc);
comboStatus = new JComboBox<>(new String[] {"Applied", "Interview", "Offer", "Rejected"});
gbc.gridx = 1;
topPanel.add(comboStatus, gbc);
// Create a panel for the Notes field.
JPanel notesPanel = new JPanel(new BorderLayout());
notesPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
notesPanel.add(new JLabel("Notes:"), BorderLayout.NORTH);
txtNotes = new JTextArea(3, 20); // Multi-line for notes
JScrollPane scrollPane = new JScrollPane(txtNotes);
notesPanel.add(scrollPane, BorderLayout.CENTER);
// Combine the top fields and notes into a form panel.
JPanel formPanel = new JPanel(new BorderLayout());
formPanel.add(topPanel, BorderLayout.NORTH);
formPanel.add(notesPanel, BorderLayout.CENTER);
// Create a panel for the Save button so it does not stretch full width.
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
btnSave = new JButton("Save Application");
btnPanel.add(btnSave);
// Add the form panel in the center and the button panel at the bottom.
add(formPanel, BorderLayout.CENTER);
add(btnPanel, BorderLayout.SOUTH);
}
// Getter methods for form values.
public String getCompany() {
return txtCompany.getText().trim();
}
public String getJobTitle() {
return txtJobTitle.getText().trim();
}
// Returns a LocalDate parsed from the JDateChooser.
public LocalDate getApplicationDate() {
if (dateChooser.getDate() != null) {
return dateChooser.getDate().toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDate();
}
return null;
}
public String getStatus() {
return (String) comboStatus.getSelectedItem();
}
public String getNotes() {
return txtNotes.getText().trim();
}
// Allow external classes (like MainApp) to attach an ActionListener to the Save button.
public void addSaveActionListener(java.awt.event.ActionListener listener) {
btnSave.addActionListener(listener);
}
// Returns the current job application ID (if editing an existing record).
public Integer getCurrentId() {
return currentId;
}
// Populates the form fields with data from the provided JobApplication.
public void setJobApplication(JobApplication app) {
if (app != null) {
currentId = app.getId();
txtCompany.setText(app.getCompany());
txtJobTitle.setText(app.getJobTitle());
if (app.getApplicationDate() != null) {
// Convert LocalDate to java.util.Date
java.util.Date date = java.util.Date.from(app.getApplicationDate()
.atStartOfDay(ZoneId.systemDefault()).toInstant());
dateChooser.setDate(date);
} else {
dateChooser.setDate(null);
}
comboStatus.setSelectedItem(app.getStatus());
txtNotes.setText(app.getNotes());
}
}
// Clears all form fields and resets the current ID.
public void clearFields() {
currentId = null;
txtCompany.setText("");
txtJobTitle.setText("");
dateChooser.setDate(null);
comboStatus.setSelectedIndex(0);
txtNotes.setText("");
}
}
DashboardPanel.java
package com.myjobtracker.app;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableRowSorter;
import java.awt.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
public class DashboardPanel extends JPanel {
private JTable table;
private DefaultTableModel tableModel;
private JButton btnRefresh;
private JButton btnEdit;
private JButton btnDelete;
// Formatter for displaying dates in MM-DD-YYYY format.
public static final DateTimeFormatter DISPLAY_FORMATTER = DateTimeFormatter.ofPattern("MM-dd-yyyy");
public DashboardPanel() {
setLayout(new BorderLayout());
// Updated column headers to include "Notes".
String[] columnNames = {"ID", "Company", "Job Title", "Application Date", "Status", "Notes"};
tableModel = new DefaultTableModel(columnNames, 0) {
@Override
public boolean isCellEditable(int row, int column) {
return false; // Make table cells non-editable.
}
};
table = new JTable(tableModel);
// Enable sorting with a TableRowSorter.
TableRowSorter<DefaultTableModel> sorter = new TableRowSorter<>(tableModel);
// Set a custom comparator for the "Application Date" column (index 3).
sorter.setComparator(3, (String s1, String s2) -> {
try {
LocalDate d1 = LocalDate.parse(s1, DISPLAY_FORMATTER);
LocalDate d2 = LocalDate.parse(s2, DISPLAY_FORMATTER);
return d1.compareTo(d2);
} catch (DateTimeParseException e) {
return s1.compareTo(s2);
}
});
table.setRowSorter(sorter);
btnRefresh = new JButton("Refresh Data");
btnEdit = new JButton("Edit Selected");
btnDelete = new JButton("Delete Selected");
JPanel buttonPanel = new JPanel(new FlowLayout());
buttonPanel.add(btnRefresh);
buttonPanel.add(btnEdit);
buttonPanel.add(btnDelete);
add(new JScrollPane(table), BorderLayout.CENTER);
add(buttonPanel, BorderLayout.SOUTH);
}
public void updateTable(Object[][] data) {
tableModel.setRowCount(0); // Clear existing rows.
for (Object[] row : data) {
tableModel.addRow(row);
}
}
public JButton getRefreshButton() {
return btnRefresh;
}
public JButton getEditButton() {
return btnEdit;
}
public JButton getDeleteButton() {
return btnDelete;
}
public JTable getTable() {
return table;
}
}
SearchPanel.java
package com.myjobtracker.app;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.time.format.DateTimeFormatter;
public class SearchPanel extends JPanel {
private JTextField txtSearch;
private JButton btnSearch;
private JButton btnEdit;
private JButton btnClear;
private JTable resultsTable;
private DefaultTableModel tableModel;
// Use the same date formatter.
public static final DateTimeFormatter DISPLAY_FORMATTER = DateTimeFormatter.ofPattern("MM-dd-yyyy");
public SearchPanel() {
setLayout(new BorderLayout());
// Top panel for search input and control buttons.
JPanel searchInputPanel = new JPanel(new FlowLayout());
txtSearch = new JTextField(20);
btnSearch = new JButton("Search");
btnEdit = new JButton("Edit");
btnClear = new JButton("Clear");
searchInputPanel.add(new JLabel("Search:"));
searchInputPanel.add(txtSearch);
searchInputPanel.add(btnSearch);
searchInputPanel.add(btnEdit);
searchInputPanel.add(btnClear);
// Table for displaying search results, with an extra "Notes" column.
String[] columnNames = {"ID", "Company", "Job Title", "Application Date", "Status", "Notes"};
tableModel = new DefaultTableModel(columnNames, 0);
resultsTable = new JTable(tableModel);
add(searchInputPanel, BorderLayout.NORTH);
add(new JScrollPane(resultsTable), BorderLayout.CENTER);
}
// Returns the search query string.
public String getSearchQuery() {
return txtSearch.getText().trim();
}
// Exposes the search button.
public JButton getSearchButton() {
return btnSearch;
}
// Exposes the edit button.
public JButton getEditButton() {
return btnEdit;
}
// Exposes the clear button.
public JButton getClearButton() {
return btnClear;
}
// Updates the search results table.
public void updateSearchResults(Object[][] data) {
tableModel.setRowCount(0);
for (Object[] row : data) {
tableModel.addRow(row);
}
}
// Returns the ID of the selected row in the search results, or -1 if none is selected.
public int getSelectedRowId() {
int selectedRow = resultsTable.getSelectedRow();
if (selectedRow != -1) {
return (int) resultsTable.getValueAt(selectedRow, 0);
}
return -1;
}
// Clears the search query and the table data.
public void clearSearchResults() {
txtSearch.setText("");
tableModel.setRowCount(0);
}
// Expose the results table if needed.
public JTable getTable() {
return resultsTable;
}
}
ReportsPanel.java
package com.myjobtracker.app;
import javax.swing.*;
import java.awt.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.data.category.DefaultCategoryDataset;
public class ReportsPanel extends JPanel {
private JButton btnGenerateReport;
private JTextArea textAreaReport;
private JPanel chartContainer; // Panel to hold the chart
private JComboBox<String> timeFrameComboBox;
private JComboBox<String> statusFilterComboBox;
private JTextField companyFilterField;
// Formatter for dates in MM-DD-YYYY format.
private static final DateTimeFormatter DISPLAY_FORMATTER = DateTimeFormatter.ofPattern("MM-dd-yyyy");
public ReportsPanel() {
setLayout(new BorderLayout());
// Top panel for filter controls
JPanel filterPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
// Time frame filter
filterPanel.add(new JLabel("Time Frame:"));
timeFrameComboBox = new JComboBox<>(new String[] {"All Time", "Past Week", "Past Month", "Past Year"});
filterPanel.add(timeFrameComboBox);
// Status filter
filterPanel.add(new JLabel("Status:"));
statusFilterComboBox = new JComboBox<>(new String[] {"All", "Applied", "Interview", "Offer", "Rejected"});
filterPanel.add(statusFilterComboBox);
// Company filter
filterPanel.add(new JLabel("Company:"));
companyFilterField = new JTextField(15);
filterPanel.add(companyFilterField);
// Generate Report button
btnGenerateReport = new JButton("Generate Report");
filterPanel.add(btnGenerateReport);
// Create a non-editable text area for the summary report.
textAreaReport = new JTextArea();
textAreaReport.setEditable(false);
JScrollPane textScrollPane = new JScrollPane(textAreaReport);
// Panel to hold the chart.
chartContainer = new JPanel(new BorderLayout());
// Use a JSplitPane to divide the view between the text summary and the chart.
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, textScrollPane, chartContainer);
splitPane.setResizeWeight(0.5);
add(filterPanel, BorderLayout.NORTH);
add(splitPane, BorderLayout.CENTER);
// Attach event listener to generate the report.
btnGenerateReport.addActionListener(e -> generateAndDisplayReport());
}
private void generateAndDisplayReport() {
// Retrieve all applications from the database.
List<JobApplication> allApps = JobApplicationDAO.getJobApplications();
// Apply time frame filter.
List<JobApplication> filteredApps = new ArrayList<>();
String selectedTimeFrame = (String) timeFrameComboBox.getSelectedItem();
LocalDate cutoff = null;
if ("Past Week".equals(selectedTimeFrame)) {
cutoff = LocalDate.now().minusDays(7);
} else if ("Past Month".equals(selectedTimeFrame)) {
cutoff = LocalDate.now().minusDays(30);
} else if ("Past Year".equals(selectedTimeFrame)) {
cutoff = LocalDate.now().minusDays(365);
}
if (cutoff != null) {
for (JobApplication app : allApps) {
LocalDate appDate = app.getApplicationDate();
if (appDate != null && (appDate.isEqual(cutoff) || appDate.isAfter(cutoff))) {
filteredApps.add(app);
}
}
} else {
filteredApps = allApps;
}
// Apply status filter.
String selectedStatus = (String) statusFilterComboBox.getSelectedItem();
List<JobApplication> statusFilteredApps = new ArrayList<>();
if (!"All".equalsIgnoreCase(selectedStatus)) {
for (JobApplication app : filteredApps) {
String status = app.getStatus();
if (status != null && status.trim().equalsIgnoreCase(selectedStatus)) {
statusFilteredApps.add(app);
}
}
} else {
statusFilteredApps = filteredApps;
}
// Apply company filter.
String companyFilter = companyFilterField.getText().trim().toLowerCase();
List<JobApplication> finalApps = new ArrayList<>();
if (!companyFilter.isEmpty()) {
for (JobApplication app : statusFilteredApps) {
String company = app.getCompany();
if (company != null && company.toLowerCase().contains(companyFilter)) {
finalApps.add(app);
}
}
} else {
finalApps = statusFilteredApps;
}
// Calculate statistics.
int total = finalApps.size();
int applied = 0, interview = 0, offer = 0, rejected = 0, unknown = 0;
for (JobApplication app : finalApps) {
String status = app.getStatus();
String normalizedStatus = (status == null) ? "" : status.trim();
if (normalizedStatus.isEmpty()) {
unknown++;
} else if (normalizedStatus.equalsIgnoreCase("Applied")) {
applied++;
} else if (normalizedStatus.equalsIgnoreCase("Interview")) {
interview++;
} else if (normalizedStatus.equalsIgnoreCase("Offer")) {
offer++;
} else if (normalizedStatus.equalsIgnoreCase("Rejected")) {
rejected++;
} else {
unknown++;
}
}
StringBuilder report = new StringBuilder();
report.append("Job Application Report\n");
report.append("========================\n\n");
report.append("Time Frame: ").append(selectedTimeFrame).append("\n");
report.append("Status Filter: ").append(selectedStatus).append("\n");
report.append("Company Filter: ").append(companyFilter.isEmpty() ? "None" : companyFilter).append("\n\n");
report.append("Total Applications: ").append(total).append("\n");
report.append("Applied: ").append(applied).append("\n");
report.append("Interview: ").append(interview).append("\n");
report.append("Offer: ").append(offer).append("\n");
report.append("Rejected: ").append(rejected).append("\n");
if (unknown > 0) {
report.append("Unknown: ").append(unknown).append("\n");
}
// Determine earliest and latest application dates.
LocalDate earliest = null;
LocalDate latest = null;
for (JobApplication app : finalApps) {
LocalDate date = app.getApplicationDate();
if (date != null) {
if (earliest == null || date.isBefore(earliest)) {
earliest = date;
}
if (latest == null || date.isAfter(latest)) {
latest = date;
}
}
}
if (earliest != null) {
report.append("Earliest Application Date: ").append(earliest.format(DISPLAY_FORMATTER)).append("\n");
}
if (latest != null) {
report.append("Latest Application Date: ").append(latest.format(DISPLAY_FORMATTER)).append("\n");
}
textAreaReport.setText(report.toString());
// Create dataset for the chart.
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
dataset.addValue(applied, "Count", "Applied");
dataset.addValue(interview, "Count", "Interview");
dataset.addValue(offer, "Count", "Offer");
dataset.addValue(rejected, "Count", "Rejected");
dataset.addValue(unknown, "Count", "Unknown");
// Create a bar chart.
JFreeChart barChart = ChartFactory.createBarChart(
"Job Applications by Status", // Chart title
"Status", // Category axis label
"Count", // Value axis label
dataset
);
// Enable anti-aliasing and set rendering hints for better quality.
barChart.setAntiAlias(true);
barChart.setTextAntiAlias(true);
barChart.getRenderingHints().put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
barChart.getRenderingHints().put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
barChart.getRenderingHints().put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
barChart.getRenderingHints().put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
// Use a high-quality font such as Segoe UI.
barChart.getTitle().setFont(new Font("Segoe UI", Font.BOLD, 18));
if (barChart.getLegend() != null) {
barChart.getLegend().setItemFont(new Font("Segoe UI", Font.PLAIN, 14));
}
CategoryPlot plot = barChart.getCategoryPlot();
plot.getDomainAxis().setLabelFont(new Font("Segoe UI", Font.PLAIN, 16));
plot.getDomainAxis().setTickLabelFont(new Font("Segoe UI", Font.PLAIN, 14));
plot.getRangeAxis().setLabelFont(new Font("Segoe UI", Font.PLAIN, 16));
plot.getRangeAxis().setTickLabelFont(new Font("Segoe UI", Font.PLAIN, 14));
ChartPanel chartPanel = new ChartPanel(barChart);
chartPanel.setPreferredSize(new Dimension(400, 300));
chartContainer.removeAll();
chartContainer.add(chartPanel, BorderLayout.CENTER);
chartContainer.validate();
}
}
MainApp.java
package com.myjobtracker.app;
import javax.swing.*;
import java.awt.*;
import java.util.List;
public class MainApp {
private JFrame frame;
private CardLayout cardLayout;
private JPanel mainPanel;
// Panel instances
private DashboardPanel dashboardPanel;
private AddEditPanel addEditPanel;
private SearchPanel searchPanel;
private ReportsPanel reportsPanel;
public MainApp() {
initialize();
}
private void initialize() {
frame = new JFrame("Job Tracker App");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(1000, 600);
// Create the menu bar with File menu.
JMenuBar menuBar = new JMenuBar();
JMenu fileMenu = new JMenu("File");
JMenuItem exportCsvItem = new JMenuItem("Export CSV");
exportCsvItem.addActionListener(e -> exportToCSV());
fileMenu.add(exportCsvItem);
JMenuItem quitItem = new JMenuItem("Quit");
quitItem.addActionListener(e -> {
int confirm = JOptionPane.showConfirmDialog(frame, "Are you sure you want to quit?", "Confirm Quit", JOptionPane.YES_NO_OPTION);
if (confirm == JOptionPane.YES_OPTION) {
System.exit(0);
}
});
fileMenu.add(quitItem);
menuBar.add(fileMenu);
frame.setJMenuBar(menuBar);
// Create a navigation toolbar with icon buttons.
JToolBar navToolBar = new JToolBar();
navToolBar.setFloatable(false);
navToolBar.setLayout(new FlowLayout(FlowLayout.CENTER));
JButton btnDashboard = new JButton(new ImageIcon(getClass().getResource("/icons/dashboard.png")));
btnDashboard.setToolTipText("Dashboard");
JButton btnAddEdit = new JButton(new ImageIcon(getClass().getResource("/icons/add_edit.png")));
btnAddEdit.setToolTipText("Add/Edit Application");
JButton btnSearch = new JButton(new ImageIcon(getClass().getResource("/icons/search.png")));
btnSearch.setToolTipText("Search/Filter");
JButton btnReports = new JButton(new ImageIcon(getClass().getResource("/icons/reports.png")));
btnReports.setToolTipText("Reports/Notifications");
btnDashboard.addActionListener(e -> cardLayout.show(mainPanel, "Dashboard"));
btnAddEdit.addActionListener(e -> {
addEditPanel.clearFields();
cardLayout.show(mainPanel, "AddEdit");
});
btnSearch.addActionListener(e -> cardLayout.show(mainPanel, "Search"));
btnReports.addActionListener(e -> cardLayout.show(mainPanel, "Reports"));
navToolBar.add(btnDashboard);
navToolBar.add(btnAddEdit);
navToolBar.add(btnSearch);
navToolBar.add(btnReports);
// Set up CardLayout for the main panel.
cardLayout = new CardLayout();
mainPanel = new JPanel(cardLayout);
dashboardPanel = new DashboardPanel();
addEditPanel = new AddEditPanel();
searchPanel = new SearchPanel();
reportsPanel = new ReportsPanel();
mainPanel.add(dashboardPanel, "Dashboard");
mainPanel.add(addEditPanel, "AddEdit");
mainPanel.add(searchPanel, "Search");
mainPanel.add(reportsPanel, "Reports");
// Attach event handlers.
// Save button in Add/Edit panel.
addEditPanel.addSaveActionListener(e -> {
JobApplication app = new JobApplication(
addEditPanel.getCompany(),
addEditPanel.getJobTitle(),
addEditPanel.getApplicationDate(), // Returns a LocalDate
addEditPanel.getStatus(),
addEditPanel.getNotes()
);
boolean success;
if (addEditPanel.getCurrentId() != null) {
app.setId(addEditPanel.getCurrentId());
success = JobApplicationDAO.updateJobApplication(app);
if (success) {
JOptionPane.showMessageDialog(frame, "Job application updated successfully!");
} else {
JOptionPane.showMessageDialog(frame, "Failed to update the job application.", "Error", JOptionPane.ERROR_MESSAGE);
}
} else {
success = JobApplicationDAO.addJobApplication(app);
if (success) {
JOptionPane.showMessageDialog(frame, "Job application saved successfully!");
} else {
JOptionPane.showMessageDialog(frame, "Failed to save the job application.", "Error", JOptionPane.ERROR_MESSAGE);
}
}
addEditPanel.clearFields();
});
// Refresh button in Dashboard panel.
dashboardPanel.getRefreshButton().addActionListener(e -> {
List<JobApplication> apps = JobApplicationDAO.getJobApplications();
Object[][] data = new Object[apps.size()][6];
for (int i = 0; i < apps.size(); i++) {
JobApplication app = apps.get(i);
data[i][0] = app.getId();
data[i][1] = app.getCompany();
data[i][2] = app.getJobTitle();
data[i][3] = (app.getApplicationDate() != null) ? app.getApplicationDate().format(DashboardPanel.DISPLAY_FORMATTER) : "";
data[i][4] = app.getStatus();
String notes = app.getNotes();
if (notes != null && notes.length() > 30) {
notes = notes.substring(0, 30) + "...";
}
data[i][5] = notes;
}
dashboardPanel.updateTable(data);
});
// Search button in Search panel.
searchPanel.getSearchButton().addActionListener(e -> {
String query = searchPanel.getSearchQuery();
List<JobApplication> apps = JobApplicationDAO.searchJobApplications(query);
Object[][] data = new Object[apps.size()][6];
for (int i = 0; i < apps.size(); i++) {
JobApplication app = apps.get(i);
data[i][0] = app.getId();
data[i][1] = app.getCompany();
data[i][2] = app.getJobTitle();
data[i][3] = (app.getApplicationDate() != null) ? app.getApplicationDate().format(DashboardPanel.DISPLAY_FORMATTER) : "";
data[i][4] = app.getStatus();
String notes = app.getNotes();
if (notes != null && notes.length() > 30) {
notes = notes.substring(0, 30) + "...";
}
data[i][5] = notes;
}
searchPanel.updateSearchResults(data);
});
// Edit button in Search panel.
searchPanel.getEditButton().addActionListener(e -> {
int id = searchPanel.getSelectedRowId();
if (id == -1) {
JOptionPane.showMessageDialog(frame, "Please select a job application to edit from the search results.");
return;
}
JobApplication app = JobApplicationDAO.getJobApplicationById(id);
if (app == null) {
JOptionPane.showMessageDialog(frame, "Could not retrieve the selected job application.");
return;
}
addEditPanel.setJobApplication(app);
cardLayout.show(mainPanel, "AddEdit");
});
// Clear button in Search panel.
searchPanel.getClearButton().addActionListener(e -> {
searchPanel.clearSearchResults();
});
// Edit button in Dashboard panel.
dashboardPanel.getEditButton().addActionListener(e -> {
int selectedRow = dashboardPanel.getTable().getSelectedRow();
if (selectedRow == -1) {
JOptionPane.showMessageDialog(frame, "Please select a job application to edit.");
return;
}
int id = (int) dashboardPanel.getTable().getValueAt(selectedRow, 0);
JobApplication app = JobApplicationDAO.getJobApplicationById(id);
if (app == null) {
JOptionPane.showMessageDialog(frame, "Could not retrieve the selected job application.");
return;
}
addEditPanel.setJobApplication(app);
cardLayout.show(mainPanel, "AddEdit");
});
// Delete button in Dashboard panel.
dashboardPanel.getDeleteButton().addActionListener(e -> {
int selectedRow = dashboardPanel.getTable().getSelectedRow();
if (selectedRow == -1) {
JOptionPane.showMessageDialog(frame, "Please select a job application to delete.");
return;
}
int id = (int) dashboardPanel.getTable().getValueAt(selectedRow, 0);
int confirm = JOptionPane.showConfirmDialog(frame, "Are you sure you want to delete this application?", "Confirm Delete", JOptionPane.YES_NO_OPTION);
if (confirm == JOptionPane.YES_OPTION) {
boolean success = JobApplicationDAO.deleteJobApplication(id);
if (success) {
JOptionPane.showMessageDialog(frame, "Job application deleted successfully!");
dashboardPanel.getRefreshButton().doClick();
} else {
JOptionPane.showMessageDialog(frame, "Failed to delete the job application.", "Error", JOptionPane.ERROR_MESSAGE);
}
}
});
// Automatically refresh the dashboard on startup.
dashboardPanel.getRefreshButton().doClick();
// Layout the frame: add the navigation toolbar at the top and the main panel in the center.
frame.getContentPane().setLayout(new BorderLayout());
frame.getContentPane().add(navToolBar, BorderLayout.NORTH);
frame.getContentPane().add(mainPanel, BorderLayout.CENTER);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
// Method to export job applications to a CSV file with a file chooser.
private void exportToCSV() {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setDialogTitle("Choose destination to export CSV");
int userSelection = fileChooser.showSaveDialog(frame);
if (userSelection == JFileChooser.APPROVE_OPTION) {
java.io.File fileToSave = fileChooser.getSelectedFile();
String filePath = fileToSave.getAbsolutePath();
if (!filePath.toLowerCase().endsWith(".csv")) {
filePath += ".csv";
}
try (java.io.BufferedWriter writer = new java.io.BufferedWriter(new java.io.FileWriter(filePath))) {
writer.write("ID,Company,Job Title,Application Date,Status,Notes");
writer.newLine();
List<JobApplication> apps = JobApplicationDAO.getJobApplications();
for (JobApplication app : apps) {
writer.write(app.getId() + ","
+ escapeCsv(app.getCompany()) + ","
+ escapeCsv(app.getJobTitle()) + ","
+ ((app.getApplicationDate() != null) ? app.getApplicationDate().format(DashboardPanel.DISPLAY_FORMATTER) : "") + ","
+ escapeCsv(app.getStatus()) + ","
+ escapeCsv(app.getNotes()));
writer.newLine();
}
JOptionPane.showMessageDialog(frame, "Data exported successfully to " + filePath);
} catch (java.io.IOException ex) {
JOptionPane.showMessageDialog(frame, "Error exporting data: " + ex.getMessage());
}
}
}
// Helper method to escape CSV fields containing commas.
private String escapeCsv(String field) {
if (field == null) return "";
if (field.contains(",")) {
field = field.replace("\"", "\"\"");
return "\"" + field + "\"";
}
return field;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new MainApp());
}
}