Some adjustments and added finance module to API, started createinvoice in frontend

This commit is contained in:
Dennis Eckerskorn 2025-05-14 00:09:04 +02:00
parent 7c1adbe2b7
commit 6177976a6f
25 changed files with 1292 additions and 128 deletions

View File

@ -60,6 +60,11 @@ public class SecurityConfig {
.requestMatchers("/api/v1/assistances/**").hasAnyAuthority("FULL_ACCESS", "MANAGE_STUDENTS")
.requestMatchers("/api/v1/training-sessions/**").hasAnyAuthority("FULL_ACCESS", "MANAGE_STUDENTS")
.requestMatchers("/api/v1/training-groups/**").hasAnyAuthority("FULL_ACCESS", "MANAGE_STUDENTS")
.requestMatchers("/api/v1/invoices/**").hasAnyAuthority("FULL_ACCESS", "MANAGE_STUDENTS")
.requestMatchers("/api/v1/invoice-lines/**").hasAnyAuthority("FULL_ACCESS", "MANAGE_STUDENTS")
.requestMatchers("/api/v1/payments/**").hasAnyAuthority("FULL_ACCESS", "MANAGE_STUDENTS")
.requestMatchers("/api/v1/products-services/**").hasAnyAuthority("FULL_ACCESS", "MANAGE_STUDENTS")
.requestMatchers("/api/v1/iva-types/**").hasAnyAuthority("FULL_ACCESS", "MANAGE_STUDENTS")
.anyRequest().authenticated()
)
.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

View File

@ -0,0 +1,65 @@
package com.denniseckerskorn.controllers.finance_management;
import com.denniseckerskorn.dtos.finance_management_dtos.IVATypeDTO;
import com.denniseckerskorn.entities.finance.IVAType;
import com.denniseckerskorn.exceptions.DuplicateEntityException;
import com.denniseckerskorn.exceptions.EntityNotFoundException;
import com.denniseckerskorn.exceptions.InvalidDataException;
import com.denniseckerskorn.services.finance_services.IVATypeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/v1/iva-types")
@Tag(name = "IVA Types", description = "Operations related to IVA type management")
public class IVATypeController {
private final IVATypeService ivaTypeService;
public IVATypeController(IVATypeService ivaTypeService) {
this.ivaTypeService = ivaTypeService;
}
@PostMapping("/create")
@Operation(summary = "Create a new IVA type")
public ResponseEntity<IVATypeDTO> create(@Valid @RequestBody IVATypeDTO dto) throws DuplicateEntityException {
IVAType saved = ivaTypeService.save(dto.toEntity());
return new ResponseEntity<>(new IVATypeDTO(saved), HttpStatus.CREATED);
}
@PutMapping("/update")
@Operation(summary = "Update an existing IVA type")
public ResponseEntity<IVATypeDTO> update(@Valid @RequestBody IVATypeDTO dto) throws EntityNotFoundException, InvalidDataException {
IVAType updated = ivaTypeService.update(dto.toEntity());
return new ResponseEntity<>(new IVATypeDTO(updated), HttpStatus.OK);
}
@GetMapping("/getById/{id}")
@Operation(summary = "Get IVA type by ID")
public ResponseEntity<IVATypeDTO> getById(@PathVariable Integer id) throws EntityNotFoundException, InvalidDataException {
IVAType entity = ivaTypeService.findById(id);
return new ResponseEntity<>(new IVATypeDTO(entity), HttpStatus.OK);
}
@GetMapping("/getAll")
@Operation(summary = "Get all IVA types")
public ResponseEntity<List<IVATypeDTO>> getAll() {
List<IVATypeDTO> dtos = ivaTypeService.findAll()
.stream().map(IVATypeDTO::new).collect(Collectors.toList());
return new ResponseEntity<>(dtos, HttpStatus.OK);
}
@DeleteMapping("/deleteById/{id}")
@Operation(summary = "Delete IVA type by ID (if not in use)")
public ResponseEntity<Void> delete(@PathVariable Integer id) throws EntityNotFoundException, InvalidDataException {
ivaTypeService.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}

View File

@ -0,0 +1,105 @@
package com.denniseckerskorn.controllers.finance_management;
import com.denniseckerskorn.dtos.finance_management_dtos.InvoiceDTO;
import com.denniseckerskorn.entities.finance.Invoice;
import com.denniseckerskorn.entities.finance.InvoiceLine;
import com.denniseckerskorn.exceptions.DuplicateEntityException;
import com.denniseckerskorn.exceptions.EntityNotFoundException;
import com.denniseckerskorn.exceptions.InvalidDataException;
import com.denniseckerskorn.services.finance_services.InvoiceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/v1/invoices")
@Tag(name = "Invoices", description = "Operations related to invoice management")
public class InvoiceController {
private final InvoiceService invoiceService;
public InvoiceController(InvoiceService invoiceService) {
this.invoiceService = invoiceService;
}
@PostMapping("/create")
@Operation(summary = "Create a new invoice")
public ResponseEntity<InvoiceDTO> createInvoice(@Valid @RequestBody InvoiceDTO dto) throws DuplicateEntityException {
Invoice saved = invoiceService.save(dto.toEntity());
return new ResponseEntity<>(new InvoiceDTO(saved), HttpStatus.CREATED);
}
@PutMapping("/update")
@Operation(summary = "Update an existing invoice")
public ResponseEntity<InvoiceDTO> updateInvoice(@Valid @RequestBody InvoiceDTO dto) throws EntityNotFoundException, InvalidDataException {
Invoice updated = invoiceService.update(dto.toEntity());
return new ResponseEntity<>(new InvoiceDTO(updated), HttpStatus.OK);
}
@GetMapping("/getById/{id}")
@Operation(summary = "Get invoice by ID")
public ResponseEntity<InvoiceDTO> getInvoiceById(@PathVariable Integer id) throws EntityNotFoundException, InvalidDataException {
Invoice invoice = invoiceService.findById(id);
return new ResponseEntity<>(new InvoiceDTO(invoice), HttpStatus.OK);
}
@GetMapping("/getAll")
@Operation(summary = "Get all invoices")
public ResponseEntity<List<InvoiceDTO>> getAllInvoices() {
List<InvoiceDTO> dtos = invoiceService.findAll()
.stream().map(InvoiceDTO::new).collect(Collectors.toList());
return new ResponseEntity<>(dtos, HttpStatus.OK);
}
@DeleteMapping("/deleteById/{id}")
@Operation(summary = "Delete invoice by ID")
public ResponseEntity<Void> deleteInvoice(@PathVariable Integer id) throws EntityNotFoundException, InvalidDataException {
invoiceService.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@GetMapping("/getAllInvoicesByUserId/{userId}")
@Operation(summary = "Get all invoices by user ID")
public ResponseEntity<List<InvoiceDTO>> getInvoicesByUserId(@PathVariable Integer userId) throws InvalidDataException {
List<InvoiceDTO> dtos = invoiceService.findAllInvoicesByUserId(userId)
.stream().map(InvoiceDTO::new).collect(Collectors.toList());
return new ResponseEntity<>(dtos, HttpStatus.OK);
}
@PostMapping("/addLinesByInvoiceId/{invoiceId}")
@Operation(summary = "Add a line to an invoice by invoice ID")
public ResponseEntity<Void> addLineToInvoice(@PathVariable Integer invoiceId, @RequestBody @Valid InvoiceLine line) {
invoiceService.addLineToInvoiceById(invoiceId, line);
return new ResponseEntity<>(HttpStatus.OK);
}
@DeleteMapping("/removeLineFromInvoiceById/{invoiceId}")
@Operation(summary = "Remove a line from an invoice by IDs")
public ResponseEntity<Void> removeLineFromInvoice(@PathVariable Integer invoiceId, @PathVariable Integer lineId) {
invoiceService.removeLineFromInvoiceById(invoiceId, lineId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
@PutMapping("/recalculateTotalOfInvoiceById/{invoiceId}")
@Operation(summary = "Recalculate the total of an invoice")
public ResponseEntity<Void> recalculateTotal(@PathVariable Integer invoiceId) throws EntityNotFoundException, InvalidDataException {
Invoice invoice = invoiceService.findById(invoiceId);
invoiceService.recalculateTotal(invoice);
return new ResponseEntity<>(HttpStatus.OK);
}
@DeleteMapping("/clearAllLinesFromInvoiceById/{invoiceId}")
@Operation(summary = "Clear all invoice lines from an invoice")
public ResponseEntity<Void> clearInvoiceLines(@PathVariable Integer invoiceId) throws EntityNotFoundException, InvalidDataException {
Invoice invoice = invoiceService.findById(invoiceId);
invoiceService.clearInvoiceLines(invoice);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}

View File

@ -0,0 +1,68 @@
package com.denniseckerskorn.controllers.finance_management;
import com.denniseckerskorn.dtos.finance_management_dtos.InvoiceLineDTO;
import com.denniseckerskorn.entities.finance.InvoiceLine;
import com.denniseckerskorn.exceptions.DuplicateEntityException;
import com.denniseckerskorn.exceptions.EntityNotFoundException;
import com.denniseckerskorn.exceptions.InvalidDataException;
import com.denniseckerskorn.services.finance_services.InvoiceLineService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
/**
* InvoiceLineController handles operations related to invoice lines (items in invoices).
*/
@RestController
@RequestMapping("/api/v1/invoice-lines")
@Tag(name = "Invoice Lines", description = "Operations related to invoice lines")
public class InvoiceLineController {
private final InvoiceLineService invoiceLineService;
public InvoiceLineController(InvoiceLineService invoiceLineService) {
this.invoiceLineService = invoiceLineService;
}
@PostMapping("/create")
@Operation(summary = "Create a new invoice line")
public ResponseEntity<InvoiceLineDTO> create(@Valid @RequestBody InvoiceLineDTO dto) throws DuplicateEntityException {
InvoiceLine saved = invoiceLineService.save(dto.toEntity());
return new ResponseEntity<>(new InvoiceLineDTO(saved), HttpStatus.CREATED);
}
@PutMapping("/update")
@Operation(summary = "Update an existing invoice line")
public ResponseEntity<InvoiceLineDTO> update(@Valid @RequestBody InvoiceLineDTO dto) throws InvalidDataException, EntityNotFoundException {
InvoiceLine updated = invoiceLineService.update(dto.toEntity());
return new ResponseEntity<>(new InvoiceLineDTO(updated), HttpStatus.OK);
}
@GetMapping("/getById/{id}")
@Operation(summary = "Get invoice line by ID")
public ResponseEntity<InvoiceLineDTO> getById(@PathVariable Integer id) throws InvalidDataException, EntityNotFoundException {
InvoiceLine line = invoiceLineService.findById(id);
return new ResponseEntity<>(new InvoiceLineDTO(line), HttpStatus.OK);
}
@GetMapping("/getAll")
@Operation(summary = "Get all invoice lines")
public ResponseEntity<List<InvoiceLineDTO>> getAll() {
List<InvoiceLineDTO> dtos = invoiceLineService.findAll()
.stream().map(InvoiceLineDTO::new).collect(Collectors.toList());
return new ResponseEntity<>(dtos, HttpStatus.OK);
}
@DeleteMapping("/deleteById/{id}")
@Operation(summary = "Delete invoice line by ID")
public ResponseEntity<Void> delete(@PathVariable Integer id) throws InvalidDataException, EntityNotFoundException {
invoiceLineService.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}

View File

@ -0,0 +1,65 @@
package com.denniseckerskorn.controllers.finance_management;
import com.denniseckerskorn.dtos.finance_management_dtos.PaymentDTO;
import com.denniseckerskorn.entities.finance.Payment;
import com.denniseckerskorn.exceptions.DuplicateEntityException;
import com.denniseckerskorn.exceptions.EntityNotFoundException;
import com.denniseckerskorn.exceptions.InvalidDataException;
import com.denniseckerskorn.services.finance_services.PaymentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/v1/payments")
@Tag(name = "Payments", description = "Operations related to payment management")
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(PaymentService paymentService) {
this.paymentService = paymentService;
}
@PostMapping("/create")
@Operation(summary = "Create a new payment and mark invoice as PAID")
public ResponseEntity<PaymentDTO> create(@Valid @RequestBody PaymentDTO dto) throws DuplicateEntityException {
Payment saved = paymentService.save(dto.toEntity());
return new ResponseEntity<>(new PaymentDTO(saved), HttpStatus.CREATED);
}
@PutMapping("/update")
@Operation(summary = "Update an existing payment and adjust invoice status")
public ResponseEntity<PaymentDTO> update(@Valid @RequestBody PaymentDTO dto) throws EntityNotFoundException, InvalidDataException {
Payment updated = paymentService.update(dto.toEntity());
return new ResponseEntity<>(new PaymentDTO(updated), HttpStatus.OK);
}
@GetMapping("/getById/{id}")
@Operation(summary = "Get payment by ID")
public ResponseEntity<PaymentDTO> getById(@PathVariable Integer id) throws EntityNotFoundException, InvalidDataException {
Payment payment = paymentService.findById(id);
return new ResponseEntity<>(new PaymentDTO(payment), HttpStatus.OK);
}
@GetMapping("/getAll")
@Operation(summary = "Get all payments")
public ResponseEntity<List<PaymentDTO>> getAll() {
List<PaymentDTO> dtos = paymentService.findAll()
.stream().map(PaymentDTO::new).collect(Collectors.toList());
return new ResponseEntity<>(dtos, HttpStatus.OK);
}
@DeleteMapping("/deleteById/{id}")
@Operation(summary = "Remove a payment and set invoice status to NOT_PAID")
public ResponseEntity<Void> removePayment(@PathVariable Integer id) throws EntityNotFoundException, InvalidDataException {
paymentService.removePayment(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}

View File

@ -0,0 +1,65 @@
package com.denniseckerskorn.controllers.finance_management;
import com.denniseckerskorn.dtos.finance_management_dtos.ProductServiceDTO;
import com.denniseckerskorn.entities.finance.ProductService;
import com.denniseckerskorn.exceptions.DuplicateEntityException;
import com.denniseckerskorn.exceptions.EntityNotFoundException;
import com.denniseckerskorn.exceptions.InvalidDataException;
import com.denniseckerskorn.services.finance_services.ProductServiceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/v1/products-services")
@Tag(name = "Product Services", description = "Operations related to product/service management")
public class ProductServiceController {
private final ProductServiceService productServiceService;
public ProductServiceController(ProductServiceService productServiceService) {
this.productServiceService = productServiceService;
}
@PostMapping("/create")
@Operation(summary = "Create a new product/service")
public ResponseEntity<ProductServiceDTO> create(@Valid @RequestBody ProductServiceDTO dto) throws DuplicateEntityException {
ProductService saved = productServiceService.save(dto.toEntity());
return new ResponseEntity<>(new ProductServiceDTO(saved), HttpStatus.CREATED);
}
@PutMapping("/update")
@Operation(summary = "Update an existing product/service")
public ResponseEntity<ProductServiceDTO> update(@Valid @RequestBody ProductServiceDTO dto) throws EntityNotFoundException, InvalidDataException {
ProductService updated = productServiceService.update(dto.toEntity());
return new ResponseEntity<>(new ProductServiceDTO(updated), HttpStatus.OK);
}
@GetMapping("/getById/{id}")
@Operation(summary = "Get product/service by ID")
public ResponseEntity<ProductServiceDTO> getById(@PathVariable Integer id) throws EntityNotFoundException, InvalidDataException {
ProductService entity = productServiceService.findById(id);
return new ResponseEntity<>(new ProductServiceDTO(entity), HttpStatus.OK);
}
@GetMapping("/getAll")
@Operation(summary = "Get all products/services")
public ResponseEntity<List<ProductServiceDTO>> getAll() {
List<ProductServiceDTO> dtos = productServiceService.findAll()
.stream().map(ProductServiceDTO::new).collect(Collectors.toList());
return new ResponseEntity<>(dtos, HttpStatus.OK);
}
@DeleteMapping("/deleteById/{id}")
@Operation(summary = "Delete product/service by ID")
public ResponseEntity<Void> delete(@PathVariable Integer id) throws EntityNotFoundException, InvalidDataException {
productServiceService.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}

View File

@ -18,7 +18,6 @@ import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
//TODO: test
@RestController
@RequestMapping("/api/v1/admins")
@Tag(name = "Administrator Management", description = "Operations related to administrator management")

View File

@ -0,0 +1,63 @@
package com.denniseckerskorn.dtos.finance_management_dtos;
import com.denniseckerskorn.entities.finance.IVAType;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
public class IVATypeDTO {
private Integer id;
@NotNull
private BigDecimal percentage;
private String description;
public IVATypeDTO() {}
public IVATypeDTO(IVAType entity) {
this.id = entity.getId();
this.percentage = entity.getPercentage();
this.description = entity.getDescription();
}
public static IVATypeDTO fromEntity(IVAType entity) {
return new IVATypeDTO(entity);
}
public IVAType toEntity() {
IVAType iva = new IVAType();
iva.setId(this.id);
iva.setPercentage(this.percentage);
iva.setDescription(this.description);
return iva;
}
// Getters y setters...
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public BigDecimal getPercentage() {
return percentage;
}
public void setPercentage(BigDecimal percentage) {
this.percentage = percentage;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@ -0,0 +1,119 @@
package com.denniseckerskorn.dtos.finance_management_dtos;
import com.denniseckerskorn.entities.finance.Invoice;
import com.denniseckerskorn.enums.StatusValues;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.stream.Collectors;
public class InvoiceDTO {
private Integer id;
@NotNull
private Integer userId;
@NotNull
private LocalDateTime date;
@NotNull
private BigDecimal total;
@NotNull
private StatusValues status;
private Integer paymentId;
private Set<Integer> invoiceLineIds;
public InvoiceDTO() {
}
public InvoiceDTO(Invoice invoice) {
this.id = invoice.getId();
this.userId = invoice.getUser() != null ? invoice.getUser().getId() : null;
this.date = invoice.getDate();
this.total = invoice.getTotal();
this.status = invoice.getStatus();
this.paymentId = invoice.getPayment() != null ? invoice.getPayment().getId() : null;
this.invoiceLineIds = invoice.getInvoiceLines()
.stream()
.map(line -> line.getId())
.collect(Collectors.toSet());
}
public static InvoiceDTO fromEntity(Invoice invoice) {
return new InvoiceDTO(invoice);
}
public Invoice toEntity() {
Invoice invoice = new Invoice();
invoice.setId(this.id);
invoice.setDate(this.date);
invoice.setTotal(this.total);
invoice.setStatus(this.status);
return invoice;
}
// Getters y setters (puedes generarlos automáticamente)
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public LocalDateTime getDate() {
return date;
}
public void setDate(LocalDateTime date) {
this.date = date;
}
public BigDecimal getTotal() {
return total;
}
public void setTotal(BigDecimal total) {
this.total = total;
}
public StatusValues getStatus() {
return status;
}
public void setStatus(StatusValues status) {
this.status = status;
}
public Integer getPaymentId() {
return paymentId;
}
public void setPaymentId(Integer paymentId) {
this.paymentId = paymentId;
}
public Set<Integer> getInvoiceLineIds() {
return invoiceLineIds;
}
public void setInvoiceLineIds(Set<Integer> invoiceLineIds) {
this.invoiceLineIds = invoiceLineIds;
}
}

View File

@ -0,0 +1,112 @@
package com.denniseckerskorn.dtos.finance_management_dtos;
import com.denniseckerskorn.entities.finance.InvoiceLine;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
public class InvoiceLineDTO {
private Integer id;
@NotNull
private Integer invoiceId;
@NotNull
private Integer productServiceId;
private String description;
@NotNull
private Integer quantity;
@NotNull
private BigDecimal unitPrice;
private BigDecimal subtotal;
public InvoiceLineDTO() {}
public InvoiceLineDTO(InvoiceLine entity) {
this.id = entity.getId();
this.invoiceId = entity.getInvoice() != null ? entity.getInvoice().getId() : null;
this.productServiceId = entity.getProductService() != null ? entity.getProductService().getId() : null;
this.description = entity.getDescription();
this.quantity = entity.getQuantity();
this.unitPrice = entity.getUnitPrice();
this.subtotal = entity.getSubtotal();
}
public static InvoiceLineDTO fromEntity(InvoiceLine entity) {
return new InvoiceLineDTO(entity);
}
public InvoiceLine toEntity() {
InvoiceLine entity = new InvoiceLine();
entity.setId(this.id);
entity.setDescription(this.description);
entity.setQuantity(this.quantity);
entity.setUnitPrice(this.unitPrice);
entity.setSubtotal(this.subtotal);
return entity;
}
// Getters y setters...
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getInvoiceId() {
return invoiceId;
}
public void setInvoiceId(Integer invoiceId) {
this.invoiceId = invoiceId;
}
public Integer getProductServiceId() {
return productServiceId;
}
public void setProductServiceId(Integer productServiceId) {
this.productServiceId = productServiceId;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public BigDecimal getUnitPrice() {
return unitPrice;
}
public void setUnitPrice(BigDecimal unitPrice) {
this.unitPrice = unitPrice;
}
public BigDecimal getSubtotal() {
return subtotal;
}
public void setSubtotal(BigDecimal subtotal) {
this.subtotal = subtotal;
}
}

View File

@ -0,0 +1,105 @@
package com.denniseckerskorn.dtos.finance_management_dtos;
import com.denniseckerskorn.entities.finance.Payment;
import com.denniseckerskorn.enums.PaymentMethodValues;
import com.denniseckerskorn.enums.StatusValues;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class PaymentDTO {
private Integer id;
@NotNull
private Integer invoiceId;
@NotNull
private LocalDateTime paymentDate;
@NotNull
private BigDecimal amount;
@NotNull
private PaymentMethodValues paymentMethod;
@NotNull
private StatusValues status;
public PaymentDTO() {}
public PaymentDTO(Payment entity) {
this.id = entity.getId();
this.invoiceId = entity.getInvoice() != null ? entity.getInvoice().getId() : null;
this.paymentDate = entity.getPaymentDate();
this.amount = entity.getAmount();
this.paymentMethod = entity.getPaymentMethod();
this.status = entity.getStatus();
}
public static PaymentDTO fromEntity(Payment entity) {
return new PaymentDTO(entity);
}
public Payment toEntity() {
Payment payment = new Payment();
payment.setId(this.id);
payment.setPaymentDate(this.paymentDate);
payment.setAmount(this.amount);
payment.setPaymentMethod(this.paymentMethod);
payment.setStatus(this.status);
return payment;
}
// Getters y setters...
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getInvoiceId() {
return invoiceId;
}
public void setInvoiceId(Integer invoiceId) {
this.invoiceId = invoiceId;
}
public LocalDateTime getPaymentDate() {
return paymentDate;
}
public void setPaymentDate(LocalDateTime paymentDate) {
this.paymentDate = paymentDate;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public PaymentMethodValues getPaymentMethod() {
return paymentMethod;
}
public void setPaymentMethod(PaymentMethodValues paymentMethod) {
this.paymentMethod = paymentMethod;
}
public StatusValues getStatus() {
return status;
}
public void setStatus(StatusValues status) {
this.status = status;
}
}

View File

@ -0,0 +1,115 @@
package com.denniseckerskorn.dtos.finance_management_dtos;
import com.denniseckerskorn.entities.finance.ProductService;
import com.denniseckerskorn.enums.StatusValues;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
public class ProductServiceDTO {
private Integer id;
@NotNull
private Integer ivaTypeId;
@NotNull
private String name;
private String description;
@NotNull
private BigDecimal price;
@NotNull
private String type;
@NotNull
private StatusValues status;
public ProductServiceDTO() {}
public ProductServiceDTO(ProductService entity) {
this.id = entity.getId();
this.ivaTypeId = entity.getIvaType() != null ? entity.getIvaType().getId() : null;
this.name = entity.getName();
this.description = entity.getDescription();
this.price = entity.getPrice();
this.type = entity.getType();
this.status = entity.getStatus();
}
public static ProductServiceDTO fromEntity(ProductService entity) {
return new ProductServiceDTO(entity);
}
public ProductService toEntity() {
ProductService ps = new ProductService();
ps.setId(this.id);
ps.setName(this.name);
ps.setDescription(this.description);
ps.setPrice(this.price);
ps.setType(this.type);
ps.setStatus(this.status);
return ps;
}
// Getters y setters...
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getIvaTypeId() {
return ivaTypeId;
}
public void setIvaTypeId(Integer ivaTypeId) {
this.ivaTypeId = ivaTypeId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public StatusValues getStatus() {
return status;
}
public void setStatus(StatusValues status) {
this.status = status;
}
}

View File

@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,127 @@
import React, { useEffect, useState } from "react";
import InvoiceLineItem from "./InvoiceLineItem";
import api from "../../api/axiosConfig";
const InvoiceForm = () => {
const [students, setStudents] = useState([]);
const [userId, setUserId] = useState("");
const [date, setDate] = useState(new Date().toISOString().slice(0, 16));
const [lines, setLines] = useState([]);
const [products, setProducts] = useState([]);
useEffect(() => {
api.get("/students/getAll").then((res) => setStudents(res.data));
api.get("/products-services/getAll").then((res) => setProducts(res.data));
}, []);
const addLine = () => {
setLines([...lines, { productServiceId: "", quantity: 1 }]);
};
const updateLine = (index, updatedLine) => {
const newLines = [...lines];
newLines[index] = updatedLine;
setLines(newLines);
};
const removeLine = (index) => {
setLines(lines.filter((_, i) => i !== index));
};
const createInvoice = async () => {
if (!userId || lines.length === 0) {
alert("Selecciona un estudiante y al menos un producto.");
return;
}
const invoiceRes = await api.post("/invoices/create", {
user: { id: parseInt(userId) },
date,
total: 0,
status: "NOT_PAID",
invoiceLineIds: [],
});
const invoice = invoiceRes.data;
for (const line of lines) {
const product = products.find(
(p) => p.id === parseInt(line.productServiceId)
);
const subtotal = product.price * line.quantity;
await api.post(`/invoices/addLinesByInvoiceId/${invoice.id}`, {
productServiceId: parseInt(line.productServiceId),
quantity: parseInt(line.quantity),
unitPrice: product.price,
subtotal,
description: product.description,
});
}
await api.put(`/invoices/recalculateTotalOfInvoiceById/${invoice.id}`);
alert("Factura creada correctamente.");
setUserId("");
setLines([]);
};
return (
<div className="content-area">
<h2>Crear Factura</h2>
<label>Seleccionar estudiante:</label>
<select
className="form-select"
value={userId}
onChange={(e) => setUserId(e.target.value)}
>
<option value="">-- Selecciona un estudiante --</option>
{students.map((s) =>
s.user ? (
<option key={s.id} value={s.user.id}>
{s.user.name} {s.user.lastName} ({s.user.email})
</option>
) : null
)}
</select>
<label>Fecha de emisión:</label>
<input
className="form-input"
type="datetime-local"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
<h3>Productos / Servicios</h3>
{lines.map((line, i) => (
<InvoiceLineItem
key={i}
index={i}
line={line}
products={products}
onUpdate={updateLine}
onRemove={removeLine}
/>
))}
<div style={{ marginTop: "1rem" }}>
<button className="btn btn-secondary" onClick={addLine}>
+ Añadir Producto
</button>
<button
className="btn btn-primary"
onClick={createInvoice}
style={{ marginLeft: "1rem" }}
>
Crear Factura
</button>
</div>
</div>
);
};
export default InvoiceForm;

View File

@ -0,0 +1,49 @@
// src/components/forms/InvoiceLineItem.jsx
import React from 'react';
const InvoiceLineItem = ({ index, line, products, onUpdate, onRemove }) => {
const handleChange = (e) => {
onUpdate(index, { ...line, [e.target.name]: e.target.value });
};
const selectedProduct = products.find(p => p.id === parseInt(line.productServiceId));
return (
<div className="content-area" style={{ marginBottom: "1rem", display: "flex", flexDirection: "column", gap: "0.5rem" }}>
<select
name="productServiceId"
value={line.productServiceId}
onChange={handleChange}
className="form-select"
>
<option value="">Selecciona producto</option>
{products.map(p => (
<option key={p.id} value={p.id}>
{p.name} - {p.price} ({p.type})
</option>
))}
</select>
<input
type="number"
name="quantity"
min="1"
value={line.quantity}
onChange={handleChange}
className="form-input"
placeholder="Cantidad"
/>
{selectedProduct && (
<div style={{ fontSize: '0.9rem', color: '#666' }}>
<strong>{selectedProduct.name}</strong>: {selectedProduct.description}
</div>
)}
<button className="btn btn-danger" onClick={() => onRemove(index)}>Eliminar producto</button>
</div>
);
};
export default InvoiceLineItem;

View File

@ -1,6 +1,6 @@
import React, { useState } from "react";
import api from "../../api/axiosConfig";
import "../styles/Login.css"
import "../styles/Login.css";
const LoginForm = ({ onLoginSuccess }) => {
const [email, setEmail] = useState('');
@ -12,8 +12,15 @@ const LoginForm = ({ onLoginSuccess }) => {
setError('');
try {
const res = await api.post('/auth/login', { email, password });
localStorage.setItem('token', res.data.token);
console.log("Token guardado:", res.data.token);
const token = res.data.token;
localStorage.setItem('token', token);
console.log("Token guardado:", token);
const profileRes = await api.get('/users/me');
const roleName = profileRes.data.roleName;
console.log("Rol del usuario:", roleName);
localStorage.setItem('roleName', roleName);
onLoginSuccess();
} catch (err) {
console.error("Error al iniciar sesión:", err);

View File

@ -4,9 +4,11 @@ import '../styles/ContentArea.css';
import UserForm from '../forms/UserForm';
import StudentForm from '../forms/StudentForm';
import TeacherForm from '../forms/TeacherForm';
import { getRoleFromToken } from '../../utils/jwtHelper';
const ContentArea = ({ section, entity, action }) => {
// Si falta info, mostramos encabezado básico
const role = getRoleFromToken();
if (!section || !entity || !action) {
return (
<div className="content-area">
@ -16,7 +18,26 @@ const ContentArea = ({ section, entity, action }) => {
);
}
// Sección de User Management
// Definir qué roles tienen acceso a qué entidades
const permissions = {
FULL_ACCESS: ['Users', 'Students', 'Teachers', 'Admin', 'Notifications', 'Assistance', 'TrainingGroups', 'TrainingSessions', 'Memberships', 'StudentHistories'],
MANAGE_STUDENTS: ['Students', 'Assistance', 'TrainingGroups', 'TrainingSessions'],
VIEW_OWN_DATA: ['Assistance', 'Timetable', 'History'],
};
// Validar si tiene acceso a la entidad actual
const hasPermission = permissions[role]?.includes(entity);
if (!hasPermission) {
return (
<div className="content-area">
<h2>Acceso denegado</h2>
<p>No tienes permiso para acceder a esta sección.</p>
</div>
);
}
// Sección de gestión de usuarios
if (section === 'User Management') {
if (action === 'create') {
switch (entity) {
@ -42,7 +63,6 @@ const ContentArea = ({ section, entity, action }) => {
);
}
// Puedes continuar con update, delete, findById, etc.
return (
<div className="content-area">
<h2>{action} {entity}</h2>
@ -51,7 +71,6 @@ const ContentArea = ({ section, entity, action }) => {
);
}
// Si no se reconoce la sección
return (
<div className="content-area">
<h2>{section}</h2>

View File

@ -22,6 +22,7 @@ import TrainingSessionList from '../lists/TrainingSessionList';
import ViewTimetable from '../forms/ViewTimetable';
import MembershipForm from '../forms/MembershipCreateForm';
import MembershipList from '../lists/MembershipList';
import InvoiceForm from '../forms/InvoiceForm';
import '../styles/MainLayout.css';
@ -52,6 +53,7 @@ const MainLayout = () => {
{/* ContentArea general para secciones sin componentes específicos */}
<Route path="/admin/user-management/*" element={<ContentArea />} />
<Route path="/admin/class-management/*" element={<ContentArea />} />
<Route path="/admin/finance/*" element={<ContentArea />} />
{/*Class Management - rutas específicas*/}
<Route path="/admin/class-management/training-groups/create" element={<TrainingGroupForm />} />
@ -66,12 +68,7 @@ const MainLayout = () => {
<Route path="/admin/finance/*" element={<ContentArea />} />
<Route path="/admin/finance/invoices/create" element={<InvoiceForm />} />
{/* Profile Page*/}
<Route path="/profile" element={<ProfilePage />} />

View File

@ -1,77 +1,45 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { getRoleFromToken } from '../../utils/jwtHelper';
import '../styles/Sidebar.css';
import React, { useState, useEffect } from 'react';
import SidebarAdmin from './SidebarAdmin';
import SidebarTeacher from './SidebarTeacher';
import SidebarStudent from './SidebarStudent';
const Sidebar = () => {
const navigate = useNavigate();
const role = getRoleFromToken();
const [role, setRole] = useState(null);
useEffect(() => {
const interval = setInterval(() => {
const storedRole = localStorage.getItem('roleName');
if (storedRole) {
setRole(storedRole);
clearInterval(interval);
}
}, 100); // intenta cada 100ms
return () => clearInterval(interval);
}, []);
if (!role) {
return (
<div className="sidebar">
<h4>Menú</h4>
<div className='sidebar-scroll'>
{/* Botón común para todos: Mi Perfil */}
<button onClick={() => navigate('/profile')}>👤 Mi Perfil</button>
{role === 'FULL_ACCESS' && (
<>
<button onClick={() => navigate('/admin/dashboard')}>🏠 Dashboard Admin</button>
<div className="submenu">
<h4>👥 Administración de Usuarios</h4>
<button onClick={() => navigate('/admin/user-management/users/create')}> Crear Usuario</button>
<button onClick={() => navigate('/admin/user-management/users/list')}>📋 Ver Usuarios</button>
<button onClick={() => navigate('/admin/user-management/notifications/create')}>🔔 Crear Notificación</button>
<button onClick={() => navigate('/admin/user-management/notifications/list')}>📨 Ver Notificaciones</button>
<button onClick={() => navigate('/admin/user-management/student-history/create')}>🕒 Crear Historial</button>
<button onClick={() => navigate('/admin/user-management/student-history/list')}>📜 Ver Historial</button>
</div>
<div className="submenu">
<h4>📚 Administración de Clases</h4>
<button onClick={() => navigate('/admin/class-management/training-groups/create')}> Crear Grupo</button>
<button onClick={() => navigate('/admin/class-management/training-groups/list')}>👥 Ver Grupos</button>
<button onClick={() => navigate('/admin/class-management/training-groups/manage-students')}>🧑🏫 Administrar Grupos</button>
<button onClick={() => navigate('/admin/class-management/training-groups/view-timetable')}>🗓 Ver Horario</button>
<button onClick={() => navigate('/admin/class-management/training-session/list')}>📆 Ver Sesiones</button>
<button onClick={() => navigate('/admin/class-management/assistance/create')}>📝 Registrar Asistencia</button>
<button onClick={() => navigate('/admin/class-management/assistance/list')}>📋 Ver Asistencias</button>
<button onClick={() => navigate('/admin/class-management/memberships/create')}> Crear Membresía</button>
<button onClick={() => navigate('/admin/class-management/memberships/list')}>🏷 Ver Membresías</button>
</div>
<div className='submenu'>
<h4>💵 Administración de Pagos y Facturas</h4>
<button onClick={() => navigate('')}>🧾 Crear Factura</button>
<button onClick={() => navigate('')}>📄 Ver Facturas</button>
<button onClick={() => navigate('')}>💳 Nuevo Pago</button>
<button onClick={() => navigate('')}>🛒 Añadir Productos</button>
<button onClick={() => navigate('')}>📦 Ver Productos</button>
<button onClick={() => navigate('')}>💱 Añadir Tipo de IVA</button>
</div>
</>
)}
{role === 'MANAGE_STUDENTS' && (
<>
<button onClick={() => navigate('/teacher/dashboard')}>🏠 Dashboard Profesor</button>
<button onClick={() => navigate('/teacher/students')}>👨🎓 Mis Estudiantes</button>
<button onClick={() => navigate('/teacher/classes')}>📚 Mis Clases</button>
</>
)}
{role === 'VIEW_OWN_DATA' && (
<>
<button onClick={() => navigate('/student/dashboard')}>🏠 Mi Panel</button>
<button onClick={() => navigate('/student/history')}>🕒 Mi Historial</button>
</>
)}
</div>
<p>Cargando menú...</p>
</div>
);
}
switch (role) {
case 'ROLE_ADMIN':
return <SidebarAdmin />;
case 'ROLE_TEACHER':
return <SidebarTeacher />;
case 'ROLE_STUDENT':
return <SidebarStudent />;
default:
return (
<div className="sidebar">
<p>Sin acceso</p>
</div>
);
}
};
export default Sidebar;

View File

@ -0,0 +1,52 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import '../styles/Sidebar.css';
const SidebarAdmin = () => {
const navigate = useNavigate();
return (
<div className="sidebar">
<h4>Menú</h4>
<div className="sidebar-scroll">
<button onClick={() => navigate('/profile')}>👤 Mi Perfil</button>
<button onClick={() => navigate('/admin/dashboard')}>🏠 Dashboard Admin</button>
<div className="submenu">
<h4>👥 Administración de Usuarios</h4>
<button onClick={() => navigate('/admin/user-management/users/create')}> Crear Usuario</button>
<button onClick={() => navigate('/admin/user-management/users/list')}>📋 Ve Usuarios</button>
<button onClick={() => navigate('/admin/user-management/notifications/create')}>🔔 Crear Notificación</button>
<button onClick={() => navigate('/admin/user-management/notifications/list')}>📨 Ver Notificaciones</button>
<button onClick={() => navigate('/admin/user-management/student-history/create')}>🕒 Crear Historial</button>
<button onClick={() => navigate('/admin/user-management/student-history/list')}>📜 Ver Historial</button>
</div>
<div className="submenu">
<h4>📚 Administración de Clases</h4>
<button onClick={() => navigate('/admin/class-management/training-groups/create')}> Crear Grupo</button>
<button onClick={() => navigate('/admin/class-management/training-groups/list')}>👥 Ver Grupos</button>
<button onClick={() => navigate('/admin/class-management/training-groups/manage-students')}>🧑🏫 Administrar Grupos</button>
<button onClick={() => navigate('/admin/class-management/training-groups/view-timetable')}>🗓 Ver Horario</button>
<button onClick={() => navigate('/admin/class-management/training-session/list')}>📆 Ver Sesiones</button>
<button onClick={() => navigate('/admin/class-management/assistance/create')}>📝 Registrar Asistencia</button>
<button onClick={() => navigate('/admin/class-management/assistance/list')}>📋 Ver Asistencias</button>
<button onClick={() => navigate('/admin/class-management/memberships/create')}> Crear Membresía</button>
<button onClick={() => navigate('/admin/class-management/memberships/list')}>🏷 Ver Membresías</button>
</div>
<div className="submenu">
<h4>💵 Finanzas</h4>
<button onClick={() => navigate('/admin/finance/invoices/create')}>🧾 Crear Factura</button>
<button onClick={() => navigate('/admin/finance/invoices/list')}>📄 Ver Facturas</button>
<button onClick={() => navigate('/admin/finance/payments/create')}>💳 Nuevo Pago</button>
<button onClick={() => navigate('/admin/finance/products/create')}>🛒 Añadir Productos</button>
<button onClick={() => navigate('/admin/finance/products/list')}>📦 Ver Productos</button>
<button onClick={() => navigate('/admin/finance/ivatypes/create')}>💱 Añadir Tipo de IVA</button>
</div>
</div>
</div>
);
};
export default SidebarAdmin;

View File

@ -0,0 +1,26 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import '../styles/Sidebar.css';
const SidebarStudent = () => {
const navigate = useNavigate();
return (
<div className="sidebar">
<h4>Menú</h4>
<div className="sidebar-scroll">
<button onClick={() => navigate('/profile')}>👤 Mi Perfil</button>
<button onClick={() => navigate('/student/dashboard')}>🏠 Mi Panel</button>
<div className="submenu">
<h4>🎓 Opciones de Estudiante</h4>
<button onClick={() => navigate('/student/history')}>🕒 Mi Historial</button>
<button onClick={() => navigate('/student/timetable')}>📅 Ver Horario</button>
<button onClick={() => navigate('/student/assistance')}>📋 Ver Asistencia</button>
</div>
</div>
</div>
);
};
export default SidebarStudent;

View File

@ -0,0 +1,25 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import '../styles/Sidebar.css';
const SidebarTeacher = () => {
const navigate = useNavigate();
return (
<div className="sidebar">
<h4>Menú</h4>
<div className="sidebar-scroll">
<button onClick={() => navigate('/profile')}>👤 Mi Perfil</button>
<button onClick={() => navigate('/teacher/dashboard')}>🏠 Dashboard Profesor</button>
<div className="submenu">
<h4>👨🏫 Gestión del Profesor</h4>
<button onClick={() => navigate('/teacher/students')}>👨🎓 Mis Estudiantes</button>
<button onClick={() => navigate('/teacher/classes')}>📚 Mis Clases</button>
</div>
</div>
</div>
);
};
export default SidebarTeacher;

View File

@ -9,8 +9,10 @@ const Topbar = () => {
const handleLogout = () => {
localStorage.removeItem('token');
localStorage.removeItem('roleName');
navigate('/');
};
};
const roleLabel = {
FULL_ACCESS: 'Admin',

View File

@ -1,6 +1,6 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import '../../components/styles/DashboardCards.css'; // lo creamos ahora
import '../../components/styles/DashboardCards.css';
const AdminDashboard = () => {
const navigate = useNavigate();

View File

@ -13,3 +13,8 @@ export const getRoleFromToken = () => {
return null;
}
};
export const getRoleFromStorage = () => {
return localStorage.getItem('roleName');
};