diff --git a/recorder/pom.xml b/recorder/pom.xml
index 3ae0933..a1241c4 100644
--- a/recorder/pom.xml
+++ b/recorder/pom.xml
@@ -25,6 +25,11 @@
flatlaf
3.1
+
+ org.jfree
+ jfreechart
+ 1.5.3
+
\ No newline at end of file
diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/AddRunRecordDialog.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/AddRunRecordDialog.java
new file mode 100644
index 0000000..4e994fb
--- /dev/null
+++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/AddRunRecordDialog.java
@@ -0,0 +1,208 @@
+package com.github.andrewlalis.running_every_day.view;
+
+import com.github.andrewlalis.running_every_day.data.DataSource;
+import com.github.andrewlalis.running_every_day.data.RunRecord;
+
+import javax.swing.*;
+import java.awt.*;
+import java.math.BigDecimal;
+import java.sql.SQLException;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class AddRunRecordDialog extends JDialog {
+ private final DataSource dataSource;
+
+ private final JTextField dateField;
+ private final JTextField timeField;
+ private final JTextField distanceField;
+ private final JTextField durationField;
+ private final JTextField weightField;
+ private final JTextArea commentField;
+
+ private boolean added = false;
+
+ public AddRunRecordDialog(Window owner, DataSource dataSource) {
+ super(owner, "Add a Record", ModalityType.APPLICATION_MODAL);
+
+ this.dataSource = dataSource;
+
+ this.dateField = new JTextField(LocalDate.now().toString(), 0);
+ this.timeField = new JTextField(LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm")), 0);
+ this.distanceField = new JTextField("5.0", 0);
+ this.durationField = new JTextField("00:00:00", 0);
+ this.weightField = new JTextField("85.0", 0);
+ this.commentField = new JTextArea(3, 0);
+
+ JPanel mainPanel = new JPanel(new BorderLayout());
+ JPanel contentPanel = new JPanel();
+ contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.PAGE_AXIS));
+ contentPanel.add(buildFieldsPanel());
+ mainPanel.add(contentPanel, BorderLayout.NORTH);
+
+ JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
+ JButton addButton = new JButton("Add");
+ addButton.addActionListener(e -> onAdd());
+ JButton cancelButton = new JButton("Cancel");
+ cancelButton.addActionListener(e -> this.dispose());
+ buttonPanel.add(addButton);
+ buttonPanel.add(cancelButton);
+ mainPanel.add(buttonPanel, BorderLayout.SOUTH);
+ this.setContentPane(mainPanel);
+
+ this.setPreferredSize(new Dimension(400, 300));
+ this.pack();
+ this.setLocationRelativeTo(owner);
+ }
+
+ public boolean isAdded() {
+ return added;
+ }
+
+ private Container buildFieldsPanel() {
+ JPanel panel = new JPanel(new GridBagLayout());
+ var c = new GridBagConstraints();
+ c.insets = new Insets(5, 5, 5, 5);
+ c.weightx = 0;
+ c.weighty = 0;
+ c.gridx = 0;
+ c.gridy = 0;
+ c.anchor = GridBagConstraints.NORTHWEST;
+ String[] labels = new String[]{"Date", "Start Time", "Distance (Km)", "Duration (HH:MM:SS)", "Weight (Kg)", "Comments"};
+ for (var label : labels) {
+ panel.add(new JLabel(label), c);
+ c.gridy++;
+ }
+ Component[] fields = new Component[]{dateField, timeField, distanceField, durationField, weightField, commentField};
+ c.weightx = 1;
+ c.weighty = 1;
+ c.gridx = 1;
+ c.gridy = 0;
+ c.anchor = GridBagConstraints.NORTHEAST;
+ for (var field : fields) {
+ if (field instanceof JTextArea) {
+ c.fill = GridBagConstraints.BOTH;
+ } else {
+ c.fill = GridBagConstraints.HORIZONTAL;
+ }
+ panel.add(field, c);
+ c.gridy++;
+ }
+ return panel;
+ }
+
+ private List validateForm() {
+ List errorMessages = new ArrayList<>();
+ LocalDate date = null;
+ try {
+ date = LocalDate.parse(dateField.getText());
+ if (date.isAfter(LocalDate.now())) {
+ errorMessages.add("Cannot add a run for a date in the future.");
+ }
+ } catch (DateTimeParseException e) {
+ errorMessages.add("Invalid date format. Should be YYYY-MM-DD.");
+ }
+ try {
+ var time = LocalTime.parse(timeField.getText());
+ if (date != null && date.atTime(time).isAfter(LocalDateTime.now())) {
+ errorMessages.add("Cannot add a run for a time in the future.");
+ }
+ } catch (DateTimeParseException e) {
+ errorMessages.add("Invalid start time format. Should be HH:MM.");
+ }
+
+ try {
+ BigDecimal distanceKm = new BigDecimal(distanceField.getText());
+ if (distanceKm.compareTo(BigDecimal.ZERO) < 0 || distanceKm.scale() > 3) {
+ errorMessages.add("Invalid distance.");
+ }
+ } catch (NumberFormatException e) {
+ errorMessages.add("Invalid or missing distance.");
+ }
+
+ try {
+ Duration duration = parseDuration(durationField.getText());
+ if (duration.isNegative() || duration.isZero()) {
+ errorMessages.add("Duration must be positive.");
+ }
+ } catch (IllegalArgumentException e) {
+ errorMessages.add(e.getMessage());
+ }
+
+ try {
+ BigDecimal weightKg = new BigDecimal(weightField.getText());
+ if (weightKg.compareTo(BigDecimal.ZERO) < 0 || weightKg.scale() > 3) {
+ errorMessages.add("Invalid weight.");
+ }
+ } catch (NumberFormatException e) {
+ errorMessages.add("Invalid or missing weight.");
+ }
+
+ if (commentField.getText().isBlank()) {
+ errorMessages.add("Comments should not be empty.");
+ }
+
+ return errorMessages;
+ }
+
+ private void onAdd() {
+ var messages = validateForm();
+ if (!messages.isEmpty()) {
+ JOptionPane.showMessageDialog(
+ this,
+ "Form validation failed:\n" + String.join("\n", messages),
+ "Validation Failed",
+ JOptionPane.WARNING_MESSAGE
+ );
+ } else {
+ try {
+ this.dataSource.runRecords().save(new RunRecord(
+ 0,
+ LocalDate.parse(dateField.getText()),
+ LocalTime.parse(timeField.getText()),
+ new BigDecimal(distanceField.getText()),
+ parseDuration(durationField.getText()),
+ new BigDecimal(weightField.getText()),
+ commentField.getText().strip()
+ ));
+ this.added = true;
+ this.dispose();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ JOptionPane.showMessageDialog(
+ this,
+ "An SQL Exception occurred: " + e.getMessage(),
+ "Error",
+ JOptionPane.ERROR_MESSAGE
+ );
+ }
+
+ }
+ }
+
+ private static Duration parseDuration(String s) {
+ String[] parts = s.strip().split(":");
+ if (parts.length < 2 || parts.length > 3) {
+ throw new IllegalArgumentException("Invalid or missing duration.");
+ }
+ int hours = 0;
+ int minutes = 0;
+ int seconds = 0;
+ if (parts.length == 3) {
+ hours = Integer.parseInt(parts[0]);
+ minutes = Integer.parseInt(parts[1]);
+ seconds = Integer.parseInt(parts[2]);
+ } else {
+ minutes = Integer.parseInt(parts[0]);
+ seconds = Integer.parseInt(parts[1]);
+ }
+ return Duration.ofSeconds(hours * 60L * 60 + minutes * 60L + seconds);
+ }
+}
diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/ChartsPanel.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/ChartsPanel.java
new file mode 100644
index 0000000..b23af7f
--- /dev/null
+++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/ChartsPanel.java
@@ -0,0 +1,48 @@
+package com.github.andrewlalis.running_every_day.view;
+
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.JFreeChart;
+import org.jfree.data.category.DefaultCategoryDataset;
+import org.jfree.data.time.Day;
+import org.jfree.data.time.TimeSeries;
+import org.jfree.data.time.TimeSeriesCollection;
+import org.jfree.data.xy.XYDataset;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.geom.Rectangle2D;
+
+public class ChartsPanel extends JPanel {
+ private final JPanel drawingPanel = new JPanel();
+
+ public ChartsPanel() {
+ super(new BorderLayout());
+ this.add(drawingPanel, BorderLayout.CENTER);
+
+ JPanel buttonPanel = new JPanel();
+ JButton drawButton = new JButton("Draw");
+ drawButton.addActionListener(e -> draw());
+ buttonPanel.add(drawButton);
+ this.add(buttonPanel, BorderLayout.NORTH);
+ }
+
+ public void draw() {
+ TimeSeriesCollection ds = new TimeSeriesCollection();
+ TimeSeries ts = new TimeSeries("Data");
+ ts.add(new Day(16, 4, 2023), 45);
+ ts.add(new Day(17, 4, 2023), 50);
+ ts.add(new Day(18, 4, 2023), 52);
+ ts.add(new Day(19, 4, 2023), 65);
+ ds.addSeries(ts);
+
+ JFreeChart chart = ChartFactory.createXYLineChart(
+ "Test XY Line Chart",
+ "Date",
+ "Value",
+ ds
+ );
+ Graphics2D g2 = (Graphics2D) drawingPanel.getGraphics();
+ Rectangle2D area = new Rectangle2D.Float(0, 0, drawingPanel.getWidth(), drawingPanel.getHeight());
+ chart.draw(g2, area);
+ }
+}
diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RecorderAppWindow.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RecorderAppWindow.java
index 9400c8e..780a1fd 100644
--- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RecorderAppWindow.java
+++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RecorderAppWindow.java
@@ -18,16 +18,8 @@ public class RecorderAppWindow extends JFrame {
private Container buildGui(DataSource dataSource) {
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("Run Records", new RunRecordsPanel(dataSource));
- tabbedPane.addTab("Aggregate Statistics", buildAggregateStatisticsPanel(dataSource));
- tabbedPane.addTab("Charts", buildChartsPanel(dataSource));
+ tabbedPane.addTab("Aggregate Statistics", new JPanel());
+ tabbedPane.addTab("Charts", new ChartsPanel());
return tabbedPane;
}
-
- private Container buildAggregateStatisticsPanel(DataSource dataSource) {
- return new JPanel();
- }
-
- private Container buildChartsPanel(DataSource dataSource) {
- return new JPanel();
- }
}
diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordTableModel.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordTableModel.java
index 078be60..c59157f 100644
--- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordTableModel.java
+++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordTableModel.java
@@ -68,7 +68,7 @@ public class RunRecordTableModel extends AbstractTableModel {
return currentPage;
}
- private void loadPage() {
+ public void loadPage() {
try {
var page = dataSource.runRecords().findAll(currentPage);
cachedPageCount = (int) dataSource.runRecords().pageCount(currentPage.size());
diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java
index 07297ca..d8ee9e6 100644
--- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java
+++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java
@@ -28,8 +28,12 @@ public class RunRecordsPanel extends JPanel {
table.getColumnModel().getColumn(1).setMaxWidth(80);
table.getColumnModel().getColumn(2).setMaxWidth(80);
table.getColumnModel().getColumn(3).setMaxWidth(100);
+ table.getColumnModel().getColumn(3).setPreferredWidth(100);
table.getColumnModel().getColumn(4).setMaxWidth(80);
table.getColumnModel().getColumn(5).setMaxWidth(80);
+ for (int i = 0; i < 6; i++) {
+ table.getColumnModel().getColumn(i).setResizable(false);
+ }
var scrollPane = new JScrollPane(table, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
this.add(scrollPane, BorderLayout.CENTER);
@@ -86,6 +90,13 @@ public class RunRecordsPanel extends JPanel {
JPanel actionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
JButton addActionButton = new JButton("Add a Record");
+ addActionButton.addActionListener(e -> {
+ var dialog = new AddRunRecordDialog(SwingUtilities.getWindowAncestor(this), dataSource);
+ dialog.setVisible(true);
+ if (dialog.isAdded()) {
+ tableModel.firstPage();
+ }
+ });
actionsPanel.add(addActionButton);
this.add(actionsPanel, BorderLayout.NORTH);
}