feat: adiciona pagina de configuracoes, tema claro/escuro e corrige loop de login
This commit is contained in:
+37
@@ -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
|
||||||
@@ -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", "/calendario.html", "/configuracoes.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,23 @@
|
|||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/config")
|
||||||
|
public String config() {
|
||||||
|
return "forward:/configuracoes.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,71 @@
|
|||||||
|
package com.agendaestudantil.controlador;
|
||||||
|
|
||||||
|
import com.agendaestudantil.dto.RespostaApi;
|
||||||
|
import com.agendaestudantil.dto.RequisicaoAtualizacaoEstudanteDTO;
|
||||||
|
import com.agendaestudantil.dto.RequisicaoCadastroDTO;
|
||||||
|
import com.agendaestudantil.dto.RespostaEstudanteDTO;
|
||||||
|
import com.agendaestudantil.dto.RequisicaoLoginDTO;
|
||||||
|
import com.agendaestudantil.dto.RespostaLoginDTO;
|
||||||
|
import com.agendaestudantil.dto.RequisicaoTrocaSenhaDTO;
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atualiza dados do usuário autenticado
|
||||||
|
@PutMapping("/me")
|
||||||
|
public ResponseEntity<RespostaApi<RespostaEstudanteDTO>> atualizar(
|
||||||
|
@AuthenticationPrincipal UserDetails userDetails,
|
||||||
|
@Valid @RequestBody RequisicaoAtualizacaoEstudanteDTO dto) {
|
||||||
|
RespostaEstudanteDTO resposta = estudanteServico.atualizar(userDetails.getUsername(), dto);
|
||||||
|
return ResponseEntity.ok(RespostaApi.sucesso(resposta));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Altera senha do usuário autenticado
|
||||||
|
@PutMapping("/senha")
|
||||||
|
public ResponseEntity<RespostaApi<Void>> trocarSenha(
|
||||||
|
@AuthenticationPrincipal UserDetails userDetails,
|
||||||
|
@Valid @RequestBody RequisicaoTrocaSenhaDTO dto) {
|
||||||
|
estudanteServico.trocarSenha(userDetails.getUsername(), dto);
|
||||||
|
return ResponseEntity.ok(RespostaApi.sucesso(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Excluir conta do usuário autenticado
|
||||||
|
@DeleteMapping("/me")
|
||||||
|
public ResponseEntity<RespostaApi<Void>> excluirConta(@AuthenticationPrincipal UserDetails userDetails) {
|
||||||
|
estudanteServico.excluirConta(userDetails.getUsername());
|
||||||
|
return ResponseEntity.ok(RespostaApi.sucesso(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,12 @@
|
|||||||
|
package com.agendaestudantil.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
|
||||||
|
public record RequisicaoAtualizacaoEstudanteDTO(
|
||||||
|
@NotBlank String nome,
|
||||||
|
@NotBlank String curso,
|
||||||
|
@NotNull @Min(1) Integer periodo
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.agendaestudantil.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
public record RequisicaoTrocaSenhaDTO(
|
||||||
|
@NotBlank String senhaAtual,
|
||||||
|
@NotBlank @Size(min = 6) String novaSenha
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
) {}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,123 @@
|
|||||||
|
package com.agendaestudantil.servico;
|
||||||
|
|
||||||
|
import com.agendaestudantil.dto.RequisicaoAtualizacaoEstudanteDTO;
|
||||||
|
import com.agendaestudantil.dto.RequisicaoCadastroDTO;
|
||||||
|
import com.agendaestudantil.dto.RespostaEstudanteDTO;
|
||||||
|
import com.agendaestudantil.dto.RequisicaoLoginDTO;
|
||||||
|
import com.agendaestudantil.dto.RespostaLoginDTO;
|
||||||
|
import com.agendaestudantil.dto.RequisicaoTrocaSenhaDTO;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RespostaEstudanteDTO atualizar(String id, RequisicaoAtualizacaoEstudanteDTO dto) {
|
||||||
|
Estudante estudante = estudanteRepositorio.findById(id)
|
||||||
|
.orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Estudante não encontrado"));
|
||||||
|
|
||||||
|
estudante.setNome(dto.nome());
|
||||||
|
estudante.setCurso(dto.curso());
|
||||||
|
estudante.setPeriodo(dto.periodo());
|
||||||
|
|
||||||
|
Estudante atualizado = estudanteRepositorio.save(estudante);
|
||||||
|
return toResponse(atualizado);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void trocarSenha(String id, RequisicaoTrocaSenhaDTO dto) {
|
||||||
|
Estudante estudante = estudanteRepositorio.findById(id)
|
||||||
|
.orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Estudante não encontrado"));
|
||||||
|
|
||||||
|
if (!passwordEncoder.matches(dto.senhaAtual(), estudante.getSenha())) {
|
||||||
|
throw new ExcecaoNegocio("Senha atual incorreta");
|
||||||
|
}
|
||||||
|
|
||||||
|
estudante.setSenha(passwordEncoder.encode(dto.novaSenha()));
|
||||||
|
estudanteRepositorio.save(estudante);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void excluirConta(String id) {
|
||||||
|
if (!estudanteRepositorio.existsById(id)) {
|
||||||
|
throw new ExcecaoRecursoNaoEncontrado("Estudante não encontrado");
|
||||||
|
}
|
||||||
|
estudanteRepositorio.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RespostaEstudanteDTO toResponse(Estudante estudante) {
|
||||||
|
return new RespostaEstudanteDTO(
|
||||||
|
estudante.getId(),
|
||||||
|
estudante.getNome(),
|
||||||
|
estudante.getEmail(),
|
||||||
|
estudante.getCurso(),
|
||||||
|
estudante.getPeriodo());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
* { 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Botão tema */
|
||||||
|
.theme-toggle-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border: 1px solid rgba(255,255,255,0.3);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s, transform 0.2s;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.theme-toggle-btn:hover { background: rgba(255,255,255,0.3); transform: translateY(-50%) scale(1.1); }
|
||||||
|
.theme-icon { width: 100%; height: 100%; object-fit: contain; }
|
||||||
|
|
||||||
|
/* ===== TEMA ESCURO ===== */
|
||||||
|
[data-theme="dark"] body {
|
||||||
|
background: #121212;
|
||||||
|
color: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .mens { color: #e8e8e8; }
|
||||||
|
[data-theme="dark"] label { color: #e8e8e8; }
|
||||||
|
|
||||||
|
[data-theme="dark"] input,
|
||||||
|
[data-theme="dark"] select {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #333;
|
||||||
|
color: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] #mensagem-erro {
|
||||||
|
background: rgba(254,226,226,0.1);
|
||||||
|
border-color: rgba(252,165,165,0.2);
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] #mensagem-sucesso {
|
||||||
|
background: rgba(209,250,229,0.1);
|
||||||
|
border-color: rgba(110,231,183,0.2);
|
||||||
|
color: #6ee7b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] a { color: #e8e8e8; }
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
<!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>
|
||||||
|
<script src="theme.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="topo">
|
||||||
|
<h1 id="textotop">Focus Agenda</h1>
|
||||||
|
<button class="theme-toggle-btn" onclick="toggleTheme()" title="Tema Escuro"><img src="imagens/moon-svgrepo-com.svg" class="theme-icon" alt="Tema"></button>
|
||||||
|
</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>
|
||||||
@@ -0,0 +1,446 @@
|
|||||||
|
* { 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; }
|
||||||
|
|
||||||
|
/* Botão configurações */
|
||||||
|
#btnConfig {
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#btnConfig:hover { opacity: 1; transform: scale(1.15); }
|
||||||
|
.config-icon { width: 22px; height: 22px; object-fit: contain; }
|
||||||
|
|
||||||
|
/* Botão tema */
|
||||||
|
.theme-toggle-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.theme-toggle-btn:hover { background: #f3f4f6; transform: scale(1.1); }
|
||||||
|
.theme-icon { width: 100%; height: 100%; object-fit: contain; }
|
||||||
|
|
||||||
|
/* 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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== TEMA ESCURO ===== */
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg-primary: #121212;
|
||||||
|
--bg-secondary: #1e1e1e;
|
||||||
|
--bg-card: #2a2a2a;
|
||||||
|
--bg-input: #1a1a1a;
|
||||||
|
--text-primary: #e8e8e8;
|
||||||
|
--text-secondary: #a0a0a0;
|
||||||
|
--text-muted: #666;
|
||||||
|
--border-color: #333;
|
||||||
|
--border-light: #282828;
|
||||||
|
--shadow: 0 1px 4px rgba(0,0,0,0.5);
|
||||||
|
--hover-bg: rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] body {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .topbar h1 { color: var(--text-primary); }
|
||||||
|
[data-theme="dark"] .nome { color: var(--text-primary); }
|
||||||
|
[data-theme="dark"] .cargo { color: var(--text-secondary); }
|
||||||
|
|
||||||
|
[data-theme="dark"] #btnLogout {
|
||||||
|
border-color: var(--border-color);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] #btnLogout:hover { background: rgba(192,57,43,0.2); border-color: #c0392b; color: #e74c3c; }
|
||||||
|
|
||||||
|
[data-theme="dark"] .calendar-header .seta {
|
||||||
|
border-color: var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .calendar-header .seta:hover { background: var(--hover-bg); }
|
||||||
|
[data-theme="dark"] .titulo-mes { color: var(--text-primary); }
|
||||||
|
|
||||||
|
[data-theme="dark"] .view-switch { background: var(--bg-secondary); }
|
||||||
|
[data-theme="dark"] .view-switch button { color: var(--text-secondary); }
|
||||||
|
[data-theme="dark"] .view-switch button.active {
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text-primary);
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .calendar-area {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .dia-semana {
|
||||||
|
background: var(--bg-card);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-bottom-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .dia-box {
|
||||||
|
border-right-color: var(--border-light);
|
||||||
|
border-bottom-color: var(--border-light);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .dia-box:hover { background: rgba(192,57,43,0.1); }
|
||||||
|
[data-theme="dark"] .dia-box.outro-mes { background: var(--bg-primary); color: #444; }
|
||||||
|
[data-theme="dark"] .dia-box.outro-mes .num-dia { color: #444; }
|
||||||
|
[data-theme="dark"] .num-dia { color: var(--text-primary); }
|
||||||
|
[data-theme="dark"] .dia-box.selecionado { background: rgba(192,57,43,0.15); }
|
||||||
|
|
||||||
|
[data-theme="dark"] .week-col { border-right-color: var(--border-color); }
|
||||||
|
[data-theme="dark"] .week-col.today { background: rgba(192,57,43,0.08); }
|
||||||
|
[data-theme="dark"] .week-col-head { color: var(--text-secondary); }
|
||||||
|
[data-theme="dark"] .week-col-date { color: var(--text-muted); }
|
||||||
|
[data-theme="dark"] .week-empty { color: #444; }
|
||||||
|
|
||||||
|
[data-theme="dark"] .day-panel-header { color: var(--text-primary); }
|
||||||
|
[data-theme="dark"] .day-empty { color: var(--text-muted); }
|
||||||
|
|
||||||
|
[data-theme="dark"] .calendar-event { background: rgba(192,57,43,0.2); }
|
||||||
|
[data-theme="dark"] .calendar-event.verde { background: rgba(22,163,74,0.2); }
|
||||||
|
[data-theme="dark"] .calendar-event.azul { background: rgba(29,78,216,0.2); }
|
||||||
|
[data-theme="dark"] .calendar-event.amarelo { background: rgba(217,119,6,0.2); }
|
||||||
|
[data-theme="dark"] .calendar-event.rosa { background: rgba(219,39,119,0.2); }
|
||||||
|
[data-theme="dark"] .calendar-event-hora { color: var(--text-secondary); }
|
||||||
|
[data-theme="dark"] .calendar-event-titulo { color: var(--text-primary); }
|
||||||
|
|
||||||
|
[data-theme="dark"] .loading { color: var(--text-muted); }
|
||||||
|
[data-theme="dark"] .spinner { border-color: var(--border-color); border-top-color: #c0392b; }
|
||||||
|
|
||||||
|
[data-theme="dark"] .modal {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .modal-titulo { color: var(--text-primary); }
|
||||||
|
[data-theme="dark"] .modal-campo label { color: var(--text-primary); }
|
||||||
|
[data-theme="dark"] .modal-campo input,
|
||||||
|
[data-theme="dark"] .modal-campo select,
|
||||||
|
[data-theme="dark"] .modal-campo textarea {
|
||||||
|
background: var(--bg-input);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .btn-secundario {
|
||||||
|
border-color: var(--border-color);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .btn-secundario:hover { background: var(--hover-bg); }
|
||||||
|
|
||||||
|
[data-theme="dark"] .btn-perigo {
|
||||||
|
border-color: rgba(252,165,165,0.3);
|
||||||
|
color: #e74c3c;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .btn-perigo:hover { background: rgba(192,57,43,0.2); }
|
||||||
|
|
||||||
|
[data-theme="dark"] #toast { background: #2a2a2a; }
|
||||||
|
|
||||||
|
[data-theme="dark"] .mais-eventos { color: var(--text-secondary); }
|
||||||
@@ -0,0 +1,960 @@
|
|||||||
|
<!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">
|
||||||
|
<script src="theme.js"></script>
|
||||||
|
</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 class="theme-toggle-btn" onclick="toggleTheme()" title="Tema Escuro"><img src="imagens/moon-svgrepo-com.svg" class="theme-icon" alt="Tema"></button>
|
||||||
|
<a href="configuracoes.html" id="btnConfig" title="Configurações"><img src="imagens/engrenagem.png" class="config-icon" alt="Configurações"></a>
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
* { 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#voltar {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border: 1px solid rgba(255,255,255,0.3);
|
||||||
|
color: #fff;
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
#voltar:hover { background: rgba(255,255,255,0.3); }
|
||||||
|
|
||||||
|
.theme-toggle-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 140px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border: 1px solid rgba(255,255,255,0.3);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s, transform 0.2s;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle-btn:hover { background: rgba(255,255,255,0.3); transform: translateY(-50%) scale(1.1); }
|
||||||
|
.theme-icon { width: 100%; height: 100%; object-fit: contain; }
|
||||||
|
|
||||||
|
#log {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 440px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titulo-pagina { text-align: center; color: #1f2937; }
|
||||||
|
.subtitulo { text-align: center; color: #6b7280; font-weight: 400; font-size: 15px; }
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:disabled {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
.secao {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secao-titulo {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btnSenha {
|
||||||
|
align-self: center;
|
||||||
|
width: 60%;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #114455;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btnSenha:hover { background-color: #0d3644; }
|
||||||
|
#btnSenha:disabled { background-color: #ccc; cursor: not-allowed; }
|
||||||
|
|
||||||
|
.zona-perigo {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #fca5a5;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zona-perigo .secao-titulo {
|
||||||
|
color: #b91c1c;
|
||||||
|
border-bottom-color: #fee2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zona-perigo p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6b7280;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btnExcluirConta {
|
||||||
|
align-self: center;
|
||||||
|
width: 60%;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #b91c1c;
|
||||||
|
border: 2px solid #b91c1c;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btnExcluirConta:hover { background-color: #fee2e2; }
|
||||||
|
#btnExcluirConta:disabled { background-color: #ccc; border-color: #ccc; color: #fff; cursor: not-allowed; }
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== TEMA ESCURO ===== */
|
||||||
|
[data-theme="dark"] body {
|
||||||
|
background: #121212;
|
||||||
|
color: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .titulo-pagina { color: #e8e8e8; }
|
||||||
|
[data-theme="dark"] .subtitulo { color: #a0a0a0; }
|
||||||
|
|
||||||
|
[data-theme="dark"] .secao {
|
||||||
|
background: #1e1e1e;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .secao-titulo {
|
||||||
|
color: #e8e8e8;
|
||||||
|
border-bottom-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] label { color: #e8e8e8; }
|
||||||
|
|
||||||
|
[data-theme="dark"] input,
|
||||||
|
[data-theme="dark"] select {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #333;
|
||||||
|
color: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] input:disabled {
|
||||||
|
background: #161616;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .zona-perigo {
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-color: rgba(252,165,165,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .zona-perigo p { color: #a0a0a0; }
|
||||||
|
|
||||||
|
[data-theme="dark"] #mensagem-erro {
|
||||||
|
background: rgba(254,226,226,0.1);
|
||||||
|
border-color: rgba(252,165,165,0.2);
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] #mensagem-sucesso {
|
||||||
|
background: rgba(209,250,229,0.1);
|
||||||
|
border-color: rgba(110,231,183,0.2);
|
||||||
|
color: #6ee7b7;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] a { color: #e8e8e8; }
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
<!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="configuracoes.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<title>Configurações – Focus Agenda</title>
|
||||||
|
<script src="theme.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="topo">
|
||||||
|
<h1 id="textotop">Focus Agenda</h1>
|
||||||
|
<button class="theme-toggle-btn" onclick="toggleTheme()" title="Tema Escuro"><img src="imagens/moon-svgrepo-com.svg" class="theme-icon" alt="Tema"></button>
|
||||||
|
<button id="voltar" onclick="window.location.href='calendario.html'">Voltar</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="log">
|
||||||
|
<h1 class="titulo-pagina">Configurações</h1>
|
||||||
|
<p class="subtitulo">Gerencie seus dados e preferências</p>
|
||||||
|
|
||||||
|
<div id="mensagem-erro" role="alert"></div>
|
||||||
|
<div id="mensagem-sucesso" role="status"></div>
|
||||||
|
|
||||||
|
<!-- DADOS PESSOAIS -->
|
||||||
|
<div class="secao">
|
||||||
|
<div class="secao-titulo">Dados Pessoais</div>
|
||||||
|
<form id="dadosForm" novalidate>
|
||||||
|
<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="emailid">Email</label>
|
||||||
|
<input type="email" placeholder="Seu email" id="emailid" autocomplete="email" required disabled>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<button type="submit" id="logbtn">Salvar Alterações</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ALTERAR SENHA -->
|
||||||
|
<div class="secao">
|
||||||
|
<div class="secao-titulo">Alterar Senha</div>
|
||||||
|
<form id="senhaForm" novalidate>
|
||||||
|
<div class="campo">
|
||||||
|
<label for="senhaAtualid">Senha Atual</label>
|
||||||
|
<input type="password" placeholder="Digite sua senha atual" id="senhaAtualid" autocomplete="current-password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="campo">
|
||||||
|
<label for="novaSenhaid">Nova Senha</label>
|
||||||
|
<input type="password" placeholder="Mínimo 6 caracteres" id="novaSenhaid" autocomplete="new-password" required minlength="6">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="campo">
|
||||||
|
<label for="confirmaNovaid">Confirmar Nova Senha</label>
|
||||||
|
<input type="password" placeholder="Confirme a nova senha" id="confirmaNovaid" autocomplete="new-password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" id="btnSenha">Alterar Senha</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ZONA DE PERIGO -->
|
||||||
|
<div class="zona-perigo">
|
||||||
|
<div class="secao-titulo">Zona de Perigo</div>
|
||||||
|
<p>Após excluir sua conta, todos os seus dados, tarefas, eventos e disciplinas serão removidos permanentemente. Esta ação não pode ser desfeita.</p>
|
||||||
|
<button type="button" id="btnExcluirConta">Excluir Minha Conta</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
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) {}
|
||||||
|
|
||||||
|
const erroEl = document.getElementById('mensagem-erro');
|
||||||
|
const sucessoEl = document.getElementById('mensagem-sucesso');
|
||||||
|
|
||||||
|
function mostrarErro(msg) {
|
||||||
|
erroEl.textContent = msg;
|
||||||
|
erroEl.style.display = 'block';
|
||||||
|
sucessoEl.style.display = 'none';
|
||||||
|
erroEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function mostrarSucesso(msg) {
|
||||||
|
sucessoEl.textContent = msg;
|
||||||
|
sucessoEl.style.display = 'block';
|
||||||
|
erroEl.style.display = 'none';
|
||||||
|
sucessoEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
const json = await res.json().catch(() => ({}));
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(json.message || 'Erro na requisição');
|
||||||
|
}
|
||||||
|
return json.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// CARREGAR 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));
|
||||||
|
|
||||||
|
document.getElementById('nomeid').value = u.nome || '';
|
||||||
|
document.getElementById('emailid').value = u.email || '';
|
||||||
|
document.getElementById('cursoid').value = u.curso || '';
|
||||||
|
document.getElementById('periodoid').value = u.periodo || '';
|
||||||
|
} catch(e) {
|
||||||
|
mostrarErro('Erro ao carregar dados: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// SALVAR DADOS PESSOAIS
|
||||||
|
// =============================================
|
||||||
|
document.getElementById('dadosForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
erroEl.style.display = 'none';
|
||||||
|
sucessoEl.style.display = 'none';
|
||||||
|
|
||||||
|
const nome = document.getElementById('nomeid').value.trim();
|
||||||
|
const curso = document.getElementById('cursoid').value.trim();
|
||||||
|
const periodo = parseInt(document.getElementById('periodoid').value, 10);
|
||||||
|
|
||||||
|
if (!nome || !curso || !periodo) {
|
||||||
|
mostrarErro('Preencha todos os campos.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById('logbtn');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Salvando...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const atualizado = await api('PUT', '/api/estudantes/me', { nome, curso, periodo });
|
||||||
|
localStorage.setItem('fa_user', JSON.stringify(atualizado));
|
||||||
|
mostrarSucesso('Dados atualizados com sucesso!');
|
||||||
|
} catch(err) {
|
||||||
|
mostrarErro(err.message);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Salvar Alterações';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// ALTERAR SENHA
|
||||||
|
// =============================================
|
||||||
|
document.getElementById('senhaForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
erroEl.style.display = 'none';
|
||||||
|
sucessoEl.style.display = 'none';
|
||||||
|
|
||||||
|
const senhaAtual = document.getElementById('senhaAtualid').value;
|
||||||
|
const novaSenha = document.getElementById('novaSenhaid').value;
|
||||||
|
const confirmaNova = document.getElementById('confirmaNovaid').value;
|
||||||
|
|
||||||
|
if (!senhaAtual || !novaSenha || !confirmaNova) {
|
||||||
|
mostrarErro('Preencha todos os campos.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (novaSenha.length < 6) {
|
||||||
|
mostrarErro('A nova senha deve ter pelo menos 6 caracteres.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (novaSenha !== confirmaNova) {
|
||||||
|
mostrarErro('As senhas não conferem.');
|
||||||
|
document.getElementById('confirmaNovaid').focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (senhaAtual === novaSenha) {
|
||||||
|
mostrarErro('A nova senha deve ser diferente da atual.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById('btnSenha');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Alterando...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api('PUT', '/api/estudantes/senha', { senhaAtual, novaSenha });
|
||||||
|
mostrarSucesso('Senha alterada com sucesso!');
|
||||||
|
document.getElementById('senhaAtualid').value = '';
|
||||||
|
document.getElementById('novaSenhaid').value = '';
|
||||||
|
document.getElementById('confirmaNovaid').value = '';
|
||||||
|
} catch(err) {
|
||||||
|
mostrarErro(err.message);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Alterar Senha';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// EXCLUIR CONTA
|
||||||
|
// =============================================
|
||||||
|
document.getElementById('btnExcluirConta').addEventListener('click', async () => {
|
||||||
|
if (!confirm('Tem certeza que deseja excluir sua conta? Esta ação é irreversível!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmacao = prompt('Digite "EXCLUIR" para confirmar a exclusão da sua conta:');
|
||||||
|
if (confirmacao !== 'EXCLUIR') {
|
||||||
|
mostrarErro('Exclusão cancelada.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = document.getElementById('btnExcluirConta');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Excluindo...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api('DELETE', '/api/estudantes/me');
|
||||||
|
localStorage.removeItem('fa_token');
|
||||||
|
localStorage.removeItem('fa_user');
|
||||||
|
mostrarSucesso('Conta excluída. Redirecionando...');
|
||||||
|
setTimeout(() => window.location.href = 'login.html', 2000);
|
||||||
|
} catch(err) {
|
||||||
|
mostrarErro(err.message);
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Excluir Minha Conta';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inicializa
|
||||||
|
carregarUsuario();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.32031 11.6835C3.32031 16.6541 7.34975 20.6835 12.3203 20.6835C16.1075 20.6835 19.3483 18.3443 20.6768 15.032C19.6402 15.4486 18.5059 15.6834 17.3203 15.6834C12.3497 15.6834 8.32031 11.654 8.32031 6.68342C8.32031 5.50338 8.55165 4.36259 8.96453 3.32996C5.65605 4.66028 3.32031 7.89912 3.32031 11.6835Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 624 B |
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 3V4M12 20V21M4 12H3M6.31412 6.31412L5.5 5.5M17.6859 6.31412L18.5 5.5M6.31412 17.69L5.5 18.5001M17.6859 17.69L18.5 18.5001M21 12H20M16 12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12C8 9.79086 9.79086 8 12 8C14.2091 8 16 9.79086 16 12Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 568 B |
@@ -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>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
* { 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Botão tema */
|
||||||
|
.theme-toggle-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border: 1px solid rgba(255,255,255,0.3);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s, transform 0.2s;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.theme-toggle-btn:hover { background: rgba(255,255,255,0.3); transform: translateY(-50%) scale(1.1); }
|
||||||
|
.theme-icon { width: 100%; height: 100%; object-fit: contain; }
|
||||||
|
|
||||||
|
/* ===== TEMA ESCURO ===== */
|
||||||
|
[data-theme="dark"] body {
|
||||||
|
background: #121212;
|
||||||
|
color: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .mens { color: #e8e8e8; }
|
||||||
|
[data-theme="dark"] label { color: #e8e8e8; }
|
||||||
|
|
||||||
|
[data-theme="dark"] input[type="email"],
|
||||||
|
[data-theme="dark"] input[type="password"] {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-color: #333;
|
||||||
|
color: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] #mensagem-erro {
|
||||||
|
background: rgba(254,226,226,0.1);
|
||||||
|
border-color: rgba(252,165,165,0.2);
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] a { color: #e8e8e8; }
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<!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>
|
||||||
|
<script src="theme.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="topo">
|
||||||
|
<h1 id="textotop">Focus Agenda</h1>
|
||||||
|
<button class="theme-toggle-btn" onclick="toggleTheme()" title="Tema Escuro"><img src="imagens/moon-svgrepo-com.svg" class="theme-icon" alt="Tema"></button>
|
||||||
|
</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>
|
||||||
@@ -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,36 @@
|
|||||||
|
(function() {
|
||||||
|
var saved = localStorage.getItem('fa_theme');
|
||||||
|
var theme = saved || 'light';
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
})();
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
var current = document.documentElement.getAttribute('data-theme');
|
||||||
|
var next = current === 'dark' ? 'light' : 'dark';
|
||||||
|
document.documentElement.setAttribute('data-theme', next);
|
||||||
|
localStorage.setItem('fa_theme', next);
|
||||||
|
updateThemeButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateThemeButtons() {
|
||||||
|
var theme = document.documentElement.getAttribute('data-theme');
|
||||||
|
var moonSrc = 'imagens/moon-svgrepo-com.svg';
|
||||||
|
var sunSrc = 'imagens/sun-svgrepo-com.svg';
|
||||||
|
|
||||||
|
var icons = document.querySelectorAll('.theme-icon');
|
||||||
|
for (var i = 0; i < icons.length; i++) {
|
||||||
|
icons[i].src = theme === 'dark' ? sunSrc : moonSrc;
|
||||||
|
icons[i].alt = theme === 'dark' ? 'Tema Claro' : 'Tema Escuro';
|
||||||
|
}
|
||||||
|
|
||||||
|
var btns = document.querySelectorAll('.theme-toggle-btn');
|
||||||
|
for (var j = 0; j < btns.length; j++) {
|
||||||
|
btns[j].title = theme === 'dark' ? 'Tema Claro' : 'Tema Escuro';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', updateThemeButtons);
|
||||||
|
} else {
|
||||||
|
updateThemeButtons();
|
||||||
|
}
|
||||||
@@ -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