Novo repositorio do projeto

This commit is contained in:
2026-03-31 19:21:11 -03:00
commit 02cfd71cf4
57 changed files with 3734 additions and 0 deletions

37
.gitignore vendored Normal file
View File

@@ -0,0 +1,37 @@
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# Eclipse
.settings/
.classpath
.project
# IntelliJ IDEA
.idea/
*.iws
*.iml
*.ipr
out/
*.factorypath
# VS Code
.vscode/
# Environment variables
.env
# Logs
*.log
# OS
.DS_Store
Thumbs.db

141
pom.xml Normal file
View File

@@ -0,0 +1,141 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.agendaestudantil</groupId>
<artifactId>agenda-digital-estudantes</artifactId>
<version>1.0.0</version>
<name>Agenda Digital para Estudantes</name>
<description>Backend para agenda digital destinado a estudantes com dificuldade de organização</description>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<lombok.version>1.18.30</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</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-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,12 @@
package com.agendaestudantil;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AgendaDigitalEstudantesApplication {
public static void main(String[] args) {
SpringApplication.run(AgendaDigitalEstudantesApplication.class, args);
}
}

View File

@@ -0,0 +1,9 @@
package com.agendaestudantil.configuracao;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
@Configuration
@EnableMongoAuditing
public class ConfiguracaoMongo {
}

View File

@@ -0,0 +1,77 @@
package com.agendaestudantil.configuracao;
import com.agendaestudantil.filtro.FiltroJwt;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@EnableWebSecurity
public class ConfiguracaoSeguranca {
private final FiltroJwt filtroJwt;
private final String corsAllowedOrigins;
public ConfiguracaoSeguranca(FiltroJwt filtroJwt, @Value("${cors.allowed.origins}") String corsAllowedOrigins) {
this.filtroJwt = filtroJwt;
this.corsAllowedOrigins = corsAllowedOrigins;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/", "/index.html", "/login.html", "/cadastro.html",
"/favicon.ico", "/imagens/**",
"/*.css", "/*.js", "/*.ico", "/*.png",
"/api/estudantes/cadastro", "/api/estudantes/login",
"/swagger-ui/**", "/v3/api-docs/**")
.permitAll()
.anyRequest().authenticated())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(filtroJwt, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of(corsAllowedOrigins.split(",")));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setExposedHeaders(List.of("Authorization"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}

View File

@@ -0,0 +1,18 @@
package com.agendaestudantil.configuracao;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ResourceController {
@GetMapping("/")
public String index() {
return "forward:/login.html";
}
@GetMapping("/app")
public String app() {
return "forward:/calendario.html";
}
}

View File

@@ -0,0 +1,74 @@
package com.agendaestudantil.controlador;
import com.agendaestudantil.dto.RequisicaoDisciplinaDTO;
import com.agendaestudantil.dto.RespostaApi;
import com.agendaestudantil.dto.RespostaDisciplinaDTO;
import com.agendaestudantil.entidade.Disciplina;
import com.agendaestudantil.servico.DisciplinaServico;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/disciplinas")
public class DisciplinaControlador {
private final DisciplinaServico disciplinaServico;
public DisciplinaControlador(DisciplinaServico disciplinaServico) {
this.disciplinaServico = disciplinaServico;
}
@PostMapping
public ResponseEntity<RespostaApi<RespostaDisciplinaDTO>> criarDisciplina(
@Valid @RequestBody RequisicaoDisciplinaDTO dto) {
Disciplina disciplina = new Disciplina();
disciplina.setNome(dto.nome());
disciplina.setProfessor(dto.professor());
disciplina.setSala(dto.sala());
disciplina.setCor(dto.cor());
RespostaDisciplinaDTO resposta = disciplinaServico.criarDisciplina(disciplina, dto.estudanteId());
return ResponseEntity.status(HttpStatus.CREATED).body(RespostaApi.sucesso(resposta));
}
@GetMapping("/estudante/{estudanteId}")
public ResponseEntity<RespostaApi<List<RespostaDisciplinaDTO>>> listarPorEstudante(
@PathVariable String estudanteId) {
List<RespostaDisciplinaDTO> disciplinas = disciplinaServico.listarPorEstudante(estudanteId);
return ResponseEntity.ok(RespostaApi.sucesso(disciplinas));
}
@GetMapping("/{id}")
public ResponseEntity<RespostaApi<RespostaDisciplinaDTO>> buscarPorId(
@PathVariable String id,
@RequestParam String estudanteId) {
RespostaDisciplinaDTO disciplina = disciplinaServico.buscarPorId(id, estudanteId);
return ResponseEntity.ok(RespostaApi.sucesso(disciplina));
}
@PutMapping("/{id}")
public ResponseEntity<RespostaApi<RespostaDisciplinaDTO>> atualizarDisciplina(
@PathVariable String id,
@Valid @RequestBody RequisicaoDisciplinaDTO dto) {
Disciplina disciplina = new Disciplina();
disciplina.setNome(dto.nome());
disciplina.setProfessor(dto.professor());
disciplina.setSala(dto.sala());
disciplina.setCor(dto.cor());
RespostaDisciplinaDTO resposta = disciplinaServico.atualizarDisciplina(id, disciplina, dto.estudanteId());
return ResponseEntity.ok(RespostaApi.sucesso(resposta));
}
@DeleteMapping("/{id}")
public ResponseEntity<RespostaApi<Void>> excluirDisciplina(
@PathVariable String id,
@RequestParam String estudanteId) {
disciplinaServico.excluirDisciplina(id, estudanteId);
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(RespostaApi.sucesso(null));
}
}

View File

@@ -0,0 +1,44 @@
package com.agendaestudantil.controlador;
import com.agendaestudantil.dto.RespostaApi;
import com.agendaestudantil.dto.RequisicaoCadastroDTO;
import com.agendaestudantil.dto.RespostaEstudanteDTO;
import com.agendaestudantil.dto.RequisicaoLoginDTO;
import com.agendaestudantil.dto.RespostaLoginDTO;
import com.agendaestudantil.servico.EstudanteServico;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/estudantes")
public class EstudanteControlador {
private final EstudanteServico estudanteServico;
public EstudanteControlador(EstudanteServico estudanteServico) {
this.estudanteServico = estudanteServico;
}
@PostMapping("/cadastro")
public ResponseEntity<RespostaApi<RespostaEstudanteDTO>> cadastrar(@Valid @RequestBody RequisicaoCadastroDTO dto) {
RespostaEstudanteDTO resposta = estudanteServico.cadastrar(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(RespostaApi.sucesso(resposta));
}
@PostMapping("/login")
public ResponseEntity<RespostaApi<RespostaLoginDTO>> login(@Valid @RequestBody RequisicaoLoginDTO dto) {
RespostaLoginDTO resposta = estudanteServico.login(dto);
return ResponseEntity.ok(RespostaApi.sucesso(resposta));
}
// Retorna dados do usuário autenticado via JWT (sem precisar do ID na URL)
@GetMapping("/me")
public ResponseEntity<RespostaApi<RespostaEstudanteDTO>> me(@AuthenticationPrincipal UserDetails userDetails) {
RespostaEstudanteDTO resposta = estudanteServico.buscarPorId(userDetails.getUsername());
return ResponseEntity.ok(RespostaApi.sucesso(resposta));
}
}

View File

@@ -0,0 +1,96 @@
package com.agendaestudantil.controlador;
import com.agendaestudantil.dto.RequisicaoEventoDTO;
import com.agendaestudantil.dto.RespostaApi;
import com.agendaestudantil.dto.RespostaEventoDTO;
import com.agendaestudantil.entidade.Evento;
import com.agendaestudantil.servico.EventoServico;
import jakarta.validation.Valid;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
@RestController
@RequestMapping("/api/eventos")
public class EventoControlador {
private final EventoServico eventoServico;
public EventoControlador(EventoServico eventoServico) {
this.eventoServico = eventoServico;
}
@PostMapping
public ResponseEntity<RespostaApi<RespostaEventoDTO>> criarEvento(
@Valid @RequestBody RequisicaoEventoDTO dto) {
Evento evento = new Evento();
evento.setTitulo(dto.titulo());
evento.setDescricao(dto.descricao());
evento.setTipo(dto.tipo());
evento.setLocal(dto.local());
evento.setDataHora(dto.dataHora());
evento.setDisciplinaId(dto.disciplinaId());
RespostaEventoDTO resposta = eventoServico.criarEvento(evento, dto.estudanteId());
return ResponseEntity.status(HttpStatus.CREATED).body(RespostaApi.sucesso(resposta));
}
@GetMapping("/estudante/{estudanteId}")
public ResponseEntity<RespostaApi<List<RespostaEventoDTO>>> listarPorEstudante(
@PathVariable String estudanteId) {
List<RespostaEventoDTO> eventos = eventoServico.listarPorEstudante(estudanteId);
return ResponseEntity.ok(RespostaApi.sucesso(eventos));
}
@GetMapping("/estudante/{estudanteId}/periodo")
public ResponseEntity<RespostaApi<List<RespostaEventoDTO>>> listarPorPeriodo(
@PathVariable String estudanteId,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime inicio,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime fim) {
List<RespostaEventoDTO> eventos = eventoServico.listarPorPeriodo(estudanteId, inicio, fim);
return ResponseEntity.ok(RespostaApi.sucesso(eventos));
}
@GetMapping("/estudante/{estudanteId}/proximos")
public ResponseEntity<RespostaApi<List<RespostaEventoDTO>>> listarProximosEventos(
@PathVariable String estudanteId) {
List<RespostaEventoDTO> eventos = eventoServico.listarProximosEventos(estudanteId);
return ResponseEntity.ok(RespostaApi.sucesso(eventos));
}
@GetMapping("/{id}")
public ResponseEntity<RespostaApi<RespostaEventoDTO>> buscarPorId(
@PathVariable String id,
@RequestParam String estudanteId) {
RespostaEventoDTO evento = eventoServico.buscarPorId(id, estudanteId);
return ResponseEntity.ok(RespostaApi.sucesso(evento));
}
@PutMapping("/{id}")
public ResponseEntity<RespostaApi<RespostaEventoDTO>> atualizarEvento(
@PathVariable String id,
@Valid @RequestBody RequisicaoEventoDTO dto) {
Evento evento = new Evento();
evento.setTitulo(dto.titulo());
evento.setDescricao(dto.descricao());
evento.setTipo(dto.tipo());
evento.setLocal(dto.local());
evento.setDataHora(dto.dataHora());
evento.setDisciplinaId(dto.disciplinaId());
RespostaEventoDTO resposta = eventoServico.atualizarEvento(id, evento, dto.estudanteId());
return ResponseEntity.ok(RespostaApi.sucesso(resposta));
}
@DeleteMapping("/{id}")
public ResponseEntity<RespostaApi<Void>> excluirEvento(
@PathVariable String id,
@RequestParam String estudanteId) {
eventoServico.excluirEvento(id, estudanteId);
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(RespostaApi.sucesso(null));
}
}

View File

@@ -0,0 +1,86 @@
package com.agendaestudantil.controlador;
import com.agendaestudantil.dto.RespostaApi;
import com.agendaestudantil.dto.RequisicaoTarefaDTO;
import com.agendaestudantil.dto.RespostaTarefaDTO;
import com.agendaestudantil.servico.TarefaServico;
import jakarta.validation.Valid;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
@RestController
@RequestMapping("/api/tarefas")
public class TarefaControlador {
private final TarefaServico tarefaServico;
public TarefaControlador(TarefaServico tarefaServico) {
this.tarefaServico = tarefaServico;
}
@PostMapping
public ResponseEntity<RespostaApi<RespostaTarefaDTO>> criarTarefa(@Valid @RequestBody RequisicaoTarefaDTO dto) {
RespostaTarefaDTO tarefa = tarefaServico.criarTarefa(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(RespostaApi.sucesso(tarefa));
}
@GetMapping("/estudante/{estudanteId}")
public ResponseEntity<RespostaApi<List<RespostaTarefaDTO>>> listarTarefasPorEstudante(
@PathVariable String estudanteId) {
List<RespostaTarefaDTO> tarefas = tarefaServico.listarTarefasPorEstudante(estudanteId);
return ResponseEntity.ok(RespostaApi.sucesso(tarefas));
}
@GetMapping("/estudante/{estudanteId}/pendentes")
public ResponseEntity<RespostaApi<List<RespostaTarefaDTO>>> listarTarefasPendentes(
@PathVariable String estudanteId) {
List<RespostaTarefaDTO> tarefas = tarefaServico.listarTarefasPendentes(estudanteId);
return ResponseEntity.ok(RespostaApi.sucesso(tarefas));
}
@GetMapping("/estudante/{estudanteId}/data")
public ResponseEntity<RespostaApi<List<RespostaTarefaDTO>>> listarTarefasPorData(
@PathVariable String estudanteId,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate data) {
List<RespostaTarefaDTO> tarefas = tarefaServico.listarTarefasPorData(estudanteId, data);
return ResponseEntity.ok(RespostaApi.sucesso(tarefas));
}
@GetMapping("/estudante/{estudanteId}/periodo")
public ResponseEntity<RespostaApi<List<RespostaTarefaDTO>>> listarTarefasPorPeriodo(
@PathVariable String estudanteId,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate inicio,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fim) {
List<RespostaTarefaDTO> tarefas = tarefaServico.listarTarefasPorPeriodo(estudanteId, inicio, fim);
return ResponseEntity.ok(RespostaApi.sucesso(tarefas));
}
@GetMapping("/{id}")
public ResponseEntity<RespostaApi<RespostaTarefaDTO>> buscarTarefaPorId(@PathVariable String id) {
RespostaTarefaDTO dto = tarefaServico.buscarTarefaPorId(id);
return ResponseEntity.ok(RespostaApi.sucesso(dto));
}
@PutMapping("/{id}")
public ResponseEntity<RespostaApi<RespostaTarefaDTO>> atualizarTarefa(@PathVariable String id,
@Valid @RequestBody RequisicaoTarefaDTO dto) {
RespostaTarefaDTO tarefa = tarefaServico.atualizarTarefa(id, dto);
return ResponseEntity.ok(RespostaApi.sucesso(tarefa));
}
@DeleteMapping("/{id}")
public ResponseEntity<RespostaApi<Void>> excluirTarefa(@PathVariable String id) {
tarefaServico.excluirTarefa(id);
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(RespostaApi.sucesso(null));
}
@PatchMapping("/{id}/concluir")
public ResponseEntity<RespostaApi<RespostaTarefaDTO>> marcarConcluida(@PathVariable String id) {
RespostaTarefaDTO tarefa = tarefaServico.marcarConcluida(id);
return ResponseEntity.ok(RespostaApi.sucesso(tarefa));
}
}

View File

@@ -0,0 +1,16 @@
package com.agendaestudantil.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public record RequisicaoCadastroDTO(
@NotBlank String nome,
@Email @NotBlank String email,
@NotBlank @Size(min = 6) String senha,
@NotBlank String curso,
@NotNull @Min(1) Integer periodo
) {
}

View File

@@ -0,0 +1,11 @@
package com.agendaestudantil.dto;
import jakarta.validation.constraints.NotBlank;
public record RequisicaoDisciplinaDTO(
@NotBlank String nome,
String professor,
String sala,
String cor,
@NotBlank String estudanteId
) {}

View File

@@ -0,0 +1,17 @@
package com.agendaestudantil.dto;
import com.agendaestudantil.entidade.Evento;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
public record RequisicaoEventoDTO(
@NotBlank String titulo,
String descricao,
@NotNull Evento.TipoEvento tipo,
String local,
String disciplinaId,
@NotNull LocalDateTime dataHora,
@NotBlank String estudanteId
) {}

View File

@@ -0,0 +1,10 @@
package com.agendaestudantil.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
public record RequisicaoLoginDTO(
@Email @NotBlank String email,
@NotBlank String senha
) {
}

View File

@@ -0,0 +1,19 @@
package com.agendaestudantil.dto;
import com.agendaestudantil.entidade.Tarefa;
import jakarta.validation.constraints.FutureOrPresent;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
public record RequisicaoTarefaDTO(
@NotBlank String titulo,
String descricao,
@NotNull(message = "Prioridade é obrigatória") Tarefa.Prioridade prioridade,
Tarefa.StatusTarefa status,
@NotNull @FutureOrPresent LocalDate dataEntrega,
String disciplinaId,
@NotBlank(message = "ID do estudante é obrigatório") String estudanteId
) {
}

View File

@@ -0,0 +1,9 @@
package com.agendaestudantil.dto;
import java.time.LocalDateTime;
public record RespostaApi<T>(T data, String message, LocalDateTime timestamp) {
public static <T> RespostaApi<T> sucesso(T data) {
return new RespostaApi<>(data, "Sucesso", LocalDateTime.now());
}
}

View File

@@ -0,0 +1,10 @@
package com.agendaestudantil.dto;
public record RespostaDisciplinaDTO(
String id,
String estudanteId,
String nome,
String professor,
String sala,
String cor
) {}

View File

@@ -0,0 +1,10 @@
package com.agendaestudantil.dto;
public record RespostaEstudanteDTO(
String id,
String nome,
String email,
String curso,
Integer periodo
) {
}

View File

@@ -0,0 +1,14 @@
package com.agendaestudantil.dto;
import java.time.LocalDateTime;
public record RespostaEventoDTO(
String id,
String estudanteId,
String titulo,
String descricao,
String tipo,
String local,
String disciplinaId,
LocalDateTime dataHora
) {}

View File

@@ -0,0 +1,4 @@
package com.agendaestudantil.dto;
public record RespostaLoginDTO(String token, RespostaEstudanteDTO estudante) {
}

View File

@@ -0,0 +1,14 @@
package com.agendaestudantil.dto;
import java.time.LocalDate;
public record RespostaTarefaDTO(
String id,
String titulo,
String descricao,
String prioridade,
String status,
LocalDate dataEntrega,
String disciplinaId,
String estudanteId
) {}

View File

@@ -0,0 +1,28 @@
package com.agendaestudantil.entidade;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Document(collection = "disciplinas")
@CompoundIndex(name = "estudante_nome_idx", def = "{estudanteId: 1, nome: 1}")
public class Disciplina extends EntidadeAuditoria {
@Id
private String id;
private String estudanteId;
private String nome;
private String professor;
private String sala;
private String cor;
}

View File

@@ -0,0 +1,21 @@
package com.agendaestudantil.entidade;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public abstract class EntidadeAuditoria {
@CreatedDate
private LocalDateTime dataCriacao;
@LastModifiedDate
private LocalDateTime dataAtualizacao;
}

View File

@@ -0,0 +1,33 @@
package com.agendaestudantil.entidade;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Document(collection = "estudantes")
public class Estudante extends EntidadeAuditoria {
@Id
private String id;
@Indexed(unique = true)
private String email;
private String nome;
private String senha;
private String curso;
private Integer periodo;
}

View File

@@ -0,0 +1,36 @@
package com.agendaestudantil.entidade;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.mapping.Document;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Document(collection = "eventos")
@CompoundIndex(name = "estudante_data_hora_idx", def = "{estudanteId: 1, dataHora: 1}")
public class Evento extends EntidadeAuditoria {
@Id
private String id;
private String estudanteId;
private String titulo;
private String descricao;
private TipoEvento tipo;
private String local;
private String disciplinaId;
private LocalDateTime dataHora;
public enum TipoEvento {
AULA, PROVA, TRABALHO, ESTUDO, EXAME, OUTRO
}
}

View File

@@ -0,0 +1,44 @@
package com.agendaestudantil.entidade;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.CompoundIndex;
import org.springframework.data.mongodb.core.index.CompoundIndexes;
import org.springframework.data.mongodb.core.mapping.Document;
import java.time.LocalDate;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Document(collection = "tarefas")
@CompoundIndexes({
@CompoundIndex(name = "estudante_data_entrega_idx", def = "{estudanteId: 1, dataEntrega: 1}"),
@CompoundIndex(name = "estudante_status_idx", def = "{estudanteId: 1, status: 1}")
})
public class Tarefa extends EntidadeAuditoria {
@Id
private String id;
private String titulo;
private String descricao;
private Prioridade prioridade;
private StatusTarefa status;
private LocalDate dataEntrega;
private String disciplinaId;
private String estudanteId;
public enum Prioridade {
BAIXA, MEDIA, ALTA, URGENTE
}
public enum StatusTarefa {
PENDENTE, EM_ANDAMENTO, CONCLUIDA, ATRASADA
}
}

View File

@@ -0,0 +1,7 @@
package com.agendaestudantil.excecao;
public class ExcecaoNegocio extends RuntimeException {
public ExcecaoNegocio(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package com.agendaestudantil.excecao;
public class ExcecaoRecursoNaoEncontrado extends RuntimeException {
public ExcecaoRecursoNaoEncontrado(String message) {
super(message);
}
}

View File

@@ -0,0 +1,53 @@
package com.agendaestudantil.excecao;
import com.agendaestudantil.dto.RespostaApi;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class ManipuladorExcecaoGlobal {
@ExceptionHandler(ExcecaoRecursoNaoEncontrado.class)
public ResponseEntity<RespostaApi<Void>> handleResourceNotFound(ExcecaoRecursoNaoEncontrado ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new RespostaApi<>(null, ex.getMessage(), LocalDateTime.now()));
}
@ExceptionHandler(ExcecaoNegocio.class)
public ResponseEntity<RespostaApi<Void>> handleExcecaoNegocio(ExcecaoNegocio ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new RespostaApi<>(null, ex.getMessage(), LocalDateTime.now()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<RespostaApi<Map<String, String>>> handleValidationException(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new RespostaApi<>(errors, "Falha na validação", LocalDateTime.now()));
}
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<RespostaApi<Void>> handleResponseStatusException(ResponseStatusException ex) {
return ResponseEntity.status(ex.getStatusCode())
.body(new RespostaApi<>(null, ex.getReason(), LocalDateTime.now()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<RespostaApi<Void>> handleGenericException(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new RespostaApi<>(null, "Erro interno no servidor: " + ex.getMessage(), LocalDateTime.now()));
}
}

View File

@@ -0,0 +1,62 @@
package com.agendaestudantil.filtro;
import com.agendaestudantil.utilitario.UtilJwt;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class FiltroJwt extends OncePerRequestFilter {
private final UtilJwt utilJwt;
private final UserDetailsService userDetailsService;
public FiltroJwt(UtilJwt utilJwt, UserDetailsService userDetailsService) {
this.utilJwt = utilJwt;
this.userDetailsService = userDetailsService;
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
return path.equals("/") || path.equals("/index.html") || path.equals("/favicon.ico")
|| path.startsWith("/static/") || path.startsWith("/css/") || path.startsWith("/js/")
|| path.startsWith("/img/") || path.endsWith(".css") || path.endsWith(".js")
|| path.endsWith(".ico") || path.endsWith(".html");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String header = request.getHeader("Authorization");
String token = null;
String estudanteId = null;
if (header != null && header.startsWith("Bearer ")) {
token = header.substring(7);
estudanteId = utilJwt.getEstudanteIdFromToken(token);
}
if (estudanteId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(estudanteId);
if (utilJwt.validateToken(token)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,11 @@
package com.agendaestudantil.repositorio;
import com.agendaestudantil.entidade.Disciplina;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface DisciplinaRepositorio extends MongoRepository<Disciplina, String> {
List<Disciplina> findByEstudanteId(String estudanteId);
}

View File

@@ -0,0 +1,12 @@
package com.agendaestudantil.repositorio;
import com.agendaestudantil.entidade.Estudante;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface EstudanteRepositorio extends MongoRepository<Estudante, String> {
Optional<Estudante> findByEmail(String email);
boolean existsByEmail(String email);
}

View File

@@ -0,0 +1,22 @@
package com.agendaestudantil.repositorio;
import com.agendaestudantil.entidade.Evento;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
@Repository
public interface EventoRepositorio extends MongoRepository<Evento, String> {
List<Evento> findByEstudanteId(String estudanteId);
List<Evento> findByDisciplinaId(String disciplinaId);
@Query("{'estudanteId': ?0, 'dataHora': {$gte: ?1, $lte: ?2}}")
List<Evento> findByEstudanteIdAndDataHoraBetween(String estudanteId, LocalDateTime inicio, LocalDateTime fim);
@Query("{'estudanteId': ?0, 'dataHora': {$gte: ?1}}")
List<Evento> findProximosEventosByEstudanteId(String estudanteId, LocalDateTime data);
}

View File

@@ -0,0 +1,27 @@
package com.agendaestudantil.repositorio;
import com.agendaestudantil.entidade.Tarefa;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
@Repository
public interface TarefaRepositorio extends MongoRepository<Tarefa, String> {
List<Tarefa> findByEstudanteId(String estudanteId);
List<Tarefa> findByEstudanteIdAndStatus(String estudanteId, Tarefa.StatusTarefa status);
List<Tarefa> findByDisciplinaId(String disciplinaId);
@Query("{'estudanteId': ?0, 'dataEntrega': ?1}")
List<Tarefa> findByEstudanteIdAndDataEntrega(String estudanteId, LocalDate data);
@Query("{'estudanteId': ?0, 'dataEntrega': {$gte: ?1, $lte: ?2}}")
List<Tarefa> findByEstudanteIdAndDataEntregaBetween(String estudanteId, LocalDate inicio, LocalDate fim);
@Query("{'estudanteId': ?0, 'status': {$ne: ?1}}")
List<Tarefa> findTarefasPendentesByEstudanteId(String estudanteId, Tarefa.StatusTarefa status);
}

View File

@@ -0,0 +1,53 @@
package com.agendaestudantil.seguranca;
import com.agendaestudantil.entidade.Estudante;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
public class DetalhesUsuarioPersonalizado implements UserDetails {
private final Estudante estudante;
public DetalhesUsuarioPersonalizado(Estudante estudante) {
this.estudante = estudante;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public String getPassword() {
return estudante.getSenha();
}
@Override
public String getUsername() {
return estudante.getId();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}

View File

@@ -0,0 +1,26 @@
package com.agendaestudantil.seguranca;
import com.agendaestudantil.entidade.Estudante;
import com.agendaestudantil.repositorio.EstudanteRepositorio;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class ServicoAutenticacaoUsuario implements UserDetailsService {
private final EstudanteRepositorio estudanteRepositorio;
public ServicoAutenticacaoUsuario(EstudanteRepositorio estudanteRepositorio) {
this.estudanteRepositorio = estudanteRepositorio;
}
@Override
public UserDetails loadUserByUsername(String estudanteId) throws UsernameNotFoundException {
Estudante estudante = estudanteRepositorio.findById(estudanteId)
.orElseThrow(() -> new UsernameNotFoundException("Estudante não encontrado"));
return new DetalhesUsuarioPersonalizado(estudante);
}
}

View File

@@ -0,0 +1,92 @@
package com.agendaestudantil.servico;
import com.agendaestudantil.dto.RespostaDisciplinaDTO;
import com.agendaestudantil.entidade.Disciplina;
import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado;
import com.agendaestudantil.repositorio.DisciplinaRepositorio;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@Service
public class DisciplinaServico {
private static final Logger log = LoggerFactory.getLogger(DisciplinaServico.class);
private final DisciplinaRepositorio disciplinaRepositorio;
public DisciplinaServico(DisciplinaRepositorio disciplinaRepositorio) {
this.disciplinaRepositorio = disciplinaRepositorio;
}
private void validarAcesso(String estudanteId) {
String authUser = SecurityContextHolder.getContext().getAuthentication().getName();
if (!authUser.equals(estudanteId)) {
log.error("Acesso negado. Usuário {} tentou acessar recurso do estudante {}", authUser, estudanteId);
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Acesso negado");
}
}
public RespostaDisciplinaDTO criarDisciplina(Disciplina disciplina, String estudanteId) {
log.debug("Criando disciplina para estudante: {}", estudanteId);
validarAcesso(estudanteId);
disciplina.setEstudanteId(estudanteId);
Disciplina salva = disciplinaRepositorio.save(disciplina);
return toDTO(salva);
}
public List<RespostaDisciplinaDTO> listarPorEstudante(String estudanteId) {
log.debug("Listando disciplinas para estudante: {}", estudanteId);
validarAcesso(estudanteId);
return disciplinaRepositorio.findByEstudanteId(estudanteId).stream()
.map(this::toDTO)
.toList();
}
public RespostaDisciplinaDTO buscarPorId(String id, String estudanteId) {
Disciplina disciplina = getDisciplinaEntity(id);
validarAcesso(disciplina.getEstudanteId());
return toDTO(disciplina);
}
public RespostaDisciplinaDTO atualizarDisciplina(String id, Disciplina disciplinaAtualizada, String estudanteId) {
Disciplina disciplina = getDisciplinaEntity(id);
validarAcesso(disciplina.getEstudanteId());
disciplina.setNome(disciplinaAtualizada.getNome());
disciplina.setProfessor(disciplinaAtualizada.getProfessor());
disciplina.setSala(disciplinaAtualizada.getSala());
disciplina.setCor(disciplinaAtualizada.getCor());
return toDTO(disciplinaRepositorio.save(disciplina));
}
public void excluirDisciplina(String id, String estudanteId) {
Disciplina disciplina = getDisciplinaEntity(id);
validarAcesso(disciplina.getEstudanteId());
disciplinaRepositorio.delete(disciplina);
}
private Disciplina getDisciplinaEntity(String id) {
return disciplinaRepositorio.findById(id)
.orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Disciplina não encontrada"));
}
private RespostaDisciplinaDTO toDTO(Disciplina disciplina) {
return new RespostaDisciplinaDTO(
disciplina.getId(),
disciplina.getEstudanteId(),
disciplina.getNome(),
disciplina.getProfessor(),
disciplina.getSala(),
disciplina.getCor()
);
}
}

View File

@@ -0,0 +1,90 @@
package com.agendaestudantil.servico;
import com.agendaestudantil.dto.RequisicaoCadastroDTO;
import com.agendaestudantil.dto.RespostaEstudanteDTO;
import com.agendaestudantil.dto.RequisicaoLoginDTO;
import com.agendaestudantil.dto.RespostaLoginDTO;
import com.agendaestudantil.entidade.Estudante;
import com.agendaestudantil.excecao.ExcecaoNegocio;
import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado;
import com.agendaestudantil.repositorio.EstudanteRepositorio;
import com.agendaestudantil.utilitario.UtilJwt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.Optional;
@Service
public class EstudanteServico {
private static final Logger log = LoggerFactory.getLogger(EstudanteServico.class);
private final EstudanteRepositorio estudanteRepositorio;
private final PasswordEncoder passwordEncoder;
private final UtilJwt utilJwt;
public EstudanteServico(EstudanteRepositorio estudanteRepositorio, PasswordEncoder passwordEncoder,
UtilJwt utilJwt) {
this.estudanteRepositorio = estudanteRepositorio;
this.passwordEncoder = passwordEncoder;
this.utilJwt = utilJwt;
}
public RespostaEstudanteDTO cadastrar(RequisicaoCadastroDTO dto) {
log.debug("Acessando método cadastrar para email: {}", dto.email());
Optional<Estudante> existente = estudanteRepositorio.findByEmail(dto.email());
if (existente.isPresent()) {
log.error("Email já cadastrado: {}", dto.email());
throw new ExcecaoNegocio("Email já cadastrado");
}
Estudante estudante = new Estudante();
estudante.setNome(dto.nome());
estudante.setEmail(dto.email());
estudante.setSenha(passwordEncoder.encode(dto.senha()));
estudante.setCurso(dto.curso());
estudante.setPeriodo(dto.periodo());
Estudante salvo = estudanteRepositorio.save(estudante);
return toResponse(salvo);
}
public RespostaLoginDTO login(RequisicaoLoginDTO dto) {
log.debug("Acessando método login para email: {}", dto.email());
Optional<Estudante> estudanteParam = estudanteRepositorio.findByEmail(dto.email());
if (estudanteParam.isEmpty()) {
log.error("Email ou senha incorretos para email: {}", dto.email());
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Email ou senha incorretos");
}
Estudante estudante = estudanteParam.get();
if (!passwordEncoder.matches(dto.senha(), estudante.getSenha())) {
log.error("Falha ao validar senha para email: {}", dto.email());
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Email ou senha incorretos");
}
String token = utilJwt.generateToken(estudante.getId());
return new RespostaLoginDTO(token, toResponse(estudante));
}
public RespostaEstudanteDTO buscarPorId(String id) {
Estudante estudante = estudanteRepositorio.findById(id)
.orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Estudante não encontrado"));
return toResponse(estudante);
}
private RespostaEstudanteDTO toResponse(Estudante estudante) {
return new RespostaEstudanteDTO(
estudante.getId(),
estudante.getNome(),
estudante.getEmail(),
estudante.getCurso(),
estudante.getPeriodo());
}
}

View File

@@ -0,0 +1,111 @@
package com.agendaestudantil.servico;
import com.agendaestudantil.dto.RespostaEventoDTO;
import com.agendaestudantil.entidade.Evento;
import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado;
import com.agendaestudantil.repositorio.EventoRepositorio;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class EventoServico {
private static final Logger log = LoggerFactory.getLogger(EventoServico.class);
private final EventoRepositorio eventoRepositorio;
public EventoServico(EventoRepositorio eventoRepositorio) {
this.eventoRepositorio = eventoRepositorio;
}
private void validarAcesso(String estudanteId) {
String authUser = SecurityContextHolder.getContext().getAuthentication().getName();
if (!authUser.equals(estudanteId)) {
log.error("Acesso negado. Usuário {} tentou acessar recurso do estudante {}", authUser, estudanteId);
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Acesso negado");
}
}
public RespostaEventoDTO criarEvento(Evento evento, String estudanteId) {
log.debug("Criando evento para estudante: {}", estudanteId);
validarAcesso(estudanteId);
evento.setEstudanteId(estudanteId);
Evento salvo = eventoRepositorio.save(evento);
return toDTO(salvo);
}
public List<RespostaEventoDTO> listarPorEstudante(String estudanteId) {
log.debug("Listando eventos para estudante: {}", estudanteId);
validarAcesso(estudanteId);
return eventoRepositorio.findByEstudanteId(estudanteId).stream()
.map(this::toDTO)
.toList();
}
public List<RespostaEventoDTO> listarPorPeriodo(String estudanteId, LocalDateTime inicio, LocalDateTime fim) {
validarAcesso(estudanteId);
return eventoRepositorio.findByEstudanteIdAndDataHoraBetween(estudanteId, inicio, fim).stream()
.map(this::toDTO)
.toList();
}
public List<RespostaEventoDTO> listarProximosEventos(String estudanteId) {
validarAcesso(estudanteId);
return eventoRepositorio.findProximosEventosByEstudanteId(estudanteId, LocalDateTime.now()).stream()
.map(this::toDTO)
.toList();
}
public RespostaEventoDTO buscarPorId(String id, String estudanteId) {
Evento evento = getEventoEntity(id);
validarAcesso(evento.getEstudanteId());
return toDTO(evento);
}
public RespostaEventoDTO atualizarEvento(String id, Evento eventoAtualizado, String estudanteId) {
Evento evento = getEventoEntity(id);
validarAcesso(evento.getEstudanteId());
evento.setTitulo(eventoAtualizado.getTitulo());
evento.setDescricao(eventoAtualizado.getDescricao());
evento.setTipo(eventoAtualizado.getTipo());
evento.setLocal(eventoAtualizado.getLocal());
evento.setDataHora(eventoAtualizado.getDataHora());
evento.setDisciplinaId(eventoAtualizado.getDisciplinaId());
return toDTO(eventoRepositorio.save(evento));
}
public void excluirEvento(String id, String estudanteId) {
Evento evento = getEventoEntity(id);
validarAcesso(evento.getEstudanteId());
eventoRepositorio.delete(evento);
}
private Evento getEventoEntity(String id) {
return eventoRepositorio.findById(id)
.orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Evento não encontrado"));
}
private RespostaEventoDTO toDTO(Evento evento) {
return new RespostaEventoDTO(
evento.getId(),
evento.getEstudanteId(),
evento.getTitulo(),
evento.getDescricao(),
evento.getTipo() != null ? evento.getTipo().name() : null,
evento.getLocal(),
evento.getDisciplinaId(),
evento.getDataHora()
);
}
}

View File

@@ -0,0 +1,139 @@
package com.agendaestudantil.servico;
import com.agendaestudantil.dto.RequisicaoTarefaDTO;
import com.agendaestudantil.dto.RespostaTarefaDTO;
import com.agendaestudantil.entidade.Tarefa;
import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado;
import com.agendaestudantil.repositorio.TarefaRepositorio;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.util.List;
@Service
public class TarefaServico {
private static final Logger log = LoggerFactory.getLogger(TarefaServico.class);
private final TarefaRepositorio tarefaRepositorio;
public TarefaServico(TarefaRepositorio tarefaRepositorio) {
this.tarefaRepositorio = tarefaRepositorio;
}
private void validarAcesso(String estudanteId) {
String authUser = SecurityContextHolder.getContext().getAuthentication().getName();
if (!authUser.equals(estudanteId)) {
log.error("Acesso negado. Usuário {} tentou acessar recurso do estudante {}", authUser, estudanteId);
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Acesso negado");
}
}
private void validarAcessoTarefa(Tarefa tarefa) {
validarAcesso(tarefa.getEstudanteId());
}
public RespostaTarefaDTO criarTarefa(RequisicaoTarefaDTO dto) {
log.debug("Criando tarefa para estudante: {}", dto.estudanteId());
validarAcesso(dto.estudanteId());
Tarefa tarefa = new Tarefa();
tarefa.setTitulo(dto.titulo());
tarefa.setDescricao(dto.descricao());
tarefa.setPrioridade(dto.prioridade() != null ? dto.prioridade() : Tarefa.Prioridade.MEDIA);
tarefa.setStatus(dto.status() != null ? dto.status() : Tarefa.StatusTarefa.PENDENTE);
tarefa.setDataEntrega(dto.dataEntrega());
tarefa.setEstudanteId(dto.estudanteId());
if (dto.disciplinaId() != null) {
tarefa.setDisciplinaId(dto.disciplinaId());
}
return toDTO(tarefaRepositorio.save(tarefa));
}
public List<RespostaTarefaDTO> listarTarefasPorEstudante(String estudanteId) {
log.debug("Listando tarefas para estudante: {}", estudanteId);
validarAcesso(estudanteId);
return tarefaRepositorio.findByEstudanteId(estudanteId).stream()
.map(this::toDTO)
.toList();
}
public List<RespostaTarefaDTO> listarTarefasPendentes(String estudanteId) {
log.debug("Listando tarefas pendentes para estudante: {}", estudanteId);
validarAcesso(estudanteId);
return tarefaRepositorio.findTarefasPendentesByEstudanteId(estudanteId, Tarefa.StatusTarefa.CONCLUIDA).stream()
.map(this::toDTO)
.toList();
}
public List<RespostaTarefaDTO> listarTarefasPorData(String estudanteId, LocalDate data) {
validarAcesso(estudanteId);
return tarefaRepositorio.findByEstudanteIdAndDataEntrega(estudanteId, data).stream()
.map(this::toDTO)
.toList();
}
public List<RespostaTarefaDTO> listarTarefasPorPeriodo(String estudanteId, LocalDate inicio, LocalDate fim) {
validarAcesso(estudanteId);
return tarefaRepositorio.findByEstudanteIdAndDataEntregaBetween(estudanteId, inicio, fim).stream()
.map(this::toDTO)
.toList();
}
public RespostaTarefaDTO buscarTarefaPorId(String id) {
return toDTO(getTarefaEntity(id));
}
public RespostaTarefaDTO atualizarTarefa(String id, RequisicaoTarefaDTO dto) {
Tarefa tarefa = getTarefaEntity(id);
validarAcesso(tarefa.getEstudanteId());
tarefa.setTitulo(dto.titulo());
tarefa.setDescricao(dto.descricao());
tarefa.setPrioridade(dto.prioridade() != null ? dto.prioridade() : tarefa.getPrioridade());
tarefa.setStatus(dto.status() != null ? dto.status() : tarefa.getStatus());
tarefa.setDataEntrega(dto.dataEntrega());
if (dto.disciplinaId() != null) {
tarefa.setDisciplinaId(dto.disciplinaId());
}
return toDTO(tarefaRepositorio.save(tarefa));
}
public void excluirTarefa(String id) {
tarefaRepositorio.delete(getTarefaEntity(id));
}
public RespostaTarefaDTO marcarConcluida(String id) {
Tarefa tarefa = getTarefaEntity(id);
tarefa.setStatus(Tarefa.StatusTarefa.CONCLUIDA);
return toDTO(tarefaRepositorio.save(tarefa));
}
private Tarefa getTarefaEntity(String id) {
Tarefa tarefa = tarefaRepositorio.findById(id)
.orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Tarefa não encontrada"));
validarAcessoTarefa(tarefa);
return tarefa;
}
private RespostaTarefaDTO toDTO(Tarefa tarefa) {
return new RespostaTarefaDTO(
tarefa.getId(),
tarefa.getTitulo(),
tarefa.getDescricao(),
tarefa.getPrioridade() != null ? tarefa.getPrioridade().name() : null,
tarefa.getStatus() != null ? tarefa.getStatus().name() : null,
tarefa.getDataEntrega(),
tarefa.getDisciplinaId(),
tarefa.getEstudanteId());
}
}

View File

@@ -0,0 +1,56 @@
package com.agendaestudantil.utilitario;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class UtilJwt {
private final String secret;
public UtilJwt(@Value("${jwt.secret}") String secret) {
this.secret = secret;
}
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String generateToken(String estudanteId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + 24 * 60 * 60 * 1000L);
return Jwts.builder()
.subject(estudanteId)
.issuedAt(now)
.expiration(expiryDate)
.signWith(getSigningKey())
.compact();
}
public String getEstudanteIdFromToken(String token) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(token);
return true;
} catch (Exception e) {
return false;
}
}
}

View File

@@ -0,0 +1,6 @@
spring.data.mongodb.uri=${MONGO_URI:mongodb://localhost:27017/agenda_estudantil}
cors.allowed.origins=${CORS_ORIGINS:http://localhost:8080,http://localhost:3000}
jwt.secret=${JWT_SECRET:8a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7A8B9C0D1E2F3}
logging.level.org.springframework.data.mongodb=DEBUG
springdoc.api-docs.enabled=true
springdoc.swagger-ui.enabled=true

View File

@@ -0,0 +1,8 @@
spring.data.mongodb.uri=${MONGO_URI}
cors.allowed.origins=${CORS_ORIGINS}
jwt.secret=${JWT_SECRET}
springdoc.api-docs.enabled=false
springdoc.swagger-ui.enabled=false
logging.level.root=WARN
logging.level.root=WARN
logging.level.com.agendaestudantil=INFO

View File

@@ -0,0 +1,13 @@
spring.application.name=${APP_NAME:Focus Agenda}
server.port=${SERVER_PORT:8080}
server.servlet.context-path=/
spring.web.resources.static-locations=classpath:/static/
spring.mvc.static-path-pattern=/**
spring.mvc.contentnegotiation.favor-parameter=true
spring.web.resources.add-mappings=true
spring.servlet.multipart.enabled=false
spring.web.resources.cache.cachecontrol.max-age=3600
spring.web.resources.cache.cachecontrol.cache-public=true
spring.profiles.active=${SPRING_PROFILES_ACTIVE:dev}

View File

@@ -0,0 +1,96 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
font-family: 'Poppins', Arial, sans-serif;
display: flex;
align-items: center;
justify-content: center;
padding: 80px 20px 20px;
background: #f5f5f5;
}
#topo {
width: 100%;
height: 50px;
position: fixed;
top: 0; left: 0;
background: linear-gradient(to right, #c0392b 47%, #7a4951 73%, #114455 87%);
display: flex;
align-items: center;
z-index: 10;
}
#textotop {
padding-left: 20px;
font-size: clamp(22px, 5vw, 38px);
color: #fff;
font-weight: 500;
}
#log {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 16px;
}
.mens { text-align: center; color: #1f2937; }
.campo { display: flex; flex-direction: column; gap: 8px; }
label { font-weight: 700; color: #1f2937; }
input, select {
height: 46px;
width: 100%;
padding: 10px;
font-size: 16px;
border: 1px solid #c7c7c7;
border-radius: 6px;
font-family: inherit;
background: #fff;
}
form { display: flex; flex-direction: column; gap: 16px; }
.linha-dupla { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
#logbtn {
align-self: center;
width: 50%;
padding: 12px;
font-size: 18px;
font-weight: bold;
background-color: #c0392b;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s ease;
}
#logbtn:hover { background-color: #a03224; }
#logbtn:disabled { background-color: #ccc; cursor: not-allowed; }
a { color: #111; text-decoration: none; }
a:hover { text-decoration: underline; }
#mensagem-erro {
background: #fee2e2;
border: 1px solid #fca5a5;
color: #b91c1c;
padding: 10px 14px;
border-radius: 6px;
font-size: 14px;
display: none;
}
#mensagem-sucesso {
background: #d1fae5;
border: 1px solid #6ee7b7;
color: #065f46;
padding: 10px 14px;
border-radius: 6px;
font-size: 14px;
display: none;
}

View File

@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="imagens/icone.png">
<link rel="stylesheet" href="cadastro.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<title>Cadastro Focus Agenda</title>
</head>
<body>
<div id="topo">
<h1 id="textotop">Focus Agenda</h1>
</div>
<div id="log">
<h1 class="mens">Crie Sua Conta</h1>
<div id="mensagem-erro" role="alert"></div>
<div id="mensagem-sucesso" role="status"></div>
<form id="cadastroForm" novalidate>
<div class="campo">
<label for="emailid">Email</label>
<input type="email" placeholder="Digite seu email" id="emailid" autocomplete="email" required>
</div>
<div class="campo">
<label for="nomeid">Nome</label>
<input type="text" placeholder="Seu nome completo" id="nomeid" autocomplete="name" required>
</div>
<div class="campo">
<label for="cursoid">Curso</label>
<input type="text" placeholder="Ex: Técnico em Informática" id="cursoid" required>
</div>
<div class="linha-dupla">
<div class="campo">
<label for="periodoid">Período</label>
<select id="periodoid" required>
<option value="">Selecione</option>
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
<option value="4"></option>
<option value="5"></option>
<option value="6"></option>
<option value="7"></option>
<option value="8"></option>
</select>
</div>
</div>
<div class="campo">
<label for="senhaid">Senha</label>
<input type="password" placeholder="Mínimo 6 caracteres" id="senhaid" autocomplete="new-password" required minlength="6">
</div>
<div class="campo">
<label for="csenhaid">Confirmar Senha</label>
<input type="password" placeholder="Confirme sua senha" id="csenhaid" autocomplete="new-password" required>
</div>
<button type="submit" id="logbtn">Cadastrar</button>
</form>
<p class="mens"><a href="login.html">Já tem uma conta?</a></p>
</div>
<script>
const form = document.getElementById('cadastroForm');
const erroEl = document.getElementById('mensagem-erro');
const sucessoEl = document.getElementById('mensagem-sucesso');
const btn = document.getElementById('logbtn');
function mostrarErro(msg) {
erroEl.textContent = msg;
erroEl.style.display = 'block';
sucessoEl.style.display = 'none';
}
function mostrarSucesso(msg) {
sucessoEl.textContent = msg;
sucessoEl.style.display = 'block';
erroEl.style.display = 'none';
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
erroEl.style.display = 'none';
sucessoEl.style.display = 'none';
const nome = document.getElementById('nomeid').value.trim();
const email = document.getElementById('emailid').value.trim();
const curso = document.getElementById('cursoid').value.trim();
const periodo = parseInt(document.getElementById('periodoid').value, 10);
const senha = document.getElementById('senhaid').value;
const csenha = document.getElementById('csenhaid').value;
if (!nome || !email || !curso || !periodo || !senha) {
mostrarErro('Preencha todos os campos.');
return;
}
if (senha.length < 6) {
mostrarErro('A senha deve ter pelo menos 6 caracteres.');
return;
}
if (senha !== csenha) {
mostrarErro('As senhas não conferem.');
document.getElementById('csenhaid').focus();
return;
}
btn.disabled = true;
btn.textContent = 'Cadastrando...';
try {
const res = await fetch('/api/estudantes/cadastro', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nome, email, senha, curso, periodo })
});
const json = await res.json();
if (!res.ok) {
// Erros de validação vêm como objeto
if (typeof json.data === 'object' && json.data !== null) {
const msgs = Object.values(json.data).join('; ');
mostrarErro(msgs);
} else {
mostrarErro(json.message || 'Erro ao cadastrar.');
}
return;
}
mostrarSucesso('Conta criada! Redirecionando para o login...');
setTimeout(() => window.location.href = 'login.html', 2000);
} catch (err) {
mostrarErro('Erro de conexão. Verifique se o servidor está rodando.');
} finally {
btn.disabled = false;
btn.textContent = 'Cadastrar';
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,300 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Poppins', sans-serif;
background: #f5f5f5;
}
/* ===== HEADER ===== */
#header {
width: 100%;
height: 50px;
position: fixed;
top: 0; left: 0;
background: linear-gradient(to right, #c0392b, #114455);
display: flex;
align-items: center;
z-index: 10;
}
#title { color: #fff; padding-left: 20px; font-size: 28px; }
/* ===== BARRA ESQUERDA ===== */
#barraesquerda {
position: fixed;
top: 50px; left: 0;
width: 280px;
height: calc(100vh - 50px);
background: #c0392b;
padding: 15px;
color: #fff;
display: flex;
flex-direction: column;
font-family: 'Inter', sans-serif;
overflow-y: auto;
z-index: 5;
}
/* Mini Calendário */
#calendario { margin-top: 10px; }
.calendariotop { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
#mes { font-size: 16px; font-weight: 600; }
#calendarseta { display: flex; gap: 10px; }
#calendarseta button { background: transparent; border: none; color: #fff; font-size: 24px; cursor: pointer; padding: 2px 8px; transition: transform 0.2s; }
#calendarseta button:hover { transform: scale(1.2); }
.calendariodia { width: 100%; border-collapse: collapse; }
.calendariodia th { font-size: 10px; opacity: 0.8; padding: 3px; text-align: center; }
.calendariodia td { text-align: center; padding: 4px; font-size: 11px; border-radius: 50%; cursor: pointer; transition: background 0.15s; }
.calendariodia td:hover { background: rgba(255,255,255,0.2); }
.calendariodia td.outromes { opacity: 0.4; }
.calendariodia td.today { background: #fff; color: #c0392b; font-weight: 700; border-radius: 50%; }
.calendariodia td.selecionado { background: rgba(255,255,255,0.35); border-radius: 50%; }
/* Agenda */
#agenda { margin-top: 18px; }
.agenda-header { font-size: 12px; font-weight: 700; opacity: 0.8; margin-bottom: 8px; letter-spacing: 0.5px; }
.agenda-empty { font-size: 12px; opacity: 0.7; font-style: italic; }
.evento { background: rgba(255,255,255,0.15); border-radius: 8px; padding: 8px 10px; margin-bottom: 8px; cursor: pointer; transition: background 0.15s; }
.evento:hover { background: rgba(255,255,255,0.25); }
.evento .hora { font-size: 11px; opacity: 0.8; }
.evento .titulo { font-size: 13px; font-weight: 600; }
/* Feriados */
#feriados { margin-top: 16px; }
.feriados-header { font-size: 12px; font-weight: 700; opacity: 0.8; margin-bottom: 8px; letter-spacing: 0.5px; }
.feriado { display: flex; align-items: center; gap: 8px; font-size: 12px; margin-bottom: 4px; }
.dot { width: 8px; height: 8px; background: #fff; border-radius: 50%; flex-shrink: 0; }
/* ===== MAIN ===== */
.main {
margin-left: 280px;
margin-top: 50px;
padding: 24px;
min-height: calc(100vh - 50px);
}
/* Topbar */
.topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.topbar h1 { font-size: 24px; color: #1f2937; }
.user-area { display: flex; align-items: center; gap: 14px; }
.icone-img { width: 28px; height: 28px; cursor: pointer; opacity: 0.7; transition: opacity 0.2s; }
.icone-img:hover { opacity: 1; }
.perfil { display: flex; align-items: center; gap: 10px; }
.avatar { width: 38px; height: 38px; border-radius: 50%; background: linear-gradient(135deg, #c0392b, #114455); }
.info { display: flex; flex-direction: column; }
.nome { font-size: 14px; font-weight: 600; color: #1f2937; }
.cargo { font-size: 12px; color: #6b7280; }
/* Botão logout */
#btnLogout {
background: transparent;
border: 1px solid #d1d5db;
border-radius: 6px;
padding: 6px 12px;
font-size: 13px;
cursor: pointer;
color: #6b7280;
transition: all 0.2s;
}
#btnLogout:hover { background: #fee2e2; border-color: #c0392b; color: #c0392b; }
/* Calendar header */
.calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 10px; }
.mes-nav { display: flex; align-items: center; gap: 12px; }
.seta { background: transparent; border: 1px solid #d1d5db; border-radius: 6px; width: 32px; height: 32px; font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background 0.15s; }
.seta:hover { background: #f3f4f6; }
.titulo-mes { font-size: 18px; font-weight: 600; color: #1f2937; min-width: 200px; text-align: center; }
.view-switch { display: flex; gap: 4px; background: #f3f4f6; padding: 4px; border-radius: 8px; }
.view-switch button { border: none; background: transparent; padding: 6px 14px; border-radius: 6px; font-size: 14px; cursor: pointer; color: #6b7280; transition: all 0.2s; font-family: inherit; }
.view-switch button.active { background: #fff; color: #1f2937; font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }
/* Botão novo evento */
#btnNovoEvento {
background: #c0392b;
color: #fff;
border: none;
border-radius: 8px;
padding: 8px 18px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
font-family: inherit;
}
#btnNovoEvento:hover { background: #a03224; }
/* Calendar area */
.calendar-area { background: #fff; border-radius: 12px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); overflow: hidden; }
/* Month view */
.month-view { display: grid; grid-template-columns: repeat(7, 1fr); }
.dia-semana { text-align: center; padding: 10px; font-size: 12px; font-weight: 600; color: #6b7280; background: #f9fafb; border-bottom: 1px solid #e5e7eb; }
.dia-box { min-height: 100px; padding: 8px; border-right: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0; cursor: pointer; transition: background 0.1s; }
.dia-box:hover { background: #fef2f2; }
.dia-box:nth-child(7n) { border-right: none; }
.dia-box.outro-mes { background: #fafafa; color: #d1d5db; }
.dia-box.outro-mes .num-dia { color: #d1d5db; }
.num-dia { font-size: 13px; font-weight: 500; color: #374151; margin-bottom: 4px; }
.dia-box.today .num-dia { background: #c0392b; color: #fff; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 12px; }
.dia-box.selecionado { background: #fef2f2; }
.evento-mini { font-size: 10px; background: #c0392b; color: #fff; border-radius: 3px; padding: 1px 4px; margin-top: 2px; truncate: clip; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.evento-mini.verde { background: #16a34a; }
.evento-mini.azul { background: #1d4ed8; }
.evento-mini.amarelo { background: #d97706; }
.mais-eventos { font-size: 10px; color: #6b7280; margin-top: 2px; cursor: pointer; }
/* Week view */
.week-view { display: grid; grid-template-columns: repeat(7, 1fr); }
.week-col { border-right: 1px solid #e5e7eb; padding: 10px 8px; min-height: 300px; }
.week-col:last-child { border-right: none; }
.week-col.today { background: #fef9f9; }
.week-col-head { font-size: 13px; font-weight: 600; color: #6b7280; margin-bottom: 6px; }
.week-col-date { display: block; font-size: 11px; font-weight: 400; color: #9ca3af; }
.week-events { display: flex; flex-direction: column; gap: 6px; }
.week-empty { font-size: 11px; color: #d1d5db; font-style: italic; }
/* Day view */
.day-panel { padding: 20px; }
.day-panel-header { font-size: 16px; font-weight: 600; color: #374151; margin-bottom: 16px; }
.day-events { display: flex; flex-direction: column; gap: 10px; }
.day-empty { color: #9ca3af; font-style: italic; font-size: 14px; }
/* Event cards */
.calendar-event { border-radius: 6px; padding: 8px 10px; background: #fee2e2; border-left: 4px solid #c0392b; }
.calendar-event.verde { background: #d1fae5; border-color: #16a34a; }
.calendar-event.azul { background: #dbeafe; border-color: #1d4ed8; }
.calendar-event.amarelo { background: #fef3c7; border-color: #d97706; }
.calendar-event.rosa { background: #fce7f3; border-color: #db2777; }
.calendar-event-hora { font-size: 11px; color: #6b7280; }
.calendar-event-titulo { font-size: 13px; font-weight: 600; color: #1f2937; }
/* ===== LOADING SPINNER ===== */
.loading { display: flex; align-items: center; justify-content: center; padding: 40px; color: #9ca3af; gap: 10px; font-size: 14px; }
.spinner { width: 20px; height: 20px; border: 2px solid #e5e7eb; border-top-color: #c0392b; border-radius: 50%; animation: spin 0.7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* ===== MODAL ===== */
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.45);
z-index: 100;
align-items: center;
justify-content: center;
}
.modal-overlay.aberto { display: flex; }
.modal {
background: #fff;
border-radius: 12px;
padding: 28px;
width: 100%;
max-width: 480px;
margin: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.2);
max-height: 90vh;
overflow-y: auto;
}
.modal-titulo { font-size: 18px; font-weight: 700; color: #1f2937; margin-bottom: 20px; }
.modal-campo { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
.modal-campo label { font-size: 13px; font-weight: 600; color: #374151; }
.modal-campo input,
.modal-campo select,
.modal-campo textarea {
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 10px 12px;
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s;
background: #fff;
}
.modal-campo input:focus,
.modal-campo select:focus,
.modal-campo textarea:focus {
outline: none;
border-color: #c0392b;
}
.modal-campo textarea { resize: vertical; min-height: 70px; }
.modal-linha { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.modal-acoes { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
.btn-secundario {
background: transparent;
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 9px 18px;
font-size: 14px;
cursor: pointer;
color: #6b7280;
font-family: inherit;
transition: all 0.2s;
}
.btn-secundario:hover { background: #f3f4f6; }
.btn-primario {
background: #c0392b;
color: #fff;
border: none;
border-radius: 8px;
padding: 9px 18px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
font-family: inherit;
transition: background 0.2s;
}
.btn-primario:hover { background: #a03224; }
.btn-primario:disabled { background: #ccc; cursor: not-allowed; }
.btn-perigo {
background: transparent;
border: 1px solid #fca5a5;
color: #c0392b;
border-radius: 8px;
padding: 9px 18px;
font-size: 14px;
cursor: pointer;
font-family: inherit;
transition: all 0.2s;
margin-right: auto;
}
.btn-perigo:hover { background: #fee2e2; }
/* Toast de feedback */
#toast {
position: fixed;
bottom: 24px;
right: 24px;
background: #1f2937;
color: #fff;
padding: 12px 18px;
border-radius: 8px;
font-size: 14px;
z-index: 200;
transform: translateY(80px);
opacity: 0;
transition: all 0.3s;
max-width: 320px;
}
#toast.visivel { transform: translateY(0); opacity: 1; }
#toast.sucesso { background: #065f46; }
#toast.erro { background: #b91c1c; }
/* Responsivo */
@media (max-width: 768px) {
#barraesquerda { display: none; }
.main { margin-left: 0; }
.month-view { font-size: 12px; }
.dia-box { min-height: 60px; padding: 4px; }
}

View File

@@ -0,0 +1,957 @@
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Focus Agenda</title>
<link rel="icon" href="imagens/icone.png">
<link rel="stylesheet" href="calendario.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<!-- HEADER -->
<div id="header">
<h1 id="title">Focus Agenda</h1>
</div>
<!-- BARRA ESQUERDA -->
<div id="barraesquerda">
<div id="calendario">
<div class="calendariotop">
<div id="mes"></div>
<div id="calendarseta">
<button class="ant" aria-label="Mês anterior">&lsaquo;</button>
<button class="prox" aria-label="Próximo mês">&rsaquo;</button>
</div>
</div>
<table class="calendariodia">
<thead>
<tr>
<th>DOM</th><th>SEG</th><th>TER</th><th>QUA</th>
<th>QUI</th><th>SEX</th><th>SAB</th>
</tr>
</thead>
<tbody id="dias"></tbody>
</table>
</div>
<div id="agenda"></div>
<div id="feriados"></div>
</div>
<!-- CONTEUDO PRINCIPAL -->
<div class="main">
<div class="topbar">
<h1>Calendário</h1>
<div class="user-area">
<div class="perfil">
<div class="avatar"></div>
<div class="info">
<span class="nome" id="nomeUsuario">Carregando...</span>
<span class="cargo" id="cursoUsuario"></span>
</div>
</div>
<button id="btnLogout" title="Sair">Sair</button>
</div>
</div>
<div class="calendar-header">
<div class="mes-nav">
<button class="seta antGrande" aria-label="Anterior">&lsaquo;</button>
<span class="titulo-mes">Janeiro, 2025</span>
<button class="seta proxGrande" aria-label="Próximo">&rsaquo;</button>
</div>
<div class="view-switch">
<button type="button" data-view="dia">Dia</button>
<button type="button" data-view="semana">Semana</button>
<button type="button" data-view="mes" class="active">Mês</button>
</div>
<button id="btnNovoEvento">+ Novo Evento</button>
</div>
<div class="calendar-area month-view" id="calendarArea"></div>
</div>
<!-- MODAL DE EVENTO -->
<div class="modal-overlay" id="modalEvento">
<div class="modal">
<div class="modal-titulo" id="modalEventoTitulo">Novo Evento</div>
<div class="modal-campo">
<label for="evTitulo">Título *</label>
<input type="text" id="evTitulo" placeholder="Ex: Prova de Matemática">
</div>
<div class="modal-campo">
<label for="evDescricao">Descrição</label>
<textarea id="evDescricao" placeholder="Detalhes do evento..."></textarea>
</div>
<div class="modal-linha">
<div class="modal-campo">
<label for="evTipo">Tipo *</label>
<select id="evTipo">
<option value="AULA">Aula</option>
<option value="PROVA">Prova</option>
<option value="TRABALHO">Trabalho</option>
<option value="ESTUDO">Estudo</option>
<option value="EXAME">Exame</option>
<option value="OUTRO">Outro</option>
</select>
</div>
<div class="modal-campo">
<label for="evDisciplina">Disciplina</label>
<select id="evDisciplina">
<option value="">Nenhuma</option>
</select>
</div>
</div>
<div class="modal-linha">
<div class="modal-campo">
<label for="evData">Data e Hora *</label>
<input type="datetime-local" id="evData">
</div>
<div class="modal-campo">
<label for="evLocal">Local</label>
<input type="text" id="evLocal" placeholder="Ex: Sala 204">
</div>
</div>
<div class="modal-acoes">
<button class="btn-perigo" id="btnExcluirEvento" style="display:none">Excluir</button>
<button class="btn-secundario" id="btnCancelarEvento">Cancelar</button>
<button class="btn-primario" id="btnSalvarEvento">Salvar</button>
</div>
</div>
</div>
<!-- MODAL DE TAREFA -->
<div class="modal-overlay" id="modalTarefa">
<div class="modal">
<div class="modal-titulo" id="modalTarefaTitulo">Nova Tarefa</div>
<div class="modal-campo">
<label for="tfTitulo">Título *</label>
<input type="text" id="tfTitulo" placeholder="Ex: Estudar para a prova">
</div>
<div class="modal-campo">
<label for="tfDescricao">Descrição</label>
<textarea id="tfDescricao" placeholder="Detalhes da tarefa..."></textarea>
</div>
<div class="modal-linha">
<div class="modal-campo">
<label for="tfPrioridade">Prioridade *</label>
<select id="tfPrioridade">
<option value="BAIXA">Baixa</option>
<option value="MEDIA" selected>Média</option>
<option value="ALTA">Alta</option>
<option value="URGENTE">Urgente</option>
</select>
</div>
<div class="modal-campo">
<label for="tfStatus">Status</label>
<select id="tfStatus">
<option value="PENDENTE" selected>Pendente</option>
<option value="EM_ANDAMENTO">Em Andamento</option>
<option value="CONCLUIDA">Concluída</option>
</select>
</div>
</div>
<div class="modal-linha">
<div class="modal-campo">
<label for="tfData">Data de Entrega *</label>
<input type="date" id="tfData">
</div>
<div class="modal-campo">
<label for="tfDisciplina">Disciplina</label>
<select id="tfDisciplina">
<option value="">Nenhuma</option>
</select>
</div>
</div>
<div class="modal-acoes">
<button class="btn-perigo" id="btnExcluirTarefa" style="display:none">Excluir</button>
<button class="btn-secundario" id="btnCancelarTarefa">Cancelar</button>
<button class="btn-primario" id="btnSalvarTarefa">Salvar</button>
</div>
</div>
</div>
<!-- TOAST -->
<div id="toast"></div>
<script>
// =============================================
// AUTENTICAÇÃO & BOOTSTRAP
// =============================================
const token = localStorage.getItem('fa_token');
const userStr = localStorage.getItem('fa_user');
if (!token) {
window.location.href = 'login.html';
}
let usuario = null;
try { usuario = JSON.parse(userStr); } catch(e) {}
function logout() {
localStorage.removeItem('fa_token');
localStorage.removeItem('fa_user');
window.location.href = 'login.html';
}
document.getElementById('btnLogout').addEventListener('click', logout);
// =============================================
// API HELPERS
// =============================================
async function api(method, path, body) {
const opts = {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
}
};
if (body) opts.body = JSON.stringify(body);
const res = await fetch(path, opts);
if (res.status === 401 || res.status === 403) {
// Token expirado ou inválido
if (res.status === 401) { logout(); return; }
}
const json = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(json.message || 'Erro na requisição');
}
return json.data;
}
// =============================================
// TOAST
// =============================================
const toastEl = document.getElementById('toast');
let toastTimer;
function mostrarToast(msg, tipo = 'sucesso') {
clearTimeout(toastTimer);
toastEl.textContent = msg;
toastEl.className = 'visivel ' + tipo;
toastTimer = setTimeout(() => { toastEl.className = ''; }, 3200);
}
// =============================================
// ESTADO GLOBAL
// =============================================
let dataMini = new Date();
let dataGrande = new Date();
let modoAtual = 'mes';
let dataSelecionada = null;
let eventos = [];
let tarefas = [];
let disciplinas = [];
let eventoEditandoId = null;
let tarefaEditandoId = null;
const meses = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho',
'Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'];
const diasCurto = ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb'];
const diasLongo = ['Domingo','Segunda','Terça','Quarta','Quinta','Sexta','Sábado'];
// =============================================
// CARREGA DADOS DO USUÁRIO
// =============================================
async function carregarUsuario() {
try {
const u = await api('GET', '/api/estudantes/me');
usuario = u;
localStorage.setItem('fa_user', JSON.stringify(u));
} catch(e) { /* usa cache */ }
if (usuario) {
document.getElementById('nomeUsuario').textContent = usuario.nome || 'Usuário';
document.getElementById('cursoUsuario').textContent =
usuario.curso ? (usuario.curso + (usuario.periodo ? ' ' + usuario.periodo + 'º período' : '')) : '';
}
}
// =============================================
// CARREGA DADOS DA API
// =============================================
async function carregarDados() {
if (!usuario) return;
const id = usuario.id;
try {
[eventos, tarefas, disciplinas] = await Promise.all([
api('GET', `/api/eventos/estudante/${id}`),
api('GET', `/api/tarefas/estudante/${id}`),
api('GET', `/api/disciplinas/estudante/${id}`)
]);
if (!eventos) eventos = [];
if (!tarefas) tarefas = [];
if (!disciplinas) disciplinas = [];
} catch(e) {
mostrarToast('Erro ao carregar dados: ' + e.message, 'erro');
}
}
function nomeDisciplina(id) {
if (!id) return null;
const d = disciplinas.find(d => d.id === id);
return d ? d.nome : null;
}
// Preenche selects de disciplina nos modais
function preencherSelectsDisciplina() {
['evDisciplina', 'tfDisciplina'].forEach(selId => {
const sel = document.getElementById(selId);
// Remove options exceto a primeira
while (sel.options.length > 1) sel.remove(1);
disciplinas.forEach(d => {
const opt = document.createElement('option');
opt.value = d.id;
opt.textContent = d.nome;
sel.appendChild(opt);
});
});
}
// =============================================
// NORMALIZAÇÃO DE DATAS
// =============================================
function normalizarData(d) {
return new Date(d.getFullYear(), d.getMonth(), d.getDate());
}
function formatarISO(d) {
const a = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2,'0');
const dia = String(d.getDate()).padStart(2,'0');
return `${a}-${m}-${dia}`;
}
function formatarCurta(d) {
return `${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}`;
}
function adicionarDias(d, n) {
const r = new Date(d);
r.setDate(r.getDate() + n);
return r;
}
function inicioSemana(d) {
const r = normalizarData(d);
r.setDate(r.getDate() - r.getDay());
return r;
}
// Pega a data ISO de um evento (pode ser dataHora ou dataEntrega)
function dataDoEvento(ev) {
const raw = ev.dataHora || ev.dataEntrega;
if (!raw) return null;
return raw.substring(0, 10); // 'YYYY-MM-DD'
}
function eventosDaData(iso) {
const evs = eventos.filter(e => dataDoEvento(e) === iso);
const tfs = tarefas.filter(t => t.dataEntrega === iso);
return { evs, tfs };
}
// Cor baseada no tipo do evento
const corPorTipo = { PROVA: 'amarelo', AULA: 'azul', TRABALHO: 'verde', EXAME: 'amarelo', OUTRO: '' };
// =============================================
// MINI CALENDÁRIO
// =============================================
function renderMini() {
const mesEl = document.getElementById('mes');
const diasEl = document.getElementById('dias');
const ano = dataMini.getFullYear();
const mes = dataMini.getMonth();
mesEl.textContent = `${meses[mes]} ${ano}`;
diasEl.replaceChildren();
const primeiroDia = new Date(ano, mes, 1).getDay();
const ultimoDia = new Date(ano, mes + 1, 0).getDate();
const ultimoAnterior = new Date(ano, mes, 0).getDate();
const totalCelulas = Math.ceil((primeiroDia + ultimoDia) / 7) * 7;
let diaAtual = 1, diaPros = 1;
for (let s = 0; s < totalCelulas / 7; s++) {
const linha = document.createElement('tr');
for (let ds = 0; ds < 7; ds++) {
const pos = s * 7 + ds;
const cel = document.createElement('td');
if (pos < primeiroDia) {
cel.textContent = ultimoAnterior - (primeiroDia - pos - 1);
cel.className = 'outromes';
} else if (diaAtual <= ultimoDia) {
cel.textContent = diaAtual;
const hoje = new Date();
if (diaAtual === hoje.getDate() && mes === hoje.getMonth() && ano === hoje.getFullYear()) {
cel.className = 'today';
}
const d = diaAtual;
cel.addEventListener('click', () => {
dataSelecionada = new Date(ano, mes, d);
dataGrande = new Date(dataSelecionada);
renderCalendarioGrande();
renderMini();
});
diaAtual++;
} else {
cel.textContent = diaPros++;
cel.className = 'outromes';
}
linha.appendChild(cel);
}
diasEl.appendChild(linha);
}
}
// =============================================
// AGENDA DO DIA (barra esquerda)
// =============================================
function renderAgenda() {
const container = document.getElementById('agenda');
container.replaceChildren();
const header = document.createElement('div');
header.className = 'agenda-header';
header.innerHTML = '<strong>HOJE</strong> ' +
new Intl.DateTimeFormat('pt-BR').format(new Date());
container.appendChild(header);
const hoje = formatarISO(normalizarData(new Date()));
const { evs, tfs } = eventosDaData(hoje);
const tudo = [
...evs.map(e => ({ titulo: e.titulo, hora: e.dataHora ? e.dataHora.substring(11,16) : '', tipo: 'evento' })),
...tfs.map(t => ({ titulo: t.titulo, hora: '', tipo: 'tarefa' }))
];
if (tudo.length === 0) {
const vazio = document.createElement('div');
vazio.className = 'agenda-empty';
vazio.textContent = 'Sem eventos para hoje.';
container.appendChild(vazio);
return;
}
tudo.forEach(item => {
const ev = document.createElement('div');
ev.className = 'evento';
ev.innerHTML = `<div class="hora">${item.hora || (item.tipo === 'tarefa' ? '📋 Tarefa' : '')}</div>
<div class="titulo">${item.titulo}</div>`;
container.appendChild(ev);
});
}
// =============================================
// FERIADOS (fixos Brasil)
// =============================================
function renderFeriados() {
const container = document.getElementById('feriados');
container.replaceChildren();
const header = document.createElement('div');
header.className = 'feriados-header';
header.textContent = 'FERIADOS NACIONAIS';
container.appendChild(header);
const feriadosMes = [
{ mes: 0, dia: 1, texto: '01 Jan Ano Novo' },
{ mes: 3, dia: 21, texto: '21 Abr Tiradentes' },
{ mes: 4, dia: 1, texto: '01 Mai Dia do Trabalho' },
{ mes: 8, dia: 7, texto: '07 Set Independência' },
{ mes: 9, dia: 12, texto: '12 Out N. Sra. Aparecida' },
{ mes: 10, dia: 2, texto: '02 Nov Finados' },
{ mes: 10, dia: 15, texto: '15 Nov República' },
{ mes: 11, dia: 25, texto: '25 Dez Natal' },
].filter(f => f.mes === dataMini.getMonth());
if (feriadosMes.length === 0) {
const item = document.createElement('div');
item.className = 'feriado';
item.innerHTML = '<span style="opacity:0.6;font-size:12px">Nenhum este mês</span>';
container.appendChild(item);
return;
}
feriadosMes.forEach(f => {
const item = document.createElement('div');
item.className = 'feriado';
item.innerHTML = `<span class="dot"></span><span>${f.texto}</span>`;
container.appendChild(item);
});
}
// =============================================
// CALENDÁRIO GRANDE
// =============================================
function atualizarTituloGrande() {
const titulo = document.querySelector('.titulo-mes');
if (modoAtual === 'mes') {
titulo.textContent = `${meses[dataGrande.getMonth()]}, ${dataGrande.getFullYear()}`;
} else if (modoAtual === 'semana') {
const ini = inicioSemana(dataGrande);
const fim = adicionarDias(ini, 6);
titulo.textContent = `Semana ${formatarCurta(ini)} ${formatarCurta(fim)}/${fim.getFullYear()}`;
} else {
titulo.textContent = `${diasLongo[dataGrande.getDay()]}, ${formatarCurta(dataGrande)}/${dataGrande.getFullYear()}`;
}
}
function configurarArea(classe) {
const area = document.getElementById('calendarArea');
area.className = 'calendar-area ' + classe;
area.replaceChildren();
return area;
}
function criarCardEvento(ev, isTarefa) {
const card = document.createElement('div');
const cor = isTarefa ? 'verde' : (corPorTipo[ev.tipo] || '');
card.className = `calendar-event ${cor}`;
card.innerHTML = `
<div class="calendar-event-hora">${isTarefa ? '📋 Tarefa' : (ev.dataHora ? ev.dataHora.substring(11,16) : '')}</div>
<div class="calendar-event-titulo">${ev.titulo}</div>`;
card.addEventListener('click', (e) => {
e.stopPropagation();
if (isTarefa) abrirModalTarefa(ev);
else abrirModalEvento(ev);
});
return card;
}
/* MÊS */
function renderMes() {
const area = configurarArea('month-view');
atualizarTituloGrande();
diasCurto.forEach(d => {
const el = document.createElement('div');
el.className = 'dia-semana';
el.textContent = d;
area.appendChild(el);
});
const ano = dataGrande.getFullYear(), mes = dataGrande.getMonth();
const primeiroDia = new Date(ano, mes, 1).getDay();
const ultimoDia = new Date(ano, mes + 1, 0).getDate();
const anteriorUltimo = new Date(ano, mes, 0).getDate();
const hoje = normalizarData(new Date());
for (let i = 0; i < 42; i++) {
const box = document.createElement('div');
box.className = 'dia-box';
let dNum, dData, outroMes = false;
if (i < primeiroDia) {
dNum = anteriorUltimo - (primeiroDia - i - 1);
dData = new Date(ano, mes - 1, dNum);
outroMes = true;
} else if (i < primeiroDia + ultimoDia) {
dNum = i - primeiroDia + 1;
dData = new Date(ano, mes, dNum);
} else {
dNum = i - primeiroDia - ultimoDia + 1;
dData = new Date(ano, mes + 1, dNum);
outroMes = true;
}
if (outroMes) box.classList.add('outro-mes');
const iso = formatarISO(dData);
const isHoje = !outroMes && normalizarData(dData).getTime() === hoje.getTime();
if (isHoje) box.classList.add('today');
if (dataSelecionada && iso === formatarISO(normalizarData(dataSelecionada))) {
box.classList.add('selecionado');
}
const numEl = document.createElement('div');
numEl.className = 'num-dia';
numEl.textContent = dNum;
box.appendChild(numEl);
// Mostra até 3 eventos
const { evs, tfs } = eventosDaData(iso);
const todos = [...evs.map(e => ({ ...e, _tipo: 'evento' })), ...tfs.map(t => ({ ...t, _tipo: 'tarefa' }))];
const limite = 2;
todos.slice(0, limite).forEach(item => {
const mini = document.createElement('div');
const isTarefa = item._tipo === 'tarefa';
mini.className = 'evento-mini ' + (isTarefa ? 'verde' : (corPorTipo[item.tipo] || ''));
mini.textContent = item.titulo;
box.appendChild(mini);
});
if (todos.length > limite) {
const mais = document.createElement('div');
mais.className = 'mais-eventos';
mais.textContent = `+${todos.length - limite} mais`;
box.appendChild(mais);
}
box.addEventListener('click', () => {
dataSelecionada = dData;
dataGrande = new Date(dData);
renderCalendarioGrande();
renderMini();
});
area.appendChild(box);
}
}
/* SEMANA */
function renderSemana() {
const area = configurarArea('week-view');
atualizarTituloGrande();
const ini = inicioSemana(dataGrande);
const hoje = normalizarData(new Date());
for (let i = 0; i < 7; i++) {
const d = adicionarDias(ini, i);
const iso = formatarISO(d);
const { evs, tfs } = eventosDaData(iso);
const col = document.createElement('div');
col.className = 'week-col';
if (normalizarData(d).getTime() === hoje.getTime()) col.classList.add('today');
const head = document.createElement('div');
head.className = 'week-col-head';
head.textContent = diasCurto[d.getDay()];
const dt = document.createElement('span');
dt.className = 'week-col-date';
dt.textContent = `${formatarCurta(d)}/${d.getFullYear()}`;
head.appendChild(dt);
const lista = document.createElement('div');
lista.className = 'week-events';
const todos = [...evs.map(e => ({ ...e, _tipo: 'evento' })), ...tfs.map(t => ({ ...t, _tipo: 'tarefa' }))];
if (todos.length === 0) {
const vazio = document.createElement('div');
vazio.className = 'week-empty';
vazio.textContent = 'Sem eventos';
lista.appendChild(vazio);
} else {
todos.forEach(item => lista.appendChild(criarCardEvento(item, item._tipo === 'tarefa')));
}
col.appendChild(head);
col.appendChild(lista);
area.appendChild(col);
}
}
/* DIA */
function renderDia() {
const area = configurarArea('day-view');
atualizarTituloGrande();
const iso = formatarISO(normalizarData(dataGrande));
const { evs, tfs } = eventosDaData(iso);
const painel = document.createElement('div');
painel.className = 'day-panel';
const head = document.createElement('div');
head.className = 'day-panel-header';
head.textContent = `Agenda de ${formatarCurta(dataGrande)}/${dataGrande.getFullYear()}`;
painel.appendChild(head);
const lista = document.createElement('div');
lista.className = 'day-events';
const todos = [...evs.map(e => ({ ...e, _tipo: 'evento' })), ...tfs.map(t => ({ ...t, _tipo: 'tarefa' }))];
if (todos.length === 0) {
const vazio = document.createElement('div');
vazio.className = 'day-empty';
vazio.textContent = 'Nenhum evento para este dia.';
lista.appendChild(vazio);
} else {
todos.forEach(item => lista.appendChild(criarCardEvento(item, item._tipo === 'tarefa')));
}
painel.appendChild(lista);
area.appendChild(painel);
}
function renderCalendarioGrande() {
if (modoAtual === 'dia') renderDia();
else if (modoAtual === 'semana') renderSemana();
else renderMes();
}
function moverPeriodo(dir) {
if (modoAtual === 'dia') dataGrande.setDate(dataGrande.getDate() + dir);
else if (modoAtual === 'semana') dataGrande.setDate(dataGrande.getDate() + dir * 7);
else dataGrande.setMonth(dataGrande.getMonth() + dir);
renderCalendarioGrande();
atualizarTituloGrande();
}
// =============================================
// MODAL DE EVENTO
// =============================================
function abrirModalEvento(ev) {
eventoEditandoId = ev ? ev.id : null;
const titulo = document.getElementById('modalEventoTitulo');
const btnExcluir = document.getElementById('btnExcluirEvento');
titulo.textContent = ev ? 'Editar Evento' : 'Novo Evento';
btnExcluir.style.display = ev ? 'inline-block' : 'none';
// Preenche ou limpa campos
document.getElementById('evTitulo').value = ev ? ev.titulo : '';
document.getElementById('evDescricao').value = ev ? (ev.descricao || '') : '';
document.getElementById('evTipo').value = ev ? (ev.tipo || 'OUTRO') : 'OUTRO';
document.getElementById('evLocal').value = ev ? (ev.local || '') : '';
if (ev && ev.dataHora) {
// dataHora vem como "2025-04-01T14:00:00" datetime-local precisa de "2025-04-01T14:00"
document.getElementById('evData').value = ev.dataHora.substring(0, 16);
} else if (dataSelecionada) {
const iso = formatarISO(normalizarData(dataSelecionada));
document.getElementById('evData').value = iso + 'T08:00';
} else {
document.getElementById('evData').value = '';
}
document.getElementById('evDisciplina').value = ev ? (ev.disciplinaId || '') : '';
document.getElementById('modalEvento').classList.add('aberto');
document.getElementById('evTitulo').focus();
}
document.getElementById('btnNovoEvento').addEventListener('click', () => abrirModalEvento(null));
document.getElementById('btnCancelarEvento').addEventListener('click', () => {
document.getElementById('modalEvento').classList.remove('aberto');
});
document.getElementById('btnSalvarEvento').addEventListener('click', async () => {
const titulo = document.getElementById('evTitulo').value.trim();
const dataHoraRaw = document.getElementById('evData').value;
if (!titulo) { mostrarToast('Título é obrigatório.', 'erro'); return; }
if (!dataHoraRaw) { mostrarToast('Data e hora são obrigatórias.', 'erro'); return; }
const btn = document.getElementById('btnSalvarEvento');
btn.disabled = true;
const payload = {
titulo,
descricao: document.getElementById('evDescricao').value.trim() || null,
tipo: document.getElementById('evTipo').value,
local: document.getElementById('evLocal').value.trim() || null,
disciplinaId: document.getElementById('evDisciplina').value || null,
dataHora: dataHoraRaw + ':00', // adiciona segundos
estudanteId: usuario.id
};
try {
if (eventoEditandoId) {
const updated = await api('PUT', `/api/eventos/${eventoEditandoId}`, payload);
const idx = eventos.findIndex(e => e.id === eventoEditandoId);
if (idx !== -1) eventos[idx] = updated;
mostrarToast('Evento atualizado!');
} else {
const novo = await api('POST', '/api/eventos', payload);
eventos.push(novo);
mostrarToast('Evento criado!');
}
document.getElementById('modalEvento').classList.remove('aberto');
renderCalendarioGrande();
renderAgenda();
} catch(e) {
mostrarToast('Erro: ' + e.message, 'erro');
} finally {
btn.disabled = false;
}
});
document.getElementById('btnExcluirEvento').addEventListener('click', async () => {
if (!confirm('Excluir este evento?')) return;
try {
await api('DELETE', `/api/eventos/${eventoEditandoId}?estudanteId=${usuario.id}`);
eventos = eventos.filter(e => e.id !== eventoEditandoId);
document.getElementById('modalEvento').classList.remove('aberto');
mostrarToast('Evento excluído.', 'sucesso');
renderCalendarioGrande();
renderAgenda();
} catch(e) {
mostrarToast('Erro: ' + e.message, 'erro');
}
});
// =============================================
// MODAL DE TAREFA
// =============================================
function abrirModalTarefa(tf) {
tarefaEditandoId = tf ? tf.id : null;
const titulo = document.getElementById('modalTarefaTitulo');
const btnExcluir = document.getElementById('btnExcluirTarefa');
titulo.textContent = tf ? 'Editar Tarefa' : 'Nova Tarefa';
btnExcluir.style.display = tf ? 'inline-block' : 'none';
document.getElementById('tfTitulo').value = tf ? tf.titulo : '';
document.getElementById('tfDescricao').value = tf ? (tf.descricao || '') : '';
document.getElementById('tfPrioridade').value = tf ? (tf.prioridade || 'MEDIA') : 'MEDIA';
document.getElementById('tfStatus').value = tf ? (tf.status || 'PENDENTE') : 'PENDENTE';
document.getElementById('tfDisciplina').value = tf ? (tf.disciplinaId || '') : '';
if (tf && tf.dataEntrega) {
document.getElementById('tfData').value = tf.dataEntrega;
} else if (dataSelecionada) {
document.getElementById('tfData').value = formatarISO(normalizarData(dataSelecionada));
} else {
document.getElementById('tfData').value = '';
}
document.getElementById('modalTarefa').classList.add('aberto');
document.getElementById('tfTitulo').focus();
}
document.getElementById('btnCancelarTarefa').addEventListener('click', () => {
document.getElementById('modalTarefa').classList.remove('aberto');
});
document.getElementById('btnSalvarTarefa').addEventListener('click', async () => {
const titulo = document.getElementById('tfTitulo').value.trim();
const dataEntrega = document.getElementById('tfData').value;
if (!titulo) { mostrarToast('Título é obrigatório.', 'erro'); return; }
if (!dataEntrega) { mostrarToast('Data de entrega é obrigatória.', 'erro'); return; }
const btn = document.getElementById('btnSalvarTarefa');
btn.disabled = true;
const payload = {
titulo,
descricao: document.getElementById('tfDescricao').value.trim() || null,
prioridade: document.getElementById('tfPrioridade').value,
status: document.getElementById('tfStatus').value,
dataEntrega,
disciplinaId: document.getElementById('tfDisciplina').value || null,
estudanteId: usuario.id
};
try {
if (tarefaEditandoId) {
const updated = await api('PUT', `/api/tarefas/${tarefaEditandoId}`, payload);
const idx = tarefas.findIndex(t => t.id === tarefaEditandoId);
if (idx !== -1) tarefas[idx] = updated;
mostrarToast('Tarefa atualizada!');
} else {
const nova = await api('POST', '/api/tarefas', payload);
tarefas.push(nova);
mostrarToast('Tarefa criada!');
}
document.getElementById('modalTarefa').classList.remove('aberto');
renderCalendarioGrande();
renderAgenda();
} catch(e) {
mostrarToast('Erro: ' + e.message, 'erro');
} finally {
btn.disabled = false;
}
});
document.getElementById('btnExcluirTarefa').addEventListener('click', async () => {
if (!confirm('Excluir esta tarefa?')) return;
try {
await api('DELETE', `/api/tarefas/${tarefaEditandoId}`);
tarefas = tarefas.filter(t => t.id !== tarefaEditandoId);
document.getElementById('modalTarefa').classList.remove('aberto');
mostrarToast('Tarefa excluída.', 'sucesso');
renderCalendarioGrande();
renderAgenda();
} catch(e) {
mostrarToast('Erro: ' + e.message, 'erro');
}
});
// Fecha modais clicando no overlay
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) overlay.classList.remove('aberto');
});
});
// Fecha modais com ESC
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
document.querySelectorAll('.modal-overlay.aberto').forEach(m => m.classList.remove('aberto'));
}
});
// =============================================
// NAVEGAÇÃO E VIEW SWITCH
// =============================================
document.querySelector('.prox').onclick = () => { dataMini.setMonth(dataMini.getMonth() + 1); renderMini(); renderFeriados(); };
document.querySelector('.ant').onclick = () => { dataMini.setMonth(dataMini.getMonth() - 1); renderMini(); renderFeriados(); };
document.querySelector('.antGrande').onclick = () => moverPeriodo(-1);
document.querySelector('.proxGrande').onclick = () => moverPeriodo(1);
document.querySelectorAll('.view-switch button').forEach(btn => {
btn.addEventListener('click', () => {
modoAtual = btn.dataset.view;
document.querySelectorAll('.view-switch button').forEach(b => b.classList.toggle('active', b === btn));
renderCalendarioGrande();
});
});
// Duplo clique em dia abre modal de novo evento
document.getElementById('calendarArea').addEventListener('dblclick', (e) => {
abrirModalEvento(null);
});
// =============================================
// INICIALIZAÇÃO
// =============================================
async function init() {
// Mostra spinner enquanto carrega
document.getElementById('calendarArea').innerHTML =
'<div class="loading"><div class="spinner"></div>Carregando...</div>';
await carregarUsuario();
await carregarDados();
preencherSelectsDisciplina();
renderMini();
renderCalendarioGrande();
renderAgenda();
renderFeriados();
}
init();
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;url=login.html">
<title>Focus Agenda</title>
</head>
<body>
<script>window.location.href='login.html';</script>
</body>
</html>

View File

@@ -0,0 +1,83 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
font-family: 'Poppins', 'Trebuchet MS', Arial, sans-serif;
display: flex;
align-items: center;
justify-content: center;
padding: 80px 20px 20px;
background: #f5f5f5;
}
#topo {
width: 100%;
height: 50px;
position: fixed;
top: 0; left: 0;
background: linear-gradient(to right, #c0392b 47%, #7a4951 73%, #114455 87%);
display: flex;
align-items: center;
z-index: 10;
}
#textotop {
padding-left: 20px;
font-size: clamp(22px, 5vw, 38px);
color: #fff;
}
#log {
width: 100%;
max-width: 360px;
display: flex;
flex-direction: column;
gap: 16px;
}
.mens { text-align: center; color: #1f2937; }
.campo { display: flex; flex-direction: column; gap: 8px; }
label { font-weight: 700; color: #1f2937; }
input[type="email"], input[type="password"] {
height: 46px;
width: 100%;
padding: 10px;
font-size: 16px;
border: 1px solid #c7c7c7;
border-radius: 6px;
font-family: inherit;
}
form { display: flex; flex-direction: column; gap: 16px; }
#logbtn {
align-self: center;
width: 50%;
padding: 12px;
font-size: 18px;
font-weight: bold;
background-color: #c0392b;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s ease;
}
#logbtn:hover { background-color: #a03224; }
#logbtn:disabled { background-color: #ccc; cursor: not-allowed; }
a { color: #111; text-decoration: none; }
a:hover { text-decoration: underline; }
#mensagem-erro {
background: #fee2e2;
border: 1px solid #fca5a5;
color: #b91c1c;
padding: 10px 14px;
border-radius: 6px;
font-size: 14px;
display: none;
}

View File

@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="imagens/icone.png">
<link rel="stylesheet" href="login.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<title>Login Focus Agenda</title>
</head>
<body>
<div id="topo">
<h1 id="textotop">Focus Agenda</h1>
</div>
<div id="log">
<h1 class="mens">Bem-vindo!</h1>
<h3 class="mens">Faça seu login</h3>
<div id="mensagem-erro" role="alert"></div>
<form id="loginForm" novalidate>
<div class="campo">
<label for="emailid">Email</label>
<input type="email" placeholder="Digite seu email" id="emailid" autocomplete="email" required>
</div>
<div class="campo">
<label for="senhaid">Senha</label>
<input type="password" placeholder="Digite sua senha" id="senhaid" autocomplete="current-password" required>
</div>
<button type="submit" id="logbtn">Entrar</button>
</form>
<p class="mens"><a href="cadastro.html">Cadastrar-se</a></p>
</div>
<script>
// Se já tiver token válido, redireciona direto
(function () {
if (localStorage.getItem('fa_token')) {
window.location.href = 'calendario.html';
}
})();
const form = document.getElementById('loginForm');
const erroEl = document.getElementById('mensagem-erro');
const btn = document.getElementById('logbtn');
function mostrarErro(msg) {
erroEl.textContent = msg;
erroEl.style.display = 'block';
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
erroEl.style.display = 'none';
const email = document.getElementById('emailid').value.trim();
const senha = document.getElementById('senhaid').value;
if (!email || !senha) {
mostrarErro('Preencha todos os campos.');
return;
}
btn.disabled = true;
btn.textContent = 'Entrando...';
try {
const res = await fetch('/api/estudantes/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, senha })
});
const json = await res.json();
if (!res.ok) {
mostrarErro(json.message || 'Email ou senha incorretos.');
return;
}
// Salva token e dados do usuário
localStorage.setItem('fa_token', json.data.token);
localStorage.setItem('fa_user', JSON.stringify(json.data.estudante));
window.location.href = 'calendario.html';
} catch (err) {
mostrarErro('Erro de conexão. Verifique se o servidor está rodando.');
} finally {
btn.disabled = false;
btn.textContent = 'Entrar';
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,124 @@
* {
padding: 0;
margin: 0;
}
p,
h1,
h2,
h3 {
color: black;
margin: 0;
font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif
}
label{
display: block;
}
body {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
@media (max-width: 768px) {
body {
padding-left: 20px;
padding-right: 20px;
justify-content: center;
}
}
@media (max-width: 480px) {
#log {
width: 100% !important;
max-width: 320px;
}
}
#topo {
width: 100%;
height: 50px;
background-color: #111;
position: fixed;
top: 0;
left: 0;
background: linear-gradient(to right, #C0392B 47%, #7A4951 73%, #114455 87%);
display: flex;
align-items: center;
justify-content: flex-start;
}
#textotop {
padding-left: 20px;
font-size: 38px;
margin: 0;
font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
}
#log {
width: 350px;
display: flex;
flex-direction: column;
gap: 15px;
color: white;
margin-top: 70px; /* Compensa a barra fixa do topo */
}
#campo {
display: flex;
flex-direction: column;
gap: 15px;
align-self: center;
}
#menslog{
font-size: 20px;
}
#emailid, #senhaid {
height: 50px;
width: 100%;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: white;
}
form {
display: flex;
flex-direction: column;
gap: 15px;
width: 100%;
}
#logbtn {
align-self: center;
width: 50%;
padding: 12px;
font-size: 18px;
font-weight: bold;
background-color: #C0392B;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}
#logbtn:hover {
background-color: #A03224;
}
.mens {
align-self: center;
}
#linkcada{
color: #111;
}

View File

@@ -0,0 +1,107 @@
package com.agendaestudantil.servico;
import com.agendaestudantil.dto.RequisicaoCadastroDTO;
import com.agendaestudantil.dto.RespostaEstudanteDTO;
import com.agendaestudantil.dto.RequisicaoLoginDTO;
import com.agendaestudantil.dto.RespostaLoginDTO;
import com.agendaestudantil.entidade.Estudante;
import com.agendaestudantil.excecao.ExcecaoNegocio;
import com.agendaestudantil.repositorio.EstudanteRepositorio;
import com.agendaestudantil.utilitario.UtilJwt;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.server.ResponseStatusException;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
public class EstudanteServicoTest {
@Mock
private EstudanteRepositorio estudanteRepositorio;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private UtilJwt utilJwt;
@InjectMocks
private EstudanteServico estudanteServico;
private RequisicaoCadastroDTO requisicaoCadastro;
private RequisicaoLoginDTO requisicaoLogin;
private Estudante estudante;
@BeforeEach
void setUp() {
requisicaoCadastro = new RequisicaoCadastroDTO("Teste", "teste@teste.com", "123456", "Curso", 1);
requisicaoLogin = new RequisicaoLoginDTO("teste@teste.com", "123456");
estudante = new Estudante();
estudante.setId("1");
estudante.setNome("Teste");
estudante.setEmail("teste@teste.com");
estudante.setSenha("encodedPassword");
estudante.setCurso("Curso");
estudante.setPeriodo(1);
}
@Test
void deveCadastrarEstudanteComSucesso() {
when(estudanteRepositorio.findByEmail(anyString())).thenReturn(Optional.empty());
when(passwordEncoder.encode(anyString())).thenReturn("encodedPassword");
when(estudanteRepositorio.save(any(Estudante.class))).thenReturn(estudante);
RespostaEstudanteDTO resposta = estudanteServico.cadastrar(requisicaoCadastro);
assertNotNull(resposta);
assertEquals("Teste", resposta.nome());
verify(estudanteRepositorio, times(1)).save(any(Estudante.class));
}
@Test
void deveLancarExcecaoAoCadastrarEmailExistente() {
when(estudanteRepositorio.findByEmail(anyString())).thenReturn(Optional.of(estudante));
assertThrows(ExcecaoNegocio.class, () -> estudanteServico.cadastrar(requisicaoCadastro));
verify(estudanteRepositorio, never()).save(any(Estudante.class));
}
@Test
void deveRealizarLoginComSucesso() {
when(estudanteRepositorio.findByEmail(anyString())).thenReturn(Optional.of(estudante));
when(passwordEncoder.matches(anyString(), anyString())).thenReturn(true);
when(utilJwt.generateToken(anyString())).thenReturn("token123");
RespostaLoginDTO resposta = estudanteServico.login(requisicaoLogin);
assertNotNull(resposta);
assertEquals("token123", resposta.token());
assertEquals("Teste", resposta.estudante().nome());
}
@Test
void deveRejeitarSenhaInvalida() {
when(estudanteRepositorio.findByEmail(anyString())).thenReturn(Optional.of(estudante));
when(passwordEncoder.matches(anyString(), anyString())).thenReturn(false);
assertThrows(ResponseStatusException.class, () -> estudanteServico.login(requisicaoLogin));
}
@Test
void deveRejeitarEmailNaoEncontrado() {
when(estudanteRepositorio.findByEmail(anyString())).thenReturn(Optional.empty());
assertThrows(ResponseStatusException.class, () -> estudanteServico.login(requisicaoLogin));
}
}

View File

@@ -0,0 +1,123 @@
package com.agendaestudantil.servico;
import com.agendaestudantil.dto.RequisicaoTarefaDTO;
import com.agendaestudantil.dto.RespostaTarefaDTO;
import com.agendaestudantil.entidade.Tarefa;
import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado;
import com.agendaestudantil.repositorio.TarefaRepositorio;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
public class TarefaServicoTest {
@Mock
private TarefaRepositorio tarefaRepositorio;
@Mock
private SecurityContext securityContext;
@Mock
private Authentication authentication;
@InjectMocks
private TarefaServico tarefaServico;
private RequisicaoTarefaDTO requisicaoTarefa;
private Tarefa tarefa;
private final String estudanteId = "estudante123";
@BeforeEach
void setUp() {
requisicaoTarefa = new RequisicaoTarefaDTO(
"Estudar Spring",
"Aprender Spring Boot 3",
Tarefa.Prioridade.ALTA,
Tarefa.StatusTarefa.PENDENTE,
LocalDate.now(),
null,
estudanteId);
tarefa = new Tarefa();
tarefa.setId("tarefa1");
tarefa.setTitulo("Estudar Spring");
tarefa.setDescricao("Aprender Spring Boot 3");
tarefa.setPrioridade(Tarefa.Prioridade.ALTA);
tarefa.setStatus(Tarefa.StatusTarefa.PENDENTE);
tarefa.setDataEntrega(LocalDate.now());
tarefa.setEstudanteId(estudanteId);
}
private void mockAuthentication(String user) {
when(securityContext.getAuthentication()).thenReturn(authentication);
when(authentication.getName()).thenReturn(user);
SecurityContextHolder.setContext(securityContext);
}
@Test
void deveCriarTarefaComSucesso() {
mockAuthentication(estudanteId);
when(tarefaRepositorio.save(any(Tarefa.class))).thenReturn(tarefa);
RespostaTarefaDTO resposta = tarefaServico.criarTarefa(requisicaoTarefa);
assertNotNull(resposta);
assertEquals("Estudar Spring", resposta.titulo());
verify(tarefaRepositorio, times(1)).save(any(Tarefa.class));
}
@Test
void deveLancarExcecaoAoCriarTarefaParaOutroUsuario() {
mockAuthentication("outroEstudante");
assertThrows(ResponseStatusException.class, () -> tarefaServico.criarTarefa(requisicaoTarefa));
verify(tarefaRepositorio, never()).save(any(Tarefa.class));
}
@Test
void deveListarTarefasPorEstudanteComSucesso() {
mockAuthentication(estudanteId);
when(tarefaRepositorio.findByEstudanteId(estudanteId)).thenReturn(List.of(tarefa));
List<RespostaTarefaDTO> tarefas = tarefaServico.listarTarefasPorEstudante(estudanteId);
assertFalse(tarefas.isEmpty());
assertEquals(1, tarefas.size());
assertEquals("Estudar Spring", tarefas.get(0).titulo());
}
@Test
void deveMudarStatusTarefaParaConcluidoComSucesso() {
mockAuthentication(estudanteId);
when(tarefaRepositorio.findById(anyString())).thenReturn(Optional.of(tarefa));
when(tarefaRepositorio.save(any(Tarefa.class))).thenReturn(tarefa);
RespostaTarefaDTO resposta = tarefaServico.marcarConcluida("tarefa1");
assertNotNull(resposta);
verify(tarefaRepositorio, times(1)).save(any(Tarefa.class));
}
@Test
void deveLancarExcecaoQuandoTarefaNaoEncontrada() {
when(tarefaRepositorio.findById(anyString())).thenReturn(Optional.empty());
assertThrows(ExcecaoRecursoNaoEncontrado.class, () -> tarefaServico.buscarTarefaPorId("tarefaNaoExistente"));
}
}