Added the ability to navigate to a specific transaction.
This commit is contained in:
parent
b477e9ab3c
commit
173204c61c
|
@ -54,14 +54,14 @@ public class TransactionViewController {
|
||||||
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
|
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
|
||||||
CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.getId());
|
CreditAndDebitAccounts accounts = repo.findLinkedAccounts(transaction.getId());
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (accounts.hasDebit()) {
|
accounts.ifDebit(acc -> {
|
||||||
debitAccountLink.setText(accounts.debitAccount().getShortName());
|
debitAccountLink.setText(acc.getShortName());
|
||||||
debitAccountLink.setOnAction(event -> router.navigate("account", accounts.debitAccount()));
|
debitAccountLink.setOnAction(event -> router.navigate("account", acc));
|
||||||
}
|
});
|
||||||
if (accounts.hasCredit()) {
|
accounts.ifCredit(acc -> {
|
||||||
creditAccountLink.setText(accounts.creditAccount().getShortName());
|
creditAccountLink.setText(acc.getShortName());
|
||||||
creditAccountLink.setOnAction(event -> router.navigate("account", accounts.creditAccount()));
|
creditAccountLink.setOnAction(event -> router.navigate("account", acc));
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -105,5 +105,6 @@ public class TransactionViewController {
|
||||||
TextFlow parent = (TextFlow) link.getParent();
|
TextFlow parent = (TextFlow) link.getParent();
|
||||||
parent.managedProperty().bind(parent.visibleProperty());
|
parent.managedProperty().bind(parent.visibleProperty());
|
||||||
parent.visibleProperty().bind(link.textProperty().isNotEmpty());
|
parent.visibleProperty().bind(link.textProperty().isNotEmpty());
|
||||||
|
link.setText(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package com.andrewlalis.perfin.control;
|
package com.andrewlalis.perfin.control;
|
||||||
|
|
||||||
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
||||||
import com.andrewlalis.perfin.data.util.Pair;
|
|
||||||
import com.andrewlalis.perfin.view.SceneUtil;
|
|
||||||
import com.andrewlalis.perfin.data.pagination.Page;
|
import com.andrewlalis.perfin.data.pagination.Page;
|
||||||
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
||||||
import com.andrewlalis.perfin.data.pagination.Sort;
|
import com.andrewlalis.perfin.data.pagination.Sort;
|
||||||
|
import com.andrewlalis.perfin.data.util.Pair;
|
||||||
import com.andrewlalis.perfin.model.Profile;
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
import com.andrewlalis.perfin.model.Transaction;
|
import com.andrewlalis.perfin.model.Transaction;
|
||||||
|
import com.andrewlalis.perfin.view.SceneUtil;
|
||||||
import com.andrewlalis.perfin.view.component.DataSourcePaginationControls;
|
import com.andrewlalis.perfin.view.component.DataSourcePaginationControls;
|
||||||
import com.andrewlalis.perfin.view.component.TransactionTile;
|
import com.andrewlalis.perfin.view.component.TransactionTile;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
@ -19,9 +19,21 @@ import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import static com.andrewlalis.perfin.PerfinApp.router;
|
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for the view of all transactions in a user's profile.
|
||||||
|
* Transactions are displayed in a paginated manner, and this controller
|
||||||
|
* accepts as a route context a {@link PageRequest} to initialize the results
|
||||||
|
* to a specific page.
|
||||||
|
*/
|
||||||
public class TransactionsViewController implements RouteSelectionListener {
|
public class TransactionsViewController implements RouteSelectionListener {
|
||||||
|
public static List<Sort> DEFAULT_SORTS = List.of(Sort.desc("timestamp"));
|
||||||
|
public static int DEFAULT_ITEMS_PER_PAGE = 5;
|
||||||
|
public record RouteContext(Long selectedTransactionId) {}
|
||||||
|
|
||||||
@FXML public BorderPane transactionsListBorderPane;
|
@FXML public BorderPane transactionsListBorderPane;
|
||||||
@FXML public VBox transactionsVBox;
|
@FXML public VBox transactionsVBox;
|
||||||
private DataSourcePaginationControls paginationControls;
|
private DataSourcePaginationControls paginationControls;
|
||||||
|
@ -80,8 +92,23 @@ public class TransactionsViewController implements RouteSelectionListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRouteSelected(Object context) {
|
public void onRouteSelected(Object context) {
|
||||||
paginationControls.sorts.setAll(Sort.desc("timestamp"));
|
paginationControls.sorts.setAll(DEFAULT_SORTS);
|
||||||
this.paginationControls.setPage(1);
|
paginationControls.itemsPerPage.set(DEFAULT_ITEMS_PER_PAGE);
|
||||||
|
|
||||||
|
// If a transaction id is given in the route context, navigate to the page it's on and select it.
|
||||||
|
if (context instanceof RouteContext ctx && ctx.selectedTransactionId != null) {
|
||||||
|
Thread.ofVirtual().start(() -> {
|
||||||
|
Profile.getCurrent().getDataSource().useTransactionRepository(repo -> {
|
||||||
|
repo.findById(ctx.selectedTransactionId).ifPresent(tx -> {
|
||||||
|
long offset = repo.countAllAfter(tx.getId());
|
||||||
|
int pageNumber = (int) (offset / DEFAULT_ITEMS_PER_PAGE) + 1;
|
||||||
|
paginationControls.setPage(pageNumber).thenRun(() -> selectedTransaction.set(tx));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
paginationControls.setPage(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
|
@ -92,13 +119,13 @@ public class TransactionsViewController implements RouteSelectionListener {
|
||||||
private TransactionTile makeTile(Transaction transaction) {
|
private TransactionTile makeTile(Transaction transaction) {
|
||||||
var tile = new TransactionTile(transaction);
|
var tile = new TransactionTile(transaction);
|
||||||
tile.setOnMouseClicked(event -> {
|
tile.setOnMouseClicked(event -> {
|
||||||
if (selectedTransaction.get() == null || selectedTransaction.get().getId() != transaction.getId()) {
|
if (selectedTransaction.get() == null || !selectedTransaction.get().equals(transaction)) {
|
||||||
selectedTransaction.set(transaction);
|
selectedTransaction.set(transaction);
|
||||||
} else {
|
} else {
|
||||||
selectedTransaction.set(null);
|
selectedTransaction.set(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
tile.selected.bind(selectedTransaction.map(t -> t != null && t.getId() == transaction.getId()));
|
tile.selected.bind(selectedTransaction.map(t -> t != null && t.equals(transaction)));
|
||||||
return tile;
|
return tile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import java.nio.file.Path;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Currency;
|
import java.util.Currency;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public interface TransactionRepository extends AutoCloseable {
|
public interface TransactionRepository extends AutoCloseable {
|
||||||
|
@ -22,8 +23,10 @@ public interface TransactionRepository extends AutoCloseable {
|
||||||
CreditAndDebitAccounts linkedAccounts,
|
CreditAndDebitAccounts linkedAccounts,
|
||||||
List<Path> attachments
|
List<Path> attachments
|
||||||
);
|
);
|
||||||
|
Optional<Transaction> findById(long id);
|
||||||
Page<Transaction> findAll(PageRequest pagination);
|
Page<Transaction> findAll(PageRequest pagination);
|
||||||
long countAll();
|
long countAll();
|
||||||
|
long countAllAfter(long transactionId);
|
||||||
Page<Transaction> findAllByAccounts(Set<Long> accountIds, PageRequest pagination);
|
Page<Transaction> findAllByAccounts(Set<Long> accountIds, PageRequest pagination);
|
||||||
CreditAndDebitAccounts findLinkedAccounts(long transactionId);
|
CreditAndDebitAccounts findLinkedAccounts(long transactionId);
|
||||||
List<Attachment> findAttachments(long transactionId);
|
List<Attachment> findAttachments(long transactionId);
|
||||||
|
|
|
@ -2,10 +2,10 @@ package com.andrewlalis.perfin.data.impl;
|
||||||
|
|
||||||
import com.andrewlalis.perfin.data.AccountEntryRepository;
|
import com.andrewlalis.perfin.data.AccountEntryRepository;
|
||||||
import com.andrewlalis.perfin.data.AttachmentRepository;
|
import com.andrewlalis.perfin.data.AttachmentRepository;
|
||||||
import com.andrewlalis.perfin.data.util.DbUtil;
|
|
||||||
import com.andrewlalis.perfin.data.TransactionRepository;
|
import com.andrewlalis.perfin.data.TransactionRepository;
|
||||||
import com.andrewlalis.perfin.data.pagination.Page;
|
import com.andrewlalis.perfin.data.pagination.Page;
|
||||||
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
||||||
|
import com.andrewlalis.perfin.data.util.DbUtil;
|
||||||
import com.andrewlalis.perfin.model.*;
|
import com.andrewlalis.perfin.model.*;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
@ -14,10 +14,7 @@ import java.sql.Connection;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Collections;
|
import java.util.*;
|
||||||
import java.util.Currency;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public record JdbcTransactionRepository(Connection conn, Path contentDir) implements TransactionRepository {
|
public record JdbcTransactionRepository(Connection conn, Path contentDir) implements TransactionRepository {
|
||||||
|
@ -55,6 +52,11 @@ public record JdbcTransactionRepository(Connection conn, Path contentDir) implem
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<Transaction> findById(long id) {
|
||||||
|
return DbUtil.findById(conn, "SELECT * FROM transaction WHERE id = ?", id, JdbcTransactionRepository::parseTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<Transaction> findAll(PageRequest pagination) {
|
public Page<Transaction> findAll(PageRequest pagination) {
|
||||||
return DbUtil.findAll(
|
return DbUtil.findAll(
|
||||||
|
@ -70,6 +72,16 @@ public record JdbcTransactionRepository(Connection conn, Path contentDir) implem
|
||||||
return DbUtil.findOne(conn, "SELECT COUNT(id) FROM transaction", Collections.emptyList(), rs -> rs.getLong(1)).orElse(0L);
|
return DbUtil.findOne(conn, "SELECT COUNT(id) FROM transaction", Collections.emptyList(), rs -> rs.getLong(1)).orElse(0L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long countAllAfter(long transactionId) {
|
||||||
|
return DbUtil.findOne(
|
||||||
|
conn,
|
||||||
|
"SELECT COUNT(id) FROM transaction WHERE timestamp > (SELECT timestamp FROM transaction WHERE id = ?)",
|
||||||
|
List.of(transactionId),
|
||||||
|
rs -> rs.getLong(1)
|
||||||
|
).orElse(0L);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<Transaction> findAllByAccounts(Set<Long> accountIds, PageRequest pagination) {
|
public Page<Transaction> findAllByAccounts(Set<Long> accountIds, PageRequest pagination) {
|
||||||
String idsStr = accountIds.stream().map(String::valueOf).collect(Collectors.joining(","));
|
String idsStr = accountIds.stream().map(String::valueOf).collect(Collectors.joining(","));
|
||||||
|
|
|
@ -11,11 +11,11 @@ import java.util.Currency;
|
||||||
* entries that apply this transaction's amount to one or more accounts.
|
* entries that apply this transaction's amount to one or more accounts.
|
||||||
*/
|
*/
|
||||||
public class Transaction {
|
public class Transaction {
|
||||||
private long id;
|
private final long id;
|
||||||
private LocalDateTime timestamp;
|
private final LocalDateTime timestamp;
|
||||||
|
|
||||||
private BigDecimal amount;
|
private final BigDecimal amount;
|
||||||
private Currency currency;
|
private final Currency currency;
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
public Transaction(long id, LocalDateTime timestamp, BigDecimal amount, Currency currency, String description) {
|
public Transaction(long id, LocalDateTime timestamp, BigDecimal amount, Currency currency, String description) {
|
||||||
|
@ -26,13 +26,6 @@ public class Transaction {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Transaction(LocalDateTime timestamp, BigDecimal amount, Currency currency, String description) {
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
this.amount = amount;
|
|
||||||
this.currency = currency;
|
|
||||||
this.description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getId() {
|
public long getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
@ -52,4 +45,9 @@ public class Transaction {
|
||||||
public String getDescription() {
|
public String getDescription() {
|
||||||
return description;
|
return description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
return other instanceof Transaction tx && id == tx.id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.andrewlalis.perfin.view.component;
|
package com.andrewlalis.perfin.view.component;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.control.TransactionsViewController;
|
||||||
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
import com.andrewlalis.perfin.data.AccountHistoryItemRepository;
|
||||||
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
import com.andrewlalis.perfin.data.util.CurrencyUtil;
|
||||||
import com.andrewlalis.perfin.data.util.DateUtil;
|
import com.andrewlalis.perfin.data.util.DateUtil;
|
||||||
|
@ -13,6 +14,8 @@ import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
import javafx.scene.text.TextFlow;
|
import javafx.scene.text.TextFlow;
|
||||||
|
|
||||||
|
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tile that shows a brief bit of information about an account history item.
|
* A tile that shows a brief bit of information about an account history item.
|
||||||
*/
|
*/
|
||||||
|
@ -41,9 +44,13 @@ public class AccountHistoryItemTile extends BorderPane {
|
||||||
private Node buildAccountEntryItem(AccountEntry entry) {
|
private Node buildAccountEntryItem(AccountEntry entry) {
|
||||||
Text amountText = new Text(CurrencyUtil.formatMoney(entry.getSignedAmount(), entry.getCurrency()));
|
Text amountText = new Text(CurrencyUtil.formatMoney(entry.getSignedAmount(), entry.getCurrency()));
|
||||||
Hyperlink transactionLink = new Hyperlink("Transaction #" + entry.getTransactionId());
|
Hyperlink transactionLink = new Hyperlink("Transaction #" + entry.getTransactionId());
|
||||||
|
transactionLink.setOnAction(event -> router.navigate(
|
||||||
|
"transactions",
|
||||||
|
new TransactionsViewController.RouteContext(entry.getTransactionId())
|
||||||
|
));
|
||||||
return new TextFlow(
|
return new TextFlow(
|
||||||
transactionLink,
|
transactionLink,
|
||||||
new Text("posted as a " + entry.getType().name().toLowerCase() + " to this account, with a value of"),
|
new Text("posted as a " + entry.getType().name().toLowerCase() + " to this account, with a value of "),
|
||||||
amountText
|
amountText
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.andrewlalis.perfin.view.component;
|
package com.andrewlalis.perfin.view.component;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.PerfinApp;
|
||||||
import com.andrewlalis.perfin.model.Attachment;
|
import com.andrewlalis.perfin.model.Attachment;
|
||||||
import com.andrewlalis.perfin.model.Profile;
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
import com.andrewlalis.perfin.view.ImageCache;
|
import com.andrewlalis.perfin.view.ImageCache;
|
||||||
|
@ -11,6 +12,7 @@ import javafx.scene.paint.Color;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -58,7 +60,8 @@ public class AttachmentPreview extends BorderPane {
|
||||||
this.setCenter(stackPane);
|
this.setCenter(stackPane);
|
||||||
this.setOnMouseClicked(event -> {
|
this.setOnMouseClicked(event -> {
|
||||||
if (this.isHover()) {
|
if (this.isHover()) {
|
||||||
System.out.println("Opening attachment: " + attachment.getFilename());
|
Path filePath = attachment.getPath(Profile.getContentDir(Profile.getCurrent().getName()));
|
||||||
|
PerfinApp.instance.getHostServices().showDocument(filePath.toAbsolutePath().toUri().toString());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ListChangeListener;
|
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
@ -20,6 +19,8 @@ import javafx.scene.text.Text;
|
||||||
import javafx.scene.text.TextAlignment;
|
import javafx.scene.text.TextAlignment;
|
||||||
import javafx.scene.text.TextFlow;
|
import javafx.scene.text.TextFlow;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A pane that contains some controls for navigating a paginated data source.
|
* A pane that contains some controls for navigating a paginated data source.
|
||||||
* That includes going to the next/previous page, setting the preferred page
|
* That includes going to the next/previous page, setting the preferred page
|
||||||
|
@ -66,10 +67,6 @@ public class DataSourcePaginationControls extends BorderPane {
|
||||||
nextPageButton.disableProperty().bind(fetching.or(currentPage.greaterThanOrEqualTo(maxPages)));
|
nextPageButton.disableProperty().bind(fetching.or(currentPage.greaterThanOrEqualTo(maxPages)));
|
||||||
nextPageButton.setOnAction(event -> setPage(currentPage.get() + 1));
|
nextPageButton.setOnAction(event -> setPage(currentPage.get() + 1));
|
||||||
|
|
||||||
// sorts.addListener((ListChangeListener<Sort>) c -> {
|
|
||||||
// setPage(1);
|
|
||||||
// });
|
|
||||||
|
|
||||||
HBox hbox = new HBox(
|
HBox hbox = new HBox(
|
||||||
previousPageButton,
|
previousPageButton,
|
||||||
pageTextContainer,
|
pageTextContainer,
|
||||||
|
@ -79,7 +76,8 @@ public class DataSourcePaginationControls extends BorderPane {
|
||||||
setCenter(hbox);
|
setCenter(hbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPage(int page) {
|
public CompletableFuture<Void> setPage(int page) {
|
||||||
|
CompletableFuture<Void> cf = new CompletableFuture<>();
|
||||||
fetching.set(true);
|
fetching.set(true);
|
||||||
PageRequest pagination = new PageRequest(page - 1, itemsPerPage.get(), sorts);
|
PageRequest pagination = new PageRequest(page - 1, itemsPerPage.get(), sorts);
|
||||||
Thread.ofVirtual().start(() -> {
|
Thread.ofVirtual().start(() -> {
|
||||||
|
@ -97,14 +95,17 @@ public class DataSourcePaginationControls extends BorderPane {
|
||||||
}
|
}
|
||||||
currentPage.set(page);
|
currentPage.set(page);
|
||||||
fetching.set(false);
|
fetching.set(false);
|
||||||
|
cf.complete(null);
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace(System.err);
|
e.printStackTrace(System.err);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
target.clear();
|
target.clear();
|
||||||
fetching.set(false);
|
fetching.set(false);
|
||||||
|
cf.complete(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return cf;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue