feat: add personajes page with level tracking and fix timer reactivity
CI / Build Native (push) Failing after 58s
CI / Build Native (push) Failing after 58s
- New /personajes page with CRUD for characters (name, class, level) - Level editable inline, deletable (blocked if has active sales) - ventas.html now loads characters list as datalist suggestions - Fix timer: add reactive 'now' property so countdown updates - Fix script loading: add defaults.js + app.js to ventas/personajes - Fix table auto-reload: use loadCharacters()/loadSales() after mutations - Timer now uses setTimeout recursion + visibilitychange handler
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
package com.l2.shots.characters;
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "characters", indexes = {
|
||||
@Index(name = "idx_characters_user", columnList = "user_id")
|
||||
})
|
||||
public class Character extends PanacheEntityBase {
|
||||
|
||||
@Id
|
||||
public UUID id;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
public UUID userId;
|
||||
|
||||
@Column(name = "name", nullable = false, length = 50)
|
||||
public String name;
|
||||
|
||||
@Column(name = "character_class", nullable = false, length = 30)
|
||||
public String characterClass;
|
||||
|
||||
@Column(name = "level", nullable = false)
|
||||
public int level;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
public Instant createdAt;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.l2.shots.characters;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
@RegisterForReflection
|
||||
public record CharacterDto(
|
||||
UUID id,
|
||||
String name,
|
||||
String characterClass,
|
||||
int level,
|
||||
Instant createdAt) {
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.l2.shots.characters;
|
||||
|
||||
public class CharacterHasSalesException extends RuntimeException {
|
||||
public CharacterHasSalesException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.l2.shots.characters;
|
||||
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection;
|
||||
|
||||
@RegisterForReflection
|
||||
public record CharacterIn(
|
||||
String name,
|
||||
String characterClass,
|
||||
int level) {
|
||||
|
||||
public CharacterIn {
|
||||
if (name == null || name.isBlank()) {
|
||||
throw new IllegalArgumentException("name is required");
|
||||
}
|
||||
if (name.length() > 50) {
|
||||
throw new IllegalArgumentException("name must be 50 chars or less");
|
||||
}
|
||||
if (characterClass == null || characterClass.isBlank()) {
|
||||
throw new IllegalArgumentException("characterClass is required");
|
||||
}
|
||||
if (characterClass.length() > 30) {
|
||||
throw new IllegalArgumentException("characterClass must be 30 chars or less");
|
||||
}
|
||||
if (level <= 0) {
|
||||
throw new IllegalArgumentException("level must be greater than 0");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.l2.shots.characters;
|
||||
|
||||
import com.l2.shots.auth.JwtCookieAuth;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.PUT;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.jwt.JsonWebToken;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Path("/api/characters")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class CharacterResource {
|
||||
|
||||
@Inject
|
||||
CharacterService characterService;
|
||||
|
||||
@Inject
|
||||
JwtCookieAuth jwtCookieAuth;
|
||||
|
||||
@GET
|
||||
public Response list(@Context HttpHeaders headers) {
|
||||
Optional<UUID> userId = extractUserId(headers);
|
||||
if (userId.isEmpty()) return Response.status(401).build();
|
||||
|
||||
List<CharacterDto> characters = characterService.listForUser(userId.get());
|
||||
return Response.ok(characters).build();
|
||||
}
|
||||
|
||||
@POST
|
||||
public Response create(@Context HttpHeaders headers, CharacterIn input) {
|
||||
Optional<UUID> userId = extractUserId(headers);
|
||||
if (userId.isEmpty()) return Response.status(401).build();
|
||||
|
||||
if (input == null) {
|
||||
return Response.status(400).entity("{\"error\":\"payload inválido\"}").build();
|
||||
}
|
||||
try {
|
||||
CharacterIn validated = new CharacterIn(
|
||||
input.name(),
|
||||
input.characterClass(),
|
||||
input.level());
|
||||
CharacterDto created = characterService.create(userId.get(), validated);
|
||||
return Response.status(201).entity(created).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(400).entity("{\"error\":\"" + e.getMessage() + "\"}").build();
|
||||
}
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/{id}/level")
|
||||
public Response updateLevel(@Context HttpHeaders headers,
|
||||
@PathParam("id") String idStr,
|
||||
UpdateLevelRequest body) {
|
||||
Optional<UUID> userId = extractUserId(headers);
|
||||
if (userId.isEmpty()) return Response.status(401).build();
|
||||
|
||||
UUID id;
|
||||
try {
|
||||
id = UUID.fromString(idStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(400).entity("{\"error\":\"id inválido\"}").build();
|
||||
}
|
||||
|
||||
if (body == null || body.level() <= 0) {
|
||||
return Response.status(400).entity("{\"error\":\"level debe ser mayor a 0\"}").build();
|
||||
}
|
||||
|
||||
Optional<CharacterDto> updated = characterService.updateLevel(userId.get(), id, body.level());
|
||||
return updated.map(c -> Response.ok(c).build())
|
||||
.orElse(Response.status(404).build());
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/{id}")
|
||||
public Response delete(@Context HttpHeaders headers, @PathParam("id") String idStr) {
|
||||
Optional<UUID> userId = extractUserId(headers);
|
||||
if (userId.isEmpty()) return Response.status(401).build();
|
||||
|
||||
UUID id;
|
||||
try {
|
||||
id = UUID.fromString(idStr);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Response.status(400).entity("{\"error\":\"id inválido\"}").build();
|
||||
}
|
||||
|
||||
try {
|
||||
characterService.delete(userId.get(), id);
|
||||
return Response.noContent().build();
|
||||
} catch (CharacterHasSalesException e) {
|
||||
return Response.status(409).entity("{\"error\":\"" + e.getMessage() + "\"}").build();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<UUID> extractUserId(HttpHeaders headers) {
|
||||
Optional<JsonWebToken> jwt = jwtCookieAuth.extractToken(headers);
|
||||
if (jwt.isEmpty()) return Optional.empty();
|
||||
try {
|
||||
return Optional.of(UUID.fromString(jwt.get().getSubject()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public record UpdateLevelRequest(int level) {}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.l2.shots.characters;
|
||||
|
||||
import com.l2.shots.sales.CharacterSale;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@ApplicationScoped
|
||||
public class CharacterService {
|
||||
|
||||
public List<CharacterDto> listForUser(UUID userId) {
|
||||
return Character.<Character>list("userId = ?1 ORDER BY name ASC", userId)
|
||||
.stream()
|
||||
.map(this::toDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public Optional<CharacterDto> getById(UUID userId, UUID id) {
|
||||
Character entity = Character.find("id = ?1 AND userId = ?2", id, userId).firstResult();
|
||||
if (entity == null) return Optional.empty();
|
||||
return Optional.of(toDto(entity));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CharacterDto create(UUID userId, CharacterIn input) {
|
||||
Character entity = new Character();
|
||||
entity.id = UUID.randomUUID();
|
||||
entity.userId = userId;
|
||||
entity.name = input.name();
|
||||
entity.characterClass = input.characterClass();
|
||||
entity.level = input.level();
|
||||
entity.createdAt = Instant.now();
|
||||
entity.persist();
|
||||
return toDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Optional<CharacterDto> updateLevel(UUID userId, UUID id, int newLevel) {
|
||||
Character entity = Character.find("id = ?1 AND userId = ?2", id, userId).firstResult();
|
||||
if (entity == null) return Optional.empty();
|
||||
entity.level = newLevel;
|
||||
return Optional.of(toDto(entity));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(UUID userId, UUID id) {
|
||||
Character entity = Character.find("id = ?1 AND userId = ?2", id, userId).firstResult();
|
||||
if (entity == null) return;
|
||||
|
||||
boolean hasSales = CharacterSale.count("userId = ?1 AND characterName = ?2",
|
||||
userId, entity.name) > 0;
|
||||
if (hasSales) {
|
||||
throw new CharacterHasSalesException(
|
||||
"No se puede eliminar el personaje porque tiene ventas asociadas");
|
||||
}
|
||||
|
||||
entity.delete();
|
||||
}
|
||||
|
||||
private CharacterDto toDto(Character entity) {
|
||||
return new CharacterDto(
|
||||
entity.id,
|
||||
entity.name,
|
||||
entity.characterClass,
|
||||
entity.level,
|
||||
entity.createdAt);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ public class PageResource {
|
||||
public static native TemplateInstance historial();
|
||||
public static native TemplateInstance usuarios();
|
||||
public static native TemplateInstance ventas();
|
||||
public static native TemplateInstance personajes();
|
||||
}
|
||||
|
||||
private Optional<AuthMeResponse> currentUser(HttpHeaders headers) {
|
||||
@@ -170,4 +171,17 @@ public class PageResource {
|
||||
}
|
||||
return Response.ok(Pages.ventas().render()).build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/personajes")
|
||||
public Response personajes(@Context HttpHeaders headers) {
|
||||
Optional<AuthMeResponse> user = currentUser(headers);
|
||||
if (user.isEmpty()) {
|
||||
return Response.seeOther(URI.create("/login")).build();
|
||||
}
|
||||
if (Boolean.TRUE.equals(user.get().mustChangePassword())) {
|
||||
return Response.seeOther(URI.create("/change-password")).build();
|
||||
}
|
||||
return Response.ok(Pages.personajes().render()).build();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user