diff --git a/JCash.iml b/JCash.iml index c90834f..c3dc060 100644 --- a/JCash.iml +++ b/JCash.iml @@ -7,5 +7,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Main.java b/src/Main.java deleted file mode 100644 index 3e59c38..0000000 --- a/src/Main.java +++ /dev/null @@ -1,5 +0,0 @@ -public class Main { - public static void main(String[] args) { - System.out.println("Hello world!"); - } -} \ No newline at end of file diff --git a/src/me/teridax/jcash/Main.java b/src/me/teridax/jcash/Main.java new file mode 100644 index 0000000..75463e8 --- /dev/null +++ b/src/me/teridax/jcash/Main.java @@ -0,0 +1,92 @@ +package me.teridax.jcash; + +import me.teridax.jcash.gui.Loader; +import me.teridax.jcash.gui.account.AccountController; +import me.teridax.jcash.gui.login.LoginController; + +import javax.swing.*; + +public final class Main { + + /** + * Main instance of this program. Contains the primary window. + */ + private static Main instance; + + /** + * Primary window of this program + */ + private final JFrame window; + + private Main() { + // create main window and set defaults + this.window = new JFrame(); + this.window.setTitle("Bankautomat"); + this.window.setLocationByPlatform(true); + this.window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } + + public static void main(String[] args) { + // create main instance and show the login screen + instance(); + getInstance().showLoginScreen(); + } + + public static Main getInstance() { + return instance; + } + + /** + * Attempts to create a new instance of the singleton class Main. + * This method throws an exception if the class is already instantiated. + * @throws IllegalStateException if the class is already instantiated + * @pre the static instance of this class should be null. + * @post the static instance of this class will be set + */ + public static void instance() { + if (null != instance) + throw new IllegalStateException(Main.class.getName() + " is already initialized"); + + Main.instance = new Main(); + } + + /** + * Shows the open dialog for selecting a database file. After selection, it then proceeds to prompt login. + * Afterward the selected account can be managed. + * This method is non-blocking and all work described is performed asynchronously on the AWT Event dispatcher. + */ + public void showLoginScreen() { + SwingUtilities.invokeLater(() -> { + try { + // select db file + var path = Loader.load(); + // read database and login + var login = new LoginController(path); + + // when we have logged in set the account viewer as window content + login.addAccountSelectionListener(account -> { + var profileCont = new AccountController(account); + this.window.setContentPane(profileCont.getView()); + this.window.revalidate(); + this.window.repaint(); + }); + + // we are not logged in yet, so show the login prompt on the main window + this.window.setContentPane(login.getView()); + this.window.setSize(800, 600); + this.window.setVisible(true); + + } catch (IllegalStateException e) { + System.out.println("no file selected. goodbye"); + } + }); + } + + /** + * Logs the user out of the database, hiding the main window. + */ + public void logout() { + window.setContentPane(new JLabel("you're logged out")); + window.setVisible(false); + } +} \ No newline at end of file diff --git a/src/me/teridax/jcash/banking/Account.java b/src/me/teridax/jcash/banking/Account.java index 7ef9445..bf4fc4f 100644 --- a/src/me/teridax/jcash/banking/Account.java +++ b/src/me/teridax/jcash/banking/Account.java @@ -1,6 +1,6 @@ package me.teridax.jcash.banking; -import me.teridax.jcash.decode.Decoder; +import me.teridax.jcash.decode.StringUtils; import java.util.Objects; @@ -9,7 +9,7 @@ public abstract class Account { private final int iban; private final int pin; - private final double balance; + private double balance; public Account(int iban, int pin, double balance) { this.iban = iban; @@ -20,17 +20,17 @@ public abstract class Account { public static Account fromColumns(String[] columns) { Objects.requireNonNull(columns); - var iban = Decoder.decodeUniqueIdentificationNumber(columns[0]); - var pin = Decoder.decodeUniqueIdentificationNumber(columns[1]); - var balance = Decoder.decodeCurrency(columns[2]); - var type = Decoder.decodeName(columns[3]); + var iban = StringUtils.decodeUniqueIdentificationNumber(columns[0]); + var pin = StringUtils.decodeUniqueIdentificationNumber(columns[1]); + var balance = StringUtils.decodeCurrency(columns[2]); + var type = StringUtils.decodeName(columns[3]); try { if (type.equals("Sparkonto")) { - var interest = Decoder.decodePercent(columns[4]); + var interest = StringUtils.decodePercent(columns[4]); return new SavingsAccount(iban, pin, balance, interest); } else if (type.equals("Girokonto")) { - var overdraft = Decoder.decodeCurrency(columns[5]); + var overdraft = StringUtils.decodeCurrency(columns[5]); return new Girokonto(iban, pin, balance, overdraft); } else { throw new IllegalArgumentException("Invalid account type: " + type); @@ -64,4 +64,21 @@ public abstract class Account { return false; } + + public String getDescription() { + return String.format("%s (%s)", iban, getClass().getSimpleName()); + } + + public void deposit(double amount) { + this.balance += amount; + } + + @Override + public String toString() { + return String.format("@Account [iban=%d, pin=%d, balance=%.2f]", iban, pin, balance); + } + + public void takeoff(double amount) { + this.balance = Math.max(0, this.balance - amount); + } } diff --git a/src/me/teridax/jcash/banking/Bank.java b/src/me/teridax/jcash/banking/Bank.java index 13bad12..03b347b 100644 --- a/src/me/teridax/jcash/banking/Bank.java +++ b/src/me/teridax/jcash/banking/Bank.java @@ -1,6 +1,7 @@ package me.teridax.jcash.banking; import java.util.*; +import java.util.regex.Pattern; @SuppressWarnings("unused") public final class Bank { @@ -46,14 +47,24 @@ public final class Bank { } public static String validateBlz(String maybeBlz) { - var builder = new StringBuilder(); - maybeBlz.trim().chars().filter(ch -> Character.isDigit(ch) || Character.isLetter(ch)).forEach(builder::append); - var blz = builder.toString(); + var pattern = Pattern.compile("[\\w\\d-_]+"); + var matcher = pattern.matcher(maybeBlz); - if (blz.isEmpty()) { - throw new IllegalArgumentException("Bank Blz must only contain letters and digits"); + if (matcher.find()) + return matcher.group(); + + throw new IllegalArgumentException("not a valid BLZ: " + maybeBlz); + } + + public Optional getAccount(int iban) { + for (var owner : this.accounts.entrySet()) { + for (var account : owner.getValue()) { + var tmp = account.getIban(); + if (tmp == iban) { + return Optional.of(new Profile(owner.getKey(), this, account, getAccountsOfOwner(owner.getKey()))); + } + } } - - return blz; + return Optional.empty(); } } diff --git a/src/me/teridax/jcash/banking/BankingManagementSystem.java b/src/me/teridax/jcash/banking/BankingManagementSystem.java index 35d8839..57fe941 100644 --- a/src/me/teridax/jcash/banking/BankingManagementSystem.java +++ b/src/me/teridax/jcash/banking/BankingManagementSystem.java @@ -1,6 +1,6 @@ package me.teridax.jcash.banking; -import me.teridax.jcash.decode.Decoder; +import me.teridax.jcash.decode.StringUtils; import java.io.IOException; import java.nio.file.Files; @@ -34,7 +34,7 @@ public final class BankingManagementSystem { var account = Account.fromColumns(tail(columns, 2)); var blz = Bank.validateBlz(columns[1]); - var name = Decoder.decodeName(columns[0]); + var name = StringUtils.decodeName(columns[0]); var bankOfLine = new Bank(blz, name); var bankOfSet = bms.banks.stream().filter(b -> b.equals(bankOfLine)).findFirst(); @@ -54,4 +54,8 @@ public final class BankingManagementSystem { throw new IllegalArgumentException("Could not parse file " + file, e); } } + + public Optional getBank(String blz) { + return this.banks.stream().filter(b -> b.getBlz().equals(blz)).findFirst(); + } } diff --git a/src/me/teridax/jcash/banking/Owner.java b/src/me/teridax/jcash/banking/Owner.java index 07b1bab..5497df1 100644 --- a/src/me/teridax/jcash/banking/Owner.java +++ b/src/me/teridax/jcash/banking/Owner.java @@ -1,6 +1,6 @@ package me.teridax.jcash.banking; -import me.teridax.jcash.decode.Decoder; +import me.teridax.jcash.decode.StringUtils; import java.util.Objects; @@ -28,12 +28,12 @@ public final class Owner { if (columns.length != 6) throw new IllegalArgumentException("Invalid number of columns: " + columns.length); - var uid = Decoder.decodeUniqueIdentificationNumber(columns[0]); - var familyName = Decoder.decodeName(columns[1]); - var name = Decoder.decodeName(columns[2]); - var street = Decoder.decodeStreet(columns[3]); - var zip = Decoder.decodeUniqueIdentificationNumber(columns[4]); - var city = Decoder.decodeName(columns[5]); + var uid = StringUtils.decodeUniqueIdentificationNumber(columns[0]); + var familyName = StringUtils.decodeName(columns[1]); + var name = StringUtils.decodeName(columns[2]); + var street = StringUtils.decodeStreet(columns[3]); + var zip = StringUtils.decodeUniqueIdentificationNumber(columns[4]); + var city = StringUtils.decodeName(columns[5]); return new Owner(uid, familyName, name, zip, city, street); } diff --git a/src/me/teridax/jcash/banking/Profile.java b/src/me/teridax/jcash/banking/Profile.java new file mode 100644 index 0000000..9ac33a6 --- /dev/null +++ b/src/me/teridax/jcash/banking/Profile.java @@ -0,0 +1,41 @@ +package me.teridax.jcash.banking; + +public class Profile { + + private Owner owner; + private Bank bank; + private Account primaryAccount; + private Account[] accounts; + + public Profile(Owner owner, Bank bank, Account account, Account[] accounts) { + this.owner = owner; + this.bank = bank; + this.accounts = accounts; + this.primaryAccount = account; + } + + public Account getPrimaryAccount() { + return primaryAccount; + } + + public Owner getOwner() { + return owner; + } + + public Bank getBank() { + return bank; + } + + public Account[] getAccounts() { + return accounts; + } + + public void setPrimaryAccount(String description) { + for (Account account : accounts) { + if (account.getDescription().equals(description)) { + this.primaryAccount = account; + break; + } + } + } +} diff --git a/src/me/teridax/jcash/banking/Testing.java b/src/me/teridax/jcash/banking/Tests.java similarity index 95% rename from src/me/teridax/jcash/banking/Testing.java rename to src/me/teridax/jcash/banking/Tests.java index dee3fa9..ab644fb 100644 --- a/src/me/teridax/jcash/banking/Testing.java +++ b/src/me/teridax/jcash/banking/Tests.java @@ -4,7 +4,7 @@ import org.junit.Test; import java.nio.file.Paths; -public class Testing { +public class Tests { @Test public void test() { diff --git a/src/me/teridax/jcash/decode/Decoder.java b/src/me/teridax/jcash/decode/StringUtils.java similarity index 67% rename from src/me/teridax/jcash/decode/Decoder.java rename to src/me/teridax/jcash/decode/StringUtils.java index 4af70b8..5145466 100644 --- a/src/me/teridax/jcash/decode/Decoder.java +++ b/src/me/teridax/jcash/decode/StringUtils.java @@ -10,11 +10,29 @@ import java.util.regex.Pattern; import static junit.framework.TestCase.assertEquals; -public class Decoder { +/** + * Utility class for converting various single line strings into a specific data type according + * to a format mostly dictated by a locale. + */ +public class StringUtils { + /** + * Locale to use when converting strings + */ private static final Locale LOCALE = Locale.GERMANY; - private static final NumberFormat LOCAL_NUMBER_FORMAT = NumberFormat.getInstance(LOCALE); + /** + * NumberFormat to use when converting strings + */ + public static final NumberFormat LOCAL_NUMBER_FORMAT = NumberFormat.getInstance(LOCALE); + /** + * Attempts to convert the given string into a double value representing a percentage. + * The percentage is stored in the range [0,1] and can linearly be mapped to [0, 100] by multiplying with 100. + * @param number the string to convert + * @return the double value + * @throws IllegalArgumentException when the format is invalid + * @throws NullPointerException when the argument is null + */ public static double decodePercent(String number) throws IllegalArgumentException, NullPointerException { Objects.requireNonNull(number); @@ -28,6 +46,13 @@ public class Decoder { } } + /** + * Attempts to convert the given string into a currency value. + * @param currency the string to convert + * @return the double value + * @throws IllegalArgumentException when the format is invalid + * @throws NullPointerException when the argument is null + */ public static double decodeCurrency(String currency) throws IllegalArgumentException, NullPointerException { Objects.requireNonNull(currency); @@ -38,6 +63,14 @@ public class Decoder { } } + /** + * Attempts to convert the given string into universally unique number. + * This function does not check for duplicates. The number must be a positive integer. + * @param number the string to convert + * @return the integer serial number + * @throws IllegalArgumentException when the format is invalid + * @throws NullPointerException when the argument is null + */ public static int decodeUniqueIdentificationNumber(String number) throws IllegalArgumentException, NullPointerException { Objects.requireNonNull(number); @@ -56,6 +89,14 @@ public class Decoder { } } + /** + * Attempts to convert the given string into a name. + * This method performs validation and trimming. + * @param name the string to convert + * @return the qualified name + * @throws IllegalArgumentException when the format is invalid + * @throws NullPointerException when the argument is null + */ public static String decodeName(String name) throws IllegalArgumentException, NullPointerException { Objects.requireNonNull(name); @@ -70,6 +111,13 @@ public class Decoder { } } + /** + * Attempts to convert the given string into a street and an optional house address. + * @param street the string to convert + * @return the address name + * @throws IllegalArgumentException when the format is invalid + * @throws NullPointerException when the argument is null + */ public static String decodeStreet(String street) throws IllegalArgumentException, NullPointerException { Objects.requireNonNull(street); diff --git a/src/me/teridax/jcash/gui/Loader.java b/src/me/teridax/jcash/gui/Loader.java new file mode 100644 index 0000000..f12593d --- /dev/null +++ b/src/me/teridax/jcash/gui/Loader.java @@ -0,0 +1,49 @@ +package me.teridax.jcash.gui; + +import me.teridax.jcash.banking.BankingManagementSystem; + +import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; +import java.io.File; + +import static javax.swing.JFileChooser.APPROVE_OPTION; + +/** + * Utility class for loading a BMS configuration from a csv file. + */ +public class Loader { + + /** + * Filter that only allows for files with *.csv extension + */ + private static final FileNameExtensionFilter FILE_FILTER = new FileNameExtensionFilter("Comma separated value spreadsheet", "csv", "CSV"); + + private Loader() {} + + /** + * Load a BMS from a csv file. Opens up a dialog which prompts the user to select a single file. + * Once the file is selected this function will try to parse the contents to a BMS and return the instance. + * @return a valid BMS instance loaded from a file + * @throws IllegalStateException When either no file is selected or the selected files content is invalid + */ + public static BankingManagementSystem load() throws IllegalStateException { + var fileChooser = new JFileChooser(); + fileChooser.setMultiSelectionEnabled(false); + fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + fileChooser.setFileFilter(FILE_FILTER); + fileChooser.setDialogType(JFileChooser.OPEN_DIALOG); + fileChooser.setAcceptAllFileFilterUsed(false); + fileChooser.setCurrentDirectory(new File("/home/teridax/IdeaProjects/JCash/res")); + + if (fileChooser.showDialog(null, "Load database") == APPROVE_OPTION) { + // parse file content + try { + return BankingManagementSystem.loadFromCsv(fileChooser.getSelectedFile().toPath()); + } catch (Exception e) { + throw new IllegalStateException("Unable to load database", e); + } + } + + throw new IllegalStateException("No file selected"); + } +} diff --git a/src/me/teridax/jcash/gui/account/AccountController.java b/src/me/teridax/jcash/gui/account/AccountController.java new file mode 100644 index 0000000..ab849b1 --- /dev/null +++ b/src/me/teridax/jcash/gui/account/AccountController.java @@ -0,0 +1,48 @@ +package me.teridax.jcash.gui.account; + +import me.teridax.jcash.Main; +import me.teridax.jcash.banking.Profile; +import me.teridax.jcash.gui.deposit.DepositDialog; +import me.teridax.jcash.gui.takeoff.TakeoffDialog; +import me.teridax.jcash.gui.transfer.TransferDialog; + +public class AccountController { + + private final AccountView view; + + public AccountController(Profile profile) { + this.view = new AccountView(); + this.view.setProfile(profile); + + createListeners(profile); + } + + private void createListeners(Profile profile) { + this.view.getAccountSelection().addActionListener(e -> { + var description = ((String) this.view.getAccountSelection().getSelectedItem()); + profile.setPrimaryAccount(description); + this.view.setProfile(profile); + }); + + this.view.getLogout().addActionListener(e -> { + Main.getInstance().logout(); + Main.getInstance().showLoginScreen(); + }); + + this.view.getDeposit().addActionListener(e -> { + new DepositDialog(profile.getPrimaryAccount(), () -> this.view.setProfile(profile)); + }); + + this.view.getTakeoff().addActionListener(e -> { + new TakeoffDialog(profile.getPrimaryAccount(), () -> this.view.setProfile(profile)); + }); + + this.view.getTransfer().addActionListener(e -> { + new TransferDialog(profile.getPrimaryAccount(), () -> this.view.setProfile(profile)); + }); + } + + public AccountView getView() { + return view; + } +} diff --git a/src/me/teridax/jcash/gui/account/AccountView.java b/src/me/teridax/jcash/gui/account/AccountView.java new file mode 100644 index 0000000..ecdf560 --- /dev/null +++ b/src/me/teridax/jcash/gui/account/AccountView.java @@ -0,0 +1,170 @@ +package me.teridax.jcash.gui.account; + +import me.teridax.jcash.banking.*; +import me.teridax.jcash.decode.StringUtils; + +import javax.swing.*; +import java.awt.*; + +public class AccountView extends JPanel { + + private JTextField iban; + private JTextField name; + private JTextField address; + private JTextField bankName; + private JTextField blz; + private JTextField type; + private JTextField typeSpecialProperty; + private JTextField balance; + + private JLabel typeSpecialLabel; + + private JComboBox accountSelection; + + private JPanel content; + + private JButton logout; + private JButton transfer; + private JButton deposit; + private JButton takeoff; + + public AccountView() { + createComponents(); + createLayout(); + + setBorder(BorderFactory.createEmptyBorder(8,8,8,8)); + } + + public void setProfile(Profile profile) { + var bank = profile.getBank(); + var account = profile.getPrimaryAccount(); + var owner = profile.getOwner(); + + this.blz.setText(bank.getBlz()); + this.bankName.setText(bank.getName()); + + this.iban.setText(String.valueOf(account.getIban())); + this.name.setText(owner.getName() + " " + owner.getFamilyName()); + this.address.setText(owner.getStreet() + " " + owner.getCity()); + + this.balance.setText(StringUtils.LOCAL_NUMBER_FORMAT.format(account.getBalance()) + " €"); + + this.type.setText(account.getClass().getSimpleName()); + if (account instanceof Girokonto) { + this.typeSpecialLabel.setText("Overdraft"); + this.typeSpecialProperty.setText( StringUtils.LOCAL_NUMBER_FORMAT.format(((Girokonto) account).getOverdraft()) + " €"); + } else if (account instanceof SavingsAccount) { + this.typeSpecialLabel.setText("Interest rate"); + this.typeSpecialProperty.setText( ((SavingsAccount) account).getInterest() + " %" ); + } + + this.accountSelection.removeAllItems(); + + for (var otherAccount : profile.getAccounts()) { + this.accountSelection.addItem(otherAccount.getDescription()); + } + this.accountSelection.setSelectedItem(account.getDescription()); + } + + private void createLayout() { + setLayout(new BorderLayout(16, 16)); + add(new JScrollPane(content), BorderLayout.CENTER); + + content.setLayout(new GridBagLayout()); + var constraints = new GridBagConstraints(); + + constraints.gridwidth = 4; + constraints.insets = new Insets(12,12,12,12); + + var accountSelectionPanel = new JPanel(new BorderLayout(12, 0)); + accountSelectionPanel.add(iban, BorderLayout.CENTER); + accountSelectionPanel.add(accountSelection, BorderLayout.EAST); + + addInputRow(constraints, content, accountSelectionPanel, 1, new JLabel("IBAN", SwingConstants.RIGHT)); + addInputRow(constraints, content, name, 2, new JLabel("Name/Family-name", SwingConstants.RIGHT)); + addInputRow(constraints, content, address, 3, new JLabel("Address", SwingConstants.RIGHT)); + addInputRow(constraints, content, bankName, 4, new JLabel("Bank", SwingConstants.RIGHT)); + addInputRow(constraints, content, blz, 5, new JLabel("BLZ", SwingConstants.RIGHT)); + addInputRow(constraints, content, type, 6, new JLabel("Account", SwingConstants.RIGHT)); + addInputRow(constraints, content, typeSpecialProperty, 7, typeSpecialLabel); + addInputRow(constraints, content, balance, 8, new JLabel("Balance", SwingConstants.RIGHT)); + + var buttonPanel = Box.createHorizontalBox(); + buttonPanel.add(Box.createHorizontalStrut(4)); + buttonPanel.add(logout); + buttonPanel.add(Box.createHorizontalStrut(4)); + buttonPanel.add(Box.createGlue()); + buttonPanel.add(transfer); + buttonPanel.add(Box.createHorizontalStrut(4)); + buttonPanel.add(deposit); + buttonPanel.add(Box.createHorizontalStrut(4)); + buttonPanel.add(takeoff); + add(buttonPanel, BorderLayout.SOUTH); + } + + private void createComponents() { + this.blz = new JTextField(); + this.iban = new JTextField(); + this.address = new JTextField(); + this.bankName = new JTextField(); + this.name = new JTextField(); + this.type = new JTextField(); + this.balance = new JTextField(); + this.typeSpecialProperty = new JTextField(); + + this.blz.setEditable(false); + this.iban.setEditable(false); + this.address.setEditable(false); + this.bankName.setEditable(false); + this.name.setEditable(false); + this.type.setEditable(false); + this.balance.setEditable(false); + this.typeSpecialProperty.setEditable(false); + + this.typeSpecialLabel = new JLabel("", SwingConstants.RIGHT); + + this.accountSelection = new JComboBox<>(); + + this.content = new JPanel(); + + this.logout = new JButton("Logout"); + this.transfer = new JButton("Transfer"); + this.deposit = new JButton("Deposit"); + this.takeoff = new JButton("Takeoff"); + } + + private void addInputRow(GridBagConstraints constraints, JComponent target, JComponent comp, int row, JLabel label) { + constraints.gridwidth = 1; + constraints.gridx = 1; + constraints.gridy = row; + constraints.weightx = 0; + constraints.fill = GridBagConstraints.HORIZONTAL; + target.add(label, constraints); + + constraints.gridx = 2; + constraints.gridy = row; + constraints.weightx = 1; + constraints.fill = GridBagConstraints.HORIZONTAL; + target.add(comp, constraints); + } + + public JComboBox getAccountSelection() { + return accountSelection; + } + + public JButton getLogout() { + return logout; + } + + public JButton getTransfer() { + return transfer; + } + + public JButton getDeposit() { + return deposit; + } + + public JButton getTakeoff() { + return takeoff; + } +} diff --git a/src/me/teridax/jcash/gui/deposit/DepositDialog.java b/src/me/teridax/jcash/gui/deposit/DepositDialog.java new file mode 100644 index 0000000..00e9e5b --- /dev/null +++ b/src/me/teridax/jcash/gui/deposit/DepositDialog.java @@ -0,0 +1,17 @@ +package me.teridax.jcash.gui.deposit; + +import me.teridax.jcash.banking.Account; + +public class DepositDialog { + + public DepositDialog(Account account, Runnable onDeposit) { + var view = new DepositView(account); + view.getDeposit().addActionListener(e -> { + account.deposit(view.getAmount()); + onDeposit.run(); + view.dispose(); + }); + view.getCancel().addActionListener(e -> view.dispose()); + view.showDialog(); + } +} diff --git a/src/me/teridax/jcash/gui/deposit/DepositView.java b/src/me/teridax/jcash/gui/deposit/DepositView.java new file mode 100644 index 0000000..06529cc --- /dev/null +++ b/src/me/teridax/jcash/gui/deposit/DepositView.java @@ -0,0 +1,115 @@ +package me.teridax.jcash.gui.deposit; + +import me.teridax.jcash.banking.Account; +import me.teridax.jcash.decode.StringUtils; + +import javax.swing.*; +import java.awt.*; +import java.text.NumberFormat; +import java.text.ParseException; + +public class DepositView { + + private JDialog dialog; + private JButton cancel; + private JButton deposit; + private JFormattedTextField value; + + public DepositView(Account account) { + createComponents(account); + layoutComponents(); + } + + public void showDialog() { + dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL); + dialog.setTitle("Deposit money"); + dialog.pack(); + dialog.setResizable(false); + dialog.setLocationRelativeTo(null); + dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + dialog.setVisible(true); + } + + private void layoutComponents() { + dialog.getContentPane().setLayout(new GridBagLayout()); + + var c = new GridBagConstraints(); + + c.gridx = 0; + c.gridy = 0; + c.weightx = 1; + c.weighty = 1; + c.gridwidth = 3; + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.CENTER; + c.insets = new Insets(4, 4, 4, 4); + dialog.getContentPane().add(new JLabel("Deposit money"), c); + + c.gridx = 0; + c.gridy = 1; + c.gridwidth = 1; + c.fill = GridBagConstraints.NONE; + c.anchor = GridBagConstraints.LAST_LINE_END; + c.weightx = 0; + dialog.getContentPane().add(new JLabel("Value", SwingConstants.RIGHT), c); + + c.gridx = 1; + c.gridy = 1; + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.LAST_LINE_END; + c.weightx = 0.5; + dialog.getContentPane().add(value, c); + + c.gridx = 2; + c.gridy = 1; + c.fill = GridBagConstraints.NONE; + c.anchor = GridBagConstraints.LINE_START; + c.weightx = 0; + dialog.getContentPane().add(new JLabel("€"), c); + + var buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING)); + buttonPanel.add(cancel); + buttonPanel.add(deposit); + + c.gridx = 0; + c.gridy = 2; + c.gridwidth = 3; + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.LAST_LINE_END; + c.insets = new Insets(10, 10, 10, 10); + dialog.getContentPane().add(buttonPanel, c); + } + + private void createComponents(Account account) { + this.dialog = new JDialog(); + + this.cancel = new JButton("Cancel"); + this.deposit = new JButton("Deposit"); + this.value = new JFormattedTextField(StringUtils.LOCAL_NUMBER_FORMAT); + + this.dialog.setContentPane(new JPanel(new GridBagLayout())); + } + + public double getAmount() { + if (value.getText().isBlank()) + return 0; + + try { + return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue(); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + public JButton getCancel() { + return cancel; + } + + public JButton getDeposit() { + return deposit; + } + + public void dispose() { + this.dialog.dispose(); + } +} diff --git a/src/me/teridax/jcash/gui/login/AccountSelectionListener.java b/src/me/teridax/jcash/gui/login/AccountSelectionListener.java new file mode 100644 index 0000000..abeac4f --- /dev/null +++ b/src/me/teridax/jcash/gui/login/AccountSelectionListener.java @@ -0,0 +1,8 @@ +package me.teridax.jcash.gui.login; + +import me.teridax.jcash.banking.Profile; + +public interface AccountSelectionListener { + + void onAccountSelected(Profile account); +} diff --git a/src/me/teridax/jcash/gui/login/LoginController.java b/src/me/teridax/jcash/gui/login/LoginController.java new file mode 100644 index 0000000..06a614f --- /dev/null +++ b/src/me/teridax/jcash/gui/login/LoginController.java @@ -0,0 +1,73 @@ +package me.teridax.jcash.gui.login; + +import me.teridax.jcash.banking.BankingManagementSystem; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.util.Optional; + +public class LoginController { + + private final LoginView view; + private final LoginData data; + + private AccountSelectionListener listener; + + public LoginController(BankingManagementSystem bms) { + this.view = new LoginView(); + this.data = new LoginData(bms); + + addActionListeners(); + } + + private void addActionListeners() { + this.view.getLogin().addActionListener(this::login); + } + + private Optional getIban() { + try { + var iban = this.view.getIban().getText(); + return Optional.of(Integer.parseUnsignedInt(iban)); + } catch (NumberFormatException e) { + return Optional.empty(); + } + } + + private Optional getPin() { + try { + var iban = this.view.getPin().getPassword(); + return Optional.of(Integer.parseUnsignedInt(new String(iban))); + } catch (NumberFormatException e) { + return Optional.empty(); + } + } + + private void login(ActionEvent ignored) { + var blz = this.view.getBlz().getText(); + var iban = this.getIban(); + if (iban.isEmpty()) { + JOptionPane.showMessageDialog(null, "invalid IBAN", "Faulty login attempt", JOptionPane.ERROR_MESSAGE); + return; + } + var pin = this.getPin(); + if (pin.isEmpty()) { + JOptionPane.showMessageDialog(null, "invalid pin", "Faulty login attempt", JOptionPane.ERROR_MESSAGE); + return; + } + + var account = this.data.authenticateAccount(blz, iban.get(), pin.get()); + if (account.isPresent()) { + this.listener.onAccountSelected(account.get()); + } else { + JOptionPane.showMessageDialog(null, "invalid login credentials", "Faulty login attempt", JOptionPane.ERROR_MESSAGE); + } + } + + public void addAccountSelectionListener(AccountSelectionListener listener) { + this.listener = listener; + } + + public LoginView getView() { + return view; + } +} diff --git a/src/me/teridax/jcash/gui/login/LoginData.java b/src/me/teridax/jcash/gui/login/LoginData.java new file mode 100644 index 0000000..3183c99 --- /dev/null +++ b/src/me/teridax/jcash/gui/login/LoginData.java @@ -0,0 +1,24 @@ +package me.teridax.jcash.gui.login; + +import me.teridax.jcash.banking.BankingManagementSystem; +import me.teridax.jcash.banking.Profile; + +import java.util.Optional; + +public class LoginData { + + private final BankingManagementSystem bms; + + public LoginData(BankingManagementSystem bms) { + this.bms = bms; + } + + public Optional authenticateAccount(String blz, int iban, int pin) { + var optionalBank = bms.getBank(blz); + if (optionalBank.isEmpty()) + return Optional.empty(); + + var profile = optionalBank.get().getAccount(iban); + return profile.filter(value -> value.getPrimaryAccount().getPin() == pin); + } +} diff --git a/src/me/teridax/jcash/gui/login/LoginView.java b/src/me/teridax/jcash/gui/login/LoginView.java new file mode 100644 index 0000000..6d869c0 --- /dev/null +++ b/src/me/teridax/jcash/gui/login/LoginView.java @@ -0,0 +1,90 @@ +package me.teridax.jcash.gui.login; + +import javax.swing.*; +import javax.swing.text.NumberFormatter; +import java.awt.*; +import java.text.NumberFormat; + +public class LoginView extends JPanel { + + private final JFormattedTextField blz; + private final JFormattedTextField iban; + private final JPasswordField pin; + private final JButton login; + + public LoginView() { + this.blz = new JFormattedTextField("MA2424"); + this.iban = new JFormattedTextField(getNumberFormat()); + this.iban.setText("4711"); + this.pin = new JPasswordField("1234"); + this.login = new JButton("Login"); + + setLayout(new BorderLayout(16, 16)); + var content = new JPanel(); + this.setBorder(BorderFactory.createEmptyBorder(8,8,8,8)); + add(new JScrollPane(content), BorderLayout.CENTER); + add(new JLabel("Bankautomat"), BorderLayout.NORTH); + + var layout = new GridBagLayout(); + var constraints = new GridBagConstraints(); + content.setLayout(layout); + + constraints.gridwidth = 4; + constraints.insets = new Insets(12,12,12,12); + + addInputRow(constraints, content, blz, 1, "BLZ"); + addInputRow(constraints, content, iban, 2, "Kontonummer"); + addInputRow(constraints, content, pin, 3, "Passwort"); + + constraints.gridy = 4; + constraints.anchor = GridBagConstraints.PAGE_END; + constraints.weightx = 0; + constraints.fill = GridBagConstraints.NONE; + constraints.insets = new Insets(12,12,12,12); + content.add(login, constraints); + } + + private NumberFormatter getNumberFormat() { + var format = NumberFormat.getIntegerInstance(); + format.setGroupingUsed(false); + + var formatter = new NumberFormatter(format); + formatter.setValueClass(Integer.class); + formatter.setMinimum(0); + formatter.setMaximum(Integer.MAX_VALUE); + formatter.setAllowsInvalid(false); + + return formatter; + } + + private void addInputRow(GridBagConstraints constraints, JComponent target, JComponent comp, int row, String name) { + constraints.gridwidth = 1; + constraints.gridx = 1; + constraints.gridy = row; + constraints.weightx = 0; + constraints.fill = GridBagConstraints.HORIZONTAL; + target.add(new JLabel(name, SwingConstants.RIGHT), constraints); + + constraints.gridx = 2; + constraints.gridy = row; + constraints.weightx = 1; + constraints.fill = GridBagConstraints.HORIZONTAL; + target.add(comp, constraints); + } + + public JTextField getBlz() { + return blz; + } + + public JTextField getIban() { + return iban; + } + + public JPasswordField getPin() { + return pin; + } + + public JButton getLogin() { + return login; + } +} diff --git a/src/me/teridax/jcash/gui/takeoff/TakeoffDialog.java b/src/me/teridax/jcash/gui/takeoff/TakeoffDialog.java new file mode 100644 index 0000000..44e05c6 --- /dev/null +++ b/src/me/teridax/jcash/gui/takeoff/TakeoffDialog.java @@ -0,0 +1,17 @@ +package me.teridax.jcash.gui.takeoff; + +import me.teridax.jcash.banking.Account; + +public class TakeoffDialog { + + public TakeoffDialog(Account account, Runnable onTakeoff) { + var view = new TakeoffView(account); + view.getTakeoff().addActionListener(e -> { + account.takeoff(view.getAmount()); + onTakeoff.run(); + view.dispose(); + }); + view.getCancel().addActionListener(e -> view.dispose()); + view.showDialog(); + } +} diff --git a/src/me/teridax/jcash/gui/takeoff/TakeoffView.java b/src/me/teridax/jcash/gui/takeoff/TakeoffView.java new file mode 100644 index 0000000..4eed846 --- /dev/null +++ b/src/me/teridax/jcash/gui/takeoff/TakeoffView.java @@ -0,0 +1,118 @@ +package me.teridax.jcash.gui.takeoff; + +import me.teridax.jcash.banking.Account; +import me.teridax.jcash.decode.StringUtils; + +import javax.swing.*; +import java.awt.*; +import java.text.NumberFormat; +import java.text.ParseException; + +import static javax.swing.JOptionPane.ERROR_MESSAGE; +import static javax.swing.JOptionPane.showMessageDialog; + +public class TakeoffView { + + private JDialog dialog; + private JButton cancel; + private JButton takeoff; + private JFormattedTextField value; + + public TakeoffView(Account account) { + createComponents(account); + layoutComponents(); + } + + public void showDialog() { + dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL); + dialog.setTitle("Takeoff money"); + dialog.pack(); + dialog.setResizable(false); + dialog.setLocationRelativeTo(null); + dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + dialog.setVisible(true); + } + + private void layoutComponents() { + var c = new GridBagConstraints(); + + c.gridx = 0; + c.gridy = 0; + c.weightx = 1; + c.weighty = 1; + c.gridwidth = 3; + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.CENTER; + c.insets = new Insets(4, 4, 4, 4); + dialog.getContentPane().add(new JLabel("Takeoff money"), c); + + c.gridx = 0; + c.gridy = 1; + c.gridwidth = 1; + c.fill = GridBagConstraints.NONE; + c.anchor = GridBagConstraints.LAST_LINE_END; + c.weightx = 0; + dialog.getContentPane().add(new JLabel("Value", SwingConstants.RIGHT), c); + + c.gridx = 1; + c.gridy = 1; + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.LAST_LINE_END; + c.weightx = 0.5; + dialog.getContentPane().add(value, c); + + c.gridx = 2; + c.gridy = 1; + c.fill = GridBagConstraints.NONE; + c.anchor = GridBagConstraints.LINE_START; + c.weightx = 0; + dialog.getContentPane().add(new JLabel("€"), c); + + var buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING)); + buttonPanel.add(cancel); + buttonPanel.add(takeoff); + + c.gridx = 0; + c.gridy = 2; + c.gridwidth = 3; + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.LAST_LINE_END; + c.insets = new Insets(10, 10, 10, 10); + dialog.getContentPane().add(buttonPanel, c); + } + + private void createComponents(Account account) { + this.dialog = new JDialog(); + + this.cancel = new JButton("Cancel"); + this.takeoff = new JButton("Takeoff"); + this.value = new JFormattedTextField(StringUtils.LOCAL_NUMBER_FORMAT); + + this.dialog.setContentPane(new JPanel(new GridBagLayout())); + } + + public double getAmount() { + if (value.getText().isBlank()) { + showMessageDialog(null, "invalid amount", "currency must not be blank", ERROR_MESSAGE); + return 0; + } + + try { + return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue(); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + public JButton getCancel() { + return cancel; + } + + public JButton getTakeoff() { + return takeoff; + } + + public void dispose() { + this.dialog.dispose(); + } +} diff --git a/src/me/teridax/jcash/gui/transfer/TransferDialog.java b/src/me/teridax/jcash/gui/transfer/TransferDialog.java new file mode 100644 index 0000000..9f3aa4d --- /dev/null +++ b/src/me/teridax/jcash/gui/transfer/TransferDialog.java @@ -0,0 +1,17 @@ +package me.teridax.jcash.gui.transfer; + +import me.teridax.jcash.banking.Account; + +public class TransferDialog { + + public TransferDialog(Account account, Runnable onDeposit) { + var view = new TransferView(account); + view.getTransfer().addActionListener(e -> { + account.takeoff(view.getAmount()); + onDeposit.run(); + view.dispose(); + }); + view.getCancel().addActionListener(e -> view.dispose()); + view.showDialog(); + } +} diff --git a/src/me/teridax/jcash/gui/transfer/TransferView.java b/src/me/teridax/jcash/gui/transfer/TransferView.java new file mode 100644 index 0000000..80b00ee --- /dev/null +++ b/src/me/teridax/jcash/gui/transfer/TransferView.java @@ -0,0 +1,144 @@ +package me.teridax.jcash.gui.transfer; + +import me.teridax.jcash.banking.Account; +import me.teridax.jcash.decode.StringUtils; + +import javax.swing.*; +import java.awt.*; +import java.text.NumberFormat; +import java.text.ParseException; + +import static javax.swing.JOptionPane.ERROR_MESSAGE; +import static javax.swing.JOptionPane.showMessageDialog; + +public class TransferView { + + private JDialog dialog; + private JButton cancel; + private JButton transfer; + private JFormattedTextField iban; + private JFormattedTextField blz; + private JFormattedTextField value; + + public TransferView(Account account) { + createComponents(account); + layoutComponents(); + } + + public void showDialog() { + dialog.setModalityType(Dialog.ModalityType.APPLICATION_MODAL); + dialog.setTitle("Transfer money"); + dialog.pack(); + dialog.setSize(dialog.getWidth() * 2, dialog.getHeight()); + dialog.setResizable(false); + dialog.setLocationRelativeTo(null); + dialog.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + dialog.setVisible(true); + } + + private void layoutComponents() { + var c = new GridBagConstraints(); + + c.gridx = 0; + c.gridy = 0; + c.weightx = 1; + c.weighty = 1; + c.gridwidth = 3; + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.CENTER; + c.insets = new Insets(4, 4, 4, 4); + dialog.getContentPane().add(new JLabel("Transfer money"), c); + + c.gridx = 0; + c.gridy = 1; + c.gridwidth = 1; + c.fill = GridBagConstraints.NONE; + c.anchor = GridBagConstraints.LAST_LINE_END; + c.weightx = 0; + dialog.getContentPane().add(new JLabel("BLZ", SwingConstants.RIGHT), c); + + c.gridx = 1; + c.gridy = 1; + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.LAST_LINE_END; + c.weightx = 0.5; + dialog.getContentPane().add(blz, c); + + c.gridx = 2; + c.gridy = 1; + c.fill = GridBagConstraints.NONE; + c.anchor = GridBagConstraints.LAST_LINE_END; + c.weightx = 0; + dialog.getContentPane().add(new JLabel("IBAN", SwingConstants.RIGHT), c); + + c.gridx = 3; + c.gridy = 1; + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.LAST_LINE_END; + c.weightx = 1; + dialog.getContentPane().add(iban, c); + + c.gridx = 0; + c.gridy = 2; + c.fill = GridBagConstraints.NONE; + c.anchor = GridBagConstraints.LAST_LINE_END; + c.weightx = 0; + dialog.getContentPane().add(new JLabel("Betrag", SwingConstants.RIGHT), c); + + c.gridx = 1; + c.gridy = 2; + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.LAST_LINE_END; + c.weightx = 0.5; + dialog.getContentPane().add(value, c); + + var buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING)); + buttonPanel.add(cancel); + buttonPanel.add(transfer); + + c.gridx = 0; + c.gridy = 3; + c.gridwidth = 4; + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.LAST_LINE_END; + c.insets = new Insets(10, 10, 10, 10); + dialog.getContentPane().add(buttonPanel, c); + } + + private void createComponents(Account account) { + this.dialog = new JDialog(); + + this.cancel = new JButton("Cancel"); + this.transfer = new JButton("Transfer"); + this.value = new JFormattedTextField(StringUtils.LOCAL_NUMBER_FORMAT); + this.iban = new JFormattedTextField(); + this.blz = new JFormattedTextField(); + + this.dialog.setContentPane(new JPanel(new GridBagLayout())); + } + + public double getAmount() { + if (value.getText().isBlank()) { + showMessageDialog(null, "invalid amount", "currency must not be blank", ERROR_MESSAGE); + return 0; + } + + try { + return NumberFormat.getNumberInstance().parse(value.getText()).doubleValue(); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + public JButton getCancel() { + return cancel; + } + + public JButton getTransfer() { + return transfer; + } + + public void dispose() { + this.dialog.dispose(); + } +}