Add Transaction Properties #15
|
@ -89,6 +89,7 @@ public class AccountViewController implements RouteSelectionListener {
|
|||
@FXML
|
||||
public void archiveAccount() {
|
||||
boolean confirmResult = Popups.confirm(
|
||||
titleLabel,
|
||||
"Are you sure you want to archive this account? It will no " +
|
||||
"longer show up in the app normally, and you won't 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() {
|
||||
boolean confirm = Popups.confirm(
|
||||
titleLabel,
|
||||
"Are you sure you want to restore this account from its archived " +
|
||||
"status?"
|
||||
);
|
||||
|
@ -115,6 +117,7 @@ public class AccountViewController implements RouteSelectionListener {
|
|||
@FXML
|
||||
public void deleteAccount() {
|
||||
boolean confirm = Popups.confirm(
|
||||
titleLabel,
|
||||
"Are you sure you want to permanently delete this account and " +
|
||||
"all data directly associated with it? This cannot be " +
|
||||
"undone; deleted accounts are not recoverable at all. " +
|
||||
|
|
|
@ -48,7 +48,10 @@ public class BalanceRecordViewController implements RouteSelectionListener {
|
|||
}
|
||||
|
||||
@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) {
|
||||
Profile.getCurrent().dataSource().useRepo(BalanceRecordRepository.class, repo -> repo.deleteById(balanceRecord.id));
|
||||
router.navigateBackAndClear();
|
||||
|
|
|
@ -89,7 +89,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
|
|||
LocalDateTime localTimestamp = LocalDateTime.parse(timestampField.getText(), DateUtil.DEFAULT_DATETIME_FORMAT);
|
||||
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(),
|
||||
CurrencyUtil.formatMoneyWithCurrencyPrefix(new MoneyValue(reportedBalance, account.getCurrency())),
|
||||
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(currentDerivedBalance, account.getCurrency()))
|
||||
);
|
||||
return Popups.confirm(msg);
|
||||
return Popups.confirm(timestampField, msg);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -117,7 +117,7 @@ public class EditAccountController implements RouteSelectionListener {
|
|||
BigDecimal initialBalance = new BigDecimal(initialBalanceField.getText().strip());
|
||||
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) {
|
||||
long id = accountRepo.insert(type, number, name, currency);
|
||||
balanceRepo.insert(LocalDateTime.now(ZoneOffset.UTC), id, initialBalance, currency, attachments);
|
||||
|
@ -138,7 +138,7 @@ public class EditAccountController implements RouteSelectionListener {
|
|||
}
|
||||
} catch (Exception 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) {
|
||||
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;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Window;
|
||||
|
||||
/**
|
||||
* Helper class for standardized popups and confirmation dialogs for the app.
|
||||
*/
|
||||
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.initOwner(owner);
|
||||
alert.initModality(Modality.APPLICATION_MODAL);
|
||||
var result = alert.showAndWait();
|
||||
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.initOwner(owner);
|
||||
alert.initModality(Modality.APPLICATION_MODAL);
|
||||
alert.getButtonTypes().setAll(ButtonType.OK);
|
||||
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.initOwner(owner);
|
||||
alert.initModality(Modality.APPLICATION_MODAL);
|
||||
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();
|
||||
boolean valid = Profile.validateName(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 (openProfile(name, false)) {
|
||||
Popups.message("Created new profile \"" + name + "\" and loaded it.");
|
||||
Popups.message(profilesVBox, "Created new profile \"" + name + "\" and loaded it.");
|
||||
}
|
||||
newProfileNameField.clear();
|
||||
}
|
||||
|
@ -108,18 +108,18 @@ public class ProfilesViewController {
|
|||
PerfinApp.profileLoader.load(name);
|
||||
ProfilesStage.closeView();
|
||||
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;
|
||||
} catch (ProfileLoadException e) {
|
||||
Popups.error("Failed to load the profile: " + e.getMessage());
|
||||
Popups.error(profilesVBox, "Failed to load the profile: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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) {
|
||||
try {
|
||||
FileUtil.deleteDirRecursive(Profile.getDir(name));
|
||||
|
|
|
@ -72,6 +72,7 @@ public class TransactionViewController {
|
|||
|
||||
@FXML public void deleteTransaction() {
|
||||
boolean confirm = Popups.confirm(
|
||||
titleLabel,
|
||||
"Are you sure you want to delete this transaction? This will " +
|
||||
"permanently remove the transaction and its effects on any linked " +
|
||||
"accounts, as well as remove any attachments from storage within " +
|
||||
|
|
|
@ -177,7 +177,7 @@ public class TransactionsViewController implements RouteSelectionListener {
|
|||
));
|
||||
}
|
||||
} 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
|
||||
* unloaded.
|
||||
* </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) {
|
||||
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);
|
||||
log.debug("Current profile set to {}.", current.name());
|
||||
}
|
||||
|
||||
public static void whenLoaded(Consumer<Profile> consumer) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.andrewlalis.perfin.model;
|
||||
|
||||
import com.andrewlalis.perfin.PerfinApp;
|
||||
import com.andrewlalis.perfin.control.Popups;
|
||||
import com.andrewlalis.perfin.data.DataSourceFactory;
|
||||
import com.andrewlalis.perfin.data.ProfileLoadException;
|
||||
import com.andrewlalis.perfin.data.util.FileUtil;
|
||||
|
@ -43,6 +44,21 @@ public class ProfileLoader {
|
|||
} catch (IOException 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));
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import javafx.stage.Stage;
|
|||
import javafx.stage.StageStyle;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
|
@ -60,6 +61,10 @@ public class StartupSplashScreen extends Stage implements Consumer<String> {
|
|||
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() {
|
||||
Thread.ofVirtual().start(() -> {
|
||||
try {
|
||||
|
@ -69,7 +74,16 @@ public class StartupSplashScreen extends Stage implements Consumer<String> {
|
|||
}
|
||||
for (var task : tasks) {
|
||||
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);
|
||||
} catch (Exception e) {
|
||||
accept("Startup failed: " + e.getMessage());
|
||||
|
|
Loading…
Reference in New Issue