Novo repositorio do projeto
This commit is contained in:
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal 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
141
pom.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
@@ -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
|
||||
) {}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
9
src/main/java/com/agendaestudantil/dto/RespostaApi.java
Normal file
9
src/main/java/com/agendaestudantil/dto/RespostaApi.java
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.agendaestudantil.dto;
|
||||
|
||||
public record RespostaDisciplinaDTO(
|
||||
String id,
|
||||
String estudanteId,
|
||||
String nome,
|
||||
String professor,
|
||||
String sala,
|
||||
String cor
|
||||
) {}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.agendaestudantil.dto;
|
||||
|
||||
public record RespostaEstudanteDTO(
|
||||
String id,
|
||||
String nome,
|
||||
String email,
|
||||
String curso,
|
||||
Integer periodo
|
||||
) {
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.agendaestudantil.dto;
|
||||
|
||||
public record RespostaLoginDTO(String token, RespostaEstudanteDTO estudante) {
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
28
src/main/java/com/agendaestudantil/entidade/Disciplina.java
Normal file
28
src/main/java/com/agendaestudantil/entidade/Disciplina.java
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
33
src/main/java/com/agendaestudantil/entidade/Estudante.java
Normal file
33
src/main/java/com/agendaestudantil/entidade/Estudante.java
Normal 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;
|
||||
}
|
||||
36
src/main/java/com/agendaestudantil/entidade/Evento.java
Normal file
36
src/main/java/com/agendaestudantil/entidade/Evento.java
Normal 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
|
||||
}
|
||||
}
|
||||
44
src/main/java/com/agendaestudantil/entidade/Tarefa.java
Normal file
44
src/main/java/com/agendaestudantil/entidade/Tarefa.java
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.agendaestudantil.excecao;
|
||||
|
||||
public class ExcecaoNegocio extends RuntimeException {
|
||||
public ExcecaoNegocio(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.agendaestudantil.excecao;
|
||||
|
||||
public class ExcecaoRecursoNaoEncontrado extends RuntimeException {
|
||||
public ExcecaoRecursoNaoEncontrado(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
62
src/main/java/com/agendaestudantil/filtro/FiltroJwt.java
Normal file
62
src/main/java/com/agendaestudantil/filtro/FiltroJwt.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
111
src/main/java/com/agendaestudantil/servico/EventoServico.java
Normal file
111
src/main/java/com/agendaestudantil/servico/EventoServico.java
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
139
src/main/java/com/agendaestudantil/servico/TarefaServico.java
Normal file
139
src/main/java/com/agendaestudantil/servico/TarefaServico.java
Normal 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());
|
||||
}
|
||||
}
|
||||
56
src/main/java/com/agendaestudantil/utilitario/UtilJwt.java
Normal file
56
src/main/java/com/agendaestudantil/utilitario/UtilJwt.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/main/resources/application-dev.properties
Normal file
6
src/main/resources/application-dev.properties
Normal 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
|
||||
8
src/main/resources/application-prod.properties
Normal file
8
src/main/resources/application-prod.properties
Normal 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
|
||||
13
src/main/resources/application.properties
Normal file
13
src/main/resources/application.properties
Normal 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}
|
||||
96
src/main/resources/static/cadastro.css
Normal file
96
src/main/resources/static/cadastro.css
Normal 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;
|
||||
}
|
||||
151
src/main/resources/static/cadastro.html
Normal file
151
src/main/resources/static/cadastro.html
Normal 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">1º</option>
|
||||
<option value="2">2º</option>
|
||||
<option value="3">3º</option>
|
||||
<option value="4">4º</option>
|
||||
<option value="5">5º</option>
|
||||
<option value="6">6º</option>
|
||||
<option value="7">7º</option>
|
||||
<option value="8">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>
|
||||
300
src/main/resources/static/calendario.css
Normal file
300
src/main/resources/static/calendario.css
Normal 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; }
|
||||
}
|
||||
957
src/main/resources/static/calendario.html
Normal file
957
src/main/resources/static/calendario.html
Normal 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">‹</button>
|
||||
<button class="prox" aria-label="Próximo mês">›</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">‹</button>
|
||||
<span class="titulo-mes">Janeiro, 2025</span>
|
||||
<button class="seta proxGrande" aria-label="Próximo">›</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>
|
||||
BIN
src/main/resources/static/imagens/engrenagem.png
Normal file
BIN
src/main/resources/static/imagens/engrenagem.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
src/main/resources/static/imagens/icone.png
Normal file
BIN
src/main/resources/static/imagens/icone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
BIN
src/main/resources/static/imagens/sino.png
Normal file
BIN
src/main/resources/static/imagens/sino.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
11
src/main/resources/static/index.html
Normal file
11
src/main/resources/static/index.html
Normal 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>
|
||||
83
src/main/resources/static/login.css
Normal file
83
src/main/resources/static/login.css
Normal 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;
|
||||
}
|
||||
97
src/main/resources/static/login.html
Normal file
97
src/main/resources/static/login.html
Normal 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>
|
||||
124
src/main/resources/static/style.css
Normal file
124
src/main/resources/static/style.css
Normal 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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user