diff --git a/package.json b/package.json index 3395e11..209a580 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "module", "dependencies": { "@f0rce/ace-widget": "1.0.2", - "@polymer/polymer": "3.5.1", + "@polymer/polymer": "3.5.2", "@vaadin-component-factory/vcf-pdf-viewer": "2.0.1", "@vaadin/bundles": "24.5.1", "@vaadin/common-frontend": "0.0.19", @@ -19,29 +19,30 @@ "@vaadin/vaadin-usage-statistics": "2.1.3", "construct-style-sheets-polyfill": "3.1.0", "date-fns": "2.29.3", - "lit": "3.1.4", + "lit": "3.2.1", "print-js": "1.6.0", "proj4": "2.12.1", "react": "18.3.1", "react-dom": "18.3.1", - "react-router-dom": "6.23.1" + "react-router-dom": "6.26.2" }, "devDependencies": { - "@babel/preset-react": "7.24.7", - "@rollup/plugin-replace": "5.0.7", - "@rollup/pluginutils": "5.1.0", - "@types/react": "18.3.3", - "@types/react-dom": "18.3.0", - "@vitejs/plugin-react": "4.3.1", - "async": "3.2.5", - "glob": "10.4.1", + "@babel/preset-react": "7.25.7", + "@preact/signals-react-transform": "0.4.0", + "@rollup/plugin-replace": "6.0.1", + "@rollup/pluginutils": "5.1.2", + "@types/react": "18.3.11", + "@types/react-dom": "18.3.1", + "@vitejs/plugin-react": "4.3.3", + "async": "3.2.6", + "glob": "10.4.5", "rollup-plugin-brotli": "3.1.0", "rollup-plugin-visualizer": "5.12.0", "strip-css-comments": "5.0.0", "transform-ast": "2.4.4", - "typescript": "5.4.5", - "vite": "5.3.3", - "vite-plugin-checker": "0.6.4", + "typescript": "5.6.3", + "vite": "5.4.9", + "vite-plugin-checker": "0.8.0", "workbox-build": "7.1.1", "workbox-core": "7.1.0", "workbox-precaching": "7.1.0" @@ -49,7 +50,7 @@ "vaadin": { "dependencies": { "@f0rce/ace-widget": "1.0.2", - "@polymer/polymer": "3.5.1", + "@polymer/polymer": "3.5.2", "@vaadin-component-factory/vcf-pdf-viewer": "2.0.1", "@vaadin/bundles": "24.5.1", "@vaadin/common-frontend": "0.0.19", @@ -64,34 +65,35 @@ "@vaadin/vaadin-usage-statistics": "2.1.3", "construct-style-sheets-polyfill": "3.1.0", "date-fns": "2.29.3", - "lit": "3.1.4", + "lit": "3.2.1", "print-js": "1.6.0", "proj4": "2.12.1", "react": "18.3.1", "react-dom": "18.3.1", - "react-router-dom": "6.23.1" + "react-router-dom": "6.26.2" }, "devDependencies": { - "@babel/preset-react": "7.24.7", - "@rollup/plugin-replace": "5.0.7", - "@rollup/pluginutils": "5.1.0", - "@types/react": "18.3.3", - "@types/react-dom": "18.3.0", - "@vitejs/plugin-react": "4.3.1", - "async": "3.2.5", - "glob": "10.4.1", + "@babel/preset-react": "7.25.7", + "@preact/signals-react-transform": "0.4.0", + "@rollup/plugin-replace": "6.0.1", + "@rollup/pluginutils": "5.1.2", + "@types/react": "18.3.11", + "@types/react-dom": "18.3.1", + "@vitejs/plugin-react": "4.3.3", + "async": "3.2.6", + "glob": "10.4.5", "rollup-plugin-brotli": "3.1.0", "rollup-plugin-visualizer": "5.12.0", "strip-css-comments": "5.0.0", "transform-ast": "2.4.4", - "typescript": "5.4.5", - "vite": "5.3.3", - "vite-plugin-checker": "0.6.4", + "typescript": "5.6.3", + "vite": "5.4.9", + "vite-plugin-checker": "0.8.0", "workbox-build": "7.1.1", "workbox-core": "7.1.0", "workbox-precaching": "7.1.0" }, - "hash": "1a0f17d48b329307b5862bc57499307d1b89f7d89260121c2b7189f76957c436" + "hash": "2dc40a4f634ae025081ca2239cba00b14a35fe94ab78ac0a4dd3023d882081d5" }, "overrides": { "@vaadin/bundles": "$@vaadin/bundles", diff --git a/src/main/java/com/primefactorsolutions/model/Employee.java b/src/main/java/com/primefactorsolutions/model/Employee.java index d8a11b0..1d3df41 100644 --- a/src/main/java/com/primefactorsolutions/model/Employee.java +++ b/src/main/java/com/primefactorsolutions/model/Employee.java @@ -2,6 +2,7 @@ package com.primefactorsolutions.model; import com.google.common.collect.Lists; import jakarta.persistence.*; +import jakarta.validation.constraints.*; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; @@ -18,30 +19,45 @@ import java.util.Collection; @NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class Employee extends BaseEntity implements UserDetails { + private String username; + @NotNull(message = "El nombre no puede estar vacío") + @Pattern(regexp = "^[a-zA-Z ]+$", message = "El nombre solo debe contener letras") private String firstName; + @NotNull(message = "El apellido no puede estar vacío") + @Pattern(regexp = "^[a-zA-Z ]+$", message = "El apellido solo debe contener letras") private String lastName; private LocalDate birthday; + @Pattern(regexp = "^[a-zA-Z ]+$", message = "La ciudad de nacimiento solo debe contener letras") private String birthCity; private String age; - + @Size(max = 100, message = "La dirección de residencia no debe exceder 100 caracteres") private String residenceAddress; + @Size(max = 100, message = "La dirección local no debe exceder 100 caracteres") private String localAddress; + @Pattern(regexp = "^[0-9]+$", message = "El número de teléfono debe contener solo números") private String phoneNumber; + @Email(message = "El correo personal no tiene un formato válido") private String personalEmail; + @Pattern(regexp = "^[a-zA-Z ]+$", message = "El cargo solo debe contener letras") private String position; @ManyToOne @JoinColumn(name = "team_id", nullable = false) private Team team; + @Size(max = 100, message = "El nombre de contacto de emergencia no debe exceder 100 caracteres") private String emergencyCName; + @Size(max = 100, message = "La dirección de contacto de emergencia no debe exceder 100 caracteres") private String emergencyCAddress; + @Pattern(regexp = "^[0-9]+$", message = "El teléfono de contacto de emergencia debe contener solo números") private String emergencyCPhone; + @Email(message = "El correo de contacto de emergencia no tiene un formato válido") private String emergencyCEmail; + @Pattern(regexp = "^[0-9]+$", message = "La cantidad de hijos debe contener solo números") private String numberOfChildren; - + @Pattern(regexp = "^[0-9]+$", message = "El CI debe contener solo números") private String ci; private String issuedIn; - + @Size(max = 100, message = "El título no debe exceder 100 caracteres") private String pTitle1; private String pTitle2; private String pTitle3; @@ -54,29 +70,36 @@ public class Employee extends BaseEntity implements UserDetails { private String certification2; private String certification3; private String certification4; - + @Size(max = 255, message = "El reconocimiento no debe exceder 255 caracteres") private String recognition; + @Size(max = 500, message = "Los logros no deben exceder 500 caracteres") private String achievements; private String language; private String languageLevel; - + @Pattern(regexp = "^[A-Za-z0-9]+$", message = "El código debe contener solo letras y números") private String cod; + @Pattern(regexp = "^[a-zA-Z ]+$", message = "El lead manager solo debe contener letras") private String leadManager; + @Pattern(regexp = "^[a-zA-Z ]+$", message = "El proyecto solo debe contener letras") private String project; private LocalDate dateOfEntry; private LocalDate dateOfExit; - + @Pattern(regexp = "^[a-zA-Z ]+$", message = "El tipo de contrato solo debe contener letras") private String contractType; + @Pattern(regexp = "^[0-9]+$", message = "La antigüedad debe contener solo números") private String seniority; + @Pattern(regexp = "^[0-9]+(\\.[0-9]{1,2})?$", message = "El salario debe ser un número con hasta dos decimales") private String salary; - + @Pattern(regexp = "^[a-zA-Z ]+$", message = "El nombre del banco solo debe contener letras") private String bankName; + @Pattern(regexp = "^[0-9]+$", message = "El número de cuenta debe contener solo números") private String accountNumber; private String gpss; private String sss; + @Pattern(regexp = "^[a-zA-Z ]+$", message = "Los derechohabientes solo deben contener letras") private String beneficiaries; @Column(columnDefinition = "TEXT") diff --git a/src/main/java/com/primefactorsolutions/views/EmployeeView.java b/src/main/java/com/primefactorsolutions/views/EmployeeView.java index df315f6..a164d0c 100644 --- a/src/main/java/com/primefactorsolutions/views/EmployeeView.java +++ b/src/main/java/com/primefactorsolutions/views/EmployeeView.java @@ -1,10 +1,10 @@ package com.primefactorsolutions.views; -import com.primefactorsolutions.model.Employee; -import com.primefactorsolutions.model.Team; +import com.primefactorsolutions.model.*; import com.primefactorsolutions.service.EmployeeService; import com.primefactorsolutions.service.ReportService; import com.primefactorsolutions.service.TeamService; +import com.primefactorsolutions.service.TimeOffRequestService; import com.vaadin.componentfactory.pdfviewer.PdfViewer; import com.vaadin.flow.component.ClickEvent; import com.vaadin.flow.component.Component; @@ -24,9 +24,6 @@ import com.vaadin.flow.component.textfield.EmailField; import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.component.upload.Upload; import com.vaadin.flow.component.upload.receivers.MemoryBuffer; -import com.vaadin.flow.data.binder.Result; -import com.vaadin.flow.data.binder.ValueContext; -import com.vaadin.flow.data.converter.Converter; import com.vaadin.flow.data.value.ValueChangeMode; import com.vaadin.flow.router.*; import com.vaadin.flow.server.StreamResource; @@ -39,6 +36,8 @@ import org.vaadin.firitin.form.BeanValidationForm; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; import java.util.Base64; import java.util.List; import java.util.UUID; @@ -52,8 +51,10 @@ public class EmployeeView extends BeanValidationForm implements HasUrl private final EmployeeService employeeService; private final ReportService reportService; + private final TimeOffRequestService requestService; private final TeamService teamService; + // TODO: campo usado para registrar al empleado en LDAP. Este campo podria estar en otro form eventualmente. private final TextField username = createTextField("Username: ", 30, true); private final TextField firstName = createTextField("Nombres: ", 30, true); @@ -143,27 +144,17 @@ public class EmployeeView extends BeanValidationForm implements HasUrl public EmployeeView(final EmployeeService employeeService, final ReportService reportService, - final TeamService teamService) { + final TeamService teamService, + final TimeOffRequestService requestService) { super(Employee.class); this.employeeService = employeeService; this.reportService = reportService; + this.requestService = requestService; this.teamService = teamService; saveButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); configureComponents(); addClassName("main-layout"); - - getBinder().setConverter("team", new Converter() { - @Override - public Result convertToModel(final Object o, final ValueContext valueContext) { - return Result.ok(new Team((String) o)); - } - - @Override - public Object convertToPresentation(final Object o, final ValueContext valueContext) { - return ((Team) o).getName(); - } - }); } private void configureComponents() { @@ -171,12 +162,18 @@ public class EmployeeView extends BeanValidationForm implements HasUrl phoneNumber.addValueChangeListener(e -> validatePhoneNumber(phoneNumber, e.getValue())); emergencyCPhone.setValueChangeMode(ValueChangeMode.EAGER); emergencyCPhone.addValueChangeListener(e -> validatePhoneNumber(emergencyCPhone, e.getValue())); + + firstName.setValueChangeMode(ValueChangeMode.EAGER); + firstName.addValueChangeListener(e -> validateNameField(firstName, e.getValue())); + lastName.setValueChangeMode(ValueChangeMode.EAGER); + lastName.addValueChangeListener(e -> validateNameField(lastName, e.getValue())); + createTeamComboBox(); + configureUpload(); saveButton.setVisible(true); editButton.setVisible(true); reportButton.setVisible(true); birthday.addValueChangeListener(event -> calculateAge()); - createTeamComboBox(); reportButton.addClickListener((ComponentEventListener>) buttonClickEvent -> { var employee = getEntity(); @@ -189,6 +186,15 @@ public class EmployeeView extends BeanValidationForm implements HasUrl initDialog(); } + private void validateNameField(final TextField textField, final String value) { + if (!value.matches("^[a-zA-ZáéíóúÁÉÍÓÚñÑ\\s]*$")) { + textField.setInvalid(true); + textField.setErrorMessage("Este campo solo debe contener letras."); + } else { + textField.setInvalid(false); + } + } + private void calculateAge() { if (birthday.getValue() != null) { int currentYear = java.time.LocalDate.now().getYear(); @@ -259,7 +265,7 @@ public class EmployeeView extends BeanValidationForm implements HasUrl ComboBox comboBox = new ComboBox<>("Estado"); comboBox.setItems(Employee.Status.values()); comboBox.setItemLabelGenerator(Employee.Status::name); - comboBox.setRequiredIndicatorVisible(true); // Indicador de campo requerido + comboBox.setRequiredIndicatorVisible(true); return comboBox; } @@ -291,6 +297,14 @@ public class EmployeeView extends BeanValidationForm implements HasUrl team.setWidthFull(); } + private ComboBox createComboBox(final String label, final T[] items) { + ComboBox comboBox = new ComboBox<>(label); + comboBox.setItems(items); + comboBox.setItemLabelGenerator(Object::toString); + comboBox.setWidthFull(); + return comboBox; + } + private ComboBox createGenderComboBox() { ComboBox comboBox = new ComboBox<>("Genero"); comboBox.setItems(Employee.Gender.values()); @@ -303,12 +317,36 @@ public class EmployeeView extends BeanValidationForm implements HasUrl return !firstName.isEmpty() && !lastName.isEmpty() && status.getValue() != null; } + private void setVacationDuration( + final Employee employee, + final TimeOffRequest request, + final LocalDate referenceDate) { + double yearsOfService = ChronoUnit.YEARS.between(employee.getDateOfEntry(), referenceDate); + request.setAvailableDays(calculateAvailableDays(yearsOfService)); + } + + private double calculateAvailableDays(final double yearsOfService) { + if (yearsOfService > 10) { + return 30.0; + } else if (yearsOfService > 5) { + return 20.0; + } else if (yearsOfService > 1) { + return 15.0; + } else { + return 0.0; + } + } + private void saveEmployee() { if (validateForm()) { Employee employee = getEntity(); employee.setStatus(status.getValue()); employee.setAge(age.getValue()); + if (employee.getDateOfEntry() != null) { + processTimeOffRequests(employee); + } + employeeService.createOrUpdate(employee); Notification.show(NOTIFICATION_SAVE_SUCCESS); getUI().ifPresent(ui -> ui.navigate(EmployeesListView.class)); @@ -317,13 +355,89 @@ public class EmployeeView extends BeanValidationForm implements HasUrl } } + private void processTimeOffRequests(final Employee employee) { + boolean isCurrentYearEntry = employee.getDateOfEntry().getYear() == LocalDate.now().getYear(); + + deleteExistingTimeOffRequests(employee); + + if (isCurrentYearEntry) { + saveCurrentYearRequest(employee); + } else if (LocalDate.now().getYear() > employee.getDateOfEntry().getYear()) { + savePreviousAndCurrentYearRequests(employee); + } + } + + private void deleteExistingTimeOffRequests(final Employee employee) { + deleteTimeOffRequestByCategory(employee, TimeOffRequestType.VACACION_GESTION_ANTERIOR); + deleteTimeOffRequestByCategory(employee, TimeOffRequestType.VACACION_GESTION_ACTUAL); + } + + private void deleteTimeOffRequestByCategory(final Employee employee, final TimeOffRequestType category) { + var requests = requestService.findByEmployeeAndCategory(employee.getId(), category); + if (!requests.isEmpty()) { + requestService.deleteTimeOffRequest(requests.getFirst().getId()); + } + } + + private void saveCurrentYearRequest(final Employee employee) { + LocalDate baseDate = LocalDate.of( + LocalDate.now().getYear(), + employee.getDateOfEntry().getMonth(), + employee.getDateOfEntry().getDayOfMonth() + ); + TimeOffRequest currentRequest = createTimeOffRequest( + employee, + TimeOffRequestType.VACACION_GESTION_ACTUAL, + baseDate, + 729 + ); + requestService.saveTimeOffRequest(currentRequest); + } + + private void savePreviousAndCurrentYearRequests(final Employee employee) { + LocalDate previousBaseDate = LocalDate.of( + LocalDate.now().getYear() - 1, + employee.getDateOfEntry().getMonth(), + employee.getDateOfEntry().getDayOfMonth() + ); + TimeOffRequest previousRequest = createTimeOffRequest( + employee, + TimeOffRequestType.VACACION_GESTION_ANTERIOR, + previousBaseDate, + 729 + ); + + LocalDate currentBaseDate = previousBaseDate.plusYears(1); + TimeOffRequest currentRequest = createTimeOffRequest( + employee, + TimeOffRequestType.VACACION_GESTION_ACTUAL, + currentBaseDate, + 1094 + ); + + requestService.saveTimeOffRequest(previousRequest); + requestService.saveTimeOffRequest(currentRequest); + } + + private TimeOffRequest createTimeOffRequest(final Employee employee, + final TimeOffRequestType category, + final LocalDate baseDate, + final int expirationDays) { + TimeOffRequest request = new TimeOffRequest(); + request.setEmployee(employee); + request.setCategory(category); + request.setState(TimeOffRequestStatus.APROBADO); + request.setExpiration(baseDate.plusDays(expirationDays)); + setVacationDuration(employee, request, baseDate); + return request; + } + private void enableEditMode() { setFieldsEditable(); saveButton.setVisible(true); editButton.setVisible(false); } - @Override public void setParameter(final BeforeEvent beforeEvent, final String action) { final RouteParameters params = beforeEvent.getRouteParameters(); @@ -338,7 +452,6 @@ public class EmployeeView extends BeanValidationForm implements HasUrl UUID employeeId = UUID.fromString(s); var employee = employeeService.getEmployee(employeeId); setEntityWithEnabledSave(employee); - team.setValue(employee.getTeam()); if ("edit".equals(action) && !s.isEmpty()) { saveButton.setVisible(true); @@ -505,5 +618,4 @@ public class EmployeeView extends BeanValidationForm implements HasUrl saveButton, editButton, reportButton, dialog ); } -} - +} \ No newline at end of file diff --git a/src/main/java/com/primefactorsolutions/views/RequestRegisterView.java b/src/main/java/com/primefactorsolutions/views/RequestRegisterView.java index ce9af15..ac733ed 100644 --- a/src/main/java/com/primefactorsolutions/views/RequestRegisterView.java +++ b/src/main/java/com/primefactorsolutions/views/RequestRegisterView.java @@ -428,7 +428,10 @@ public class RequestRegisterView extends VerticalLayout { } private void handleVacationRequest(final TimeOffRequest request) { - List existingRequests = requestService.findByEmployeeAndCategory(employee.getId(), TimeOffRequestType.VACACION_GESTION_ACTUAL); + List existingRequests = requestService.findByEmployeeAndCategory( + employee.getId(), + TimeOffRequestType.VACACION_GESTION_ACTUAL + ); if (!existingRequests.isEmpty()) { TimeOffRequest existingRequest = existingRequests.getFirst(); existingRequest.setCategory(TimeOffRequestType.VACACION_GESTION_ANTERIOR);