Skip to content

Commit c3cd74b

Browse files
Experiment
1 parent ac40741 commit c3cd74b

7 files changed

Lines changed: 240 additions & 16 deletions

File tree

src/main/java/com/malcolm/expensesplitter/controllers/AddExpenseController.java

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
import javafx.scene.control.*;
1212
import javafx.application.Platform;
1313
import javafx.stage.Stage;
14+
import javafx.stage.FileChooser;
15+
import javafx.scene.image.Image;
16+
import javafx.scene.image.ImageView;
17+
import java.io.File;
18+
import com.malcolm.expensesplitter.services.ReceiptService;
1419
import org.springframework.beans.factory.annotation.Autowired;
1520
import org.springframework.context.annotation.Scope;
1621
import org.springframework.stereotype.Controller;
@@ -50,6 +55,9 @@ public class AddExpenseController {
5055
@Autowired
5156
private ExchangeRateService exchangeRateService;
5257

58+
@Autowired
59+
private ReceiptService receiptService;
60+
5361
@FXML
5462
private TextField descriptionField;
5563

@@ -94,6 +102,16 @@ public class AddExpenseController {
94102
@FXML
95103
private Button saveAndNewButton;
96104

105+
@FXML
106+
private Label receiptPathLabel;
107+
108+
@FXML
109+
private ImageView receiptPreview;
110+
111+
@FXML
112+
private Button removeReceiptButton;
113+
114+
private String currentReceiptPath;
97115
private Group currentGroup;
98116
private Stage dialogStage;
99117
private boolean saveClicked = false;
@@ -162,6 +180,7 @@ protected void updateItem(User item, boolean empty) {
162180
categoryComboBox.getSelectionModel().selectFirst();
163181

164182
setupDatePicker();
183+
currentReceiptPath = null;
165184
}
166185

167186
private void setupDatePicker() {
@@ -261,6 +280,15 @@ public void setExpenseToEdit(Expense expense) {
261280
} else {
262281
currencyComboBox.getSelectionModel().selectFirst();
263282
}
283+
284+
if (expense.getReceiptPath() != null && !expense.getReceiptPath().isEmpty()) {
285+
this.currentReceiptPath = expense.getReceiptPath();
286+
updateReceiptUI();
287+
} else {
288+
this.currentReceiptPath = null;
289+
updateReceiptUI();
290+
}
291+
264292
if (saveAndNewButton != null) {
265293
saveAndNewButton.setVisible(false);
266294
saveAndNewButton.setManaged(false);
@@ -311,6 +339,9 @@ public void setExpenseToEdit(Expense expense) {
311339
currencyComboBox.getSelectionModel().selectFirst();
312340
expenseDatePicker.setValue(LocalDate.now());
313341

342+
currentReceiptPath = null;
343+
updateReceiptUI();
344+
314345
if (saveAndNewButton != null) {
315346
saveAndNewButton.setVisible(true);
316347
saveAndNewButton.setManaged(true);
@@ -499,10 +530,10 @@ private boolean saveInternal() {
499530

500531
if (expenseToEdit != null) {
501532
expenseService.updateExpense(expenseToEdit.getId(), currentGroup.getId(), paymentInputs, amount,
502-
description, paymentMode, category, expenseDate, splitType, splitInputs, currency);
533+
description, paymentMode, category, expenseDate, splitType, splitInputs, currency, currentReceiptPath);
503534
} else {
504535
expenseService.addExpense(currentGroup.getId(), paymentInputs, amount, description, paymentMode,
505-
category, expenseDate, splitType, splitInputs, currency);
536+
category, expenseDate, splitType, splitInputs, currency, currentReceiptPath);
506537
}
507538

508539
saveClicked = true;
@@ -644,6 +675,64 @@ private void handlePercentageAdjustment(TextField changedField, int totalMembers
644675
}
645676
}
646677

678+
@FXML
679+
private void handleChooseReceipt() {
680+
FileChooser fileChooser = new FileChooser();
681+
fileChooser.setTitle("Choose Bill/Receipt Image");
682+
fileChooser.getExtensionFilters().addAll(
683+
new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg", "*.jpeg", "*.gif")
684+
);
685+
686+
File selectedFile = fileChooser.showOpenDialog(dialogStage);
687+
if (selectedFile != null) {
688+
String savedPath = receiptService.saveReceipt(selectedFile);
689+
if (savedPath != null) {
690+
this.currentReceiptPath = savedPath;
691+
updateReceiptUI();
692+
} else {
693+
Alert alert = new Alert(Alert.AlertType.ERROR);
694+
alert.setTitle("Error");
695+
alert.setContentText("Could not save the receipt file.");
696+
alert.showAndWait();
697+
}
698+
}
699+
}
700+
701+
@FXML
702+
private void handleRemoveReceipt() {
703+
this.currentReceiptPath = null;
704+
updateReceiptUI();
705+
}
706+
707+
private void updateReceiptUI() {
708+
if (currentReceiptPath != null && !currentReceiptPath.isEmpty()) {
709+
File file = new File(currentReceiptPath);
710+
if (file.exists()) {
711+
receiptPathLabel.setText("Bill attached");
712+
receiptPathLabel.setStyle("-fx-text-fill: green;");
713+
receiptPreview.setImage(new Image(file.toURI().toString()));
714+
receiptPreview.setVisible(true);
715+
receiptPreview.setManaged(true);
716+
removeReceiptButton.setVisible(true);
717+
removeReceiptButton.setManaged(true);
718+
} else {
719+
resetReceiptUI();
720+
}
721+
} else {
722+
resetReceiptUI();
723+
}
724+
}
725+
726+
private void resetReceiptUI() {
727+
receiptPathLabel.setText("No bill attached");
728+
receiptPathLabel.setStyle("-fx-text-fill: gray;");
729+
receiptPreview.setImage(null);
730+
receiptPreview.setVisible(false);
731+
receiptPreview.setManaged(false);
732+
removeReceiptButton.setVisible(false);
733+
removeReceiptButton.setManaged(false);
734+
}
735+
647736
private void handleOtherCurrency(String previousValue) {
648737
try {
649738
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/currency_selection_modal.fxml"));

src/main/java/com/malcolm/expensesplitter/controllers/ExpenseDetailsController.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
import javafx.scene.layout.HBox;
1313
import javafx.scene.layout.VBox;
1414
import javafx.stage.Stage;
15+
import javafx.scene.image.Image;
16+
import javafx.scene.image.ImageView;
17+
import java.io.File;
18+
import java.io.IOException;
19+
import java.awt.Desktop;
1520
import org.springframework.beans.factory.annotation.Autowired;
1621
import org.springframework.stereotype.Controller;
1722
import java.time.format.DateTimeFormatter;
@@ -35,6 +40,12 @@ public class ExpenseDetailsController {
3540
@FXML
3641
private VBox splitsContainer;
3742

43+
@FXML
44+
private VBox receiptContainer;
45+
46+
@FXML
47+
private ImageView receiptImageView;
48+
3849
private Expense currentExpense;
3950
private Stage dialogStage;
4051
private boolean changed = false;
@@ -67,6 +78,23 @@ public void setExpense(Expense expense) {
6778
}
6879

6980
loadSplits();
81+
loadReceipt();
82+
}
83+
84+
private void loadReceipt() {
85+
String path = currentExpense.getReceiptPath();
86+
if (path != null && !path.isEmpty()) {
87+
File file = new File(path);
88+
if (file.exists()) {
89+
Image image = new Image(file.toURI().toString());
90+
receiptImageView.setImage(image);
91+
receiptContainer.setVisible(true);
92+
receiptContainer.setManaged(true);
93+
return;
94+
}
95+
}
96+
receiptContainer.setVisible(false);
97+
receiptContainer.setManaged(false);
7098
}
7199

72100
private void loadSplits() {
@@ -135,4 +163,21 @@ public boolean isChanged() {
135163
private void handleClose() {
136164
dialogStage.close();
137165
}
166+
167+
@FXML
168+
private void handleOpenReceiptExternally() {
169+
String path = currentExpense.getReceiptPath();
170+
if (path != null && !path.isEmpty()) {
171+
File file = new File(path);
172+
if (file.exists()) {
173+
if (Desktop.isDesktopSupported()) {
174+
try {
175+
Desktop.getDesktop().open(file);
176+
} catch (IOException e) {
177+
e.printStackTrace();
178+
}
179+
}
180+
}
181+
}
182+
}
138183
}

src/main/java/com/malcolm/expensesplitter/controllers/rest/ExpenseRestController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public ResponseEntity<?> createExpense(
109109
category,
110110
expenseDate != null ? expenseDate : LocalDate.now(),
111111
splitType, splitInputs,
112-
currency != null ? currency : "INR");
112+
currency != null ? currency : "INR", null);
113113

114114
return ResponseEntity.ok(expense);
115115
}

src/main/java/com/malcolm/expensesplitter/services/ExpenseService.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,14 @@ public Expense updateEqualExpense(UUID expenseId, UUID groupId, UUID paidById, B
115115

116116
public Expense addExpense(UUID groupId, Map<UUID, BigDecimal> paymentInputs, BigDecimal amount, String description,
117117
String paymentMode, String category, LocalDate expenseDate, SplitType splitType,
118-
Map<UUID, BigDecimal> splitInputs, String currency) {
118+
Map<UUID, BigDecimal> splitInputs, String currency, String receiptPath) {
119119
Group group = groupRepository.findById(groupId).orElseThrow();
120120

121121
Expense expense = new Expense(group, amount, description, splitType);
122122
expense.setPaymentMode(paymentMode);
123123
expense.setCategory(category);
124124
expense.setCurrency(currency);
125+
expense.setReceiptPath(receiptPath);
125126
if (expenseDate != null)
126127
expense.setExpenseDate(expenseDate);
127128

@@ -150,15 +151,15 @@ public Expense addExpense(UUID groupId, Map<UUID, BigDecimal> paymentInputs, Big
150151
// For legacy/simple calls with single payer
151152
public Expense addExpense(UUID groupId, UUID paidById, BigDecimal amount, String description,
152153
String paymentMode, String category, LocalDate expenseDate, SplitType splitType,
153-
Map<UUID, BigDecimal> splitInputs, String currency) {
154+
Map<UUID, BigDecimal> splitInputs, String currency, String receiptPath) {
154155
Map<UUID, BigDecimal> paymentInputs = java.util.Collections.singletonMap(paidById, amount);
155156
return addExpense(groupId, paymentInputs, amount, description, paymentMode, category, expenseDate, splitType,
156-
splitInputs, currency);
157+
splitInputs, currency, receiptPath);
157158
}
158159

159160
public Expense updateExpense(UUID expenseId, UUID groupId, Map<UUID, BigDecimal> paymentInputs, BigDecimal amount,
160161
String description, String paymentMode, String category, LocalDate expenseDate, SplitType splitType,
161-
Map<UUID, BigDecimal> splitInputs, String currency) {
162+
Map<UUID, BigDecimal> splitInputs, String currency, String receiptPath) {
162163
Expense expense = expenseRepository.findById(expenseId).orElseThrow();
163164
Group group = groupRepository.findById(groupId).orElseThrow();
164165

@@ -168,6 +169,7 @@ public Expense updateExpense(UUID expenseId, UUID groupId, Map<UUID, BigDecimal>
168169
expense.setCategory(category);
169170
expense.setSplitType(splitType);
170171
expense.setCurrency(currency);
172+
expense.setReceiptPath(receiptPath);
171173
if (expenseDate != null)
172174
expense.setExpenseDate(expenseDate);
173175

@@ -195,10 +197,10 @@ public Expense updateExpense(UUID expenseId, UUID groupId, Map<UUID, BigDecimal>
195197
// For legacy/simple calls with single payer
196198
public Expense updateExpense(UUID expenseId, UUID groupId, UUID paidById, BigDecimal amount,
197199
String description, String paymentMode, String category, LocalDate expenseDate, SplitType splitType,
198-
Map<UUID, BigDecimal> splitInputs, String currency) {
200+
Map<UUID, BigDecimal> splitInputs, String currency, String receiptPath) {
199201
Map<UUID, BigDecimal> paymentInputs = java.util.Collections.singletonMap(paidById, amount);
200202
return updateExpense(expenseId, groupId, paymentInputs, amount, description, paymentMode, category, expenseDate,
201-
splitType, splitInputs, currency);
203+
splitType, splitInputs, currency, receiptPath);
202204
}
203205

204206
private void calculateAndAddSplits(Expense expense, SplitType splitType, Map<UUID, BigDecimal> splitInputs,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.malcolm.expensesplitter.services;
2+
3+
import org.springframework.stereotype.Service;
4+
import java.io.File;
5+
import java.io.IOException;
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
8+
import java.nio.file.Paths;
9+
import java.nio.file.StandardCopyOption;
10+
import java.util.UUID;
11+
12+
@Service
13+
public class ReceiptService {
14+
15+
private static final String RECEIPT_DIR = "data/receipts/";
16+
17+
/**
18+
* Copies a selected file to the local data/receipts directory and returns the relative path.
19+
*
20+
* @param sourceFile The file selected by the user.
21+
* @return The path to the stored file, or null if storage failed.
22+
*/
23+
public String saveReceipt(File sourceFile) {
24+
if (sourceFile == null) return null;
25+
26+
try {
27+
// Ensure the receipts directory exists
28+
Path directory = Paths.get(RECEIPT_DIR);
29+
if (!Files.exists(directory)) {
30+
Files.createDirectories(directory);
31+
}
32+
33+
// Generate a unique filename to prevent overwrites
34+
String extension = "";
35+
int i = sourceFile.getName().lastIndexOf('.');
36+
if (i > 0) {
37+
extension = sourceFile.getName().substring(i);
38+
}
39+
40+
String fileName = UUID.randomUUID().toString() + extension;
41+
Path targetPath = directory.resolve(fileName);
42+
43+
// Copy the file
44+
Files.copy(sourceFile.toPath(), targetPath, StandardCopyOption.REPLACE_EXISTING);
45+
46+
return targetPath.toString();
47+
} catch (IOException e) {
48+
e.printStackTrace();
49+
return null;
50+
}
51+
}
52+
53+
/**
54+
* Checks if a receipt file exists for a given path.
55+
*/
56+
public boolean receiptExists(String path) {
57+
if (path == null || path.isEmpty()) return false;
58+
return Files.exists(Paths.get(path));
59+
}
60+
}

src/main/resources/fxml/add_expense_modal.fxml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
<?import javafx.geometry.Insets?>
44
<?import javafx.scene.control.*?>
5+
<?import javafx.scene.image.ImageView?>
6+
<?import javafx.scene.image.Image?>
57
<?import javafx.scene.layout.*?>
68

79
<BorderPane xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"
810
fx:controller="com.malcolm.expensesplitter.controllers.AddExpenseController"
9-
prefWidth="500.0" prefHeight="600.0" styleClass="background">
11+
prefWidth="500.0" prefHeight="650.0" styleClass="background">
1012
<padding>
1113
<Insets top="20" right="20" bottom="20" left="20"/>
1214
</padding>
@@ -49,6 +51,16 @@
4951

5052
<Label text="Date:" GridPane.columnIndex="0" GridPane.rowIndex="6"/>
5153
<DatePicker fx:id="expenseDatePicker" GridPane.columnIndex="1" GridPane.rowIndex="6" maxWidth="Infinity"/>
54+
55+
<Label text="Receipt (Bill):" GridPane.columnIndex="0" GridPane.rowIndex="7"/>
56+
<VBox spacing="5" GridPane.columnIndex="1" GridPane.rowIndex="7">
57+
<HBox spacing="10" alignment="CENTER_LEFT">
58+
<Button text="Attach Bill..." onAction="#handleChooseReceipt"/>
59+
<Button fx:id="removeReceiptButton" text="Remove" onAction="#handleRemoveReceipt" styleClass="flat" visible="false" managed="false"/>
60+
<Label fx:id="receiptPathLabel" text="No bill attached" style="-fx-text-fill: gray;"/>
61+
</HBox>
62+
<ImageView fx:id="receiptPreview" fitWidth="120" fitHeight="120" preserveRatio="true" visible="false" managed="false"/>
63+
</VBox>
5264
</GridPane>
5365

5466
<Separator/>

0 commit comments

Comments
 (0)