feat: otimização de performance e ajustes finais

This commit is contained in:
Idrissa Banora
2026-05-18 10:49:32 +00:00
commit 52a7c4f9cf
579 changed files with 156489 additions and 0 deletions
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>br.gov.sigefp</groupId>
<artifactId>sigefp-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>sigefp-org</artifactId>
<packaging>jar</packaging>
<name>SIGEFP Org</name>
<description>Módulo de organização: ministérios, unidades, posições</description>
<dependencies>
<dependency>
<groupId>br.gov.sigefp</groupId>
<artifactId>sigefp-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,84 @@
package br.gov.sigefp.org.api;
import br.gov.sigefp.org.api.dto.MinistryDTO;
import br.gov.sigefp.org.service.MinistryService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
/**
* Controller REST para gestão de ministérios.
*/
@RestController
@RequestMapping("/api/org/ministries")
@RequiredArgsConstructor
public class MinistryController {
private final MinistryService ministryService;
@GetMapping
public ResponseEntity<Page<MinistryDTO>> findAll(
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "20") int size,
@RequestParam(value = "sortBy", required = false) String sortBy,
@RequestParam(value = "sortDirection", required = false, defaultValue = "ASC") String sortDirection) {
Sort sort = sortBy != null
? Sort.by(Sort.Direction.fromString(sortDirection), sortBy)
: Sort.by(Sort.Direction.ASC, "code");
Pageable pageable = PageRequest.of(page, size, sort);
Page<MinistryDTO> result = ministryService.findAll(pageable);
return ResponseEntity.ok(result);
}
@GetMapping("/{id}")
public ResponseEntity<MinistryDTO> findById(@PathVariable("id") UUID id) {
try {
MinistryDTO dto = ministryService.findById(id);
return ResponseEntity.ok(dto);
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
}
}
@PostMapping
public ResponseEntity<MinistryDTO> create(@Valid @RequestBody MinistryDTO dto) {
try {
MinistryDTO created = ministryService.create(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
}
}
@PutMapping("/{id}")
public ResponseEntity<MinistryDTO> update(
@PathVariable("id") UUID id,
@Valid @RequestBody MinistryDTO dto) {
try {
MinistryDTO updated = ministryService.update(id, dto);
return ResponseEntity.ok(updated);
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable("id") UUID id) {
try {
ministryService.delete(id);
return ResponseEntity.noContent().build();
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
}
}
}
@@ -0,0 +1,108 @@
package br.gov.sigefp.org.api;
import br.gov.sigefp.org.api.dto.OrgUnitDTO;
import br.gov.sigefp.org.service.OrgUnitService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
/**
* Controller REST para gestão de unidades organizacionais.
*/
@RestController
@RequestMapping("/api/org/org-units")
@RequiredArgsConstructor
public class OrgUnitController {
private final OrgUnitService orgUnitService;
@GetMapping
public ResponseEntity<?> findAll(
@RequestParam(value = "ministryId", required = false) UUID ministryId,
@RequestParam(value = "parentUnitId", required = false) UUID parentUnitId,
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "20") int size,
@RequestParam(value = "sortBy", required = false) String sortBy,
@RequestParam(value = "sortDirection", required = false, defaultValue = "ASC") String sortDirection) {
// Se há filtros, retornar lista (sem paginação)
if (ministryId != null || parentUnitId != null) {
List<OrgUnitDTO> result;
if (ministryId != null) {
result = orgUnitService.findByMinistryId(ministryId);
if (parentUnitId != null) {
result = result.stream()
.filter(ou -> parentUnitId.equals(ou.getParentUnitId()))
.toList();
}
} else {
// Apenas parentUnitId - buscar todas e filtrar
result = orgUnitService.findAll(PageRequest.of(0, Integer.MAX_VALUE)).getContent().stream()
.filter(ou -> parentUnitId.equals(ou.getParentUnitId()))
.toList();
}
return ResponseEntity.ok(result);
}
// Sem filtros, retornar paginado
Sort sort = sortBy != null
? Sort.by(Sort.Direction.fromString(sortDirection), sortBy)
: Sort.by(Sort.Direction.ASC, "code");
Pageable pageable = PageRequest.of(page, size, sort);
Page<OrgUnitDTO> result = orgUnitService.findAll(pageable);
return ResponseEntity.ok(result);
}
@GetMapping("/tree/{ministryId}")
public ResponseEntity<List<OrgUnitDTO>> findTreeByMinistry(@PathVariable("ministryId") UUID ministryId) {
try {
List<OrgUnitDTO> tree = orgUnitService.findTreeByMinistryId(ministryId);
return ResponseEntity.ok(tree);
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
}
}
@GetMapping("/{id}")
public ResponseEntity<OrgUnitDTO> findById(@PathVariable("id") UUID id) {
try {
OrgUnitDTO dto = orgUnitService.findById(id);
return ResponseEntity.ok(dto);
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
}
}
@PostMapping
public ResponseEntity<OrgUnitDTO> create(@Valid @RequestBody OrgUnitDTO dto) {
try {
OrgUnitDTO created = orgUnitService.create(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
}
}
@PutMapping("/{id}")
public ResponseEntity<OrgUnitDTO> update(
@PathVariable("id") UUID id,
@Valid @RequestBody OrgUnitDTO dto) {
try {
OrgUnitDTO updated = orgUnitService.update(id, dto);
return ResponseEntity.ok(updated);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
}
}
}
@@ -0,0 +1,83 @@
package br.gov.sigefp.org.api;
import br.gov.sigefp.org.api.dto.PositionDTO;
import br.gov.sigefp.org.service.PositionService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
/**
* Controller REST para gestão de posições/cargos.
*/
@RestController
@RequestMapping("/api/org/positions")
@RequiredArgsConstructor
public class PositionController {
private final PositionService positionService;
@GetMapping
public ResponseEntity<?> findAll(
@RequestParam(value = "orgUnitId", required = false) UUID orgUnitId,
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "20") int size,
@RequestParam(value = "sortBy", required = false) String sortBy,
@RequestParam(value = "sortDirection", required = false, defaultValue = "ASC") String sortDirection) {
if (orgUnitId != null) {
// Retornar lista quando filtrado por orgUnitId
List<PositionDTO> positions = positionService.findByOrgUnitId(orgUnitId);
return ResponseEntity.ok(positions);
}
Sort sort = sortBy != null
? Sort.by(Sort.Direction.fromString(sortDirection), sortBy)
: Sort.by(Sort.Direction.ASC, "code");
Pageable pageable = PageRequest.of(page, size, sort);
Page<PositionDTO> result = positionService.findAll(pageable);
return ResponseEntity.ok(result);
}
@GetMapping("/{id}")
public ResponseEntity<PositionDTO> findById(@PathVariable("id") UUID id) {
try {
PositionDTO dto = positionService.findById(id);
return ResponseEntity.ok(dto);
} catch (IllegalArgumentException e) {
return ResponseEntity.notFound().build();
}
}
@PostMapping
public ResponseEntity<PositionDTO> create(@Valid @RequestBody PositionDTO dto) {
try {
PositionDTO created = positionService.create(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
}
}
@PutMapping("/{id}")
public ResponseEntity<PositionDTO> update(
@PathVariable("id") UUID id,
@Valid @RequestBody PositionDTO dto) {
try {
PositionDTO updated = positionService.update(id, dto);
return ResponseEntity.ok(updated);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().build();
}
}
}
@@ -0,0 +1,41 @@
package br.gov.sigefp.org.api.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
/**
* DTO para transferência de dados de ministério.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MinistryDTO {
private UUID id;
@NotBlank(message = "Código é obrigatório")
@Size(min = 1, max = 50, message = "Código deve ter entre 1 e 50 caracteres")
private String code;
@NotBlank(message = "Nome é obrigatório")
@Size(max = 200, message = "Nome deve ter no máximo 200 caracteres")
private String name;
@NotBlank(message = "Sigla é obrigatória")
@Size(max = 20, message = "Sigla deve ter no máximo 20 caracteres")
private String acronym;
private String description;
private String logo;
private Boolean isActive;
}
@@ -0,0 +1,59 @@
package br.gov.sigefp.org.api.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.UUID;
/**
* DTO para transferência de dados de unidade organizacional.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrgUnitDTO {
private UUID id;
@NotNull(message = "ID do ministério é obrigatório")
private UUID ministryId;
private UUID parentUnitId;
@NotBlank(message = "Código é obrigatório")
@Size(min = 1, max = 50, message = "Código deve ter entre 1 e 50 caracteres")
private String code;
@NotBlank(message = "Nome é obrigatório")
@Size(max = 200, message = "Nome deve ter no máximo 200 caracteres")
private String name;
@NotBlank(message = "Tipo de unidade é obrigatório")
@Size(max = 50, message = "Tipo de unidade deve ter no máximo 50 caracteres")
private String type;
@Size(max = 20, message = "Sigla deve ter no máximo 20 caracteres")
private String acronym;
@Size(max = 255, message = "Endereço deve ter no máximo 255 caracteres")
private String address;
@Size(max = 100, message = "Email deve ter no máximo 100 caracteres")
private String email;
@Size(max = 20, message = "Telefone deve ter no máximo 20 caracteres")
private String phone;
private Boolean isActive;
// Para representar a árvore hierárquica
private List<OrgUnitDTO> children;
}
@@ -0,0 +1,39 @@
package br.gov.sigefp.org.api.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
/**
* DTO para transferência de dados de posição/cargo.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PositionDTO {
private UUID id;
@NotNull(message = "ID da unidade organizacional é obrigatório")
private UUID orgUnitId;
@NotBlank(message = "Código é obrigatório")
@Size(min = 1, max = 50, message = "Código deve ter entre 1 e 50 caracteres")
private String code;
@NotBlank(message = "Título é obrigatório")
@Size(max = 200, message = "Título deve ter no máximo 200 caracteres")
private String title;
@Size(max = 50, message = "Nível deve ter no máximo 50 caracteres")
private String level;
private Boolean isActive;
}
@@ -0,0 +1,40 @@
package br.gov.sigefp.org.domain;
import br.gov.sigefp.common.domain.BaseEntity;
import jakarta.persistence.*;
import lombok.*;
/**
* Entidade que representa um ministério.
*/
@Entity
@Table(name = "ministry", indexes = {
@Index(name = "idx_ministry_code", columnList = "code")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Ministry extends BaseEntity {
@Column(nullable = false, unique = true, length = 50)
private String code;
@Column(nullable = false, length = 200)
private String name;
@Column(length = 20)
private String acronym;
@Column(length = 255)
private String description;
@Column(columnDefinition = "TEXT")
private String logo;
@Column(nullable = false, name = "is_active")
@Builder.Default
private Boolean isActive = true;
}
@@ -0,0 +1,57 @@
package br.gov.sigefp.org.domain;
import br.gov.sigefp.common.domain.BaseEntity;
import jakarta.persistence.*;
import lombok.*;
/**
* Entidade que representa uma unidade organizacional.
* Suporta hierarquia através de parentUnit (auto-relacionamento).
*/
@Entity
@Table(name = "org_unit", indexes = {
@Index(name = "idx_org_unit_code", columnList = "code"),
@Index(name = "idx_org_unit_ministry", columnList = "ministry_id"),
@Index(name = "idx_org_unit_parent", columnList = "parent_unit_id")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class OrgUnit extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ministry_id", nullable = false)
private Ministry ministry;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_unit_id")
private OrgUnit parentUnit;
@Column(nullable = false, unique = true, length = 50)
private String code;
@Column(nullable = false, length = 200)
private String name;
@Column(nullable = false, length = 50, name = "unit_type")
private String unitType; // DEPARTMENT, DIVISION, SECTION, etc.
@Column(length = 20)
private String acronym;
@Column(length = 255)
private String address;
@Column(length = 100)
private String email;
@Column(length = 20)
private String phone;
@Column(nullable = false, name = "is_active")
@Builder.Default
private Boolean isActive = true;
}
@@ -0,0 +1,39 @@
package br.gov.sigefp.org.domain;
import br.gov.sigefp.common.domain.BaseEntity;
import jakarta.persistence.*;
import lombok.*;
/**
* Entidade que representa uma posição/cargo dentro de uma unidade organizacional.
*/
@Entity
@Table(name = "position", indexes = {
@Index(name = "idx_position_code", columnList = "code"),
@Index(name = "idx_position_org_unit", columnList = "org_unit_id")
})
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Position extends BaseEntity {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "org_unit_id", nullable = false)
private OrgUnit orgUnit;
@Column(nullable = false, unique = true, length = 50)
private String code;
@Column(nullable = false, length = 200)
private String title;
@Column(length = 50)
private String level;
@Column(nullable = false, name = "is_active")
@Builder.Default
private Boolean isActive = true;
}
@@ -0,0 +1,21 @@
package br.gov.sigefp.org.repository;
import br.gov.sigefp.org.domain.Ministry;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface MinistryRepository extends JpaRepository<Ministry, UUID> {
Optional<Ministry> findByCode(String code);
boolean existsByCode(String code);
Page<Ministry> findAllByIsActiveTrue(Pageable pageable);
}
@@ -0,0 +1,37 @@
package br.gov.sigefp.org.repository;
import br.gov.sigefp.org.domain.OrgUnit;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface OrgUnitRepository extends JpaRepository<OrgUnit, UUID> {
@EntityGraph(attributePaths = { "ministry", "parentUnit" })
Page<OrgUnit> findAll(Pageable pageable);
Optional<OrgUnit> findByCode(String code);
boolean existsByCode(String code);
@EntityGraph(attributePaths = { "ministry", "parentUnit" })
List<OrgUnit> findByMinistryId(UUID ministryId);
List<OrgUnit> findByParentUnitId(UUID parentUnitId);
@Query("SELECT ou FROM OrgUnit ou WHERE ou.ministry.id = :ministryId AND ou.parentUnit IS NULL")
List<OrgUnit> findRootUnitsByMinistryId(@Param("ministryId") UUID ministryId);
@Query("SELECT ou FROM OrgUnit ou WHERE ou.ministry.id = :ministryId AND ou.parentUnit.id = :parentUnitId")
List<OrgUnit> findByMinistryIdAndParentUnitId(@Param("ministryId") UUID ministryId,
@Param("parentUnitId") UUID parentUnitId);
}
@@ -0,0 +1,26 @@
package br.gov.sigefp.org.repository;
import br.gov.sigefp.org.domain.Position;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface PositionRepository extends JpaRepository<Position, UUID> {
Optional<Position> findByCode(String code);
boolean existsByCode(String code);
@EntityGraph(attributePaths = { "orgUnit" })
Page<Position> findAll(Pageable pageable);
@EntityGraph(attributePaths = { "orgUnit" })
List<Position> findByOrgUnitId(UUID orgUnitId);
}
@@ -0,0 +1,121 @@
package br.gov.sigefp.org.service;
import br.gov.sigefp.org.api.dto.MinistryDTO;
import br.gov.sigefp.org.domain.Ministry;
import br.gov.sigefp.org.repository.MinistryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
/**
* Serviço de aplicação para gestão de ministérios.
*/
@Service
@RequiredArgsConstructor
@Transactional
public class MinistryService {
private final MinistryRepository ministryRepository;
public MinistryDTO create(MinistryDTO dto) {
if (ministryRepository.existsByCode(dto.getCode())) {
throw new IllegalArgumentException("Código de ministério já existe: " + dto.getCode());
}
Ministry ministry = Ministry.builder()
.code(dto.getCode())
.name(dto.getName())
.acronym(dto.getAcronym())
.description(dto.getDescription())
.logo(dto.getLogo())
.isActive(dto.getIsActive() != null ? dto.getIsActive() : true)
.build();
Ministry saved = ministryRepository.save(ministry);
return toDTO(saved);
}
public MinistryDTO update(UUID id, MinistryDTO dto) {
Ministry ministry = ministryRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Ministério não encontrado: " + id));
if (dto.getCode() != null && !dto.getCode().equals(ministry.getCode())) {
if (ministryRepository.existsByCode(dto.getCode())) {
throw new IllegalArgumentException("Código de ministério já existe: " + dto.getCode());
}
ministry.setCode(dto.getCode());
}
if (dto.getName() != null) {
ministry.setName(dto.getName());
}
if (dto.getAcronym() != null) {
ministry.setAcronym(dto.getAcronym());
}
if (dto.getDescription() != null) {
ministry.setDescription(dto.getDescription());
}
if (dto.getLogo() != null) {
ministry.setLogo(dto.getLogo());
}
if (dto.getIsActive() != null) {
ministry.setIsActive(dto.getIsActive());
}
Ministry saved = ministryRepository.save(ministry);
return toDTO(saved);
}
public MinistryDTO activate(UUID id) {
Ministry ministry = ministryRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Ministério não encontrado: " + id));
ministry.setIsActive(true);
Ministry saved = ministryRepository.save(ministry);
return toDTO(saved);
}
public MinistryDTO deactivate(UUID id) {
Ministry ministry = ministryRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Ministério não encontrado: " + id));
ministry.setIsActive(false);
Ministry saved = ministryRepository.save(ministry);
return toDTO(saved);
}
@Transactional(readOnly = true)
public MinistryDTO findById(UUID id) {
Ministry ministry = ministryRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Ministério não encontrado: " + id));
return toDTO(ministry);
}
@Transactional(readOnly = true)
public Page<MinistryDTO> findAll(Pageable pageable) {
return ministryRepository.findAllByIsActiveTrue(pageable).map(this::toDTO);
}
private MinistryDTO toDTO(Ministry ministry) {
return MinistryDTO.builder()
.id(ministry.getId())
.code(ministry.getCode())
.name(ministry.getName())
.acronym(ministry.getAcronym())
.description(ministry.getDescription())
.logo(ministry.getLogo())
.isActive(ministry.getIsActive())
.build();
}
public void delete(UUID id) {
Ministry ministry = ministryRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Ministério não encontrado: " + id));
ministry.setIsActive(false);
ministryRepository.save(ministry);
}
}
@@ -0,0 +1,215 @@
package br.gov.sigefp.org.service;
import br.gov.sigefp.org.api.dto.OrgUnitDTO;
import br.gov.sigefp.org.domain.Ministry;
import br.gov.sigefp.org.domain.OrgUnit;
import br.gov.sigefp.org.repository.MinistryRepository;
import br.gov.sigefp.org.repository.OrgUnitRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Serviço de aplicação para gestão de unidades organizacionais.
*/
@Service
@RequiredArgsConstructor
@Transactional
public class OrgUnitService {
private final OrgUnitRepository orgUnitRepository;
private final MinistryRepository ministryRepository;
private final br.gov.sigefp.common.service.GlobalAuditLogService auditLogService;
@CacheEvict(value = { "orgUnits", "orgUnitsByMinistry" }, allEntries = true)
public OrgUnitDTO create(OrgUnitDTO dto) {
// Validação: unicidade de código
if (orgUnitRepository.existsByCode(dto.getCode())) {
throw new IllegalArgumentException("Código de unidade já existe: " + dto.getCode());
}
Ministry ministry = ministryRepository.findById(dto.getMinistryId())
.orElseThrow(() -> new IllegalArgumentException("Ministério não encontrado: " + dto.getMinistryId()));
OrgUnit parentUnit = null;
if (dto.getParentUnitId() != null) {
parentUnit = orgUnitRepository.findById(dto.getParentUnitId())
.orElseThrow(
() -> new IllegalArgumentException("Unidade pai não encontrada: " + dto.getParentUnitId()));
// Validação: parentUnit deve pertencer ao mesmo ministério
if (!parentUnit.getMinistry().getId().equals(ministry.getId())) {
throw new IllegalArgumentException("Unidade pai deve pertencer ao mesmo ministério");
}
}
OrgUnit orgUnit = OrgUnit.builder()
.ministry(ministry)
.parentUnit(parentUnit)
.code(dto.getCode())
.name(dto.getName())
.unitType(dto.getType())
.acronym(dto.getAcronym())
.address(dto.getAddress())
.email(dto.getEmail())
.phone(dto.getPhone())
.isActive(dto.getIsActive() != null ? dto.getIsActive() : true)
.build();
OrgUnit saved = orgUnitRepository.save(orgUnit);
return toDTO(saved);
}
@CacheEvict(value = { "orgUnits", "orgUnitsByMinistry" }, allEntries = true)
public OrgUnitDTO update(UUID id, OrgUnitDTO dto) {
OrgUnit orgUnit = orgUnitRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Unidade organizacional não encontrada: " + id));
StringBuilder log = new StringBuilder();
if (dto.getCode() != null && !dto.getCode().equals(orgUnit.getCode())) {
if (orgUnitRepository.existsByCode(dto.getCode())) {
throw new IllegalArgumentException("Código de unidade já existe: " + dto.getCode());
}
log.append("Código: [").append(orgUnit.getCode()).append("] -> [").append(dto.getCode()).append("]. ");
orgUnit.setCode(dto.getCode());
}
if (dto.getMinistryId() != null && !dto.getMinistryId().equals(orgUnit.getMinistry().getId())) {
Ministry ministry = ministryRepository.findById(dto.getMinistryId())
.orElseThrow(
() -> new IllegalArgumentException("Ministério não encontrado: " + dto.getMinistryId()));
log.append("Ministério: [").append(orgUnit.getMinistry().getName()).append("] -> [")
.append(ministry.getName()).append("]. ");
orgUnit.setMinistry(ministry);
}
if (dto.getParentUnitId() != null) {
if (orgUnit.getParentUnit() == null || !dto.getParentUnitId().equals(orgUnit.getParentUnit().getId())) {
OrgUnit parentUnit = orgUnitRepository.findById(dto.getParentUnitId())
.orElseThrow(() -> new IllegalArgumentException(
"Unidade pai não encontrada: " + dto.getParentUnitId()));
if (!parentUnit.getMinistry().getId().equals(orgUnit.getMinistry().getId())) {
throw new IllegalArgumentException("Unidade pai deve pertencer ao mesmo ministério");
}
String oldParent = orgUnit.getParentUnit() != null ? orgUnit.getParentUnit().getName() : "N/A";
log.append("Unidade Pai: [").append(oldParent).append("] -> [").append(parentUnit.getName())
.append("]. ");
orgUnit.setParentUnit(parentUnit);
}
} else if (dto.getParentUnitId() == null && orgUnit.getParentUnit() != null) {
log.append("Unidade Pai: [").append(orgUnit.getParentUnit().getName()).append("] -> [N/A]. ");
orgUnit.setParentUnit(null);
}
if (dto.getName() != null && !dto.getName().equals(orgUnit.getName())) {
log.append("Nome: [").append(orgUnit.getName()).append("] -> [").append(dto.getName()).append("]. ");
orgUnit.setName(dto.getName());
}
if (dto.getType() != null && !dto.getType().equals(orgUnit.getUnitType())) {
log.append("Tipo: [").append(orgUnit.getUnitType()).append("] -> [").append(dto.getType()).append("]. ");
orgUnit.setUnitType(dto.getType());
}
if (dto.getAcronym() != null && !dto.getAcronym().equals(orgUnit.getAcronym())) {
log.append("Sigla: [").append(orgUnit.getAcronym()).append("] -> [").append(dto.getAcronym()).append("]. ");
orgUnit.setAcronym(dto.getAcronym());
}
if (dto.getIsActive() != null && !dto.getIsActive().equals(orgUnit.getIsActive())) {
log.append("Ativo: [").append(orgUnit.getIsActive()).append("] -> [").append(dto.getIsActive())
.append("]. ");
orgUnit.setIsActive(dto.getIsActive());
}
// Outros campos técnicos (contato/endereço) - log simplificado
if (dto.getEmail() != null && !dto.getEmail().equals(orgUnit.getEmail()))
orgUnit.setEmail(dto.getEmail());
if (dto.getPhone() != null && !dto.getPhone().equals(orgUnit.getPhone()))
orgUnit.setPhone(dto.getPhone());
if (dto.getAddress() != null && !dto.getAddress().equals(orgUnit.getAddress()))
orgUnit.setAddress(dto.getAddress());
OrgUnit saved = orgUnitRepository.save(orgUnit);
if (log.length() > 0) {
// Nota: id do usuário viria do SecurityContext em produção
auditLogService.logAction(null, "system", "ORG", "UPDATE", "OrgUnit", id, log.toString());
}
return toDTO(saved);
}
@Transactional(readOnly = true)
@Cacheable(value = "orgUnits", key = "#id")
public OrgUnitDTO findById(UUID id) {
OrgUnit orgUnit = orgUnitRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Unidade organizacional não encontrada: " + id));
return toDTO(orgUnit);
}
@Transactional(readOnly = true)
public Page<OrgUnitDTO> findAll(Pageable pageable) {
return orgUnitRepository.findAll(pageable).map(this::toDTO);
}
@Transactional(readOnly = true)
@Cacheable(value = "orgUnitsByMinistry", key = "#ministryId")
public List<OrgUnitDTO> findByMinistryId(UUID ministryId) {
return orgUnitRepository.findByMinistryId(ministryId).stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
@Transactional(readOnly = true)
public List<OrgUnitDTO> findTreeByMinistryId(UUID ministryId) {
// Buscar unidades raiz (sem parentUnit) do ministério
List<OrgUnit> rootUnits = orgUnitRepository.findRootUnitsByMinistryId(ministryId);
// Construir árvore recursivamente
return rootUnits.stream()
.map(this::buildTree)
.collect(Collectors.toList());
}
private OrgUnitDTO buildTree(OrgUnit orgUnit) {
OrgUnitDTO dto = toDTO(orgUnit);
// Buscar filhos
List<OrgUnit> children = orgUnitRepository.findByParentUnitId(orgUnit.getId());
if (!children.isEmpty()) {
List<OrgUnitDTO> childrenDTOs = children.stream()
.map(this::buildTree)
.collect(Collectors.toList());
dto.setChildren(childrenDTOs);
} else {
dto.setChildren(new ArrayList<>());
}
return dto;
}
private OrgUnitDTO toDTO(OrgUnit orgUnit) {
return OrgUnitDTO.builder()
.id(orgUnit.getId())
.ministryId(orgUnit.getMinistry().getId())
.parentUnitId(orgUnit.getParentUnit() != null ? orgUnit.getParentUnit().getId() : null)
.code(orgUnit.getCode())
.name(orgUnit.getName())
.type(orgUnit.getUnitType())
.acronym(orgUnit.getAcronym())
.address(orgUnit.getAddress())
.email(orgUnit.getEmail())
.phone(orgUnit.getPhone())
.isActive(orgUnit.getIsActive())
.build();
}
}
@@ -0,0 +1,128 @@
package br.gov.sigefp.org.service;
import br.gov.sigefp.org.api.dto.PositionDTO;
import br.gov.sigefp.org.domain.OrgUnit;
import br.gov.sigefp.org.domain.Position;
import br.gov.sigefp.org.repository.OrgUnitRepository;
import br.gov.sigefp.org.repository.PositionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Serviço de aplicação para gestão de posições/cargos.
*/
@Service
@RequiredArgsConstructor
@Transactional
public class PositionService {
private final PositionRepository positionRepository;
private final OrgUnitRepository orgUnitRepository;
private final br.gov.sigefp.common.service.GlobalAuditLogService auditLogService;
public PositionDTO create(PositionDTO dto) {
// Validação: unicidade de código
if (positionRepository.existsByCode(dto.getCode())) {
throw new IllegalArgumentException("Código de posição já existe: " + dto.getCode());
}
OrgUnit orgUnit = orgUnitRepository.findById(dto.getOrgUnitId())
.orElseThrow(() -> new IllegalArgumentException(
"Unidade organizacional não encontrada: " + dto.getOrgUnitId()));
Position position = Position.builder()
.orgUnit(orgUnit)
.code(dto.getCode())
.title(dto.getTitle())
.level(dto.getLevel())
.isActive(dto.getIsActive() != null ? dto.getIsActive() : true)
.build();
Position saved = positionRepository.save(position);
return toDTO(saved);
}
public PositionDTO update(UUID id, PositionDTO dto) {
Position position = positionRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Posição não encontrada: " + id));
StringBuilder log = new StringBuilder();
if (dto.getCode() != null && !dto.getCode().equals(position.getCode())) {
if (positionRepository.existsByCode(dto.getCode())) {
throw new IllegalArgumentException("Código de posição já existe: " + dto.getCode());
}
log.append("Código: [").append(position.getCode()).append("] -> [").append(dto.getCode()).append("]. ");
position.setCode(dto.getCode());
}
if (dto.getOrgUnitId() != null && !dto.getOrgUnitId().equals(position.getOrgUnit().getId())) {
OrgUnit orgUnit = orgUnitRepository.findById(dto.getOrgUnitId())
.orElseThrow(() -> new IllegalArgumentException(
"Unidade organizacional não encontrada: " + dto.getOrgUnitId()));
log.append("Unidade: [").append(position.getOrgUnit().getName()).append("] -> [").append(orgUnit.getName())
.append("]. ");
position.setOrgUnit(orgUnit);
}
if (dto.getTitle() != null && !dto.getTitle().equals(position.getTitle())) {
log.append("Título: [").append(position.getTitle()).append("] -> [").append(dto.getTitle()).append("]. ");
position.setTitle(dto.getTitle());
}
if (dto.getLevel() != null && !dto.getLevel().equals(position.getLevel())) {
log.append("Nível: [").append(position.getLevel()).append("] -> [").append(dto.getLevel()).append("]. ");
position.setLevel(dto.getLevel());
}
if (dto.getIsActive() != null && !dto.getIsActive().equals(position.getIsActive())) {
log.append("Ativo: [").append(position.getIsActive()).append("] -> [").append(dto.getIsActive())
.append("]. ");
position.setIsActive(dto.getIsActive());
}
Position saved = positionRepository.save(position);
if (log.length() > 0) {
// Nota: id do usuário viria do SecurityContext em produção
auditLogService.logAction(null, "system", "ORG", "UPDATE", "Position", id, log.toString());
}
return toDTO(saved);
}
@Transactional(readOnly = true)
public PositionDTO findById(UUID id) {
Position position = positionRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Posição não encontrada: " + id));
return toDTO(position);
}
@Transactional(readOnly = true)
public Page<PositionDTO> findAll(Pageable pageable) {
return positionRepository.findAll(pageable).map(this::toDTO);
}
@Transactional(readOnly = true)
public List<PositionDTO> findByOrgUnitId(UUID orgUnitId) {
return positionRepository.findByOrgUnitId(orgUnitId).stream()
.map(this::toDTO)
.collect(Collectors.toList());
}
private PositionDTO toDTO(Position position) {
return PositionDTO.builder()
.id(position.getId())
.orgUnitId(position.getOrgUnit().getId())
.code(position.getCode())
.title(position.getTitle())
.level(position.getLevel())
.isActive(position.getIsActive())
.build();
}
}