Por qué cuesta cada vez mas dar servicios como programador a un proyecto Peruano


Como consejo para los emprendedores o empresas que contratan servicios de un programador, si este programador les termina el trabajo, pues, PAGUENLE. No puedes andar diciendole: "Mi cliente no me paga, esperame unos días y luego se hace 1 mes (2 meses, 3 meses, etc), y así hasta que el programador se ve forzado a quemar a su próximo ex-cliente por redes sociales o grupos de google.

Te ofrecen 5k por un mes de trabajo y te terminan pagando 2 ó 3 meses despues.

Lamentablemente, esto va a seguir, mientras haya chicos que por querer practicar o trabajar aceptan estas condiciones. Recordemos en primer lugar NO SOMOS SOCIOS de esta EMPRESA que ha contrato nuestro servicio.

Sugerencia

No cobren por proyecto. Ejemplo: Ya esto te cuesta 10k por 1 o 2 meses de trabajo. Terminaran pagando cuando quieren y siempre la última factura es la llave para que no "los abandones". Se han dado cuenta, que siempre esperan lo peor del programador, que firme contrato, que garantía me das, que quiero el código fuente, etc Pero, ellos nunca pierden y te pagan cuando quieren?

Entonces, Qué hacemos tio Joe?

Cobra por mes por recibos x honorarios, yo te cobro por mes tanto, tu me tienes que dar actividades ese mes como si fuera tu empleado.  Mi sueldo es 5k y hare lo que me pidas haga en ese rango. Terminado el mes me pagas. Claro esta, me firmas un documento por servicios profesionales y pago a recibos x honorarios; por actividades de programación; de manera que si no te paga vas al Ministerio de Trabajo.

Es hora que seamos mas formales, y no dejarnos llevar por las ganas de practicar, malogramos el mercado, malogramos las opciones de buenos programadores. Espero las Universidades, Institutos formen a sus chicos no sólo en tecnología, lenguajes de programación, frameworks, sino, también en como llevar su situación financiera, sus derechos laborales y como  hacer negocios en Perú. 

A cuantos les han robado sus proyectos, ideas, por no tener ni idea de lo que es propiedad intelectual, derechos de autor. 

Chicos informense bien. No mas "programadores combi". Si todos trabajamos formalmente, no habrá quienes se aprovechen de nuestra necesidad por trabajar. 

Ah! y terminen sus trabajos, también pongamosno del otro lado de la mesa. El emprendedor o empleado va a pagarte y no es justificable abandonar proyectos a medio camino, o hacer mál código. Recuerda todo se sabe, el mundo de la informática es chico. Hay una lista negra también. 

Nos leemos.

Enjoy!

Joe


Share:

Webinar para Gestión de Clientes


Si bien había escrito sobre la demo de Gestión de Clientes en 3 partes:


  1. Spring Boot + Thymeleaf + Bootstrap
  2. Agregando JasperReports a Gestión de Clientes
  3. Spring Security - Control de Permisos
Aproveche para este Domingo 24 de Mayo 2020 hacer un webinar para explicar todo en vivo. 
Así que si interesa tener detalles sobre el mismo, aquí te dejo el vídeo.






Todos los webinars y vídeos los puedes encontrar en nuestro canal de youtube.


Enjoy!

Joe



Share:

Spring Security - Control de Permisos de Url Dinámico



En un primer post mostramos como crear una aplicación con Spring Boot + Thymelaf + BooStrap + MySQL.  En un segundo post mostramos como generar reportes con JasperReports.

En este último post vamos a ver como agregar el proyecto Spring Security para manejar usuarios, roles, urls y permisos de forma dinámica a dichos urls sin necesidad de cambiar el código, todo trabajado desde la base de datos.

Código Fuente:  https://github.com/joedayz/gestionclientes


Configurando Spring Security

  • En el proyecto agregaremos la dependencia Starter de Spring Security. 



  • Empezaremos con la página de Login donde tendremos un formulario y enviaremos el username y password.


Configuración de Seguridad


Para que la seguridad funcione hay que crear un archivo de configuración Java denominado SecurityConfig.java en donde tendremos lo siguiente:


La clase es anotada con @EnableWebSecurity para permitir la integración entre Spring Security y Spring MVC. Este también hereda de WebSecurityConfigurerAdapter y te invita a sobreescribir un par de métodos para establecer configuración específica de seguridad web. 

@EnableGlobalMethodSecurity te permite habilitar seguridad glogal a nivel de métodos. 
  • El prePostEnable es una propiedad que permite anotaciones pre/post de seguridad.
  • proxyTargetClass es una propiedad  que indica que se usará proxies basados en CGLIB como alternativa a proxies basado en interfaces Java. La ventaja está explicada en este link.
  • @Import({CustomAuthorizationConfig.class}) permite tener configuración adicional en otro archivo y unirla con la configuración actual. 
  • @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER):  Para sobreescribir las reglas de acceso sin cambiar ninguna caracteristica auto configurada que viene por spring boot por defecto (más información en la documentación).

Mi CustomUserAuthenticationProvider


En Spring Security se pueden declarar varios Filtros para personalizar tu seguridad.



El primero filtro a implementar es el AuthenticationProvider y es una interface de Spring Security que nos permite implementar nuestra propia autenticación.


@Autowired
private AuthenticationProvider customUserAuthenticationProvider;

Entonces tengo que implementar dicha interface. A continuación la primera parte de la implementación:


Nos aseguramos que tenga la dependencias necesarias, cómo: Un UsuarioRepository para consultar la tabla Usuarios y una clase para consultar el Password acorde a la encriptación BCrypt que nos provee Spring.

Esta interface nos pide implementar 2 métodos:




En este método vemos si el usuario existe en la BD. Sino arrojamos una excepción UsernameNotFoundException. También validamos que el password corresponda y de no ser así arrojamos una excepción BadCredentialsException.







Esta clase que es invocada en dicho método se encarga de obtener los roles del usuario y devolverlos como una colección de GrantedAuthority

 Gracias a que vía JPA tenemos mapeado los usuario y roles podemos hacer eso sin problema.
El método final a implementar es este donde especificamos que la clase es de tipo UsernamePasswordAuthenticationToken. @Overridepublic boolean supports(Class authentication) { return UsernamePasswordAuthenticationToken.class.equals(authentication); } La tabla Usuario se verá de esta forma:

El AccessDecisionManager


Esta se encarga de la autorización. En mi caso no cambiare nada y tomare la autorización basada en roles.



Roles que tendrán la forma ROLE_XXX. Por eso guardaremos los Roles en esta tabla:





CustomWebSecurityExpressionHandler


Esta clase es para declarar que manejaremos la seguridad web basada en roles (getRoleHierarchy()) y permisos basado en expressiones (getPermissionEvaluator()).


@Componentpublic class CustomWebSecurityExpressionHandler extends DefaultWebSecurityExpressionHandler {
   private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

   @Override   protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) {
      WebSecurityExpressionRoot root = new CustomWebSecurityExpressionRoot(authentication, fi);
      root.setPermissionEvaluator(getPermissionEvaluator());
      root.setTrustResolver(trustResolver);
      root.setRoleHierarchy(getRoleHierarchy());
      return root;
   }
}


FilterInvocationServiceSecurityMetadataSource

Implementar esta interface nos va a permitir que podamos tener de forma dinámica los URLs y los ROLES que pueden acceder a ellos.


Implementando InitializingBean nos permite al implementar afterPropertiesSet tener un método init() en nuestro bean.

Y como se puede apreciar en este método:


@Overridepublic void afterPropertiesSet() throws Exception {
   List<RequestConfigMapping> requestConfigMappings = requestConfigMappingService.getRequestConfigMappings();
   LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>(requestConfigMappings.size());
   for(RequestConfigMapping requestConfigMapping : requestConfigMappings) {
      RequestMatcher matcher = requestConfigMapping.getMatcher();
      requestMap.put(matcher,requestConfigMapping.getAttributes());
   }
   this.delegate = new ExpressionBasedFilterInvocationSecurityMetadataSource(requestMap, expressionHandler);
}

El requestConfigMappingService nos da la relación entre URLs y permisos. Por lo tanto, implementamos JdbcRequestConfigMappingService implements RequestConfigMappingService



Y aquí el query que trae la relación:

private static final String SELECT_SQL = "select ant_pattern, expression from Filter_Metadata order by sort_order";



SecurityConfig

Implementando estas interfaces logramos que nuestra configuración sea minimalista y USUARIOS, ROLES, USUARIOS_ROLES, FILTERSECURITY_METADA (Urls y Roles) se manejen por base de datos.

Quedando nuestra clase de esta manera:




Ignoramos la seguridad para ciertos recursos y


Con esto terminamos nuestra demo de Clientes ( https://github.com/joedayz/gestionclientes) y ya puede manejar la seguridad con Spring Security.

Enjoy!


Joe


Share:

Agregando JasperReports a Gestión de Clientes


En un post anterior presente una integración con Spring Boot + Thymelaf + BooStrap + MySQL.

En esta nueva versión veremos la integración de JasperReports al proyecto Gestión de Clientes.

Código Fuente:  https://github.com/joedayz/gestionclientes

JasperReports

1. Al proyecto de Gestión de Clientes le hacía falta generar reportes.  Y para eso vamos a usar la librería gratuita de código abierto JasperReports

Agregaremos en el pom.xml:



NOTA: Nos aseguramos de tener la librereia itextpdf porque vamos a generar los reportes en PDF. 

Existe también un Editor de Reportes gratuito denominado JasperSoft Studio. Para el presente post yo voy a utilizar la versión JasperSoft Studio 6.8.0.





2. En el application.properties definiremos la ruta donde se ubicarán los reportes. Estos tienen extensión jrxml y compilado extensión jasper.  En el código fuente ver la carpeta Reportes



3. Para los reportes se ha reutilizado la búsqueda de clientes y agregado en el listado la opción de exportar o generar el reporte en PDF. 


En el Thymeleaf delegamos dicha tarea a un URL de la controladora:


4. En la controladora delegamos el trabajo de generar el reporte a RepositoryService.java


NOTA: 
- Se busca que al exportar el reporte se use el File Saver de manera que el usuario decida donde guardar su archivo.
- Verifique su navegador tenga habilitada dicha opción. 




5. ReporteService.java obtiene la data del cliente y delega al Repository el trabajo de generar el reporte.



6. El ReporteRepository.java busca los reportes compilados anteriormente con JasperSoft Studio y genera el reporte necesario.


Y eso es todo.

Vídeo del Resultado Final




Enjoy!!

Joe


Share:

Breves consejos para no tomar proyectos o como cerrarlos


Si vas a tomar un proyecto terminalo

La satisfacción del último commit y que el cliente quede contento es lo mejor que nos puede pasar. Nada de desaparecer o dejar el proyecto a medias. Lo que me ayuda y aconsejo con varios años ya en el mercado es evitar problemas desde el inicio y lo comparto con Ud. amigo lector: Debe existir un documento que describa la funcionalidad (llámele casos de usos o historias de usuario). Se los recomiendo porque si solo te da pantallas y no describes como va a funcionar, ambos terminaran como candidatos al programa de "Andrea" y pedirán "prueba de ADN" para ver quien dice la verdad. Pelea, tras pelea.
Con el documento, ya le dices, "eso no esta en el documento que acordamos. Si lo quieres es un cambio y costará X". Cuidado y la "necesidad" de tomar el proyecto te haga cerrar el trato de palabra. Esas se las lleva el viento. O peor abandonar a medio proyecto, no solo es que no trabajaras con este cliente más, sino que no tendrán confianza de ti para otro trabajo y el mundo de la informática es "pueblo chico". Hablara mál de ti y te hará la injusticia con otros clientes y ante la duda, pueden no elegirte los nuevos prospectos.

He tenido proyectos que han durado meses o años, por no hacer un documento de las funcionalidad y solo firmar la propuesta como listado de caracteristicas deseadas por aprovechar el negocio es un futuro fracaso para tí como independiente.
Déjame decirte que ahí estas frito (expresión peruana de que estas en serios problemas), ya que como no están las cosas claras, se reinventan funcionalidades todo el tiempo, se desgasta tu tiempo, tu cuerpo, tu presupuesto y nunca hay cuando acabar. El fin es hablar mal tu de él, o él de ti.

Si no te interesa, si no puedes hacerlo, dilo de inmediato

Y tambien si no puedes hacer algo por X motivos, mejor dilo de inicio. Sin dolor, no trates de quedar bien con el que te solicita tus servicios diciéndole que vas a ver, buscar alguien, etc. Dile de arranque no me interesa, no es el presupuesto que manejo, estoy ocupado, disculpame o la que no falla: "No se como hacerlo, sorry, yo hago otras cosas, no quiero quedar mal contigo".
Yo he perdido hasta amistades por que cuando me han pedido ayuda, no se las he podido atender y ojo, ni siquiera teníamos un proyecto, sino por demorarme en decirle, no puedo apoyarte, estoy en otros temas.
Es mas ni te comprometas a buscarle otra persona, hasta en eso te van a juzgar el desinterés. Hay que aprender a decir que NO.

Y para los que buscan buenos programadores

Hay de los que dicen que son "los magnificos" (de por sí, ¿que informatico no tiene ego?), que ha terminado nanodegress, que ha barrido con todos los cursos de coursera, que sabe GO, ELIXIR, CRISTAL, SWIFT, KOTLIN, etc. etc. Que pertenece a una comunidad hot y que recibe el apoyo de Marvel. Que es commiter de DC.
Sabes muchos de los que conozco que son muy buenos, son más de perfil bajo. Y la pregunta correcta que debes hacer es:

¿Terminas tus proyectos?, ¿me puedes dar los contactos de 3 ex clientes tuyos?
Es tu plata, tu tiempo y si lo vas a contratar, es mejor como en el futbol: Ir por quien ya tiene varios campeonatos y experiencia y no te deja como "novia frente al altar".

Nos leemos.
Joe











Share:

Spring Boot + Thymeleaf + BootStrap

Para este artículo vamos a ver como trabajar un administrador de clientes (abonados, inquilinos, miembros, etc) utilizando las siguientes tecnologías:




[

Si lo bajas y quieres probar de una vez, usa estos datos:

usuario: joe
clave: 123

]

1. Dependencias del Proyecto


En el código fuente veremos que tenemos spring boot, spring web (mvc), spring security, spring data, mysql, thymeleaf, y la integración de thymeleaf con spring security.



No tendrás ningún problema en la descarga de librerías, todas están disponibles en los repositorios de maven.


2. Estructura del Proyecto

Es la estructura típica de un proyecto Spring Boot. La parte del template y archivos HTML con Thymeleaf están en src/main/resources/templates.




3. Configuración de la Seguridad


Para manejar la autenticación y autorización usamos Spring Security. La verdad es muy sencillo. Hasta el encoder /decoder te lo brinda Spring Security gracias al soporte que tiene de BCrypt.



@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private PasswordEncoder passwordEncoder;

 private static final String SQL_ROLE
   = "select u.Cod_Usuario, p.permiso_value as authority "
             + "from Usuarios u "
             + "inner join Tipos_Usuario r on u.Id_Role = r.Id "
             + "inner join Role_Permisos rp on rp.Id_Role = r.id "
             + "inner join Permisos p on rp.Id_Permiso = p.id "
             + "where u.Cod_Usuario = ?";

    private static final String SQL_LOGIN
            = "select u.Cod_Usuario as username, u.Password_Usuario as password, u.active "
            + "from Usuarios u "
            + "where u.Cod_Usuario = ?";


    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(daoAuthenticationProvider());
    }

    @Bean
    public AuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(userDetailsService());
        return provider;
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        JdbcDaoImpl userDetails = new JdbcDaoImpl();
        userDetails.setDataSource(dataSource);
        userDetails.setUsersByUsernameQuery(SQL_LOGIN);
        userDetails.setAuthoritiesByUsernameQuery(SQL_ROLE);
        return userDetails;
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable();

        http
                .authorizeRequests()
                .antMatchers("/js/**").permitAll()
                .antMatchers("/css/**").permitAll()
                .antMatchers("/fonts/**").permitAll()
                .antMatchers("/forgot_password/**").permitAll()
                .antMatchers("/reset_password/**").permitAll()

                .antMatchers("/clientes/**").hasAnyRole("ADMINISTRADOR")

                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
    .failureUrl("/login-error")
                .permitAll()
                .and()
                .logout()
                .permitAll();
    }
}


Para trabajar con tu modelo de datos, tienes que implementar un método (anotado con @Bean) que retorne UserDetailsService y para ello usamos dos sentencias SQL:  SQL_LOGIN (da los datos de autenticación) y SQL_ROLE (Donde obtienes los permisos por rol).

Finalmente en el método configure(HttpSecurity http) ves a que recursos ("urls") darás permiso a todos, o especificar que Roles si pueden hacer uso de él. Ahí es donde va la parte de autorización.

La última parte indica que cualquier URL que no esta marcado con "permitAll()" solo se puede acceder si se esta autenticado. De manera, que si se trata de acceder de forma directa sin haber iniciado sesión sera redireccionado al formulario "login". 


Y también podemos ver que existe la configuración para el "logout".  Al cual podemos invocar todos. 

4. Entidades

Nuestras entidades JPA son mapeadas acorde al esquema de BD.




@Entity
@Table(name = "Socios")
@NamedQueries({
  @NamedQuery(name = "Socio.findAll", query = "SELECT t FROM Socio t")})
public class Socio implements Serializable {

 private static final long serialVersionUID = 1L;
 @Id
 @GeneratedValue(strategy = GenerationType.AUTO)
 @Column(name = "Cod_Socio")
 private Integer codSocio;
 @Column(name = "Tipo_Persona")
 private String tipoPersona;
 @Column(name = "Tipo_Documento")
 private String tipoDocumento;
 @Column(name = "Nro_Documento")
 private String nroDocumento;
 @Column(name = "Ape_Paterno")
 private String apePaterno;
 @Column(name = "Ape_Materno")
 private String apeMaterno;
 @Column(name = "Nombres")
 private String nombres;
 @Column(name = "Nom_Completo")
 private String nomCompleto;

 @Column(name = "Fecha_Nacimiento")
 @Temporal(TemporalType.DATE)
 @DateTimeFormat(pattern = "dd/MM/yyyy")
 private Date fechaNacimiento;

 @Column(name = "Nacionalidad")
 private String nacionalidad;
 @Column(name = "Sexo")
 private String sexo;
 @Column(name = "Estado_Civil")
 private String estadoCivil;
 @Column(name = "Educacion")
 private String educacion;
 @Column(name = "Condicion_Laboral")
 private String condicionLaboral;
 @Column(name = "CIIU")
 private String ciiu;
 @Column(name = "Profesion")
 private String profesion;
 @Column(name = "Tip_Doc_Conyuge")
 private String tipDocConyuge;
 @Column(name = "Doc_Conyuge")
 private String docConyuge;
 @Column(name = "Ape_Pat_Conyuge")
 private String apePatConyuge;
 @Column(name = "Ape_Mat_Conyuge")
 private String apeMatConyuge;
 @Column(name = "Nom_Conyuge")
 private String nomConyuge;
 @Column(name = "Telefono_Fijo")
 private String telefonoFijo;
 @Column(name = "Telefono_Celular")
 private String telefonoCelular;
 @Column(name = "Correo_Electronico")
 private String correoElectronico;

 @Column(name = "Fecha_Apertura")
 @Temporal(TemporalType.DATE)
 @DateTimeFormat(pattern = "dd/MM/yyyy")
 private Date fechaApertura;

 @Column(name = "Carga_Familiar")
 private Integer cargaFamiliar;
 @Column(name = "Tipo_Vivienda")
 private String tipoVivienda;
 @Column(name = "Ruc_Laboral")
 private String rucLaboral;
 @Column(name = "Centro_Laboral")
 private String centroLaboral;
 @Column(name = "Cargo")
 private String cargo;

 @Column(name = "Fecha_Ingreso")
 @Temporal(TemporalType.DATE)
 @DateTimeFormat(pattern = "dd/MM/yyyy")
 private Date fechaIngreso;

 @Column(name = "Telefono_Laboral")
 private String telefonoLaboral;
 @Column(name = "Razon_Social")
 private String razonSocial;

 @Column(name = "Fecha_Constitucion")
 @Temporal(TemporalType.DATE)
 @DateTimeFormat(pattern = "dd/MM/yyyy")
 private Date fechaConstitucion;

 @Column(name = "Tipo_Empresa")
 private String tipoEmpresa;
 @Column(name = "RRPP")
 private String rrpp;
 @Column(name = "Tamano_Empresa")
 private Integer tamanoEmpresa;
 @Column(name = "Cal_Interna")
 private String calInterna;
 @Column(name = "Cal_Externa")
 private String calExterna;
 @Column(name = "Activo")
 private Integer activo;
 @Column(name = "Usuario_Registro")
 private String usuarioRegistro;

 @Column(name = "Fecha_Registro")
 @Temporal(TemporalType.DATE)
 @DateTimeFormat(pattern = "dd/MM/yyyy")
 private Date fechaRegistro;

 @Column(name = "Hora_Registro")
 @Temporal(TemporalType.TIME)
 private Date horaRegistro;
 @Column(name = "Usuario_Modifica")
 private String usuarioModifica;
 @Column(name = "Fecha_Modifica")

 @Temporal(TemporalType.DATE)
 @DateTimeFormat(pattern = "dd/MM/yyyy")
 private Date fechaModifica;

 @Column(name = "Hora_Modifica")
 @Temporal(TemporalType.TIME)
 private Date horaModifica;

 @Column(name = "Nom_Tipo_Persona")
 private String nomTipoPersona;
 @Column(name = "Nom_Tipo_Documento")
 private String nomTipoDocumento;

 @OneToMany(mappedBy = "socio", cascade = CascadeType.ALL, orphanRemoval = true)
 private List direccionList = new ArrayList<>();

 @OneToMany(mappedBy = "socio", cascade = CascadeType.ALL, orphanRemoval = true)
 private List representanteList = new ArrayList<>();

....



Con esta configuración podremos guardar en la tabla Socio (Los datos del cliente, sea Natural o Jurídico) , las Direcciones (para ambos tipos) y Representantes (sólo Juridicos) del cliente. Para esta demo trabajaremos la historia de Gestión de Clientes.

5. Los Repositorios


Estos los implementaremos con Spring Data. En caso necesites un query particular, lo puedes implementar usando @Query. Aquí un ejemplo para obtener Departamentos, Provincias y Distritos.



public interface UbigeoRepository extends JpaRepository {

 @Query("SELECT u FROM Ubigeo u WHERE u.codProvincia = '00' and u.codDistrito = '00'")
 List findDepartamentos();

 @Query("SELECT u FROM Ubigeo u WHERE u.codDpto= :codDpto and u.codProvincia <> '00' and u.codDistrito = '00'")
 List findProvincias(@Param("codDpto") String codDpto);

 @Query("SELECT u FROM Ubigeo u WHERE u.codDpto= :codDpto and u.codProvincia = :codProvincia and u.codDistrito <> '00'")
 List findDistritos(@Param("codDpto") String codDpto, @Param("codProvincia") String codProvincia);
}




6. Controladoras

Spring MVC es el framework para implementar el patrón Model View Controller en Spring.  El módelo es bien sencillo. 

6.1 Home



El URL que se invocará por GET, POST, PUT, DELETE ya sea por navegador, postman, link, formulario de una vista, etc. tiene que estar asociado a un método de una controladora. Por eso Spring tiene por ejemplo @GetMapping (para GET) y @PostMapping (para POST).

El return "home" indica que se debe ir a la vista con dicho nombre ubicada en src/main/resources/templates. Eso es una convención. No necesitas configurar eso en ningún lado. 

6.2 Login


La vista de Login se puede invocar directamente o al querer entrar a un URL sin habernos autenticado.

Por eso tendremos esta controladora:



Al cual podemos llamar desde un URL en el navegador (GET) o cuando haya un intento fallido de autenticación con "/login-error".

La vista Login es simple, un simple formulario que hace un POST a "/login" enviando un username y password (Recuerda la configuración de Security). Todo eso es pura convención de Spring Security. Como no quiero complicarme, sigo estas convenciones.


En esta parte ya estamos usando Thymeleaf:


En caso de error, se muestra el mensaje que existe en el messages.properties (src/main/resources):

Login.Error=El nombre de usuario o la contraseña es incorrecto. El acceso fue denegado.

6.3. Panel Principal


Luego de la autenticación, el URL a cargar es http://localhost:8088/  (el puerto 8088 esta definido en el src/main/resources/application.properties).

Como vemos el  "/" es manejado por el HomeController, que buscará cargar la vista "home".

La vista "home" es la siguiente:



Si bien parece muy simple. En realidad carga un Layout o Template que tendrá un ménu lateral izquierdo, header y body. La vista "home" solo coloca un texto en

en el fragmento denominado "content".




El Layout tiene los fragmentos que arman el template, ahí podemos apreciar el fragment "content":



El Layout declara los namespaces, incluido el que tiene integración con la seguridad (sec) y que nos permite ocultar secciones de la vista acorde a tu Rol:


En el menú lateral, por ejemplo, vemos el uso de la seguridad en la vista:


Sólo el Rol ADMINISTRADOR puede hacer uso de esta sección.

6.4. Listado Clientes



  • Todo lo relacionado a clientes lo encontraremos en templates/clientes/:





  • La primera opción a invocar es el listado ("/clientes/list"):






  • Y esto será atendido en ClientesController:



En dicho método obtenemos el listado de clientes por buscador o por el link del menú lateral que vimos anteriormente. Enviamos a la vista el listado de clientes en el objeto del modelo con nombre "clientes".


  • La vista será clientes/list.html que tiene un buscador de clientes y una grilla que mostrará los resultados. Es buscador aparece en esta sección:





  • La grilla de clientes se muestra en base al objeto "clientes" enviado desde la controladora. Contamos para cada registro mostrado la opción para eliminar y editar.

Podemos ver el uso de thymeleaf con:  th:each (para iterar), th:text (para mostrar el valor de un objeto), th:switch (if/else), th:href (para invocar links).



6.5. Nuevo Cliente o Editar Cliente


Aquí vemos una forma simple de manejar el Nuevo o Editar cliente. Veamos la controladora:


Si no viene el "id" es porque hicimos clic en el botón "Nuevo Cliente":



Caso contrario, si vamos a editar un cliente de la grilla, se envía el "id" respectivo:


Ud. dirá, y en que momento se hace la consulta findById, ahí la magia de Spring MVC. El llenará el argumento del método con una consulta findById(). Pruebelo y verá. 


6.6. Vista Cliente/Form


¿Y que hay de la vista? , pues la vista es "/clientes/form", es decir el form.html dentro del directorio clientes

  • La vista form.html es como sigue:


Pero, como quería poner un poco de complejidad, decidí manejar tabs para agregar DATOS GENERALES, DIRECCIONES, OTROS DATOS. Veamos el resultado, en la primera pestaña tenemos que ingresar datos para las Personas Jurídicas:




En la segunda pestaña ingresamos las direcciones tanto para personas naturales o jurídicas (Es válido para ambos):


Si es una persona JURIDICA podemos ingresar varios representantes legales:



Como se habrá dado cuenta, si es persona  NATURAL o JURIDICA, la primera y tercera pantalla cambian:

Para persona NATURAL se puede ingresar toda esta información en la primera pestaña:


Para la ultima pantalla, para personas NATURALES, se ingresan los datos del conyuge:



Ahí si esta mejor.  Nos permitirá ver como usar CSS, JQuery, AJAX con thymeleaf y Spring MVC para conseguir estos resultados.



  • Para cargar los combos de todas las pantallas, podemos hacerlo de la siguiente manera:




Y después simplemente lo mostramos de esta manera sencilla:


El resultado es:




  • Por suerte el template AdminLTE viene con soporte a JQuery, BootStrap, DatePicker, otros controles, estilos de botones, etc. Estos se configuran en el template o Layout.html.


  • Así que los campos fecha se configurarán de esta manera:

Y en dicha función el DatePicker es seteado:


NOTA: Sólo muestro un ejemplo, hay varios combobox a llenar de la misma forma. 

En el html no hay mucho que hacer, solo setear el campo del modelo asociado:


El resultado es:


  • ¿Cómo mostramos ciertos campos si eres PERSONA NATURAL, ocultar y mostrar otros si eres PERSONA JURIDICA?
Esto al principio pense sería super complicado, pero, es más sencillo de lo que pense. 

Sólo defines una clase (inputTipoPersona_hide) que puedas asociar a un componente del formulario y luego si escoges un valor del combobox TipoPersona (con nombre inputTipoPersona), ocultas todos los que no son de dicho valor y muestras los otros. Ahí el truco.



Y si es persona NATURAL (2) o JURIDICA(1) se mostrarán o no los componentes:



Cuando pruebes la demo, escoge el primer combo de la primera pantalla, que es Tipo Persona (Natural o Juridica) y verás como cambian sobretodo la primera y tercera pestaña. 

  • Cuando ya estaba a punto de terminar la primera pantalla. En caso de Persona NATURAL ví que un campo "Carga Familiar", sólo debe aceptar dígitos. 
El tratamiento también es sencillo.


Si intentas colocar letras, te saldrá un mensaje.


Pruebelo y me comenta (sobretodo si existe una mejor solución - espero su pull request). 

Con esto he comentado todo lo que necesita saber para cargar los campos textbox, combobox, datepickers de las 3 pestañas. La segunda y tercer pestaña si tendrán su propio apartado, porque, hay cosas interesantes por ver ahí. 


6.7.  Direcciones


En esta pestaña podemos agregar direcciones para personas NATURALES o JURIDICAS. 

La primera vez, luego de ingresar los datos de la dirección, si queremos agregar una nueva dirección hay que dar clic en el botón GUARDAR. 

Si queremos ingresar una nueva dirección, hacemos clic en el botón NUEVO. Volvemos a ingresar los datos y luego clic en GUARDAR. 



Luego de ingresar una dirección, si queremos editar algún campo, hacemos clic en el botón EDITAR. Los datos son cargados en el formulario arriba de la grilla. Haces los cambios y para actualizar la dirección haces clic en GUARDAR.

Si queremos eliminar una dirección hacemos clic en el botón ELIMINAR del registro seleccionado.



  • Entonces como se imaginará esta pestaña tendrá una estructura como la siguiente:
Cabecera
Detalle (grilla de Direcciones)

  • Para evitar un refresco total de la pantalla, haremos las llamadas AJAX respectivas al backend para agregar al objeto del formulario "cliente" las direcciones que vayamos agregando. Lo mismo para editar o eliminar una dirección.  Por tanto, veamos primero el HTML.
El formulario lo tenemos bien arriba para cubrir las 3 pestañas. 


Los botones Guardar y Cancelar cliente, van en el en el div box-footer. 
  • Los botones Nueva y Guardar dirección se trabajaran de esta manera:


No es necesario para una nueva dirección ir al backend con Ajax (fue una decisión mía). Así que lo haremos del lado del cliente con la función de JavaScript: clearDireccionNueva().


Para manejar el agregar/editar las direcciones tendremos 2 hiddens de apoyo:  Parametro (addDireccion o editDireccion) e Indice (0, 1, 2, ... , n) que es el registro seleccionado de la grilla. 

Pero, al hacer clic en el botón GUARDAR si tendremos que ir por Ajax al backend para agregar al objeto "socio" del formulario esa nueva dirección.  Esta para mi fue la parte mas dificil del proyecto. Lo anterior super fácil con JQuery

Pero, para agregar un poco más de sabor a la pantalla, los combos Departamento, Provincia y Distrito deberían ser anidados y cargados vía Ajax. Esto también es full JQuery y llamar al backend para cargar las provincias y/o distritos respectivamente. 

Solución:

1. Antes de Guardar una nueva Dirección, me preocupe por resolver la carga dinámica de combobox (Departamento, Provincia y Distrito).


Gracias a JQuery cuando escogemos el valor de un combo llamamos al backend usando Ajax. 


El truco para no refrescar toda la pantalla es solo actualizar el fragmento que tu deseas. Por ejemplo, aquí invocamos a  "/clientes/form::provincias" o "/clientes/form::distritos". 

En la vista se espera este resultado de la siguiente manera:


El th:fragment hace la magia. 

¿Qué te pareció?, Cool, ¿no?.


2. Al hacer clic en "Guardar" que tiene como nombre "addDireccion" capturamos el evento, obtenemos los textos de ciertos combobox, y en la variable data guardamos los datos del formulario con $('form').serialize(), luego verificamos si estamos agregando o editando para agregar a data dicho parámetro.  Finalmente hacemos la llamada Ajax al backend con $.post enviandole dicha data, para luego al obtener el resultado, modificar el fragmento html que se debe actualizar con la función replaceItems




En la controladora para agregar la nueva dirección hacemos lo siguiente:


El método addDireccion(socio) es como sigue:


Usted se preguntará y ¿de donde sale esa DireccionNueva?.

Mi artificio para poder guardar los datos de una nueva o existente dirección asociada al formulario es agregar un campo no persistente en la misma entidad Socio.


Por eso en la vista recibo los datos de la nueva o actual dirección así:



Al retornar del método vemos que se regresa al fragmento "/clientes/form::#direcciones" el cual será refrescado por la función de JavaScript replaceItems():


Que actualizará el html #direcciones:

Y los datos de las direcciones se mostrarán acorde a esta parte:





Es necesario los hidden, porque sino al editar la dirección, perdemos dicha información, llega a la contraladora dichos campos en null. 


3. Si hacemos clic en el botón Nuevo, que significa Nueva Dirección, sólo limpiamos los campos y actualizamos los parámetros del lado del cliente. Veamos a continuación:


Cajas de texto en blanco, combobox a sus valores por defecto.

4. Para Editar una Dirección, hacemos clic en el botón Eliminar correspondiente.


La función de JavaScript editarDireccion(button) se encargará de cargar los datos de la dirección seleccionada en el formulario de arriba.


La única parte complicada aquí es volver a cargar los valores de los combobox Departamento, Provincia y Distrito, esto lo resolvemos así:


Si hacemos clic en GUARDAR, como hemos seteado el parámetro editDireccion, el método de la controladora a llamar será:


El proceso de regresar y cambiar el fragmento es similar a lo explicado anteriormente cuando se agrega una nueva dirección. 

5. Finalmente, la eliminación de una dirección es haciendo clic en el botón ELIMINAR:

La función de JavaScript eliminarDireccion actualiza el parámetro con la acción removeDireccion y le pasa el indice seleccionado. Se llama luego al método de la controladora con dicha data


El método de la controladora encargado de eliminar el registro de la lista de direcciones es:


6.8.  Representantes

La tercera pestaña es diferente para Personas Naturales o Jurídicas. Si es Jurídica si podemos agregar varios representantes y es muy similar el funcionamiento a la ventana de Direcciones. Así que si da una revisión,  verá que se implemento muy parecido a direcciones.

6.9.  Eliminar Cliente

Hay que confirmar, antes de eliminar un cliente. Por eso seremos derivado a una ventana que solicita la confirmación. En caso de aceptar, se elimina las direcciones, representantes y datos del cliente seleccionado. 




Si tienes alguna mejora, bienvenido sea de antemano tu pull requests.   

7. ¿Que falta?


Aún faltan validaciones, el buscador y manejar los mensajes en properties, pero, ya eso para un segundo post.

Enjoy!


Joe



Share: