Add Transaction Properties #15
|
@ -89,6 +89,7 @@ public class AccountViewController implements RouteSelectionListener {
|
||||||
@FXML
|
@FXML
|
||||||
public void archiveAccount() {
|
public void archiveAccount() {
|
||||||
boolean confirmResult = Popups.confirm(
|
boolean confirmResult = Popups.confirm(
|
||||||
|
titleLabel,
|
||||||
"Are you sure you want to archive this account? It will no " +
|
"Are you sure you want to archive this account? It will no " +
|
||||||
"longer show up in the app normally, and you won't be " +
|
"longer show up in the app normally, and you won't be " +
|
||||||
"able to add new transactions to it. You'll still be " +
|
"able to add new transactions to it. You'll still be " +
|
||||||
|
@ -103,6 +104,7 @@ public class AccountViewController implements RouteSelectionListener {
|
||||||
|
|
||||||
@FXML public void unarchiveAccount() {
|
@FXML public void unarchiveAccount() {
|
||||||
boolean confirm = Popups.confirm(
|
boolean confirm = Popups.confirm(
|
||||||
|
titleLabel,
|
||||||
"Are you sure you want to restore this account from its archived " +
|
"Are you sure you want to restore this account from its archived " +
|
||||||
"status?"
|
"status?"
|
||||||
);
|
);
|
||||||
|
@ -115,6 +117,7 @@ public class AccountViewController implements RouteSelectionListener {
|
||||||
@FXML
|
@FXML
|
||||||
public void deleteAccount() {
|
public void deleteAccount() {
|
||||||
boolean confirm = Popups.confirm(
|
boolean confirm = Popups.confirm(
|
||||||
|
titleLabel,
|
||||||
"Are you sure you want to permanently delete this account and " +
|
"Are you sure you want to permanently delete this account and " +
|
||||||
"all data directly associated with it? This cannot be " +
|
"all data directly associated with it? This cannot be " +
|
||||||
"undone; deleted accounts are not recoverable at all. " +
|
"undone; deleted accounts are not recoverable at all. " +
|
||||||
|
|
|
@ -48,7 +48,10 @@ public class BalanceRecordViewController implements RouteSelectionListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML public void delete() {
|
@FXML public void delete() {
|
||||||
boolean confirm = Popups.confirm("Are you sure you want to delete this balance record? This may have an effect on the derived balance of your account, as shown in Perfin.");
|
boolean confirm = Popups.confirm(
|
||||||
|
titleLabel,
|
||||||
|
"Are you sure you want to delete this balance record? This may have an effect on the derived balance of your account, as shown in Perfin."
|
||||||
|
);
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
Profile.getCurrent().dataSource().useRepo(BalanceRecordRepository.class, repo -> repo.deleteById(balanceRecord.id));
|
Profile.getCurrent().dataSource().useRepo(BalanceRecordRepository.class, repo -> repo.deleteById(balanceRecord.id));
|
||||||
router.navigateBackAndClear();
|
router.navigateBackAndClear();
|
||||||
|
|
|
@ -89,7 +89,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
|
||||||
LocalDateTime localTimestamp = LocalDateTime.parse(timestampField.getText(), DateUtil.DEFAULT_DATETIME_FORMAT);
|
LocalDateTime localTimestamp = LocalDateTime.parse(timestampField.getText(), DateUtil.DEFAULT_DATETIME_FORMAT);
|
||||||
BigDecimal reportedBalance = new BigDecimal(balanceField.getText());
|
BigDecimal reportedBalance = new BigDecimal(balanceField.getText());
|
||||||
|
|
||||||
boolean confirm = Popups.confirm("Are you sure that you want to record the balance of account\n%s\nas %s,\nas of %s?".formatted(
|
boolean confirm = Popups.confirm(timestampField, "Are you sure that you want to record the balance of account\n%s\nas %s,\nas of %s?".formatted(
|
||||||
account.getShortName(),
|
account.getShortName(),
|
||||||
CurrencyUtil.formatMoneyWithCurrencyPrefix(new MoneyValue(reportedBalance, account.getCurrency())),
|
CurrencyUtil.formatMoneyWithCurrencyPrefix(new MoneyValue(reportedBalance, account.getCurrency())),
|
||||||
localTimestamp.atZone(ZoneId.systemDefault()).format(DateUtil.DEFAULT_DATETIME_FORMAT_WITH_ZONE)
|
localTimestamp.atZone(ZoneId.systemDefault()).format(DateUtil.DEFAULT_DATETIME_FORMAT_WITH_ZONE)
|
||||||
|
@ -122,7 +122,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
|
||||||
CurrencyUtil.formatMoney(new MoneyValue(reportedBalance, account.getCurrency())),
|
CurrencyUtil.formatMoney(new MoneyValue(reportedBalance, account.getCurrency())),
|
||||||
CurrencyUtil.formatMoney(new MoneyValue(currentDerivedBalance, account.getCurrency()))
|
CurrencyUtil.formatMoney(new MoneyValue(currentDerivedBalance, account.getCurrency()))
|
||||||
);
|
);
|
||||||
return Popups.confirm(msg);
|
return Popups.confirm(timestampField, msg);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,7 +117,7 @@ public class EditAccountController implements RouteSelectionListener {
|
||||||
BigDecimal initialBalance = new BigDecimal(initialBalanceField.getText().strip());
|
BigDecimal initialBalance = new BigDecimal(initialBalanceField.getText().strip());
|
||||||
List<Path> attachments = Collections.emptyList();
|
List<Path> attachments = Collections.emptyList();
|
||||||
|
|
||||||
boolean success = Popups.confirm("Are you sure you want to create this account?");
|
boolean success = Popups.confirm(accountNameField, "Are you sure you want to create this account?");
|
||||||
if (success) {
|
if (success) {
|
||||||
long id = accountRepo.insert(type, number, name, currency);
|
long id = accountRepo.insert(type, number, name, currency);
|
||||||
balanceRepo.insert(LocalDateTime.now(ZoneOffset.UTC), id, initialBalance, currency, attachments);
|
balanceRepo.insert(LocalDateTime.now(ZoneOffset.UTC), id, initialBalance, currency, attachments);
|
||||||
|
@ -138,7 +138,7 @@ public class EditAccountController implements RouteSelectionListener {
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to save (or update) account " + account.id, e);
|
log.error("Failed to save (or update) account " + account.id, e);
|
||||||
Popups.error("Failed to save the account: " + e.getMessage());
|
Popups.error(accountNameField, "Failed to save the account: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -203,7 +203,7 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to get repositories.", e);
|
log.error("Failed to get repositories.", e);
|
||||||
Popups.error("Failed to fetch account-specific data: " + e.getMessage());
|
Popups.error(container, "Failed to fetch account-specific data: " + e.getMessage());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,65 @@
|
||||||
package com.andrewlalis.perfin.control;
|
package com.andrewlalis.perfin.control;
|
||||||
|
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.ButtonType;
|
import javafx.scene.control.ButtonType;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Window;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper class for standardized popups and confirmation dialogs for the app.
|
* Helper class for standardized popups and confirmation dialogs for the app.
|
||||||
*/
|
*/
|
||||||
public class Popups {
|
public class Popups {
|
||||||
public static boolean confirm(String text) {
|
public static boolean confirm(Window owner, String text) {
|
||||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, text);
|
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, text);
|
||||||
|
alert.initOwner(owner);
|
||||||
alert.initModality(Modality.APPLICATION_MODAL);
|
alert.initModality(Modality.APPLICATION_MODAL);
|
||||||
var result = alert.showAndWait();
|
var result = alert.showAndWait();
|
||||||
return result.isPresent() && result.get() == ButtonType.OK;
|
return result.isPresent() && result.get() == ButtonType.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void message(String text) {
|
public static boolean confirm(Node node, String text) {
|
||||||
|
return confirm(getWindowFromNode(node), text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void message(Window owner, String text) {
|
||||||
Alert alert = new Alert(Alert.AlertType.NONE, text);
|
Alert alert = new Alert(Alert.AlertType.NONE, text);
|
||||||
|
alert.initOwner(owner);
|
||||||
alert.initModality(Modality.APPLICATION_MODAL);
|
alert.initModality(Modality.APPLICATION_MODAL);
|
||||||
alert.getButtonTypes().setAll(ButtonType.OK);
|
alert.getButtonTypes().setAll(ButtonType.OK);
|
||||||
alert.showAndWait();
|
alert.showAndWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void error(String text) {
|
public static void message(Node node, String text) {
|
||||||
|
message(getWindowFromNode(node), text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void error(Window owner, String text) {
|
||||||
Alert alert = new Alert(Alert.AlertType.WARNING, text);
|
Alert alert = new Alert(Alert.AlertType.WARNING, text);
|
||||||
|
alert.initOwner(owner);
|
||||||
alert.initModality(Modality.APPLICATION_MODAL);
|
alert.initModality(Modality.APPLICATION_MODAL);
|
||||||
alert.showAndWait();
|
alert.showAndWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void error(Node node, String text) {
|
||||||
|
error(getWindowFromNode(node), text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void error(Window owner, Exception e) {
|
||||||
|
error(owner, "An " + e.getClass().getSimpleName() + " occurred: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void error(Node node, Exception e) {
|
||||||
|
error(getWindowFromNode(node), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Window getWindowFromNode(Node n) {
|
||||||
|
Window owner = null;
|
||||||
|
Scene scene = n.getScene();
|
||||||
|
if (scene != null) {
|
||||||
|
owner = scene.getWindow();
|
||||||
|
}
|
||||||
|
return owner;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,10 +46,10 @@ public class ProfilesViewController {
|
||||||
String name = newProfileNameField.getText();
|
String name = newProfileNameField.getText();
|
||||||
boolean valid = Profile.validateName(name);
|
boolean valid = Profile.validateName(name);
|
||||||
if (valid && !ProfileLoader.getAvailableProfiles().contains(name)) {
|
if (valid && !ProfileLoader.getAvailableProfiles().contains(name)) {
|
||||||
boolean confirm = Popups.confirm("Are you sure you want to add a new profile named \"" + name + "\"?");
|
boolean confirm = Popups.confirm(profilesVBox, "Are you sure you want to add a new profile named \"" + name + "\"?");
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
if (openProfile(name, false)) {
|
if (openProfile(name, false)) {
|
||||||
Popups.message("Created new profile \"" + name + "\" and loaded it.");
|
Popups.message(profilesVBox, "Created new profile \"" + name + "\" and loaded it.");
|
||||||
}
|
}
|
||||||
newProfileNameField.clear();
|
newProfileNameField.clear();
|
||||||
}
|
}
|
||||||
|
@ -108,18 +108,18 @@ public class ProfilesViewController {
|
||||||
PerfinApp.profileLoader.load(name);
|
PerfinApp.profileLoader.load(name);
|
||||||
ProfilesStage.closeView();
|
ProfilesStage.closeView();
|
||||||
router.replace("accounts");
|
router.replace("accounts");
|
||||||
if (showPopup) Popups.message("The profile \"" + name + "\" has been loaded.");
|
if (showPopup) Popups.message(profilesVBox, "The profile \"" + name + "\" has been loaded.");
|
||||||
return true;
|
return true;
|
||||||
} catch (ProfileLoadException e) {
|
} catch (ProfileLoadException e) {
|
||||||
Popups.error("Failed to load the profile: " + e.getMessage());
|
Popups.error(profilesVBox, "Failed to load the profile: " + e.getMessage());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteProfile(String name) {
|
private void deleteProfile(String name) {
|
||||||
boolean confirmA = Popups.confirm("Are you sure you want to delete the profile \"" + name + "\"? This will permanently delete ALL accounts, transactions, files, and other data for this profile, and it cannot be recovered.");
|
boolean confirmA = Popups.confirm(profilesVBox, "Are you sure you want to delete the profile \"" + name + "\"? This will permanently delete ALL accounts, transactions, files, and other data for this profile, and it cannot be recovered.");
|
||||||
if (confirmA) {
|
if (confirmA) {
|
||||||
boolean confirmB = Popups.confirm("Press \"OK\" to confirm that you really want to delete the profile \"" + name + "\". There's no going back.");
|
boolean confirmB = Popups.confirm(profilesVBox, "Press \"OK\" to confirm that you really want to delete the profile \"" + name + "\". There's no going back.");
|
||||||
if (confirmB) {
|
if (confirmB) {
|
||||||
try {
|
try {
|
||||||
FileUtil.deleteDirRecursive(Profile.getDir(name));
|
FileUtil.deleteDirRecursive(Profile.getDir(name));
|
||||||
|
|
|
@ -72,6 +72,7 @@ public class TransactionViewController {
|
||||||
|
|
||||||
@FXML public void deleteTransaction() {
|
@FXML public void deleteTransaction() {
|
||||||
boolean confirm = Popups.confirm(
|
boolean confirm = Popups.confirm(
|
||||||
|
titleLabel,
|
||||||
"Are you sure you want to delete this transaction? This will " +
|
"Are you sure you want to delete this transaction? This will " +
|
||||||
"permanently remove the transaction and its effects on any linked " +
|
"permanently remove the transaction and its effects on any linked " +
|
||||||
"accounts, as well as remove any attachments from storage within " +
|
"accounts, as well as remove any attachments from storage within " +
|
||||||
|
|
|
@ -177,7 +177,7 @@ public class TransactionsViewController implements RouteSelectionListener {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Popups.error("An error occurred: " + e.getMessage());
|
Popups.error(transactionsListBorderPane, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,10 @@ import java.util.function.Consumer;
|
||||||
* class maintains a static <em>current</em> profile that can be loaded and
|
* class maintains a static <em>current</em> profile that can be loaded and
|
||||||
* unloaded.
|
* unloaded.
|
||||||
* </p>
|
* </p>
|
||||||
|
*
|
||||||
|
* @param name The name of the profile.
|
||||||
|
* @param settings The profile's settings.
|
||||||
|
* @param dataSource The profile's data source.
|
||||||
*/
|
*/
|
||||||
public record Profile(String name, Properties settings, DataSource dataSource) {
|
public record Profile(String name, Properties settings, DataSource dataSource) {
|
||||||
private static final Logger log = LoggerFactory.getLogger(Profile.class);
|
private static final Logger log = LoggerFactory.getLogger(Profile.class);
|
||||||
|
@ -65,6 +69,7 @@ public record Profile(String name, Properties settings, DataSource dataSource) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentProfileListeners.removeIf(ref -> ref.get() == null);
|
currentProfileListeners.removeIf(ref -> ref.get() == null);
|
||||||
|
log.debug("Current profile set to {}.", current.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void whenLoaded(Consumer<Profile> consumer) {
|
public static void whenLoaded(Consumer<Profile> consumer) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.andrewlalis.perfin.model;
|
package com.andrewlalis.perfin.model;
|
||||||
|
|
||||||
import com.andrewlalis.perfin.PerfinApp;
|
import com.andrewlalis.perfin.PerfinApp;
|
||||||
|
import com.andrewlalis.perfin.control.Popups;
|
||||||
import com.andrewlalis.perfin.data.DataSourceFactory;
|
import com.andrewlalis.perfin.data.DataSourceFactory;
|
||||||
import com.andrewlalis.perfin.data.ProfileLoadException;
|
import com.andrewlalis.perfin.data.ProfileLoadException;
|
||||||
import com.andrewlalis.perfin.data.util.FileUtil;
|
import com.andrewlalis.perfin.data.util.FileUtil;
|
||||||
|
@ -43,6 +44,21 @@ public class ProfileLoader {
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ProfileLoadException("Failed to load profile settings.", e);
|
throw new ProfileLoadException("Failed to load profile settings.", e);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
DataSourceFactory.SchemaStatus status = dataSourceFactory.getSchemaStatus(name);
|
||||||
|
if (status == DataSourceFactory.SchemaStatus.NEEDS_MIGRATION) {
|
||||||
|
boolean confirm = Popups.confirm(window, "The profile \"" + name + "\" has an outdated data schema and needs to be migrated to the latest version. Is this okay?");
|
||||||
|
if (!confirm) {
|
||||||
|
throw new ProfileLoadException("User rejected migration.");
|
||||||
|
}
|
||||||
|
} else if (status == DataSourceFactory.SchemaStatus.INCOMPATIBLE) {
|
||||||
|
Popups.error(window, "The profile \"" + name + "\" has a data schema that's incompatible with this app.");
|
||||||
|
throw new ProfileLoadException("Incompatible schema version.");
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ProfileLoadException("Failed to get profile's schema status.", e);
|
||||||
|
}
|
||||||
|
Popups.message(window, "Test!");
|
||||||
return new Profile(name, settings, dataSourceFactory.getDataSource(name));
|
return new Profile(name, settings, dataSourceFactory.getDataSource(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import javafx.stage.Stage;
|
||||||
import javafx.stage.StageStyle;
|
import javafx.stage.StageStyle;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,6 +61,10 @@ public class StartupSplashScreen extends Stage implements Consumer<String> {
|
||||||
return scene;
|
return scene;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs all tasks sequentially, invoking each one on the JavaFX main thread,
|
||||||
|
* and quitting if there's any exception thrown.
|
||||||
|
*/
|
||||||
private void runTasks() {
|
private void runTasks() {
|
||||||
Thread.ofVirtual().start(() -> {
|
Thread.ofVirtual().start(() -> {
|
||||||
try {
|
try {
|
||||||
|
@ -69,7 +74,16 @@ public class StartupSplashScreen extends Stage implements Consumer<String> {
|
||||||
}
|
}
|
||||||
for (var task : tasks) {
|
for (var task : tasks) {
|
||||||
try {
|
try {
|
||||||
task.accept(this);
|
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
try {
|
||||||
|
task.accept(this);
|
||||||
|
future.complete(null);
|
||||||
|
} catch (Exception e) {
|
||||||
|
future.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
future.join();
|
||||||
Thread.sleep(500);
|
Thread.sleep(500);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
accept("Startup failed: " + e.getMessage());
|
accept("Startup failed: " + e.getMessage());
|
||||||
|
|
Loading…
Reference in New Issue