• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

abdulkader138 / personal-expense-tracker / #39

29 Dec 2025 12:10AM UTC coverage: 98.266% (-0.1%) from 98.382%
#39

push

abdulkader138
Working on code coverage

850 of 865 relevant lines covered (98.27%)

0.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.41
/src/main/java/com/mycompany/pet/ui/MainWindow.java
1
package com.mycompany.pet.ui;
2

3
import com.mycompany.pet.controller.CategoryController;
4
import com.mycompany.pet.controller.ExpenseController;
5
import com.mycompany.pet.model.Category;
6
import com.mycompany.pet.model.Expense;
7

8
import javax.swing.*;
9
import javax.swing.table.DefaultTableModel;
10
import java.awt.*;
11
import java.sql.SQLException;
12
import java.time.LocalDate;
13

14
/**
15
 * Main window for the Expense Tracker application.
16
 * 
17
 * This window uses ExpenseController and CategoryController to separate UI concerns from business logic.
18
 * All database operations are handled asynchronously by the controllers.
19
 */
20
public class MainWindow extends JFrame {
21
    private static final long serialVersionUID = 1L;
22
    private static final String ERROR_TITLE = "Error";
23
    
24
    // Controllers (preferred) - package-private for testing
25
    final transient ExpenseController expenseController;
26
    final transient CategoryController categoryController;
27

28
    // UI Components (package-private for testing)
29
    JTable expenseTable;
30
    DefaultTableModel expenseTableModel;
31
    JComboBox<Category> categoryComboBox;
32
    JComboBox<String> monthComboBox;
33
    JComboBox<String> yearComboBox;
34
    JLabel monthlyTotalLabel; // Package-private for testing
35
    JLabel categoryTotalLabel; // Package-private for testing
36
    boolean isInitializing = true; // Flag to prevent action listeners during initialization
1✔
37

38
    /**
39
     * Creates a new MainWindow with controllers.
40
     * 
41
     * @param expenseController Expense controller
42
     * @param categoryController Category controller
43
     */
44
    public MainWindow(ExpenseController expenseController, CategoryController categoryController) {
1✔
45
        this.expenseController = expenseController;
1✔
46
        this.categoryController = categoryController;
1✔
47
        initializeUI();
1✔
48
    }
1✔
49
    
50

51
    private void initializeUI() {
52
        setTitle("Personal Expense Tracker");
1✔
53
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
1✔
54
        setBounds(100, 100, 1000, 700);
1✔
55

56
        // Create menu bar
57
        JMenuBar menuBar = new JMenuBar();
1✔
58
        JMenu fileMenu = new JMenu("File");
1✔
59
        JMenuItem exitItem = new JMenuItem("Exit");
1✔
60
        exitItem.addActionListener(e -> System.exit(0));
1✔
61
        fileMenu.add(exitItem);
1✔
62
        menuBar.add(fileMenu);
1✔
63

64
        JMenu manageMenu = new JMenu("Manage");
1✔
65
        JMenuItem categoriesItem = new JMenuItem("Categories");
1✔
66
        categoriesItem.addActionListener(e -> showCategoryDialog());
1✔
67
        manageMenu.add(categoriesItem);
1✔
68
        menuBar.add(manageMenu);
1✔
69

70
        setJMenuBar(menuBar);
1✔
71

72
        // Main panel with border layout
73
        JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
1✔
74
        mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
1✔
75

76
        // Top panel for controls
77
        JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
1✔
78
        
79
        JButton addButton = new JButton("Add Expense");
1✔
80
        addButton.addActionListener(e -> showAddExpenseDialog());
1✔
81
        topPanel.add(addButton);
1✔
82

83
        JButton editButton = new JButton("Edit Expense");
1✔
84
        editButton.addActionListener(e -> showEditExpenseDialog());
1✔
85
        topPanel.add(editButton);
1✔
86

87
        JButton deleteButton = new JButton("Delete Expense");
1✔
88
        deleteButton.addActionListener(e -> deleteSelectedExpense());
1✔
89
        topPanel.add(deleteButton);
1✔
90

91
        topPanel.add(new JLabel("Month:"));
1✔
92
        monthComboBox = new JComboBox<>(new String[]{"All", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"});
1✔
93
        monthComboBox.setSelectedItem("All");
1✔
94
        monthComboBox.addActionListener(e -> {
1✔
95
            if (!isInitializing && expenseController != null && expenseTableModel != null) {
1✔
96
                filterExpenses();
1✔
97
            }
98
        });
1✔
99
        topPanel.add(monthComboBox);
1✔
100

101
        topPanel.add(new JLabel("Year:"));
1✔
102
        String[] yearOptions = getYearOptions();
1✔
103
        yearComboBox = new JComboBox<>(yearOptions);
1✔
104
        yearComboBox.setSelectedItem(yearOptions[2]); // Current year
1✔
105
        yearComboBox.addActionListener(e -> {
1✔
106
            if (!isInitializing && expenseController != null && expenseTableModel != null) {
1✔
107
                filterExpenses();
1✔
108
            }
109
        });
1✔
110
        topPanel.add(yearComboBox);
1✔
111

112
        mainPanel.add(topPanel, BorderLayout.NORTH);
1✔
113

114
        // Center panel for expense table
115
        String[] columnNames = {"ID", "Date", "Amount", "Description", "Category"};
1✔
116
        expenseTableModel = new DefaultTableModel(columnNames, 0) {
1✔
117
            private static final long serialVersionUID = 1L;
118
            
119
            @Override
120
            public boolean isCellEditable(int row, int column) {
121
                return false;
1✔
122
            }
123
        };
124
        expenseTable = new JTable(expenseTableModel);
1✔
125
        expenseTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
1✔
126
        JScrollPane scrollPane = new JScrollPane(expenseTable);
1✔
127
        mainPanel.add(scrollPane, BorderLayout.CENTER);
1✔
128

129
        // Bottom panel for summary
130
        JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
1✔
131
        monthlyTotalLabel = new JLabel("Monthly Total: $0.00");
1✔
132
        bottomPanel.add(monthlyTotalLabel);
1✔
133
        bottomPanel.add(new JLabel("  |  "));
1✔
134
        bottomPanel.add(new JLabel("Category:"));
1✔
135
        categoryComboBox = new JComboBox<>();
1✔
136
        categoryComboBox.addItem(null); // "All categories" option
1✔
137
        categoryComboBox.addActionListener(e -> {
1✔
138
            if (!isInitializing && expenseController != null) {
1✔
139
                updateCategoryTotal();
1✔
140
            }
141
        });
1✔
142
        bottomPanel.add(categoryComboBox);
1✔
143
        categoryTotalLabel = new JLabel("Category Total: $0.00");
1✔
144
        bottomPanel.add(categoryTotalLabel);
1✔
145
        mainPanel.add(bottomPanel, BorderLayout.SOUTH);
1✔
146

147
        setContentPane(mainPanel);
1✔
148
        isInitializing = false;
1✔
149
    }
1✔
150

151
    private String[] getYearOptions() {
152
        int currentYear = LocalDate.now().getYear();
1✔
153
        String[] years = new String[5];
1✔
154
        for (int i = 0; i < 5; i++) {
1✔
155
            years[i] = String.valueOf(currentYear - 2 + i);
1✔
156
        }
157
        return years;
1✔
158
    }
159

160
    /**
161
     * Loads all data (categories, expenses, and summary).
162
     * Uses controllers for async operations.
163
     */
164
    public void loadData() {
165
        loadCategories();
1✔
166
        loadExpenses();
1✔
167
        updateSummary();
1✔
168
    }
1✔
169

170
    /**
171
     * Loads categories into the combo box.
172
     * Uses controller for async operation.
173
     */
174
    void loadCategories() {
175
        categoryController.loadCategories(
1✔
176
            categories -> {
177
                // Success: populate combo box
178
                categoryComboBox.removeAllItems();
1✔
179
                categoryComboBox.addItem(null);
1✔
180
                for (Category category : categories) {
1✔
181
                    categoryComboBox.addItem(category);
1✔
182
                }
1✔
183
            },
1✔
184
            error -> {
185
                // Error: show message only if window is visible
186
                if (isVisible() && isShowing()) {
1✔
187
                    JOptionPane.showMessageDialog(this,
1✔
188
                        error,
189
                        ERROR_TITLE,
190
                        JOptionPane.ERROR_MESSAGE);
191
                }
192
            }
1✔
193
        );
194
    }
1✔
195

196
    /**
197
     * Loads expenses into the table.
198
     * Uses controller for async operation.
199
     */
200
    void loadExpenses() {
201
        expenseController.loadExpenses(
1✔
202
            this::populateExpenseTable,
203
            error -> {
204
                if (isVisible() && isShowing()) {
1✔
205
                    JOptionPane.showMessageDialog(this,
1✔
206
                        error,
207
                        ERROR_TITLE,
208
                        JOptionPane.ERROR_MESSAGE);
209
                }
210
            }
1✔
211
        );
212
    }
1✔
213

214
    /**
215
     * Populates expense table with expenses.
216
     * Package-private for testing.
217
     */
218
    void populateExpenseTable(java.util.List<Expense> expenses) {
219
        expenseTableModel.setRowCount(0);
1✔
220
        for (Expense expense : expenses) {
1✔
221
            Category category = null;
1✔
222
            try {
223
                category = categoryController.getCategory(expense.getCategoryId());
1✔
224
            } catch (SQLException e) {
×
225
                // Ignore - will show "Unknown"
226
            }
1✔
227
            String categoryName = category != null ? category.getName() : "Unknown";
1✔
228
            expenseTableModel.addRow(new Object[]{
1✔
229
                expense.getExpenseId(),
1✔
230
                expense.getDate().toString(),
1✔
231
                expense.getAmount().toString(),
1✔
232
                expense.getDescription(),
1✔
233
                categoryName
234
            });
235
        }
1✔
236
    }
1✔
237

238
    /**
239
     * Filters expenses by month and year.
240
     * Uses controller for async operation.
241
     */
242
    public void filterExpenses() {
243
        String selectedMonth = (String) monthComboBox.getSelectedItem();
1✔
244
        String selectedYear = (String) yearComboBox.getSelectedItem();
1✔
245

246
        if (selectedMonth == null || selectedYear == null) {
1✔
247
            return;
1✔
248
        }
249

250
        if ("All".equals(selectedMonth)) {
1✔
251
            loadExpenses();
1✔
252
        } else {
253
            try {
254
                int year = Integer.parseInt(selectedYear);
1✔
255
                int month = Integer.parseInt(selectedMonth);
1✔
256
                expenseController.loadExpensesByMonth(year, month,
1✔
257
                    expenses -> {
258
                        populateExpenseTable(expenses);
1✔
259
                        updateSummary();
1✔
260
                    },
1✔
261
                    error -> {
262
                        if (isVisible() && isShowing()) {
1✔
263
                            JOptionPane.showMessageDialog(this,
1✔
264
                                error,
265
                                ERROR_TITLE,
266
                                JOptionPane.ERROR_MESSAGE);
267
                        }
268
                    }
1✔
269
                );
270
            } catch (NumberFormatException e) {
1✔
271
                // Invalid month/year - ignore
272
            }
1✔
273
        }
274
    }
1✔
275

276
    /**
277
     * Updates the monthly total summary.
278
     * Uses controller for async operation.
279
     */
280
    public void updateSummary() {
281
        String selectedMonth = (String) monthComboBox.getSelectedItem();
1✔
282
        String selectedYear = (String) yearComboBox.getSelectedItem();
1✔
283

284
        if (selectedMonth == null || selectedYear == null) {
1✔
285
            monthlyTotalLabel.setText("Monthly Total: N/A");
1✔
286
            return;
1✔
287
        }
288

289
        if ("All".equals(selectedMonth)) {
1✔
290
            monthlyTotalLabel.setText("Monthly Total: N/A");
1✔
291
        } else {
292
            try {
293
                int year = Integer.parseInt(selectedYear);
1✔
294
                int month = Integer.parseInt(selectedMonth);
1✔
295
                expenseController.getMonthlyTotal(year, month,
1✔
296
                    total -> {
297
                        monthlyTotalLabel.setText("Monthly Total: $" + total.toString());
1✔
298
                        updateCategoryTotal();
1✔
299
                    },
1✔
300
                    error -> monthlyTotalLabel.setText("Monthly Total: Error")
×
301
                );
302
            } catch (NumberFormatException e) {
1✔
303
                monthlyTotalLabel.setText("Monthly Total: Error");
1✔
304
            }
1✔
305
        }
306
    }
1✔
307

308
    /**
309
     * Updates the category total summary.
310
     * Uses controller for async operation.
311
     */
312
    public void updateCategoryTotal() {
313
        Category selectedCategory = (Category) categoryComboBox.getSelectedItem();
1✔
314
        if (selectedCategory == null) {
1✔
315
            categoryTotalLabel.setText("Category Total: N/A");
1✔
316
        } else {
317
            expenseController.getTotalByCategory(selectedCategory.getCategoryId(),
1✔
318
                total -> categoryTotalLabel.setText("Category Total: $" + total.toString()),
1✔
319
                error -> categoryTotalLabel.setText("Category Total: Error")
1✔
320
            );
321
        }
322
    }
1✔
323

324
    /**
325
     * Shows the add expense dialog.
326
     */
327
    public void showAddExpenseDialog() {
328
        ExpenseDialog dialog = new ExpenseDialog(this, expenseController, categoryController, null);
1✔
329
        dialog.setVisible(true);
1✔
330
        if (dialog.isSaved()) {
1✔
331
            loadData();
×
332
        }
333
    }
1✔
334

335
    /**
336
     * Shows the edit expense dialog.
337
     */
338
    public void showEditExpenseDialog() {
339
        int selectedRow = expenseTable.getSelectedRow();
1✔
340
        if (selectedRow < 0) {
1✔
341
            JOptionPane.showMessageDialog(this,
1✔
342
                "Please select an expense to edit.",
343
                "No Selection",
344
                JOptionPane.WARNING_MESSAGE);
345
            return;
1✔
346
        }
347

348
        Integer expenseId = (Integer) expenseTableModel.getValueAt(selectedRow, 0);
1✔
349
        try {
350
            Expense expense = expenseController.getExpense(expenseId);
1✔
351
            ExpenseDialog dialog = new ExpenseDialog(this, expenseController, categoryController, expense);
1✔
352
            dialog.setVisible(true);
1✔
353
            if (dialog.isSaved()) {
1✔
354
                loadData();
×
355
            }
356
        } catch (SQLException e) {
1✔
357
            JOptionPane.showMessageDialog(this,
1✔
358
                "Error loading expense: " + e.getMessage(),
1✔
359
                ERROR_TITLE,
360
                JOptionPane.ERROR_MESSAGE);
361
        }
1✔
362
    }
1✔
363

364
    /**
365
     * Deletes the selected expense.
366
     * Uses controller for async operation.
367
     */
368
    public void deleteSelectedExpense() {
369
        int selectedRow = expenseTable.getSelectedRow();
1✔
370
        if (selectedRow < 0) {
1✔
371
            JOptionPane.showMessageDialog(this,
1✔
372
                "Please select an expense to delete.",
373
                "No Selection",
374
                JOptionPane.WARNING_MESSAGE);
375
            return;
1✔
376
        }
377

378
        // In test mode, bypass confirmation dialog
379
        boolean isTestMode = "true".equals(System.getProperty("test.mode"));
1✔
380
        int confirm = isTestMode ? JOptionPane.YES_OPTION : JOptionPane.showConfirmDialog(this,
1✔
381
            "Are you sure you want to delete this expense?",
382
            "Confirm Delete",
383
            JOptionPane.YES_NO_OPTION);
384

385
        if (confirm == JOptionPane.YES_OPTION) {
1✔
386
            Integer expenseId = (Integer) expenseTableModel.getValueAt(selectedRow, 0);
1✔
387
            expenseController.deleteExpense(expenseId,
1✔
388
                this::loadData,
389
                error -> JOptionPane.showMessageDialog(this,
×
390
                    error,
391
                    ERROR_TITLE,
392
                    JOptionPane.ERROR_MESSAGE)
393
            );
394
        }
395
    }
1✔
396

397
    /**
398
     * Shows the category management dialog.
399
     */
400
    public void showCategoryDialog() {
401
        CategoryDialog dialog = new CategoryDialog(this, categoryController);
1✔
402
        dialog.setVisible(true);
1✔
403
        loadData();
1✔
404
    }
1✔
405

406
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc