From 0b2c7bda131e787f26b3ce28c01bbd1efb36b5ee Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 14 Apr 2026 03:48:46 -0600 Subject: [PATCH] added clendar to adoptions and appointments on desktop --- .../controllers/AdoptionController.java | 39 +++- .../controllers/AppointmentController.java | 31 +++ .../petshopdesktop/ui/CalendarPane.java | 204 ++++++++++++++++++ .../modelviews/adoption-view.fxml | 2 + .../modelviews/appointment-view.fxml | 2 + 5 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 desktop/src/main/java/org/example/petshopdesktop/ui/CalendarPane.java diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java index 10452d27..e135328d 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java @@ -4,6 +4,7 @@ import javafx.application.Platform; import org.example.petshopdesktop.auth.UserSession; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; @@ -16,13 +17,16 @@ import org.example.petshopdesktop.api.dto.adoption.AdoptionResponse; import org.example.petshopdesktop.api.endpoints.AdoptionApi; import org.example.petshopdesktop.controllers.dialogcontrollers.AdoptionDialogController; import org.example.petshopdesktop.models.Adoption; +import org.example.petshopdesktop.ui.CalendarPane; import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.TableViewSupport; import java.io.IOException; +import java.time.LocalDate; import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; public class AdoptionController { @@ -72,6 +76,11 @@ public class AdoptionController { @FXML private TextField txtSearch; + @FXML + private CalendarPane calendarPane; + private LocalDate selectedCalendarDate = null; + private FilteredList filteredAdoptions; + private ObservableList data = FXCollections.observableArrayList(); private String mode = null; @@ -92,6 +101,9 @@ public class AdoptionController { colStoreName.setCellValueFactory(new PropertyValueFactory<>("storeName")); TableViewSupport.applyCurrencyColumn(colAdoptionFee); + filteredAdoptions = new FilteredList<>(data, a -> true); + TableViewSupport.bindSortedItems(tvAdoptions, filteredAdoptions); + displayAdoptions(); TableViewSupport.installDoubleClickAction(tvAdoptions, selected -> openDialog(selected, "Edit")); @@ -106,6 +118,11 @@ public class AdoptionController { displayFilteredAdoptions(newValue); }); + calendarPane.setOnDateSelected(date -> { + selectedCalendarDate = date; + filteredAdoptions.setPredicate(a -> date == null || a.getAdoptionDate().equals(date.toString())); + }); + tvAdoptions.setOnKeyPressed(event -> { if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { if (tvAdoptions.getSelectionModel().getSelectedItem() != null) { @@ -118,6 +135,8 @@ public class AdoptionController { @FXML void btnRefresh(ActionEvent event) { txtSearch.clear(); + selectedCalendarDate = null; + if (filteredAdoptions != null) filteredAdoptions.setPredicate(a -> true); tvAdoptions.getSortOrder().clear(); displayAdoptions(); TableViewSupport.flashStatus(lblStatus, "Refreshed"); @@ -198,7 +217,15 @@ public class AdoptionController { Platform.runLater(() -> { data.setAll(adoptionList); - tvAdoptions.setItems(data); + Set dates = adoptionList.stream() + .filter(a -> a.getAdoptionDate() != null && !a.getAdoptionDate().isEmpty()) + .map(a -> { try { return LocalDate.parse(a.getAdoptionDate()); } catch (Exception ex) { return null; } }) + .filter(d -> d != null) + .collect(java.util.stream.Collectors.toSet()); + calendarPane.setEventDates(dates); + if (selectedCalendarDate != null) { + filteredAdoptions.setPredicate(a -> a.getAdoptionDate().equals(selectedCalendarDate.toString())); + } }); } catch (Exception e) { Platform.runLater(() -> { @@ -225,7 +252,15 @@ public class AdoptionController { Platform.runLater(() -> { data.setAll(adoptionList); - tvAdoptions.setItems(data); + Set dates = adoptionList.stream() + .filter(a -> a.getAdoptionDate() != null && !a.getAdoptionDate().isEmpty()) + .map(a -> { try { return LocalDate.parse(a.getAdoptionDate()); } catch (Exception ex) { return null; } }) + .filter(d -> d != null) + .collect(java.util.stream.Collectors.toSet()); + calendarPane.setEventDates(dates); + if (selectedCalendarDate != null) { + filteredAdoptions.setPredicate(a -> a.getAdoptionDate().equals(selectedCalendarDate.toString())); + } }); } catch (Exception e) { Platform.runLater(() -> { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java index 7fdd5396..96461286 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java @@ -18,10 +18,13 @@ import org.example.petshopdesktop.DTOs.AppointmentDTO; import org.example.petshopdesktop.api.dto.appointment.AppointmentResponse; import org.example.petshopdesktop.api.endpoints.AppointmentApi; import org.example.petshopdesktop.controllers.dialogcontrollers.AppointmentDialogController; +import org.example.petshopdesktop.ui.CalendarPane; import org.example.petshopdesktop.util.ActivityLogger; import org.example.petshopdesktop.util.TableViewSupport; +import java.time.LocalDate; import java.util.List; +import java.util.Set; import java.util.Comparator; import java.util.stream.Collectors; @@ -49,6 +52,9 @@ public class AppointmentController { @FXML private TextField txtSearch; + @FXML private CalendarPane calendarPane; + private LocalDate selectedCalendarDate = null; + private final ObservableList appointments = FXCollections.observableArrayList(); private FilteredList filtered; @@ -96,6 +102,11 @@ public class AppointmentController { }); loadAppointments(); + + calendarPane.setOnDateSelected(date -> { + selectedCalendarDate = date; + filtered.setPredicate(apt -> date == null || apt.getAppointmentDate().equals(date.toString())); + }); } @FXML @@ -116,6 +127,15 @@ public class AppointmentController { Platform.runLater(() -> { appointments.setAll(appointmentDTOs); + Set dates = appointmentDTOs.stream() + .filter(a -> a.getAppointmentDate() != null && !a.getAppointmentDate().isEmpty()) + .map(a -> { try { return LocalDate.parse(a.getAppointmentDate()); } catch (Exception ex) { return null; } }) + .filter(d -> d != null) + .collect(java.util.stream.Collectors.toSet()); + calendarPane.setEventDates(dates); + if (selectedCalendarDate != null) { + filtered.setPredicate(apt -> apt.getAppointmentDate().equals(selectedCalendarDate.toString())); + } }); }catch(Exception e){ Platform.runLater(() -> { @@ -143,6 +163,15 @@ public class AppointmentController { Platform.runLater(() -> { appointments.setAll(appointmentDTOs); + Set dates = appointmentDTOs.stream() + .filter(a -> a.getAppointmentDate() != null && !a.getAppointmentDate().isEmpty()) + .map(a -> { try { return LocalDate.parse(a.getAppointmentDate()); } catch (Exception ex) { return null; } }) + .filter(d -> d != null) + .collect(java.util.stream.Collectors.toSet()); + calendarPane.setEventDates(dates); + if (selectedCalendarDate != null) { + filtered.setPredicate(apt -> apt.getAppointmentDate().equals(selectedCalendarDate.toString())); + } }); } catch (Exception e) { Platform.runLater(() -> { @@ -159,6 +188,8 @@ public class AppointmentController { @FXML void btnRefresh(ActionEvent event) { txtSearch.clear(); + selectedCalendarDate = null; + filtered.setPredicate(a -> true); tvAppointments.getSortOrder().clear(); loadAppointments(); TableViewSupport.flashStatus(lblStatus, "Refreshed"); diff --git a/desktop/src/main/java/org/example/petshopdesktop/ui/CalendarPane.java b/desktop/src/main/java/org/example/petshopdesktop/ui/CalendarPane.java new file mode 100644 index 00000000..2b2afeab --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/ui/CalendarPane.java @@ -0,0 +1,204 @@ +package org.example.petshopdesktop.ui; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; + +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +public class CalendarPane extends VBox { + + private LocalDate currentDate = LocalDate.now(); + private LocalDate selectedDate = null; + private Set eventDates = new HashSet<>(); + private boolean weekMode = true; + private Consumer onDateSelected; + private Label lblHeader; + private GridPane dayGrid; + private Button btnToggle; + + public CalendarPane() { + setSpacing(6); + setPadding(new Insets(10, 12, 10, 12)); + setStyle("-fx-background-color: white; -fx-border-color: #e6e6e6; -fx-border-radius: 12; -fx-background-radius: 12;"); + + lblHeader = new Label(); + lblHeader.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(lblHeader, Priority.ALWAYS); + lblHeader.setAlignment(Pos.CENTER); + lblHeader.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); + + Button btnPrev = new Button("‹"); + btnPrev.setStyle("-fx-background-color: transparent; -fx-font-size: 18; -fx-text-fill: #7f8c8d; -fx-cursor: hand;"); + btnPrev.setOnAction(e -> navigate(-1)); + + Button btnNext = new Button("›"); + btnNext.setStyle("-fx-background-color: transparent; -fx-font-size: 18; -fx-text-fill: #7f8c8d; -fx-cursor: hand;"); + btnNext.setOnAction(e -> navigate(1)); + + btnToggle = new Button("Month"); + btnToggle.setStyle("-fx-background-color: #4ECDC4; -fx-text-fill: white; -fx-font-size: 11; -fx-font-weight: bold; -fx-background-radius: 6; -fx-cursor: hand;"); + btnToggle.setPadding(new Insets(4, 10, 4, 10)); + btnToggle.setOnAction(e -> { + weekMode = !weekMode; + btnToggle.setText(weekMode ? "Month" : "Week"); + rebuild(); + }); + + HBox navHeader = new HBox(4, btnPrev, lblHeader, btnNext, btnToggle); + navHeader.setAlignment(Pos.CENTER_LEFT); + + GridPane dowHeader = new GridPane(); + String[] days = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + for (int i = 0; i < 7; i++) { + ColumnConstraints cc = new ColumnConstraints(); + cc.setHgrow(Priority.ALWAYS); + dowHeader.getColumnConstraints().add(cc); + Label lbl = new Label(days[i]); + lbl.setStyle("-fx-font-size: 11; -fx-text-fill: #95a5a6; -fx-font-weight: bold;"); + lbl.setMaxWidth(Double.MAX_VALUE); + lbl.setAlignment(Pos.CENTER); + dowHeader.add(lbl, i, 0); + } + + dayGrid = new GridPane(); + dayGrid.setHgap(2); + dayGrid.setVgap(2); + + getChildren().addAll(navHeader, dowHeader, dayGrid); + rebuild(); + } + + public void setEventDates(Set dates) { + eventDates = new HashSet<>(dates); + rebuild(); + } + + public void setOnDateSelected(Consumer handler) { + onDateSelected = handler; + } + + private void navigate(int direction) { + if (weekMode) { + currentDate = currentDate.plusWeeks(direction); + } else { + currentDate = currentDate.plusMonths(direction); + } + rebuild(); + } + + private void rebuild() { + dayGrid.getChildren().clear(); + dayGrid.getColumnConstraints().clear(); + + for (int i = 0; i < 7; i++) { + ColumnConstraints cc = new ColumnConstraints(); + cc.setHgrow(Priority.ALWAYS); + cc.setFillWidth(true); + dayGrid.getColumnConstraints().add(cc); + } + + if (weekMode) { + int dow = currentDate.getDayOfWeek().getValue() % 7; + LocalDate startOfWeek = currentDate.minusDays(dow); + lblHeader.setText("Week of " + startOfWeek.format(DateTimeFormatter.ofPattern("MMM d, yyyy"))); + for (int col = 0; col < 7; col++) { + dayGrid.add(buildDayCell(startOfWeek.plusDays(col)), col, 0); + } + } else { + YearMonth ym = YearMonth.of(currentDate.getYear(), currentDate.getMonthValue()); + lblHeader.setText(currentDate.format(DateTimeFormatter.ofPattern("MMMM yyyy"))); + int firstDow = ym.atDay(1).getDayOfWeek().getValue() % 7; + int daysInMonth = ym.lengthOfMonth(); + int rows = (int) Math.ceil((firstDow + daysInMonth) / 7.0); + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < 7; col++) { + int dayNum = row * 7 + col - firstDow + 1; + if (dayNum >= 1 && dayNum <= daysInMonth) { + dayGrid.add(buildDayCell(ym.atDay(dayNum)), col, row); + } else { + Region spacer = new Region(); + spacer.setPrefSize(36, 36); + dayGrid.add(spacer, col, row); + } + } + } + } + } + + private StackPane buildDayCell(LocalDate date) { + boolean isSelected = date.equals(selectedDate); + boolean isToday = date.equals(LocalDate.now()); + boolean hasEvent = eventDates.contains(date); + + StackPane cell = new StackPane(); + cell.setPrefSize(36, 36); + cell.setMaxWidth(Double.MAX_VALUE); + + String baseStyle; + String labelColor; + String labelWeight = "-fx-font-weight: bold;"; + + if (isSelected) { + baseStyle = "-fx-background-color: #4ECDC4; -fx-background-radius: 8; -fx-cursor: hand;"; + labelColor = "-fx-text-fill: white;"; + } else if (isToday) { + baseStyle = "-fx-background-color: #e8faf8; -fx-background-radius: 8; -fx-border-color: #4ECDC4; -fx-border-radius: 8; -fx-border-width: 1.5; -fx-cursor: hand;"; + labelColor = "-fx-text-fill: #2abb9b;"; + } else { + baseStyle = "-fx-background-radius: 8; -fx-cursor: hand;"; + labelColor = "-fx-text-fill: #2c3e50;"; + labelWeight = ""; + } + + cell.setStyle(baseStyle); + + Label dayLabel = new Label(String.valueOf(date.getDayOfMonth())); + dayLabel.setStyle("-fx-font-size: 12; " + labelColor + labelWeight); + + VBox inner = new VBox(1); + inner.setAlignment(Pos.CENTER); + inner.getChildren().add(dayLabel); + + if (hasEvent) { + Circle dot = new Circle(3, Color.web("#FF6b6b")); + inner.getChildren().add(dot); + } else { + Region spacer = new Region(); + spacer.setPrefHeight(6); + inner.getChildren().add(spacer); + } + + cell.getChildren().add(inner); + + if (!isSelected && !isToday) { + cell.setOnMouseEntered(e -> cell.setStyle("-fx-background-color: #f0f9f8; -fx-background-radius: 8; -fx-cursor: hand;")); + cell.setOnMouseExited(e -> cell.setStyle(baseStyle)); + } + + cell.setOnMouseClicked(e -> { + if (date.equals(selectedDate)) { + selectedDate = null; + } else { + selectedDate = date; + currentDate = date; + } + rebuild(); + if (onDateSelected != null) { + onDateSelected.accept(selectedDate); + } + }); + + return cell; + } +} diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml index c24826d3..be6feef8 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml @@ -10,6 +10,7 @@ + @@ -73,6 +74,7 @@ +