Employee phase #138

Merged
RecentRunner merged 12 commits from employee-phase into main 2026-04-06 13:37:38 -06:00
13 changed files with 135 additions and 79 deletions
Showing only changes of commit cd5dd32c73 - Show all commits

View File

@@ -16,7 +16,6 @@ public class AdoptionDTO {
private String createdAt;
private String updatedAt;
// Constructor for create/update requests
public AdoptionDTO(Long petId, Long customerId, String adoptionDate, String adoptionStatus) {
this(petId, customerId, null, adoptionDate, adoptionStatus);
}

View File

@@ -4,7 +4,7 @@ import java.math.BigDecimal;
import java.util.List;
public class AppointmentDTO {
// Response fields (from server)
private Long appointmentId;
private Long customerId;
private String customerName;
@@ -22,8 +22,6 @@ public class AppointmentDTO {
private String createdAt;
private String updatedAt;
// Constructor for CREATE/UPDATE request body
// Matches AppointmentRequest exactly
public AppointmentDTO(Long customerId, Long storeId, Long serviceId,
String appointmentDate, String appointmentTime,
String appointmentStatus, List<Long> petIds) {
@@ -43,7 +41,6 @@ public class AppointmentDTO {
this.petIds = petIds;
}
// Getters
public Long getAppointmentId() {
return appointmentId;
}
@@ -108,7 +105,6 @@ public class AppointmentDTO {
return updatedAt;
}
// Convenience getters for adapter/list display
public String getPetName() {
return (petNames != null && !petNames.isEmpty()) ? petNames.get(0) : "";
}
@@ -121,7 +117,6 @@ public class AppointmentDTO {
return getPetID();
}
// Keep old name so adapter doesn't break
public String getServiceType() {
return serviceName;
}
@@ -130,7 +125,6 @@ public class AppointmentDTO {
return serviceId;
}
// Status alias
public String getStatus() {
return appointmentStatus;
}

View File

@@ -2069,6 +2069,37 @@
{
"name": "Appointments",
"item": [
{
"name": "Get Appointment Customers Dropdown",
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/v1/dropdowns/appointment-customers",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{staffToken}}",
"type": "text"
}
]
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});"
]
}
}
]
},
{
"name": "Check Appointment Availability",
"request": {
@@ -2180,7 +2211,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"10:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [1]\n}",
"raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"10:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [\n 1\n ],\n \"employeeId\": 1\n}",
"options": {
"raw": {
"language": "json"
@@ -2222,7 +2253,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"11:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [1]\n}",
"raw": "{\n \"customerId\": 1,\n \"storeId\": 1,\n \"serviceId\": 1,\n \"appointmentDate\": \"2026-12-20\",\n \"appointmentTime\": \"11:00:00\",\n \"appointmentStatus\": \"Booked\",\n \"petIds\": [\n 1\n ],\n \"employeeId\": 1\n}",
"options": {
"raw": {
"language": "json"
@@ -2315,6 +2346,37 @@
{
"name": "Adoptions",
"item": [
{
"name": "Get Adoption Pets Dropdown",
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/v1/dropdowns/adoption-pets",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{staffToken}}",
"type": "text"
}
]
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});"
]
}
}
]
},
{
"name": "List Adoptions",
"request": {
@@ -2395,7 +2457,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-21\",\n \"adoptionStatus\": \"Pending\"\n}",
"raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-21\",\n \"adoptionStatus\": \"Pending\",\n \"employeeId\": 1\n}",
"options": {
"raw": {
"language": "json"
@@ -2437,7 +2499,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-22\",\n \"adoptionStatus\": \"Completed\"\n}",
"raw": "{\n \"petId\": 3,\n \"customerId\": 1,\n \"adoptionDate\": \"2026-12-22\",\n \"adoptionStatus\": \"Completed\",\n \"employeeId\": 1\n}",
"options": {
"raw": {
"language": "json"
@@ -3719,6 +3781,68 @@
}
]
},
{
"name": "Get Store Employees Dropdown",
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/v1/dropdowns/stores/{{storeId}}/employees",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{staffToken}}",
"type": "text"
}
]
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});"
]
}
}
]
},
{
"name": "Get All Employees Dropdown",
"request": {
"method": "GET",
"url": "{{baseUrl}}/api/v1/dropdowns/employees",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{staffToken}}",
"type": "text"
}
]
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('Status code is 200', function () {",
" pm.response.to.have.status(200);",
"});"
]
}
}
]
},
{
"name": "List Stores",
"request": {

View File

@@ -218,7 +218,6 @@ public class AppointmentService {
List<Long> employeeIds = assignableEmployees.stream().map(Employee::getEmployeeId).collect(Collectors.toList());
List<Appointment> allAppointments = appointmentRepository.findByEmployeeEmployeeIdInAndAppointmentDate(employeeIds, date);
// Group by employee for faster lookup in the loop
java.util.Map<Long, List<Appointment>> appointmentsByEmployee = allAppointments.stream()
.collect(Collectors.groupingBy(a -> a.getEmployee().getEmployeeId()));
@@ -350,7 +349,6 @@ public class AppointmentService {
.isPresent();
}
//------------------------------------
private void validateAvailability(Employee employee, com.petshop.backend.entity.Service service, LocalDate date, LocalTime time, Long appointmentIdToIgnore) {
List<Appointment> existingAppointments = appointmentRepository
.findByEmployeeEmployeeIdAndAppointmentDate(employee.getEmployeeId(), date);
@@ -359,8 +357,6 @@ public class AppointmentService {
}
}
//------------------------------------------------
private boolean isSlotAvailable(List<Appointment> existingAppointments, com.petshop.backend.entity.Service requestedService, LocalTime requestedStart, Long appointmentIdToIgnore) {
LocalTime requestedEnd = requestedStart.plusMinutes(requestedService.getServiceDuration());
for (Appointment existingAppointment : existingAppointments) {

View File

@@ -1,4 +1,3 @@
-- Activate all employees in the users table so they appear in dropdowns
UPDATE users u
SET u.active = TRUE
WHERE u.role IN ('STAFF', 'ADMIN')

View File

@@ -1,7 +1,3 @@
-- V17: Normalize legacy appointmentPet data into customer_pet and appointment_customer_pet
-- Step 1: Ensure a customer_pet exists for every pet linked in appointmentPet
-- Note: pet species and breed might be null in pet table, but we copy them over if present
INSERT INTO customer_pet (customer_id, pet_name, species, breed)
SELECT DISTINCT a.customerId, p.petName, p.petSpecies, p.petBreed
FROM appointmentPet ap
@@ -12,7 +8,6 @@ WHERE NOT EXISTS (
WHERE cp.customer_id = a.customerId AND cp.pet_name = p.petName
);
-- Step 2: Link the appointment to the customer_pet
INSERT INTO appointment_customer_pet (appointment_id, customer_pet_id)
SELECT ap.appointmentId, cp.customer_pet_id
FROM appointmentPet ap
@@ -24,5 +19,4 @@ WHERE NOT EXISTS (
WHERE acp.appointment_id = ap.appointmentId AND acp.customer_pet_id = cp.customer_pet_id
);
-- Step 3: Remove the old legacy relationships so it strictly uses the new ones
DELETE FROM appointmentPet;

View File

@@ -1,7 +1,3 @@
-- V18: Normalize past appointments and resolve initial employee double-bookings
-- Part 1: Normalize past appointments.
-- Any appointment that is still 'Booked' but the date/time has passed should be marked as 'Missed'.
UPDATE appointment
SET appointmentStatus = 'Missed'
WHERE LOWER(appointmentStatus) = 'booked'
@@ -10,8 +6,6 @@ WHERE LOWER(appointmentStatus) = 'booked'
OR (appointmentDate = CURRENT_DATE AND appointmentTime < CURRENT_TIME)
);
-- Part 2: Resolve potential double-bookings caused by V15's simple backfill.
-- MySQL Error 1093 workaround: wrap same-table subqueries in derived tables.
UPDATE appointment a1
JOIN (
SELECT a3.appointmentId

View File

@@ -90,7 +90,7 @@ class AdoptionServiceTest {
void createAdoptionAutoAssignsFirstStaffEmployee() {
when(petRepository.findById(1L)).thenReturn(Optional.of(pet));
when(customerRepository.findById(1L)).thenReturn(Optional.of(customer));
// resolveAdoptionEmployee filters for staff
when(employeeRepository.findAllByIsActiveTrueOrderByEmployeeIdAsc()).thenReturn(List.of(adminEmployee, staffEmployee));
when(adoptionRepository.save(any(Adoption.class))).thenAnswer(invocation -> {
Adoption adoption = invocation.getArgument(0);

View File

@@ -22,7 +22,6 @@ public class AppointmentDTO {
private SimpleStringProperty appointmentTime;
private SimpleStringProperty appointmentStatus;
// Constructor
public AppointmentDTO(int appointmentId,
int customerId, String customerName,
int petId, String petName,
@@ -47,7 +46,6 @@ public class AppointmentDTO {
this.appointmentStatus = new SimpleStringProperty(appointmentStatus);
}
// Getters
public int getAppointmentId() { return appointmentId.get(); }
public int getCustomerId() { return customerId.get(); }

View File

@@ -68,7 +68,7 @@ public class AdoptionController {
void initialize() {
btnEdit.setDisable(true);
btnDelete.setDisable(true);
//Enable multiple selection
tvAdoptions.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
colAdoptionId.setCellValueFactory(new PropertyValueFactory<>("adoptionId"));
@@ -91,7 +91,6 @@ public class AdoptionController {
displayFilteredAdoptions(newValue);
});
//EventListener for DELETE key
tvAdoptions.setOnKeyPressed(event -> {
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
if (tvAdoptions.getSelectionModel().getSelectedItem() != null) {
@@ -109,11 +108,10 @@ public class AdoptionController {
@FXML
void btnDeleteClicked(ActionEvent event) {
//get selected adoptions
var selectedAdoptions = tvAdoptions.getSelectionModel().getSelectedItems();
if (selectedAdoptions.isEmpty()) return;
//ask user to confirm
Alert question = new Alert(Alert.AlertType.CONFIRMATION);
question.setHeaderText("Please confirm delete");
String message = selectedAdoptions.size() == 1
@@ -123,7 +121,6 @@ public class AdoptionController {
question.getDialogPane().lookupButton(ButtonType.OK).requestFocus();
Optional<ButtonType> result = question.showAndWait();
//if confirmed, start deletion
if (result.isPresent() && result.get() == ButtonType.OK) {
List<Long> ids = selectedAdoptions.stream()
.map(a -> (long) a.getAdoptionId())
@@ -146,7 +143,6 @@ public class AdoptionController {
alert.showAndWait();
}
//refresh display and reset inputs
displayAdoptions();
btnDelete.setDisable(true);
btnEdit.setDisable(true);

View File

@@ -47,7 +47,7 @@ public class AppointmentController {
@FXML
public void initialize(){
//Enable multiple selection
tvAppointments.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
colAppointmentId.setCellValueFactory(new PropertyValueFactory<>("appointmentId"));
@@ -66,7 +66,6 @@ public class AppointmentController {
txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n));
}
//EventListener for DELETE key
tvAppointments.setOnKeyPressed(event -> {
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
if (tvAppointments.getSelectionModel().getSelectedItem() != null) {
@@ -148,11 +147,10 @@ public class AppointmentController {
@FXML
void btnDeleteClicked(ActionEvent event){
//get selected appointments
var selectedAppointments = tvAppointments.getSelectionModel().getSelectedItems();
if (selectedAppointments.isEmpty()) return;
//ask user to confirm
Alert question = new Alert(Alert.AlertType.CONFIRMATION);
question.setHeaderText("Please confirm delete");
String message = selectedAppointments.size() == 1
@@ -162,7 +160,6 @@ public class AppointmentController {
question.getDialogPane().lookupButton(ButtonType.OK).requestFocus();
java.util.Optional<ButtonType> result = question.showAndWait();
//if confirmed, start deletion
if (result.isPresent() && result.get() == ButtonType.OK) {
List<Long> ids = selectedAppointments.stream()
.map(a -> (long) a.getAppointmentId())
@@ -185,7 +182,6 @@ public class AppointmentController {
alert.showAndWait();
}
//refresh display
loadAppointments();
}
}

View File

@@ -28,7 +28,6 @@ import java.util.Objects;
public class AdoptionDialogController {
//FXML elements
@FXML
private Button btnCancel;
@@ -56,11 +55,9 @@ public class AdoptionDialogController {
@FXML
private Label lblMode;
//Stores if the dialog view is in add/edit mode
private String mode = null;
private Adoption selectedAdoption = null;
//Adoption statuses
private ObservableList<String> statusList = FXCollections.observableArrayList(
"Pending", "Completed", "Cancelled"
);
@@ -234,7 +231,6 @@ public class AdoptionDialogController {
}
}
private void closeStage(MouseEvent mouseEvent) {
Node node = (Node) mouseEvent.getSource();
Stage stage = (Stage) node.getScene().getWindow();

View File

@@ -26,10 +26,6 @@ import java.util.Objects;
public class AppointmentDialogController {
// ============================
// FXML
// ============================
@FXML private Button btnCancel;
@FXML private Button btnSave;
@@ -47,11 +43,7 @@ public class AppointmentDialogController {
@FXML private Label lblAppointmentId;
@FXML private Label lblMode;
// ============================
// DATA
// ============================
private String mode = null; // Add | Edit
private String mode = null;
private AppointmentDTO selectedAppointment = null;
private Long pendingPetSelectionId = null;
@@ -60,20 +52,12 @@ public class AppointmentDialogController {
"Booked", "Completed", "Cancelled", "Missed"
);
//
// MODE
//
public void setMode(String mode) {
this.mode = mode;
lblMode.setText(mode + " Appointment");
lblAppointmentId.setVisible(!mode.equals("Add"));
}
//
// INITIALIZE
//
@FXML
public void initialize() {
cbAppointmentStatus.setItems(statusList);
@@ -85,14 +69,12 @@ public class AppointmentDialogController {
dpAppointmentDate.setValue(LocalDate.now().plusDays(1));
cbAppointmentStatus.setValue("Booked");
// Hours 9 AM - 5 PM
for (int i = 9; i <= 17; i++) {
cbHour.getItems().add(i);
}
cbMinute.getItems().addAll(0, 15, 30, 45);
// Show dropdown labels
cbService.setCellFactory(param -> new ListCell<>() {
@Override
protected void updateItem(DropdownOption option, boolean empty) {
@@ -175,10 +157,6 @@ public class AppointmentDialogController {
loadEmployees();
}
//
// DISPLAY FOR EDIT
//
public void displayAppointmentDetails(AppointmentDTO appt) {
selectedAppointment = appt;
@@ -214,10 +192,6 @@ public class AppointmentDialogController {
applySelectedEmployee();
}
//
// SAVE
//
private void buttonSaveClicked(MouseEvent e) {
if (cbService.getValue() == null ||
@@ -276,10 +250,6 @@ public class AppointmentDialogController {
}).start();
}
//
// UTIL
//
private void closeStage(MouseEvent e) {
Stage stage = (Stage) ((Node) e.getSource()).getScene().getWindow();
stage.close();