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:

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());
    }
}

Leave a Comment