Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f7a0f2c7a | |||
| ef20162351 |
+28
@@ -0,0 +1,28 @@
|
||||
FROM maven:3.9-eclipse-temurin-17 AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pom.xml .
|
||||
RUN mvn dependency:go-offline -B
|
||||
|
||||
COPY src ./src
|
||||
RUN mvn clean package -DskipTests
|
||||
|
||||
FROM eclipse-temurin:17-jre-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||
|
||||
COPY --from=build /app/target/*.jar app.jar
|
||||
|
||||
RUN chown appuser:appgroup app.jar
|
||||
|
||||
USER appuser
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENV SPRING_PROFILES_ACTIVE=prod
|
||||
ENV SERVER_PORT=8080
|
||||
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
@@ -0,0 +1,133 @@
|
||||
# Focus Agenda - Agenda Digital para Estudantes
|
||||
|
||||
CENTRO ESTADUAL DE EDUCACAO TECNOLOGICA "PAULA SOUZA"
|
||||
ETEC PEDRO D'ARCADIA NETO
|
||||
Tecnico em Desenvolvimento de Sistemas
|
||||
|
||||
## Autores
|
||||
|
||||
- BORGES, Gabriel H. M.
|
||||
- CRUZ, Fernando M. B. da
|
||||
- ARAUJO, Gustavo Ferreira
|
||||
- OLIVEIRA, Henry E. de
|
||||
- HABU, Nadia Sakae
|
||||
|
||||
##Descricao
|
||||
|
||||
Plataforma digital para organizacao de estudos destinada a alunos do ensino medio e tecnico. A ferramenta auxilia na gestao de rotinas academicas, enviando notificacoes sobre atividades diarias, horarios de estudo, datas de provas e outros compromissos academicos.
|
||||
|
||||
## Funcionalidades
|
||||
|
||||
- Cadastro e autenticacao de usuarios
|
||||
- Calendario mensal, semanal e diario
|
||||
- Criacao e gerenciamentos de eventos
|
||||
- Criacao e gerenciamento de tarefas com prioridades
|
||||
- Gerenciamento de disciplinas
|
||||
- Sistema de notificacoes
|
||||
- Tema claro e escuro
|
||||
- Painel informativo com feriados nacionais
|
||||
|
||||
## Tecnologias
|
||||
|
||||
### Frontend
|
||||
- HTML5
|
||||
- CSS3
|
||||
- JavaScript
|
||||
|
||||
### Backend
|
||||
- Java 17
|
||||
- Spring Boot 3.2.0
|
||||
- Spring Security
|
||||
- JWT (JSON Web Token)
|
||||
- MongoDB
|
||||
|
||||
## Requisitos
|
||||
|
||||
- Java 17 ou superior
|
||||
- Maven 3.8+
|
||||
- MongoDB
|
||||
|
||||
## Execucao
|
||||
|
||||
### Build do projeto
|
||||
|
||||
```bash
|
||||
mvn clean package
|
||||
```
|
||||
|
||||
### Execucao com Maven
|
||||
|
||||
```bash
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
### Execucao com JAR
|
||||
|
||||
```bash
|
||||
java -jar target/agenda-digital-estudantes-1.0.0.jar
|
||||
```
|
||||
|
||||
### Variaveis de ambiente
|
||||
|
||||
| Variavel | Descricao | Valor padrao |
|
||||
|---|---|---|
|
||||
| APP_NAME | Nome da aplicacao | Focus Agenda |
|
||||
| SERVER_PORT | Porta do servidor | 8080 |
|
||||
| SPRING_PROFILES_ACTIVE | Perfil ativo | dev |
|
||||
| MONGO_URI | URI de conexao com MongoDB | mongodb://localhost:27017/agenda_estudantil |
|
||||
| CORS_ORIGINS | Origens permitidas para CORS | http://localhost:8080,http://localhost:3000 |
|
||||
| JWT_SECRET | Chave secreta para JWT | (chave padrao) |
|
||||
| JWT_EXPIRATION | Expiracao do token em milissegundos | 86400000 |
|
||||
|
||||
## Docker
|
||||
|
||||
### Build da imagem
|
||||
|
||||
```bash
|
||||
docker build -t focus-agenda .
|
||||
```
|
||||
|
||||
### Execucao do container
|
||||
|
||||
```bash
|
||||
docker run -d -p 8080:8080 --name focus-agenda focus-agenda
|
||||
```
|
||||
|
||||
### Execucao com MongoDB
|
||||
|
||||
```bash
|
||||
docker run -d -p 8080:8080 -e MONGO_URI=mongodb://host.docker.internal:27017/agenda_estudantil --name focus-agenda focus-agenda
|
||||
```
|
||||
|
||||
## Acesso
|
||||
|
||||
Apos iniciar a aplicacao, acesse:
|
||||
|
||||
- Aplicacao: http://localhost:8080
|
||||
- Swagger UI: http://localhost:8080/swagger-ui.html
|
||||
- API Docs: http://localhost:8080/v3/api-docs
|
||||
|
||||
## Estrutura do Projeto
|
||||
|
||||
```
|
||||
src/
|
||||
├── main/
|
||||
│ ├── java/com/agendaestudantil/
|
||||
│ │ ├── configuracao/ # Configuracoes de seguranca e MongoDB
|
||||
│ │ ├── controlador/ # Controladores REST
|
||||
│ │ ├── dto/ # Objetos de transferencia de dados
|
||||
│ │ ├── entidade/ # Entidades do dominio
|
||||
│ │ ├── excecao/ # Excecoes personalizadas e manipulador global
|
||||
│ │ ├── filtro/ # Filtro JWT
|
||||
│ │ ├── repositorio/ # Interfaces de repositorio
|
||||
│ │ ├── seguranca/ # Autenticacao e detalhes do usuario
|
||||
│ │ ├── servico/ # Regras de negocio
|
||||
│ │ └── utilitario/ # Utilitarios (JWT)
|
||||
│ └── resources/
|
||||
│ ├── static/ # Frontend (HTML, CSS, JS)
|
||||
│ └── application*.properties
|
||||
```
|
||||
|
||||
## Licenca
|
||||
|
||||
Projeto academico desenvolvido para o Curso Tecnico em Desenvolvimento de Sistemas da ETEC Pedro D'Arcadia Neto.
|
||||
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
mongodb:
|
||||
image: mongo:7
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- mongo_data:/data/db
|
||||
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- mongodb
|
||||
environment:
|
||||
MONGO_URI: mongodb://mongodb:27017/agenda_estudantil
|
||||
JWT_SECRET: 4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b
|
||||
CORS_ORIGINS: http://localhost:8080
|
||||
SPRING_PROFILES_ACTIVE: prod
|
||||
|
||||
volumes:
|
||||
mongo_data:
|
||||
@@ -50,7 +50,7 @@
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.12.3</version>
|
||||
<version>0.12.6</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -63,7 +63,7 @@
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.12.3</version>
|
||||
<version>0.12.6</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@ package com.agendaestudantil;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class AgendaDigitalEstudantesApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.agendaestudantil.configuracao;
|
||||
|
||||
import com.agendaestudantil.dto.RespostaApi;
|
||||
import com.agendaestudantil.filtro.FiltroJwt;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
@@ -17,6 +21,8 @@ import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@@ -25,25 +31,43 @@ public class ConfiguracaoSeguranca {
|
||||
|
||||
private final FiltroJwt filtroJwt;
|
||||
private final String corsAllowedOrigins;
|
||||
private final String perfilAtivo;
|
||||
|
||||
public ConfiguracaoSeguranca(FiltroJwt filtroJwt, @Value("${cors.allowed.origins}") String corsAllowedOrigins) {
|
||||
public ConfiguracaoSeguranca(FiltroJwt filtroJwt,
|
||||
@Value("${cors.allowed.origins}") String corsAllowedOrigins,
|
||||
@Value("${spring.profiles.active:dev}") String perfilAtivo) {
|
||||
this.filtroJwt = filtroJwt;
|
||||
this.corsAllowedOrigins = corsAllowedOrigins;
|
||||
this.perfilAtivo = perfilAtivo;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
List<String> pathsPublicos = new ArrayList<>(List.of(
|
||||
"/", "/index.html", "/login.html", "/cadastro.html", "/calendario.html", "/configuracoes.html",
|
||||
"/politica-privacidade.html",
|
||||
"/favicon.ico", "/imagens/**",
|
||||
"/*.css", "/*.js", "/*.ico", "/*.png",
|
||||
"/api/estudantes/cadastro", "/api/estudantes/login"));
|
||||
|
||||
if ("dev".equals(perfilAtivo)) {
|
||||
pathsPublicos.add("/swagger-ui/**");
|
||||
pathsPublicos.add("/v3/api-docs/**");
|
||||
}
|
||||
|
||||
http.csrf(csrf -> csrf.disable())
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(
|
||||
"/", "/index.html", "/login.html", "/cadastro.html",
|
||||
"/favicon.ico", "/imagens/**",
|
||||
"/*.css", "/*.js", "/*.ico", "/*.png",
|
||||
"/api/estudantes/cadastro", "/api/estudantes/login",
|
||||
"/swagger-ui/**", "/v3/api-docs/**")
|
||||
.requestMatchers(pathsPublicos.toArray(new String[0]))
|
||||
.permitAll()
|
||||
.anyRequest().authenticated())
|
||||
.exceptionHandling(ex -> ex
|
||||
.authenticationEntryPoint((request, response, authException) -> {
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
RespostaApi<Void> body = new RespostaApi<>(null, "Acesso nao autorizado", LocalDateTime.now());
|
||||
new ObjectMapper().writeValue(response.getOutputStream(), body);
|
||||
}))
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.addFilterBefore(filtroJwt, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
@@ -15,4 +15,9 @@ public class ResourceController {
|
||||
public String app() {
|
||||
return "forward:/calendario.html";
|
||||
}
|
||||
|
||||
@GetMapping("/config")
|
||||
public String config() {
|
||||
return "forward:/configuracoes.html";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import com.agendaestudantil.servico.DisciplinaServico;
|
||||
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.*;
|
||||
|
||||
import java.util.List;
|
||||
@@ -24,20 +26,23 @@ public class DisciplinaControlador {
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<RespostaApi<RespostaDisciplinaDTO>> criarDisciplina(
|
||||
@Valid @RequestBody RequisicaoDisciplinaDTO dto) {
|
||||
@Valid @RequestBody RequisicaoDisciplinaDTO dto,
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
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());
|
||||
RespostaDisciplinaDTO resposta = disciplinaServico.criarDisciplina(disciplina, estudanteId);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(RespostaApi.sucesso(resposta));
|
||||
}
|
||||
|
||||
@GetMapping("/estudante/{estudanteId}")
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<RespostaApi<List<RespostaDisciplinaDTO>>> listarPorEstudante(
|
||||
@PathVariable String estudanteId) {
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
List<RespostaDisciplinaDTO> disciplinas = disciplinaServico.listarPorEstudante(estudanteId);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(disciplinas));
|
||||
}
|
||||
@@ -45,7 +50,8 @@ public class DisciplinaControlador {
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<RespostaApi<RespostaDisciplinaDTO>> buscarPorId(
|
||||
@PathVariable String id,
|
||||
@RequestParam String estudanteId) {
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
RespostaDisciplinaDTO disciplina = disciplinaServico.buscarPorId(id, estudanteId);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(disciplina));
|
||||
}
|
||||
@@ -53,21 +59,24 @@ public class DisciplinaControlador {
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<RespostaApi<RespostaDisciplinaDTO>> atualizarDisciplina(
|
||||
@PathVariable String id,
|
||||
@Valid @RequestBody RequisicaoDisciplinaDTO dto) {
|
||||
@Valid @RequestBody RequisicaoDisciplinaDTO dto,
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
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());
|
||||
RespostaDisciplinaDTO resposta = disciplinaServico.atualizarDisciplina(id, disciplina, estudanteId);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(resposta));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<RespostaApi<Void>> excluirDisciplina(
|
||||
@PathVariable String id,
|
||||
@RequestParam String estudanteId) {
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
disciplinaServico.excluirDisciplina(id, estudanteId);
|
||||
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(RespostaApi.sucesso(null));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.agendaestudantil.controlador;
|
||||
|
||||
import com.agendaestudantil.dto.RespostaApi;
|
||||
import com.agendaestudantil.dto.RequisicaoAtualizacaoEstudanteDTO;
|
||||
import com.agendaestudantil.dto.RequisicaoCadastroDTO;
|
||||
import com.agendaestudantil.dto.RespostaDadosCompletoDTO;
|
||||
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;
|
||||
@@ -35,10 +38,38 @@ public class EstudanteControlador {
|
||||
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));
|
||||
}
|
||||
|
||||
@GetMapping("/me/dados")
|
||||
public ResponseEntity<RespostaApi<RespostaDadosCompletoDTO>> exportarDados(
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
RespostaDadosCompletoDTO dados = estudanteServico.exportarDados(userDetails.getUsername());
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(dados));
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
@DeleteMapping("/me")
|
||||
public ResponseEntity<RespostaApi<Void>> excluirConta(@AuthenticationPrincipal UserDetails userDetails) {
|
||||
estudanteServico.excluirConta(userDetails.getUsername());
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import jakarta.validation.Valid;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
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.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@@ -26,7 +28,9 @@ public class EventoControlador {
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<RespostaApi<RespostaEventoDTO>> criarEvento(
|
||||
@Valid @RequestBody RequisicaoEventoDTO dto) {
|
||||
@Valid @RequestBody RequisicaoEventoDTO dto,
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
Evento evento = new Evento();
|
||||
evento.setTitulo(dto.titulo());
|
||||
evento.setDescricao(dto.descricao());
|
||||
@@ -35,38 +39,39 @@ public class EventoControlador {
|
||||
evento.setDataHora(dto.dataHora());
|
||||
evento.setDisciplinaId(dto.disciplinaId());
|
||||
|
||||
RespostaEventoDTO resposta = eventoServico.criarEvento(evento, dto.estudanteId());
|
||||
RespostaEventoDTO resposta = eventoServico.criarEvento(evento, estudanteId);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(RespostaApi.sucesso(resposta));
|
||||
}
|
||||
|
||||
@GetMapping("/estudante/{estudanteId}")
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<RespostaApi<List<RespostaEventoDTO>>> listarPorEstudante(
|
||||
@PathVariable String estudanteId) {
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
List<RespostaEventoDTO> eventos = eventoServico.listarPorEstudante(estudanteId);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(eventos));
|
||||
}
|
||||
|
||||
@GetMapping("/estudante/{estudanteId}/periodo")
|
||||
@GetMapping("/me/periodo")
|
||||
public ResponseEntity<RespostaApi<List<RespostaEventoDTO>>> listarPorPeriodo(
|
||||
@PathVariable String estudanteId,
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime inicio,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime fim) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
List<RespostaEventoDTO> eventos = eventoServico.listarPorPeriodo(estudanteId, inicio, fim);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(eventos));
|
||||
}
|
||||
|
||||
@GetMapping("/estudante/{estudanteId}/proximos")
|
||||
@GetMapping("/me/proximos")
|
||||
public ResponseEntity<RespostaApi<List<RespostaEventoDTO>>> listarProximosEventos(
|
||||
@PathVariable String estudanteId) {
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
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);
|
||||
public ResponseEntity<RespostaApi<RespostaEventoDTO>> buscarPorId(@PathVariable String id) {
|
||||
RespostaEventoDTO evento = eventoServico.buscarPorId(id);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(evento));
|
||||
}
|
||||
|
||||
@@ -74,23 +79,19 @@ public class EventoControlador {
|
||||
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());
|
||||
RespostaEventoDTO resposta = eventoServico.atualizarEvento(id, dto);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(resposta));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<RespostaApi<Void>> excluirEvento(
|
||||
@PathVariable String id,
|
||||
@RequestParam String estudanteId) {
|
||||
eventoServico.excluirEvento(id, estudanteId);
|
||||
public ResponseEntity<RespostaApi<Void>> excluirEvento(@PathVariable String id) {
|
||||
eventoServico.excluirEvento(id);
|
||||
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(RespostaApi.sucesso(null));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}/cancelar")
|
||||
public ResponseEntity<RespostaApi<RespostaEventoDTO>> cancelarEvento(@PathVariable String id) {
|
||||
RespostaEventoDTO evento = eventoServico.cancelarEvento(id);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(evento));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.agendaestudantil.controlador;
|
||||
|
||||
import com.agendaestudantil.dto.RespostaApi;
|
||||
import com.agendaestudantil.dto.RespostaNotificacaoDTO;
|
||||
import com.agendaestudantil.servico.NotificacaoServico;
|
||||
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.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/notificacoes")
|
||||
public class NotificacaoControlador {
|
||||
|
||||
private final NotificacaoServico notificacaoServico;
|
||||
|
||||
public NotificacaoControlador(NotificacaoServico notificacaoServico) {
|
||||
this.notificacaoServico = notificacaoServico;
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<RespostaApi<List<RespostaNotificacaoDTO>>> listarTodas(
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
List<RespostaNotificacaoDTO> notificacoes = notificacaoServico.listarTodas(estudanteId);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(notificacoes));
|
||||
}
|
||||
|
||||
@GetMapping("/me/nao-lidas")
|
||||
public ResponseEntity<RespostaApi<List<RespostaNotificacaoDTO>>> listarNaoLidas(
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
List<RespostaNotificacaoDTO> notificacoes = notificacaoServico.listarNaoLidas(estudanteId);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(notificacoes));
|
||||
}
|
||||
|
||||
@GetMapping("/me/contagem")
|
||||
public ResponseEntity<RespostaApi<Map<String, Long>>> contarNaoLidas(
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
long total = notificacaoServico.contarNaoLidas(estudanteId);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(Map.of("total", total)));
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}/ler")
|
||||
public ResponseEntity<RespostaApi<RespostaNotificacaoDTO>> marcarComoLida(@PathVariable String id) {
|
||||
RespostaNotificacaoDTO notificacao = notificacaoServico.marcarComoLida(id);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(notificacao));
|
||||
}
|
||||
|
||||
@PatchMapping("/me/ler-todas")
|
||||
public ResponseEntity<RespostaApi<Void>> marcarTodasComoLidas(
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
notificacaoServico.marcarTodasComoLidas(estudanteId);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(null));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<RespostaApi<Void>> excluirNotificacao(@PathVariable String id) {
|
||||
notificacaoServico.excluirNotificacao(id);
|
||||
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(RespostaApi.sucesso(null));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import jakarta.validation.Valid;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
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.*;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
@@ -28,33 +30,37 @@ public class TarefaControlador {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(RespostaApi.sucesso(tarefa));
|
||||
}
|
||||
|
||||
@GetMapping("/estudante/{estudanteId}")
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<RespostaApi<List<RespostaTarefaDTO>>> listarTarefasPorEstudante(
|
||||
@PathVariable String estudanteId) {
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
List<RespostaTarefaDTO> tarefas = tarefaServico.listarTarefasPorEstudante(estudanteId);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(tarefas));
|
||||
}
|
||||
|
||||
@GetMapping("/estudante/{estudanteId}/pendentes")
|
||||
@GetMapping("/me/pendentes")
|
||||
public ResponseEntity<RespostaApi<List<RespostaTarefaDTO>>> listarTarefasPendentes(
|
||||
@PathVariable String estudanteId) {
|
||||
@AuthenticationPrincipal UserDetails userDetails) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
List<RespostaTarefaDTO> tarefas = tarefaServico.listarTarefasPendentes(estudanteId);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(tarefas));
|
||||
}
|
||||
|
||||
@GetMapping("/estudante/{estudanteId}/data")
|
||||
@GetMapping("/me/data")
|
||||
public ResponseEntity<RespostaApi<List<RespostaTarefaDTO>>> listarTarefasPorData(
|
||||
@PathVariable String estudanteId,
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate data) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
List<RespostaTarefaDTO> tarefas = tarefaServico.listarTarefasPorData(estudanteId, data);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(tarefas));
|
||||
}
|
||||
|
||||
@GetMapping("/estudante/{estudanteId}/periodo")
|
||||
@GetMapping("/me/periodo")
|
||||
public ResponseEntity<RespostaApi<List<RespostaTarefaDTO>>> listarTarefasPorPeriodo(
|
||||
@PathVariable String estudanteId,
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate inicio,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate fim) {
|
||||
String estudanteId = userDetails.getUsername();
|
||||
List<RespostaTarefaDTO> tarefas = tarefaServico.listarTarefasPorPeriodo(estudanteId, inicio, fim);
|
||||
return ResponseEntity.ok(RespostaApi.sucesso(tarefas));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -11,6 +11,7 @@ public record RequisicaoCadastroDTO(
|
||||
@Email @NotBlank String email,
|
||||
@NotBlank @Size(min = 6) String senha,
|
||||
@NotBlank String curso,
|
||||
@NotNull @Min(1) Integer periodo
|
||||
@NotNull @Min(1) Integer periodo,
|
||||
Boolean consentimentoLgpd
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -6,6 +6,5 @@ public record RequisicaoDisciplinaDTO(
|
||||
@NotBlank String nome,
|
||||
String professor,
|
||||
String sala,
|
||||
String cor,
|
||||
@NotBlank String estudanteId
|
||||
String cor
|
||||
) {}
|
||||
|
||||
@@ -12,6 +12,5 @@ public record RequisicaoEventoDTO(
|
||||
@NotNull Evento.TipoEvento tipo,
|
||||
String local,
|
||||
String disciplinaId,
|
||||
@NotNull LocalDateTime dataHora,
|
||||
@NotBlank String estudanteId
|
||||
@NotNull LocalDateTime dataHora
|
||||
) {}
|
||||
|
||||
@@ -13,7 +13,6 @@ public record RequisicaoTarefaDTO(
|
||||
@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
|
||||
String disciplinaId
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -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,12 @@
|
||||
package com.agendaestudantil.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record RespostaDadosCompletoDTO(
|
||||
RespostaEstudanteDTO estudante,
|
||||
List<RespostaTarefaDTO> tarefas,
|
||||
List<RespostaEventoDTO> eventos,
|
||||
List<RespostaDisciplinaDTO> disciplinas,
|
||||
LocalDateTime geradoEm
|
||||
) {}
|
||||
@@ -10,5 +10,7 @@ public record RespostaEventoDTO(
|
||||
String tipo,
|
||||
String local,
|
||||
String disciplinaId,
|
||||
LocalDateTime dataHora
|
||||
LocalDateTime dataHora,
|
||||
String status,
|
||||
String nomeDisciplina
|
||||
) {}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.agendaestudantil.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record RespostaNotificacaoDTO(
|
||||
String id,
|
||||
String titulo,
|
||||
String mensagem,
|
||||
String tipo,
|
||||
String referenciaId,
|
||||
String tipoReferencia,
|
||||
boolean lida,
|
||||
LocalDateTime dataGeracao
|
||||
) {}
|
||||
@@ -7,6 +7,7 @@ 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.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
@Data
|
||||
@@ -20,6 +21,7 @@ public class Disciplina extends EntidadeAuditoria {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
@Indexed
|
||||
private String estudanteId;
|
||||
private String nome;
|
||||
private String professor;
|
||||
|
||||
@@ -9,6 +9,8 @@ import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@@ -30,4 +32,10 @@ public class Estudante extends EntidadeAuditoria {
|
||||
private String curso;
|
||||
|
||||
private Integer periodo;
|
||||
|
||||
private Boolean consentimentoLgpd;
|
||||
|
||||
private LocalDateTime dataConsentimento;
|
||||
|
||||
private String versaoTermos;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@@ -22,6 +23,7 @@ public class Evento extends EntidadeAuditoria {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
@Indexed
|
||||
private String estudanteId;
|
||||
private String titulo;
|
||||
private String descricao;
|
||||
@@ -29,8 +31,13 @@ public class Evento extends EntidadeAuditoria {
|
||||
private String local;
|
||||
private String disciplinaId;
|
||||
private LocalDateTime dataHora;
|
||||
private StatusEvento status;
|
||||
|
||||
public enum TipoEvento {
|
||||
AULA, PROVA, TRABALHO, ESTUDO, EXAME, OUTRO
|
||||
}
|
||||
|
||||
public enum StatusEvento {
|
||||
ATIVO, CANCELADO
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Document(collection = "notificacoes")
|
||||
@CompoundIndex(name = "estudante_lida_idx", def = "{estudanteId: 1, lida: 1}")
|
||||
public class Notificacao extends EntidadeAuditoria {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
@Indexed
|
||||
private String estudanteId;
|
||||
private String titulo;
|
||||
private String mensagem;
|
||||
private TipoNotificacao tipo;
|
||||
private String referenciaId;
|
||||
private TipoReferencia tipoReferencia;
|
||||
private boolean lida;
|
||||
private LocalDateTime dataGeracao;
|
||||
|
||||
public enum TipoNotificacao {
|
||||
PRAZO_PROXIMO, TAREFA_ATRASADA, EVENTO_PROXIMO
|
||||
}
|
||||
|
||||
public enum TipoReferencia {
|
||||
TAREFA, EVENTO
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ 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.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
|
||||
import java.time.LocalDate;
|
||||
@@ -32,6 +33,7 @@ public class Tarefa extends EntidadeAuditoria {
|
||||
private StatusTarefa status;
|
||||
private LocalDate dataEntrega;
|
||||
private String disciplinaId;
|
||||
@Indexed
|
||||
private String estudanteId;
|
||||
|
||||
public enum Prioridade {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.agendaestudantil.excecao;
|
||||
|
||||
import com.agendaestudantil.dto.RespostaApi;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.FieldError;
|
||||
@@ -16,6 +18,8 @@ import java.util.Map;
|
||||
@RestControllerAdvice
|
||||
public class ManipuladorExcecaoGlobal {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ManipuladorExcecaoGlobal.class);
|
||||
|
||||
@ExceptionHandler(ExcecaoRecursoNaoEncontrado.class)
|
||||
public ResponseEntity<RespostaApi<Void>> handleResourceNotFound(ExcecaoRecursoNaoEncontrado ex) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
@@ -41,13 +45,18 @@ public class ManipuladorExcecaoGlobal {
|
||||
|
||||
@ExceptionHandler(ResponseStatusException.class)
|
||||
public ResponseEntity<RespostaApi<Void>> handleResponseStatusException(ResponseStatusException ex) {
|
||||
String reason = ex.getReason();
|
||||
if (reason == null || reason.isBlank()) {
|
||||
reason = "Acesso não autorizado";
|
||||
}
|
||||
return ResponseEntity.status(ex.getStatusCode())
|
||||
.body(new RespostaApi<>(null, ex.getReason(), LocalDateTime.now()));
|
||||
.body(new RespostaApi<>(null, reason, LocalDateTime.now()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<RespostaApi<Void>> handleGenericException(Exception ex) {
|
||||
log.error("Erro interno no servidor", ex);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(new RespostaApi<>(null, "Erro interno no servidor: " + ex.getMessage(), LocalDateTime.now()));
|
||||
.body(new RespostaApi<>(null, "Erro interno no servidor", LocalDateTime.now()));
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,14 @@ package com.agendaestudantil.repositorio;
|
||||
import com.agendaestudantil.entidade.Disciplina;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface DisciplinaRepositorio extends MongoRepository<Disciplina, String> {
|
||||
List<Disciplina> findByEstudanteId(String estudanteId);
|
||||
|
||||
List<Disciplina> findByIdIn(Collection<String> ids);
|
||||
|
||||
void deleteByEstudanteId(String estudanteId);
|
||||
}
|
||||
|
||||
@@ -19,4 +19,9 @@ public interface EventoRepositorio extends MongoRepository<Evento, String> {
|
||||
|
||||
@Query("{'estudanteId': ?0, 'dataHora': {$gte: ?1}}")
|
||||
List<Evento> findProximosEventosByEstudanteId(String estudanteId, LocalDateTime data);
|
||||
|
||||
@Query("{'dataHora': {$gte: ?0, $lte: ?1}}")
|
||||
List<Evento> findEventosNasProximas24h(LocalDateTime inicio, LocalDateTime fim);
|
||||
|
||||
void deleteByEstudanteId(String estudanteId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.agendaestudantil.repositorio;
|
||||
|
||||
import com.agendaestudantil.entidade.Notificacao;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface NotificacaoRepositorio extends MongoRepository<Notificacao, String> {
|
||||
|
||||
List<Notificacao> findByEstudanteIdAndLidaFalse(String estudanteId);
|
||||
|
||||
List<Notificacao> findByEstudanteId(String estudanteId);
|
||||
|
||||
long countByEstudanteIdAndLidaFalse(String estudanteId);
|
||||
|
||||
void deleteByEstudanteId(String estudanteId);
|
||||
|
||||
boolean existsByEstudanteIdAndReferenciaId(String estudanteId, String referenciaId);
|
||||
|
||||
boolean existsByEstudanteIdAndReferenciaIdAndTipo(String estudanteId, String referenciaId, Notificacao.TipoNotificacao tipo);
|
||||
|
||||
List<Notificacao> findByEstudanteIdAndReferenciaIdAndTipo(String estudanteId, String referenciaId, Notificacao.TipoNotificacao tipo);
|
||||
}
|
||||
@@ -24,4 +24,9 @@ public interface TarefaRepositorio extends MongoRepository<Tarefa, String> {
|
||||
|
||||
@Query("{'estudanteId': ?0, 'status': {$ne: ?1}}")
|
||||
List<Tarefa> findTarefasPendentesByEstudanteId(String estudanteId, Tarefa.StatusTarefa status);
|
||||
|
||||
@Query("{'status': {$ne: 'CONCLUIDA'}, 'dataEntrega': {$gte: ?0, $lte: ?1}}")
|
||||
List<Tarefa> findTarefasNaoConcluidasComDataEntre(LocalDate inicio, LocalDate fim);
|
||||
|
||||
void deleteByEstudanteId(String estudanteId);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.agendaestudantil.dto.RespostaDisciplinaDTO;
|
||||
import com.agendaestudantil.entidade.Disciplina;
|
||||
import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado;
|
||||
import com.agendaestudantil.repositorio.DisciplinaRepositorio;
|
||||
import com.agendaestudantil.repositorio.TarefaRepositorio;
|
||||
import com.agendaestudantil.repositorio.EventoRepositorio;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -19,9 +21,14 @@ public class DisciplinaServico {
|
||||
private static final Logger log = LoggerFactory.getLogger(DisciplinaServico.class);
|
||||
|
||||
private final DisciplinaRepositorio disciplinaRepositorio;
|
||||
private final TarefaRepositorio tarefaRepositorio;
|
||||
private final EventoRepositorio eventoRepositorio;
|
||||
|
||||
public DisciplinaServico(DisciplinaRepositorio disciplinaRepositorio) {
|
||||
public DisciplinaServico(DisciplinaRepositorio disciplinaRepositorio, TarefaRepositorio tarefaRepositorio,
|
||||
EventoRepositorio eventoRepositorio) {
|
||||
this.disciplinaRepositorio = disciplinaRepositorio;
|
||||
this.tarefaRepositorio = tarefaRepositorio;
|
||||
this.eventoRepositorio = eventoRepositorio;
|
||||
}
|
||||
|
||||
private void validarAcesso(String estudanteId) {
|
||||
@@ -71,6 +78,14 @@ public class DisciplinaServico {
|
||||
public void excluirDisciplina(String id, String estudanteId) {
|
||||
Disciplina disciplina = getDisciplinaEntity(id);
|
||||
validarAcesso(disciplina.getEstudanteId());
|
||||
tarefaRepositorio.findByDisciplinaId(id).forEach(tarefa -> {
|
||||
tarefa.setDisciplinaId(null);
|
||||
tarefaRepositorio.save(tarefa);
|
||||
});
|
||||
eventoRepositorio.findByDisciplinaId(id).forEach(evento -> {
|
||||
evento.setDisciplinaId(null);
|
||||
eventoRepositorio.save(evento);
|
||||
});
|
||||
disciplinaRepositorio.delete(disciplina);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
package com.agendaestudantil.servico;
|
||||
|
||||
import com.agendaestudantil.dto.RequisicaoAtualizacaoEstudanteDTO;
|
||||
import com.agendaestudantil.dto.RequisicaoCadastroDTO;
|
||||
import com.agendaestudantil.dto.RespostaDadosCompletoDTO;
|
||||
import com.agendaestudantil.dto.RespostaEstudanteDTO;
|
||||
import com.agendaestudantil.dto.RequisicaoLoginDTO;
|
||||
import com.agendaestudantil.dto.RespostaLoginDTO;
|
||||
import com.agendaestudantil.dto.RequisicaoTrocaSenhaDTO;
|
||||
import com.agendaestudantil.dto.RespostaDisciplinaDTO;
|
||||
import com.agendaestudantil.dto.RespostaEventoDTO;
|
||||
import com.agendaestudantil.dto.RespostaTarefaDTO;
|
||||
import com.agendaestudantil.entidade.Estudante;
|
||||
import com.agendaestudantil.excecao.ExcecaoNegocio;
|
||||
import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado;
|
||||
import com.agendaestudantil.repositorio.EstudanteRepositorio;
|
||||
import com.agendaestudantil.repositorio.EventoRepositorio;
|
||||
import com.agendaestudantil.repositorio.TarefaRepositorio;
|
||||
import com.agendaestudantil.repositorio.DisciplinaRepositorio;
|
||||
import com.agendaestudantil.repositorio.NotificacaoRepositorio;
|
||||
import com.agendaestudantil.utilitario.UtilJwt;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -16,6 +26,8 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@@ -26,20 +38,32 @@ public class EstudanteServico {
|
||||
private final EstudanteRepositorio estudanteRepositorio;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final UtilJwt utilJwt;
|
||||
private final TarefaRepositorio tarefaRepositorio;
|
||||
private final EventoRepositorio eventoRepositorio;
|
||||
private final DisciplinaRepositorio disciplinaRepositorio;
|
||||
private final NotificacaoRepositorio notificacaoRepositorio;
|
||||
|
||||
public EstudanteServico(EstudanteRepositorio estudanteRepositorio, PasswordEncoder passwordEncoder,
|
||||
UtilJwt utilJwt) {
|
||||
UtilJwt utilJwt, TarefaRepositorio tarefaRepositorio, EventoRepositorio eventoRepositorio,
|
||||
DisciplinaRepositorio disciplinaRepositorio, NotificacaoRepositorio notificacaoRepositorio) {
|
||||
this.estudanteRepositorio = estudanteRepositorio;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.utilJwt = utilJwt;
|
||||
this.tarefaRepositorio = tarefaRepositorio;
|
||||
this.eventoRepositorio = eventoRepositorio;
|
||||
this.disciplinaRepositorio = disciplinaRepositorio;
|
||||
this.notificacaoRepositorio = notificacaoRepositorio;
|
||||
}
|
||||
|
||||
public RespostaEstudanteDTO cadastrar(RequisicaoCadastroDTO dto) {
|
||||
log.debug("Acessando método cadastrar para email: {}", dto.email());
|
||||
log.debug("Acessando metodo cadastrar para email: {}", dto.email());
|
||||
if (!Boolean.TRUE.equals(dto.consentimentoLgpd())) {
|
||||
throw new ExcecaoNegocio("E necessario aceitar os termos para se cadastrar");
|
||||
}
|
||||
Optional<Estudante> existente = estudanteRepositorio.findByEmail(dto.email());
|
||||
if (existente.isPresent()) {
|
||||
log.error("Email já cadastrado: {}", dto.email());
|
||||
throw new ExcecaoNegocio("Email já cadastrado");
|
||||
log.error("Email ja cadastrado: {}", dto.email());
|
||||
throw new ExcecaoNegocio("Email ja cadastrado");
|
||||
}
|
||||
|
||||
Estudante estudante = new Estudante();
|
||||
@@ -48,13 +72,16 @@ public class EstudanteServico {
|
||||
estudante.setSenha(passwordEncoder.encode(dto.senha()));
|
||||
estudante.setCurso(dto.curso());
|
||||
estudante.setPeriodo(dto.periodo());
|
||||
estudante.setConsentimentoLgpd(dto.consentimentoLgpd());
|
||||
estudante.setDataConsentimento(LocalDateTime.now());
|
||||
estudante.setVersaoTermos("1.0");
|
||||
|
||||
Estudante salvo = estudanteRepositorio.save(estudante);
|
||||
return toResponse(salvo);
|
||||
}
|
||||
|
||||
public RespostaLoginDTO login(RequisicaoLoginDTO dto) {
|
||||
log.debug("Acessando método login para email: {}", dto.email());
|
||||
log.debug("Acessando metodo login para email: {}", dto.email());
|
||||
Optional<Estudante> estudanteParam = estudanteRepositorio.findByEmail(dto.email());
|
||||
|
||||
if (estudanteParam.isEmpty()) {
|
||||
@@ -75,10 +102,77 @@ public class EstudanteServico {
|
||||
|
||||
public RespostaEstudanteDTO buscarPorId(String id) {
|
||||
Estudante estudante = estudanteRepositorio.findById(id)
|
||||
.orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Estudante não encontrado"));
|
||||
.orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Estudante nao encontrado"));
|
||||
return toResponse(estudante);
|
||||
}
|
||||
|
||||
public RespostaEstudanteDTO atualizar(String id, RequisicaoAtualizacaoEstudanteDTO dto) {
|
||||
Estudante estudante = estudanteRepositorio.findById(id)
|
||||
.orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Estudante nao 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 nao 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 nao encontrado");
|
||||
}
|
||||
tarefaRepositorio.deleteByEstudanteId(id);
|
||||
eventoRepositorio.deleteByEstudanteId(id);
|
||||
disciplinaRepositorio.deleteByEstudanteId(id);
|
||||
notificacaoRepositorio.deleteByEstudanteId(id);
|
||||
estudanteRepositorio.deleteById(id);
|
||||
log.info("Conta e todos os dados do estudante {} removidos (LGPD Art. 18-VI)", id);
|
||||
}
|
||||
|
||||
public RespostaDadosCompletoDTO exportarDados(String estudanteId) {
|
||||
RespostaEstudanteDTO estudante = buscarPorId(estudanteId);
|
||||
|
||||
List<RespostaTarefaDTO> tarefas = tarefaRepositorio
|
||||
.findByEstudanteId(estudanteId).stream()
|
||||
.map(t -> new RespostaTarefaDTO(
|
||||
t.getId(), t.getTitulo(), t.getDescricao(),
|
||||
t.getPrioridade() != null ? t.getPrioridade().name() : null,
|
||||
t.getStatus() != null ? t.getStatus().name() : null,
|
||||
t.getDataEntrega(), t.getDisciplinaId(), t.getEstudanteId()))
|
||||
.toList();
|
||||
|
||||
List<RespostaEventoDTO> eventos = eventoRepositorio
|
||||
.findByEstudanteId(estudanteId).stream()
|
||||
.map(e -> new RespostaEventoDTO(
|
||||
e.getId(), e.getEstudanteId(), e.getTitulo(), e.getDescricao(),
|
||||
e.getTipo() != null ? e.getTipo().name() : null,
|
||||
e.getLocal(), e.getDisciplinaId(), e.getDataHora(),
|
||||
e.getStatus() != null ? e.getStatus().name() : null,
|
||||
null))
|
||||
.toList();
|
||||
|
||||
List<RespostaDisciplinaDTO> disciplinas = disciplinaRepositorio
|
||||
.findByEstudanteId(estudanteId).stream()
|
||||
.map(d -> new RespostaDisciplinaDTO(
|
||||
d.getId(), d.getEstudanteId(), d.getNome(), d.getProfessor(), d.getSala(), d.getCor()))
|
||||
.toList();
|
||||
|
||||
return new RespostaDadosCompletoDTO(estudante, tarefas, eventos, disciplinas, LocalDateTime.now());
|
||||
}
|
||||
|
||||
private RespostaEstudanteDTO toResponse(Estudante estudante) {
|
||||
return new RespostaEstudanteDTO(
|
||||
estudante.getId(),
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
package com.agendaestudantil.servico;
|
||||
|
||||
import com.agendaestudantil.dto.RequisicaoEventoDTO;
|
||||
import com.agendaestudantil.dto.RespostaEventoDTO;
|
||||
import com.agendaestudantil.entidade.Disciplina;
|
||||
import com.agendaestudantil.entidade.Evento;
|
||||
import com.agendaestudantil.excecao.ExcecaoNegocio;
|
||||
import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado;
|
||||
import com.agendaestudantil.repositorio.DisciplinaRepositorio;
|
||||
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;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class EventoServico {
|
||||
@@ -20,16 +24,29 @@ public class EventoServico {
|
||||
private static final Logger log = LoggerFactory.getLogger(EventoServico.class);
|
||||
|
||||
private final EventoRepositorio eventoRepositorio;
|
||||
private final DisciplinaRepositorio disciplinaRepositorio;
|
||||
|
||||
public EventoServico(EventoRepositorio eventoRepositorio) {
|
||||
public EventoServico(EventoRepositorio eventoRepositorio, DisciplinaRepositorio disciplinaRepositorio) {
|
||||
this.eventoRepositorio = eventoRepositorio;
|
||||
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");
|
||||
log.error("Acesso negado. Usuario {} tentou acessar recurso do estudante {}", authUser, estudanteId);
|
||||
throw new ExcecaoNegocio("Acesso negado");
|
||||
}
|
||||
}
|
||||
|
||||
private void validarDisciplina(String disciplinaId, String estudanteId) {
|
||||
if (disciplinaId == null || disciplinaId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
Disciplina disciplina = disciplinaRepositorio.findById(disciplinaId)
|
||||
.orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Disciplina não encontrada"));
|
||||
if (!disciplina.getEstudanteId().equals(estudanteId)) {
|
||||
throw new ExcecaoNegocio("Disciplina não pertence ao estudante");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,66 +54,106 @@ public class EventoServico {
|
||||
log.debug("Criando evento para estudante: {}", estudanteId);
|
||||
validarAcesso(estudanteId);
|
||||
|
||||
if (evento.getDisciplinaId() != null) {
|
||||
validarDisciplina(evento.getDisciplinaId(), estudanteId);
|
||||
}
|
||||
|
||||
evento.setEstudanteId(estudanteId);
|
||||
Evento salvo = eventoRepositorio.save(evento);
|
||||
return toDTO(salvo);
|
||||
return toDTO(salvo, Map.of());
|
||||
}
|
||||
|
||||
public List<RespostaEventoDTO> listarPorEstudante(String estudanteId) {
|
||||
log.debug("Listando eventos para estudante: {}", estudanteId);
|
||||
validarAcesso(estudanteId);
|
||||
|
||||
return eventoRepositorio.findByEstudanteId(estudanteId).stream()
|
||||
.map(this::toDTO)
|
||||
List<Evento> eventos = eventoRepositorio.findByEstudanteId(estudanteId);
|
||||
Map<String, String> disciplinasMap = resolverDisciplinas(eventos);
|
||||
return eventos.stream()
|
||||
.map(e -> toDTO(e, disciplinasMap))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<RespostaEventoDTO> listarPorPeriodo(String estudanteId, LocalDateTime inicio, LocalDateTime fim) {
|
||||
public List<RespostaEventoDTO> listarPorPeriodo(String estudanteId, java.time.LocalDateTime inicio, java.time.LocalDateTime fim) {
|
||||
validarAcesso(estudanteId);
|
||||
return eventoRepositorio.findByEstudanteIdAndDataHoraBetween(estudanteId, inicio, fim).stream()
|
||||
.map(this::toDTO)
|
||||
List<Evento> eventos = eventoRepositorio.findByEstudanteIdAndDataHoraBetween(estudanteId, inicio, fim);
|
||||
Map<String, String> disciplinasMap = resolverDisciplinas(eventos);
|
||||
return eventos.stream()
|
||||
.map(e -> toDTO(e, disciplinasMap))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<RespostaEventoDTO> listarProximosEventos(String estudanteId) {
|
||||
validarAcesso(estudanteId);
|
||||
return eventoRepositorio.findProximosEventosByEstudanteId(estudanteId, LocalDateTime.now()).stream()
|
||||
.map(this::toDTO)
|
||||
List<Evento> eventos = eventoRepositorio.findProximosEventosByEstudanteId(estudanteId, java.time.LocalDateTime.now());
|
||||
Map<String, String> disciplinasMap = resolverDisciplinas(eventos);
|
||||
return eventos.stream()
|
||||
.map(e -> toDTO(e, disciplinasMap))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public RespostaEventoDTO buscarPorId(String id, String estudanteId) {
|
||||
public RespostaEventoDTO buscarPorId(String id) {
|
||||
Evento evento = getEventoEntity(id);
|
||||
validarAcesso(evento.getEstudanteId());
|
||||
return toDTO(evento);
|
||||
return toDTO(evento, Map.of());
|
||||
}
|
||||
|
||||
public RespostaEventoDTO atualizarEvento(String id, Evento eventoAtualizado, String estudanteId) {
|
||||
public RespostaEventoDTO atualizarEvento(String id, RequisicaoEventoDTO dto) {
|
||||
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());
|
||||
evento.setTitulo(dto.titulo());
|
||||
evento.setDescricao(dto.descricao());
|
||||
evento.setTipo(dto.tipo());
|
||||
evento.setLocal(dto.local());
|
||||
evento.setDataHora(dto.dataHora());
|
||||
|
||||
return toDTO(eventoRepositorio.save(evento));
|
||||
if (dto.disciplinaId() != null) {
|
||||
validarDisciplina(dto.disciplinaId(), evento.getEstudanteId());
|
||||
evento.setDisciplinaId(dto.disciplinaId());
|
||||
}
|
||||
|
||||
public void excluirEvento(String id, String estudanteId) {
|
||||
return toDTO(eventoRepositorio.save(evento), Map.of());
|
||||
}
|
||||
|
||||
public void excluirEvento(String id) {
|
||||
Evento evento = getEventoEntity(id);
|
||||
validarAcesso(evento.getEstudanteId());
|
||||
eventoRepositorio.delete(evento);
|
||||
}
|
||||
|
||||
public RespostaEventoDTO cancelarEvento(String id) {
|
||||
Evento evento = getEventoEntity(id);
|
||||
validarAcesso(evento.getEstudanteId());
|
||||
evento.setStatus(Evento.StatusEvento.CANCELADO);
|
||||
return toDTO(eventoRepositorio.save(evento), Map.of());
|
||||
}
|
||||
|
||||
private Evento getEventoEntity(String id) {
|
||||
return eventoRepositorio.findById(id)
|
||||
.orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Evento não encontrado"));
|
||||
}
|
||||
|
||||
private RespostaEventoDTO toDTO(Evento evento) {
|
||||
private Map<String, String> resolverDisciplinas(List<Evento> eventos) {
|
||||
Set<String> disciplinaIds = eventos.stream()
|
||||
.map(Evento::getDisciplinaId)
|
||||
.filter(id -> id != null && !id.isBlank())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (disciplinaIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
return disciplinaRepositorio.findByIdIn(disciplinaIds).stream()
|
||||
.collect(Collectors.toMap(Disciplina::getId, Disciplina::getNome));
|
||||
}
|
||||
|
||||
private RespostaEventoDTO toDTO(Evento evento, Map<String, String> disciplinasMap) {
|
||||
String nomeDisciplina = null;
|
||||
final String disciplinaId = evento.getDisciplinaId();
|
||||
if (disciplinaId != null) {
|
||||
nomeDisciplina = disciplinasMap.get(disciplinaId);
|
||||
}
|
||||
return new RespostaEventoDTO(
|
||||
evento.getId(),
|
||||
evento.getEstudanteId(),
|
||||
@@ -105,7 +162,9 @@ public class EventoServico {
|
||||
evento.getTipo() != null ? evento.getTipo().name() : null,
|
||||
evento.getLocal(),
|
||||
evento.getDisciplinaId(),
|
||||
evento.getDataHora()
|
||||
evento.getDataHora(),
|
||||
evento.getStatus() != null ? evento.getStatus().name() : null,
|
||||
nomeDisciplina
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.agendaestudantil.servico;
|
||||
|
||||
import com.agendaestudantil.entidade.Evento;
|
||||
import com.agendaestudantil.entidade.Notificacao;
|
||||
import com.agendaestudantil.entidade.Tarefa;
|
||||
import com.agendaestudantil.repositorio.EventoRepositorio;
|
||||
import com.agendaestudantil.repositorio.NotificacaoRepositorio;
|
||||
import com.agendaestudantil.repositorio.TarefaRepositorio;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class NotificacaoAgendadorServico {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NotificacaoAgendadorServico.class);
|
||||
|
||||
private final TarefaRepositorio tarefaRepositorio;
|
||||
private final EventoRepositorio eventoRepositorio;
|
||||
private final NotificacaoRepositorio notificacaoRepositorio;
|
||||
|
||||
public NotificacaoAgendadorServico(TarefaRepositorio tarefaRepositorio, EventoRepositorio eventoRepositorio,
|
||||
NotificacaoRepositorio notificacaoRepositorio) {
|
||||
this.tarefaRepositorio = tarefaRepositorio;
|
||||
this.eventoRepositorio = eventoRepositorio;
|
||||
this.notificacaoRepositorio = notificacaoRepositorio;
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 3600000)
|
||||
public void gerarNotificacoes() {
|
||||
log.debug("Iniciando geracao de notificacoes");
|
||||
LocalDate hoje = LocalDate.now();
|
||||
LocalDate ontem = hoje.minusDays(1);
|
||||
LocalDate tresDiasDepois = hoje.plusDays(3);
|
||||
|
||||
List<Tarefa> tarefas = tarefaRepositorio.findTarefasNaoConcluidasComDataEntre(ontem, tresDiasDepois);
|
||||
for (Tarefa tarefa : tarefas) {
|
||||
if (tarefa.getStatus() == Tarefa.StatusTarefa.CONCLUIDA) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LocalDate dataEntrega = tarefa.getDataEntrega();
|
||||
if (dataEntrega == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long diasAteEntrega = ChronoUnit.DAYS.between(hoje, dataEntrega);
|
||||
|
||||
if (dataEntrega.isBefore(hoje)) {
|
||||
if (!tarefa.getStatus().equals(Tarefa.StatusTarefa.ATRASADA)) {
|
||||
tarefa.setStatus(Tarefa.StatusTarefa.ATRASADA);
|
||||
tarefaRepositorio.save(tarefa);
|
||||
}
|
||||
|
||||
if (!notificacaoRepositorio.existsByEstudanteIdAndReferenciaIdAndTipo(
|
||||
tarefa.getEstudanteId(), tarefa.getId(), Notificacao.TipoNotificacao.TAREFA_ATRASADA)) {
|
||||
criarNotificacao(tarefa.getEstudanteId(), tarefa.getId(),
|
||||
"Tarefa atrasada: " + tarefa.getTitulo(),
|
||||
"A tarefa '" + tarefa.getTitulo() + "' esta atrasada desde " + dataEntrega,
|
||||
Notificacao.TipoNotificacao.TAREFA_ATRASADA,
|
||||
Notificacao.TipoReferencia.TAREFA);
|
||||
}
|
||||
} else if (diasAteEntrega == 0 || diasAteEntrega == 1 || diasAteEntrega == 2 || diasAteEntrega == 3) {
|
||||
boolean enviarNotificacao = false;
|
||||
if (diasAteEntrega == 0 || diasAteEntrega == 1) {
|
||||
enviarNotificacao = true;
|
||||
} else if (diasAteEntrega == 2 || diasAteEntrega == 3) {
|
||||
if (tarefa.getPrioridade() == Tarefa.Prioridade.ALTA || tarefa.getPrioridade() == Tarefa.Prioridade.URGENTE) {
|
||||
enviarNotificacao = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (enviarNotificacao) {
|
||||
boolean jaNotificouHoje = notificacaoRepositorio.findByEstudanteIdAndReferenciaIdAndTipo(
|
||||
tarefa.getEstudanteId(), tarefa.getId(), Notificacao.TipoNotificacao.PRAZO_PROXIMO)
|
||||
.stream()
|
||||
.anyMatch(n -> n.getDataGeracao().toLocalDate().equals(hoje));
|
||||
|
||||
if (!jaNotificouHoje) {
|
||||
String prazo = diasAteEntrega == 0 ? "hoje" : (diasAteEntrega == 1 ? "amanha" : "em " + diasAteEntrega + " dias");
|
||||
criarNotificacao(tarefa.getEstudanteId(), tarefa.getId(),
|
||||
"Tarefa com prazo proximo: " + tarefa.getTitulo(),
|
||||
"A tarefa '" + tarefa.getTitulo() + "' vence " + prazo,
|
||||
Notificacao.TipoNotificacao.PRAZO_PROXIMO,
|
||||
Notificacao.TipoReferencia.TAREFA);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LocalDateTime agora = LocalDateTime.now();
|
||||
LocalDateTime em24Horas = agora.plusHours(24);
|
||||
List<Evento> eventos = eventoRepositorio.findEventosNasProximas24h(agora, em24Horas);
|
||||
for (Evento evento : eventos) {
|
||||
if (evento.getDataHora() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!notificacaoRepositorio.existsByEstudanteIdAndReferenciaIdAndTipo(
|
||||
evento.getEstudanteId(), evento.getId(), Notificacao.TipoNotificacao.EVENTO_PROXIMO)) {
|
||||
criarNotificacao(evento.getEstudanteId(), evento.getId(),
|
||||
"Evento nas proximas 24h: " + evento.getTitulo(),
|
||||
"O evento '" + evento.getTitulo() + "' sera em " + evento.getDataHora(),
|
||||
Notificacao.TipoNotificacao.EVENTO_PROXIMO,
|
||||
Notificacao.TipoReferencia.EVENTO);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Geracao de notificacoes concluida");
|
||||
}
|
||||
|
||||
private void criarNotificacao(String estudanteId, String referenciaId, String titulo, String mensagem,
|
||||
Notificacao.TipoNotificacao tipo, Notificacao.TipoReferencia tipoReferencia) {
|
||||
Notificacao notif = new Notificacao();
|
||||
notif.setEstudanteId(estudanteId);
|
||||
notif.setReferenciaId(referenciaId);
|
||||
notif.setTitulo(titulo);
|
||||
notif.setMensagem(mensagem);
|
||||
notif.setTipo(tipo);
|
||||
notif.setTipoReferencia(tipoReferencia);
|
||||
notif.setLida(false);
|
||||
notif.setDataGeracao(LocalDateTime.now());
|
||||
notificacaoRepositorio.save(notif);
|
||||
log.debug("Notificacao criada para estudante {}", estudanteId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.agendaestudantil.servico;
|
||||
|
||||
import com.agendaestudantil.dto.RespostaNotificacaoDTO;
|
||||
import com.agendaestudantil.entidade.Notificacao;
|
||||
import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado;
|
||||
import com.agendaestudantil.repositorio.NotificacaoRepositorio;
|
||||
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 NotificacaoServico {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(NotificacaoServico.class);
|
||||
|
||||
private final NotificacaoRepositorio notificacaoRepositorio;
|
||||
|
||||
public NotificacaoServico(NotificacaoRepositorio notificacaoRepositorio) {
|
||||
this.notificacaoRepositorio = notificacaoRepositorio;
|
||||
}
|
||||
|
||||
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 List<RespostaNotificacaoDTO> listarNaoLidas(String estudanteId) {
|
||||
log.debug("Listando notificações não lidas para estudante: {}", estudanteId);
|
||||
validarAcesso(estudanteId);
|
||||
return notificacaoRepositorio.findByEstudanteIdAndLidaFalse(estudanteId).stream()
|
||||
.map(this::toDTO)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<RespostaNotificacaoDTO> listarTodas(String estudanteId) {
|
||||
log.debug("Listando todas as notificações para estudante: {}", estudanteId);
|
||||
validarAcesso(estudanteId);
|
||||
return notificacaoRepositorio.findByEstudanteId(estudanteId).stream()
|
||||
.map(this::toDTO)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public long contarNaoLidas(String estudanteId) {
|
||||
log.debug("Contando notificações não lidas para estudante: {}", estudanteId);
|
||||
validarAcesso(estudanteId);
|
||||
return notificacaoRepositorio.countByEstudanteIdAndLidaFalse(estudanteId);
|
||||
}
|
||||
|
||||
public RespostaNotificacaoDTO marcarComoLida(String id) {
|
||||
Notificacao notificacao = getNotificacaoEntity(id);
|
||||
validarAcesso(notificacao.getEstudanteId());
|
||||
notificacao.setLida(true);
|
||||
return toDTO(notificacaoRepositorio.save(notificacao));
|
||||
}
|
||||
|
||||
public void marcarTodasComoLidas(String estudanteId) {
|
||||
log.debug("Marcando todas as notificações como lidas para estudante: {}", estudanteId);
|
||||
validarAcesso(estudanteId);
|
||||
List<Notificacao> notificacoes = notificacaoRepositorio.findByEstudanteIdAndLidaFalse(estudanteId);
|
||||
for (Notificacao n : notificacoes) {
|
||||
n.setLida(true);
|
||||
notificacaoRepositorio.save(n);
|
||||
}
|
||||
}
|
||||
|
||||
public void excluirNotificacao(String id) {
|
||||
Notificacao notificacao = getNotificacaoEntity(id);
|
||||
validarAcesso(notificacao.getEstudanteId());
|
||||
notificacaoRepositorio.delete(notificacao);
|
||||
}
|
||||
|
||||
private Notificacao getNotificacaoEntity(String id) {
|
||||
return notificacaoRepositorio.findById(id)
|
||||
.orElseThrow(() -> new ExcecaoRecursoNaoEncontrado("Notificação não encontrada"));
|
||||
}
|
||||
|
||||
private RespostaNotificacaoDTO toDTO(Notificacao notificacao) {
|
||||
return new RespostaNotificacaoDTO(
|
||||
notificacao.getId(),
|
||||
notificacao.getTitulo(),
|
||||
notificacao.getMensagem(),
|
||||
notificacao.getTipo() != null ? notificacao.getTipo().name() : null,
|
||||
notificacao.getReferenciaId(),
|
||||
notificacao.getTipoReferencia() != null ? notificacao.getTipoReferencia().name() : null,
|
||||
notificacao.isLida(),
|
||||
notificacao.getDataGeracao()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,13 @@ package com.agendaestudantil.servico;
|
||||
import com.agendaestudantil.dto.RequisicaoTarefaDTO;
|
||||
import com.agendaestudantil.dto.RespostaTarefaDTO;
|
||||
import com.agendaestudantil.entidade.Tarefa;
|
||||
import com.agendaestudantil.excecao.ExcecaoNegocio;
|
||||
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;
|
||||
@@ -29,8 +28,8 @@ public class TarefaServico {
|
||||
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");
|
||||
log.error("Acesso negado. Usuario {} tentou acessar recurso do estudante {}", authUser, estudanteId);
|
||||
throw new ExcecaoNegocio("Acesso negado");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +38,8 @@ public class TarefaServico {
|
||||
}
|
||||
|
||||
public RespostaTarefaDTO criarTarefa(RequisicaoTarefaDTO dto) {
|
||||
log.debug("Criando tarefa para estudante: {}", dto.estudanteId());
|
||||
validarAcesso(dto.estudanteId());
|
||||
String estudanteId = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
log.debug("Criando tarefa para estudante: {}", estudanteId);
|
||||
|
||||
Tarefa tarefa = new Tarefa();
|
||||
tarefa.setTitulo(dto.titulo());
|
||||
@@ -48,7 +47,7 @@ public class TarefaServico {
|
||||
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());
|
||||
tarefa.setEstudanteId(estudanteId);
|
||||
|
||||
if (dto.disciplinaId() != null) {
|
||||
tarefa.setDisciplinaId(dto.disciplinaId());
|
||||
@@ -68,7 +67,7 @@ public class TarefaServico {
|
||||
public List<RespostaTarefaDTO> listarTarefasPendentes(String estudanteId) {
|
||||
log.debug("Listando tarefas pendentes para estudante: {}", estudanteId);
|
||||
validarAcesso(estudanteId);
|
||||
return tarefaRepositorio.findTarefasPendentesByEstudanteId(estudanteId, Tarefa.StatusTarefa.CONCLUIDA).stream()
|
||||
return tarefaRepositorio.findByEstudanteIdAndStatus(estudanteId, Tarefa.StatusTarefa.PENDENTE).stream()
|
||||
.map(this::toDTO)
|
||||
.toList();
|
||||
}
|
||||
@@ -93,13 +92,18 @@ public class TarefaServico {
|
||||
|
||||
public RespostaTarefaDTO atualizarTarefa(String id, RequisicaoTarefaDTO dto) {
|
||||
Tarefa tarefa = getTarefaEntity(id);
|
||||
validarAcesso(tarefa.getEstudanteId());
|
||||
String authUser = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
if (!authUser.equals(tarefa.getEstudanteId())) {
|
||||
throw new ExcecaoNegocio("Acesso negado");
|
||||
}
|
||||
|
||||
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());
|
||||
if (dto.dataEntrega() != null) {
|
||||
tarefa.setDataEntrega(dto.dataEntrega());
|
||||
}
|
||||
|
||||
if (dto.disciplinaId() != null) {
|
||||
tarefa.setDisciplinaId(dto.disciplinaId());
|
||||
|
||||
@@ -14,9 +14,11 @@ import java.util.Date;
|
||||
public class UtilJwt {
|
||||
|
||||
private final String secret;
|
||||
private final long jwtExpiration;
|
||||
|
||||
public UtilJwt(@Value("${jwt.secret}") String secret) {
|
||||
public UtilJwt(@Value("${jwt.secret}") String secret, @Value("${jwt.expiration}") long jwtExpiration) {
|
||||
this.secret = secret;
|
||||
this.jwtExpiration = jwtExpiration;
|
||||
}
|
||||
|
||||
private SecretKey getSigningKey() {
|
||||
@@ -25,7 +27,7 @@ public class UtilJwt {
|
||||
|
||||
public String generateToken(String estudanteId) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + 24 * 60 * 60 * 1000L);
|
||||
Date expiryDate = new Date(now.getTime() + jwtExpiration);
|
||||
|
||||
return Jwts.builder()
|
||||
.subject(estudanteId)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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}
|
||||
jwt.expiration=${JWT_EXPIRATION:86400000}
|
||||
logging.level.org.springframework.data.mongodb=DEBUG
|
||||
springdoc.api-docs.enabled=true
|
||||
springdoc.swagger-ui.enabled=true
|
||||
|
||||
@@ -94,3 +94,89 @@ a:hover { text-decoration: underline; }
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.campo-consentimento {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.label-checkbox {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.label-checkbox input[type="checkbox"] {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
accent-color: #c0392b;
|
||||
}
|
||||
|
||||
.label-checkbox a {
|
||||
color: #c0392b;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.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; }
|
||||
|
||||
[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; }
|
||||
|
||||
[data-theme="dark"] .campo-consentimento {
|
||||
background: #1a1a1a;
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .label-checkbox { color: #e8e8e8; }
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
<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>
|
||||
<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">
|
||||
@@ -32,29 +34,29 @@
|
||||
|
||||
<div class="campo">
|
||||
<label for="cursoid">Curso</label>
|
||||
<input type="text" placeholder="Ex: Técnico em Informática" id="cursoid" required>
|
||||
<input type="text" placeholder="Ex: Tecnico em Informatica" id="cursoid" required>
|
||||
</div>
|
||||
|
||||
<div class="linha-dupla">
|
||||
<div class="campo">
|
||||
<label for="periodoid">Período</label>
|
||||
<label for="periodoid">Periodo</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>
|
||||
<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">
|
||||
<input type="password" placeholder="Minimo 6 caracteres" id="senhaid" autocomplete="new-password" required minlength="6">
|
||||
</div>
|
||||
|
||||
<div class="campo">
|
||||
@@ -62,10 +64,25 @@
|
||||
<input type="password" placeholder="Confirme sua senha" id="csenhaid" autocomplete="new-password" required>
|
||||
</div>
|
||||
|
||||
<div class="campo campo-consentimento">
|
||||
<label class="label-checkbox">
|
||||
<input type="checkbox" id="consentimentoId" required>
|
||||
<span>
|
||||
Li e concordo com a
|
||||
<a href="politica-privacidade.html" target="_blank">Politica de Privacidade</a>
|
||||
e autorizo o tratamento dos meus dados pessoais para uso do FocusAgenda,
|
||||
conforme a LGPD (Lei n 13.709/2018).
|
||||
</span>
|
||||
</label>
|
||||
<span class="erro-inline" id="erroConsentimento" style="display:none; color:#c0392b; font-size:12px;">
|
||||
Voce precisa aceitar os termos para continuar.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="logbtn">Cadastrar</button>
|
||||
</form>
|
||||
|
||||
<p class="mens"><a href="login.html">Já tem uma conta?</a></p>
|
||||
<p class="mens"><a href="login.html">Ja tem uma conta?</a></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -97,6 +114,7 @@
|
||||
const periodo = parseInt(document.getElementById('periodoid').value, 10);
|
||||
const senha = document.getElementById('senhaid').value;
|
||||
const csenha = document.getElementById('csenhaid').value;
|
||||
const consentimento = document.getElementById('consentimentoId').checked;
|
||||
|
||||
if (!nome || !email || !curso || !periodo || !senha) {
|
||||
mostrarErro('Preencha todos os campos.');
|
||||
@@ -109,11 +127,17 @@
|
||||
}
|
||||
|
||||
if (senha !== csenha) {
|
||||
mostrarErro('As senhas não conferem.');
|
||||
mostrarErro('As senhas nao conferem.');
|
||||
document.getElementById('csenhaid').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!consentimento) {
|
||||
document.getElementById('erroConsentimento').style.display = 'block';
|
||||
mostrarErro('Voce precisa aceitar os termos para continuar.');
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Cadastrando...';
|
||||
|
||||
@@ -121,13 +145,12 @@
|
||||
const res = await fetch('/api/estudantes/cadastro', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nome, email, senha, curso, periodo })
|
||||
body: JSON.stringify({ nome, email, senha, curso, periodo, consentimentoLgpd: true })
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -140,7 +163,7 @@
|
||||
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.');
|
||||
mostrarErro('Erro de conexao. Verifique se o servidor esta rodando.');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Cadastrar';
|
||||
|
||||
@@ -5,7 +5,6 @@ body {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* ===== HEADER ===== */
|
||||
#header {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
@@ -19,7 +18,6 @@ body {
|
||||
|
||||
#title { color: #fff; padding-left: 20px; font-size: 28px; }
|
||||
|
||||
/* ===== BARRA ESQUERDA ===== */
|
||||
#barraesquerda {
|
||||
position: fixed;
|
||||
top: 50px; left: 0;
|
||||
@@ -35,7 +33,6 @@ body {
|
||||
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; }
|
||||
@@ -51,7 +48,6 @@ body {
|
||||
.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; }
|
||||
@@ -60,13 +56,11 @@ body {
|
||||
.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;
|
||||
@@ -74,7 +68,6 @@ body {
|
||||
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; }
|
||||
|
||||
@@ -88,7 +81,6 @@ body {
|
||||
.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;
|
||||
@@ -101,7 +93,34 @@ body {
|
||||
}
|
||||
#btnLogout:hover { background: #fee2e2; border-color: #c0392b; color: #c0392b; }
|
||||
|
||||
/* Calendar header */
|
||||
#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; }
|
||||
|
||||
.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 { 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; }
|
||||
@@ -113,7 +132,6 @@ body {
|
||||
.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;
|
||||
@@ -127,11 +145,22 @@ body {
|
||||
font-family: inherit;
|
||||
}
|
||||
#btnNovoEvento:hover { background: #a03224; }
|
||||
#btnGerenciarDisciplinas {
|
||||
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;
|
||||
}
|
||||
#btnGerenciarDisciplinas: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; }
|
||||
@@ -148,7 +177,6 @@ body {
|
||||
.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; }
|
||||
@@ -158,13 +186,11 @@ body {
|
||||
.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; }
|
||||
@@ -173,12 +199,10 @@ body {
|
||||
.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;
|
||||
@@ -271,7 +295,6 @@ body {
|
||||
}
|
||||
.btn-perigo:hover { background: #fee2e2; }
|
||||
|
||||
/* Toast de feedback */
|
||||
#toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
@@ -291,10 +314,206 @@ body {
|
||||
#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; }
|
||||
}
|
||||
|
||||
[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); }
|
||||
|
||||
[data-theme="dark"] #btnGerenciarDisciplinas {
|
||||
background: #c0392b;
|
||||
color: #fff;
|
||||
}
|
||||
[data-theme="dark"] #btnGerenciarDisciplinas:hover {
|
||||
background: #a03224;
|
||||
}
|
||||
|
||||
.modal-disc-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.modal-disc-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
[data-theme="dark"] .modal-disc-header h2 { color: var(--text-primary); }
|
||||
|
||||
.modal-fechar-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
padding: 2px 7px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
.modal-fechar-btn:hover { background: #f3f4f6; color: #374151; }
|
||||
[data-theme="dark"] .modal-fechar-btn:hover { background: var(--hover-bg); color: var(--text-primary); }
|
||||
|
||||
.modal-corpo-lista {
|
||||
max-height: 310px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.modal-corpo-form { margin-bottom: 4px; }
|
||||
|
||||
.disc-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 11px 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.disc-item:last-child { border-bottom: none; }
|
||||
.disc-item:hover { background: #f9fafb; }
|
||||
[data-theme="dark"] .disc-item { border-bottom-color: var(--border-color); }
|
||||
[data-theme="dark"] .disc-item:hover { background: var(--hover-bg); }
|
||||
|
||||
.disc-cor {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.disc-nome {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
color: #111827;
|
||||
}
|
||||
[data-theme="dark"] .disc-nome { color: var(--text-primary); }
|
||||
.disc-info {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
[data-theme="dark"] .disc-info { color: var(--text-secondary); }
|
||||
.disc-seta {
|
||||
color: #9ca3af;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,333 @@
|
||||
* { 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;
|
||||
}
|
||||
|
||||
.secao-descricao {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.acoes-lgpd {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.acao-lgpd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 16px;
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.acao-lgpd strong {
|
||||
font-size: 14px;
|
||||
color: #111827;
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.acao-lgpd p {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.acao-lgpd button,
|
||||
.btn-link-politica {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #c0392b;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #c0392b;
|
||||
font-size: 13px;
|
||||
font-family: 'Poppins', sans-serif;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.acao-lgpd button:hover,
|
||||
.btn-link-politica:hover {
|
||||
background: #c0392b;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
[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"] .secao-descricao { color: #a0a0a0; }
|
||||
|
||||
[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; }
|
||||
|
||||
[data-theme="dark"] .acao-lgpd {
|
||||
background: #1a1a1a;
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .acao-lgpd strong { color: #e8e8e8; }
|
||||
[data-theme="dark"] .acao-lgpd p { color: #a0a0a0; }
|
||||
@@ -0,0 +1,323 @@
|
||||
<!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>Configuracoes - 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">Configuracoes</h1>
|
||||
<p class="subtitulo">Gerencie seus dados e preferencias</p>
|
||||
|
||||
<div id="mensagem-erro" role="alert"></div>
|
||||
<div id="mensagem-sucesso" role="status"></div>
|
||||
|
||||
<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: Tecnico em Informatica" id="cursoid" required>
|
||||
</div>
|
||||
|
||||
<div class="linha-dupla">
|
||||
<div class="campo">
|
||||
<label for="periodoid">Periodo</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 Alteracoes</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<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="Minimo 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>
|
||||
|
||||
<div class="secao">
|
||||
<div class="secao-titulo">Seus Dados (LGPD)</div>
|
||||
<p class="secao-descricao">
|
||||
Conforme a Lei Geral de Protecao de Dados (Lei n 13.709/2018),
|
||||
voce tem direito de acessar, exportar e excluir seus dados pessoais.
|
||||
</p>
|
||||
<div class="acoes-lgpd">
|
||||
<div class="acao-lgpd">
|
||||
<div>
|
||||
<strong>Exportar meus dados</strong>
|
||||
<p>Baixe um arquivo JSON com todas as suas tarefas, eventos e disciplinas.</p>
|
||||
</div>
|
||||
<button type="button" id="btnExportarDados">Exportar JSON</button>
|
||||
</div>
|
||||
<div class="acao-lgpd">
|
||||
<div>
|
||||
<strong>Politica de Privacidade</strong>
|
||||
<p>Veja como seus dados sao tratados e quais sao seus direitos.</p>
|
||||
</div>
|
||||
<a href="politica-privacidade.html" class="btn-link-politica">Ver politica</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="zona-perigo">
|
||||
<div class="secao-titulo">Zona de Perigo</div>
|
||||
<p>
|
||||
Ao excluir sua conta, exercendo o direito previsto no Art. 18, VI da LGPD,
|
||||
todos os seus dados pessoais (tarefas, eventos, disciplinas e notificacoes)
|
||||
serao removidos <strong>permanentemente e imediatamente</strong> dos nossos servidores.
|
||||
Esta acao e irreversivel.
|
||||
</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 requisicao');
|
||||
}
|
||||
return json.data;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 Alteracoes';
|
||||
}
|
||||
});
|
||||
|
||||
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 nao 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';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btnExportarDados').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('btnExportarDados');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Gerando...';
|
||||
try {
|
||||
const res = await fetch('/api/estudantes/me/dados', {
|
||||
headers: { 'Authorization': 'Bearer ' + localStorage.getItem('fa_token') }
|
||||
});
|
||||
if (res.status === 401) {
|
||||
localStorage.clear();
|
||||
window.location.href = '/login.html?sessao=expirada';
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
const blob = new Blob([JSON.stringify(data.data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'focusagenda-meus-dados.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
alert('Erro ao exportar dados: ' + err.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Exportar JSON';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btnExcluirConta').addEventListener('click', async () => {
|
||||
if (!confirm('Tem certeza que deseja excluir sua conta? Esta acao e irreversivel!')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmacao = prompt('Digite "EXCLUIR" para confirmar a exclusao da sua conta:');
|
||||
if (confirmacao !== 'EXCLUIR') {
|
||||
mostrarErro('Exclusao 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 excluida. Redirecionando...');
|
||||
setTimeout(() => window.location.href = 'login.html', 2000);
|
||||
} catch(err) {
|
||||
mostrarErro(err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Excluir Minha Conta';
|
||||
}
|
||||
});
|
||||
|
||||
carregarUsuario();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 |
@@ -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 |
@@ -81,3 +81,46 @@ a:hover { text-decoration: underline; }
|
||||
font-size: 14px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.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; }
|
||||
|
||||
[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,119 @@
|
||||
<!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>Politica de Privacidade - Focus Agenda</title>
|
||||
<script src="theme.js"></script>
|
||||
<style>
|
||||
.politica-container {
|
||||
max-width: 720px;
|
||||
margin: 80px auto 40px;
|
||||
padding: 32px 24px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
font-family: 'Poppins', sans-serif;
|
||||
color: #1f2937;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.politica-container h1 {
|
||||
font-size: 24px;
|
||||
color: #c0392b;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.politica-container h2 {
|
||||
font-size: 18px;
|
||||
color: #114455;
|
||||
margin-top: 28px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.politica-container p, .politica-container li {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
}
|
||||
.politica-container ul {
|
||||
padding-left: 20px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.politica-container li {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.voltar-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
color: #c0392b;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.voltar-link:hover { text-decoration: underline; }
|
||||
[data-theme="dark"] .politica-container {
|
||||
background: #1e1e1e;
|
||||
color: #e8e8e8;
|
||||
}
|
||||
[data-theme="dark"] .politica-container h1 { color: #e74c3c; }
|
||||
[data-theme="dark"] .politica-container h2 { color: #5dade2; border-bottom-color: #333; }
|
||||
[data-theme="dark"] .politica-container p,
|
||||
[data-theme="dark"] .politica-container li { color: #d0d0d0; }
|
||||
[data-theme="dark"] .voltar-link { color: #e74c3c; }
|
||||
</style>
|
||||
</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 class="politica-container">
|
||||
<a href="cadastro.html" class="voltar-link">← Voltar</a>
|
||||
<h1>Politica de Privacidade</h1>
|
||||
<p>Versao 1.0 - Vigente a partir de maio de 2026</p>
|
||||
|
||||
<h2>1. Quem somos</h2>
|
||||
<p>FocusAgenda e um sistema de agenda estudantil desenvolvido como Trabalho de Conclusao de Curso (TCC) no curso Tecnico em Desenvolvimento de Sistemas - ETEC Pedro D'Arcadia Neto.</p>
|
||||
|
||||
<h2>2. Quais dados coletamos e por que (Art. 9, LGPD)</h2>
|
||||
<ul>
|
||||
<li><strong>Nome completo:</strong> identificacao na plataforma</li>
|
||||
<li><strong>Endereco de email:</strong> autenticacao e comunicacao</li>
|
||||
<li><strong>Senha:</strong> armazenada com criptografia BCrypt (nunca em texto simples)</li>
|
||||
<li><strong>Curso e periodo:</strong> personalizao da experiencia</li>
|
||||
<li><strong>Tarefas, eventos e disciplinas:</strong> funcionalidade principal do servico</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Base legal para o tratamento (Art. 7, I, LGPD)</h2>
|
||||
<p>Consentimento livre, informado e inequivoco do titular. Ao criar uma conta no FocusAgenda, voce aceita explicitamente o tratamento dos seus dados pessoais conforme descrito nesta politica.</p>
|
||||
|
||||
<h2>4. Como protegemos seus dados (Art. 46, LGPD)</h2>
|
||||
<ul>
|
||||
<li>Senhas criptografadas com BCrypt</li>
|
||||
<li>Comunicacao protegida por HTTPS</li>
|
||||
<li>Autenticacao via JWT com expiracao de 24 horas</li>
|
||||
<li>Acesso restrito aos dados do proprio usuario autenticado</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Seus direitos como titular (Art. 18, LGPD)</h2>
|
||||
<ul>
|
||||
<li><strong>Acesso:</strong> visualize seus dados em Configuracoes</li>
|
||||
<li><strong>Portabilidade:</strong> exporte todos os seus dados em JSON via Configuracoes</li>
|
||||
<li><strong>Correcao:</strong> edite seus dados em Configuracoes > Dados Pessoais</li>
|
||||
<li><strong>Eliminacao:</strong> exclua sua conta em Configuracoes > Zona de Perigo (todos os dados sao removidos permanentemente e imediatamente)</li>
|
||||
</ul>
|
||||
|
||||
<h2>6. Retencao de dados</h2>
|
||||
<p>Os dados sao mantidos enquanto a conta estiver ativa. Apos a exclusao da conta, todos os dados sao removidos imediatamente e de forma irreversivel.</p>
|
||||
|
||||
<h2>7. Contato</h2>
|
||||
<p>Para duvidas sobre privacidade, entre em contato com a equipe do FocusAgenda.</p>
|
||||
|
||||
<h2>8. Versao e vigencia</h2>
|
||||
<p>Versao 1.0 - vigente a partir de maio de 2026.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -63,7 +63,7 @@ body {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
color: white;
|
||||
margin-top: 70px; /* Compensa a barra fixa do topo */
|
||||
margin-top: 70px;
|
||||
}
|
||||
#campo {
|
||||
display: flex;
|
||||
|
||||
@@ -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,83 @@
|
||||
async function apiFetch(url, options = {}) {
|
||||
const token = localStorage.getItem('fa_token');
|
||||
const headers = { 'Content-Type': 'application/json', ...(options.headers || {}) };
|
||||
if (token) headers['Authorization'] = 'Bearer ' + token;
|
||||
|
||||
const res = await fetch(url, { ...options, headers });
|
||||
|
||||
if (res.status === 401) {
|
||||
localStorage.clear();
|
||||
window.location.href = '/login.html?sessao=expirada';
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.message || 'Erro desconhecido');
|
||||
return data;
|
||||
}
|
||||
|
||||
function showToast(mensagem, tipo = 'info') {
|
||||
const existing = document.querySelector('.fa-toast');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const styleId = 'fa-toast-style';
|
||||
if (!document.getElementById(styleId)) {
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = `
|
||||
.fa-toast {
|
||||
position: fixed; bottom: 24px; right: 24px; z-index: 99999;
|
||||
padding: 12px 18px; border-radius: 8px; font-size: 14px;
|
||||
font-family: 'Poppins', sans-serif; max-width: 340px;
|
||||
animation: faSlideIn 0.3s ease forwards;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
.fa-toast.fa-toast-sai { animation: faSlideOut 0.3s ease forwards; }
|
||||
.fa-toast-sucesso { background: #114455; color: #fff; }
|
||||
.fa-toast-erro { background: #c0392b; color: #fff; }
|
||||
.fa-toast-info { background: #f5f5f5; color: #1f2937; border: 1px solid #c0392b; }
|
||||
@keyframes faSlideIn { from { transform: translateY(30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
||||
@keyframes faSlideOut { from { transform: translateY(0); opacity: 1; } to { transform: translateY(30px); opacity: 0; } }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = 'fa-toast fa-toast-' + tipo;
|
||||
toast.textContent = mensagem;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('fa-toast-sai');
|
||||
toast.addEventListener('animationend', () => toast.remove());
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function setButtonLoading(btn, loading, textoOriginal) {
|
||||
if (loading) {
|
||||
btn.disabled = true;
|
||||
btn.dataset.textoOriginal = textoOriginal || btn.textContent;
|
||||
btn.textContent = 'Salvando...';
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
if (btn.dataset.textoOriginal) {
|
||||
btn.textContent = btn.dataset.textoOriginal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(function() {
|
||||
window.addEventListener('offline', () => {
|
||||
let banner = document.getElementById('fa-offline-banner');
|
||||
if (!banner) {
|
||||
banner = document.createElement('div');
|
||||
banner.id = 'fa-offline-banner';
|
||||
banner.textContent = 'Sem conexao — algumas funcoes podem nao funcionar';
|
||||
banner.style.cssText = 'position:fixed;top:0;left:0;width:100%;background:#c0392b;color:#fff;text-align:center;padding:10px;font-family:Poppins,sans-serif;font-size:14px;z-index:9999;';
|
||||
document.body.prepend(banner);
|
||||
}
|
||||
});
|
||||
window.addEventListener('online', () => {
|
||||
document.getElementById('fa-offline-banner')?.remove();
|
||||
});
|
||||
})();
|
||||
@@ -44,7 +44,7 @@ public class EstudanteServicoTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
requisicaoCadastro = new RequisicaoCadastroDTO("Teste", "teste@teste.com", "123456", "Curso", 1);
|
||||
requisicaoCadastro = new RequisicaoCadastroDTO("Teste", "teste@teste.com", "123456", "Curso", 1, true);
|
||||
requisicaoLogin = new RequisicaoLoginDTO("teste@teste.com", "123456");
|
||||
|
||||
estudante = new Estudante();
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.agendaestudantil.servico;
|
||||
import com.agendaestudantil.dto.RequisicaoTarefaDTO;
|
||||
import com.agendaestudantil.dto.RespostaTarefaDTO;
|
||||
import com.agendaestudantil.entidade.Tarefa;
|
||||
import com.agendaestudantil.excecao.ExcecaoNegocio;
|
||||
import com.agendaestudantil.excecao.ExcecaoRecursoNaoEncontrado;
|
||||
import com.agendaestudantil.repositorio.TarefaRepositorio;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -14,7 +15,6 @@ 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;
|
||||
@@ -51,8 +51,7 @@ public class TarefaServicoTest {
|
||||
Tarefa.Prioridade.ALTA,
|
||||
Tarefa.StatusTarefa.PENDENTE,
|
||||
LocalDate.now(),
|
||||
null,
|
||||
estudanteId);
|
||||
null);
|
||||
|
||||
tarefa = new Tarefa();
|
||||
tarefa.setId("tarefa1");
|
||||
@@ -84,10 +83,18 @@ public class TarefaServicoTest {
|
||||
|
||||
@Test
|
||||
void deveLancarExcecaoAoCriarTarefaParaOutroUsuario() {
|
||||
mockAuthentication("outroEstudante");
|
||||
String outroId = "outroEstudante";
|
||||
mockAuthentication(outroId);
|
||||
when(tarefaRepositorio.save(any(Tarefa.class))).thenAnswer(inv -> {
|
||||
Tarefa t = inv.getArgument(0);
|
||||
t.setId("tarefa2");
|
||||
return t;
|
||||
});
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> tarefaServico.criarTarefa(requisicaoTarefa));
|
||||
verify(tarefaRepositorio, never()).save(any(Tarefa.class));
|
||||
RespostaTarefaDTO resposta = tarefaServico.criarTarefa(requisicaoTarefa);
|
||||
|
||||
assertNotNull(resposta);
|
||||
assertEquals(outroId, resposta.estudanteId());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user