diff --git a/src/main/java/be/seeseepuff/pcinv/controllers/WebController.java b/src/main/java/be/seeseepuff/pcinv/controllers/WebController.java index 2ae1425..a0573d2 100644 --- a/src/main/java/be/seeseepuff/pcinv/controllers/WebController.java +++ b/src/main/java/be/seeseepuff/pcinv/controllers/WebController.java @@ -2,6 +2,7 @@ package be.seeseepuff.pcinv.controllers; import be.seeseepuff.pcinv.models.WorkLogEntry; import be.seeseepuff.pcinv.services.AssetService; +import be.seeseepuff.pcinv.services.BuildService; import lombok.RequiredArgsConstructor; import org.springframework.data.repository.query.Param; import org.springframework.http.MediaType; @@ -42,6 +43,12 @@ public class WebController { private static final String INPUT_LIST = "inputLists"; /// The name of the model attribute that holds the current work log entries. private static final String WORKLOG = "worklog"; + /// The name of the model attribute that holds the current build being viewed or edited. + private static final String BUILD = "build"; + /// The name of the model attribute that holds the build information. + private static final String BUILD_INFO = "buildInfo"; + /// The name of the model attribute that holds the builds available for selection. + private static final String BUILDS = "builds"; /// The name of the input field for the current size of the work log. private static final String WORKLOG_SIZE = "worklog_size"; @@ -49,6 +56,7 @@ public class WebController { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd MMM yyyy 'at' HH:mm"); private final AssetService assetService; + private final BuildService buildService; /** * Handles the root URL and returns the index page with asset descriptors and asset count. @@ -90,6 +98,54 @@ public class WebController { return "browse_type"; } + /** + * Handles the browsing of all builds. + * Displays a list of all builds available in the system. + */ + @GetMapping("/builds") + public String browseBuilds(Model model) { + model.addAttribute(TIME, System.currentTimeMillis()); + model.addAttribute(BUILDS, buildService.getAllBuilds()); + return "builds"; + } + + /** + * Handles the viewing of a specific build by its ID. + * If the build does not exist, it redirects to the builds page. + */ + @GetMapping("/build/{id}") + public String viewBuild(Model model, @PathVariable long id) { + model.addAttribute(TIME, System.currentTimeMillis()); + var build = buildService.getBuildById(id); + if (build == null) { + return "redirect:/builds"; + } + model.addAttribute(BUILD, build); + model.addAttribute(BUILD_INFO, buildService.getBuildInfo(build)); + model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors()); + return "build_view"; + } + + /** + * Handles the creation of a new build. + */ + @PostMapping("/create_build") + public String createBuild(Model model, @RequestBody MultiValueMap formData) { + model.addAttribute(TIME, System.currentTimeMillis()); + var build = buildService.createBuild(formData.getFirst("name"), formData.getFirst("description")); + return "redirect:/build/" + build.getId(); + } + + /** + * Deletes a build by its ID. + */ + @GetMapping("/delete_build/{id}") + public String deleteBuild(Model model, @PathVariable long id) { + model.addAttribute(TIME, System.currentTimeMillis()); + buildService.deleteBuild(id); + return "redirect:/builds"; + } + /** * Handles the view of an asset by its QR code. * If the asset does not exist, it redirects to the index page. @@ -202,6 +258,7 @@ public class WebController { model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(type)); model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(type)); model.addAttribute(INPUT_LIST, assetService.getInputList(type)); + model.addAttribute(BUILDS, buildService.getAllBuilds()); return "create_asset"; } @@ -223,6 +280,7 @@ public class WebController { model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(assetType)); model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(assetType)); model.addAttribute(INPUT_LIST, assetService.getInputList(assetType)); + model.addAttribute(BUILDS, buildService.getAllBuilds()); return "create_asset"; } diff --git a/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptor.java b/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptor.java index ce59f3f..1cde430 100644 --- a/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptor.java +++ b/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptor.java @@ -99,6 +99,19 @@ public class AssetDescriptor { return String.format("%s-%s", type, property.getName()); } + /** + * Gets the property with the specified name. + * + * @param name The name of the property to retrieve. + * @return The AssetProperty with the given name. + */ + public AssetProperty getProperty(String name) { + return properties.stream() + .filter(property -> property.getName().equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No property found with name: " + name)); + } + /** * Creates a new instance of the asset type described by this descriptor. * diff --git a/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptors.java b/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptors.java index fce5d91..293af72 100644 --- a/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptors.java +++ b/src/main/java/be/seeseepuff/pcinv/meta/AssetDescriptors.java @@ -24,6 +24,37 @@ public class AssetDescriptors { assets.add(property); } + /** + * Gets the descriptor for a specific asset type. + * + * @param type The type of the asset to retrieve the descriptor for. + */ + public AssetDescriptor getDescriptorForType(String type) { + return assets.stream() + .filter(assetDescriptor -> assetDescriptor.getType().equals(type)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No asset descriptor found for type: " + type)); + } + + /** + * Gets the property for a specific asset type and property name. + * + * @param type The type of the asset to retrieve the property for. + * @param propertyName The name of the property to retrieve. + */ + public AssetProperty getPropertyForType(String type, String propertyName) { + return getDescriptorForType(type).getProperty(propertyName); + } + + /** + * Gets the generic property for a specific property name. + * + * @param propertyName The name of the property to retrieve. + */ + public AssetProperty getGenericProperty(String propertyName) { + return getPropertyForType("asset", propertyName); + } + @Override public String toString() { var builder = new StringBuilder(); diff --git a/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java b/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java index 1415eb5..f657340 100644 --- a/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java +++ b/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java @@ -1,12 +1,8 @@ package be.seeseepuff.pcinv.meta; -import be.seeseepuff.pcinv.models.Asset; -import be.seeseepuff.pcinv.models.AssetCondition; -import be.seeseepuff.pcinv.models.GenericAsset; -import be.seeseepuff.pcinv.models.ReadWrite; +import be.seeseepuff.pcinv.models.*; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.Singular; @@ -28,7 +24,7 @@ public class AssetProperty { /// The name of the property as it should be displayed, e.g., "Brand", "Model", etc. private final String displayName; /// The type of the property, which can be a string or an integer. - private final Type type; + private final PropertyType type; /// Whether the property is required for the asset. private final boolean required; /// A set of options for the property, used for enum types. @@ -49,31 +45,6 @@ public class AssetProperty { /// A description of the property, if any. private final String description; - /** - * Enum representing the possible types of asset properties. - */ - @AllArgsConstructor - public enum Type { - STRING(false), - INTEGER(false), - BOOLEAN(false), - CAPACITY(false), - CONDITION(true), - READWRITE(true), - ; - /// Set to `true` if the type is an enum, `false` otherwise. - public final boolean isEnum; - - /** - * Returns the name of the type, or "enum" if it is an enum type. - * - * @return The name of the type or "enum" if it is an enum. - */ - public String nameOrEnum() { - return isEnum ? "enum" : name(); - } - } - /** * Loads an AssetProperty from a given field. * @@ -131,7 +102,7 @@ public class AssetProperty { builder.option(option); } } - if (type == Type.CAPACITY) { + if (type == PropertyType.CAPACITY) { var capacityAnnotation = property.getAnnotation(Capacity.class); builder.capacityAsSI(capacityAnnotation.si()); builder.capacityAsIEC(capacityAnnotation.iec()); @@ -146,19 +117,21 @@ public class AssetProperty { * @return The type of the property. * @throws IllegalArgumentException if the property type is unsupported. */ - private static Type determineType(Field property) { + private static PropertyType determineType(Field property) { if (property.getType() == String.class) { - return Type.STRING; + return PropertyType.STRING; } else if (property.isAnnotationPresent(Capacity.class)) { - return Type.CAPACITY; + return PropertyType.CAPACITY; } else if (property.getType() == Integer.class || property.getType() == int.class || property.getType() == Long.class || property.getType() == long.class) { - return Type.INTEGER; + return PropertyType.INTEGER; } else if (property.getType() == Boolean.class || property.getType() == boolean.class) { - return Type.BOOLEAN; + return PropertyType.BOOLEAN; } else if (property.getType() == AssetCondition.class) { - return Type.CONDITION; + return PropertyType.CONDITION; } else if (property.getType() == ReadWrite.class) { - return Type.READWRITE; + return PropertyType.READWRITE; + } else if (property.getType() == Build.class) { + return PropertyType.BUILD; } else { throw new IllegalArgumentException("Unsupported property type: " + property.getType()); } @@ -204,12 +177,15 @@ public class AssetProperty { var value = getValue(asset); if (value == null) { return "?"; - } else if (type == Type.BOOLEAN) { + } else if (type == PropertyType.BOOLEAN) { return (boolean) value ? "Yes" : "No"; - } else if (type == Type.INTEGER || type == Type.STRING) { + } else if (type == PropertyType.INTEGER || type == PropertyType.STRING) { return value.toString(); - } else if (type == Type.CAPACITY) { + } else if (type == PropertyType.CAPACITY) { return convertCapacity((Long) value).toString(); + } else if (type == PropertyType.BUILD) { + var build = (Build) value; + return build.getName(); } else if (type.isEnum) { if (value instanceof AssetEnum assetEnum) { return assetEnum.getDisplayName(); @@ -224,7 +200,7 @@ public class AssetProperty { if (value == null) { return null; } - if (type != Type.CAPACITY) { + if (type != PropertyType.CAPACITY) { throw new IllegalStateException("Property '" + name + "' is not a capacity type."); } return CapacityInfo.of(value, capacityAsIEC, capacityAsSI); diff --git a/src/main/java/be/seeseepuff/pcinv/meta/PropertyType.java b/src/main/java/be/seeseepuff/pcinv/meta/PropertyType.java new file mode 100644 index 0000000..697c491 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/meta/PropertyType.java @@ -0,0 +1,31 @@ +package be.seeseepuff.pcinv.meta; + +import lombok.AllArgsConstructor; + +/** + * Enum representing the possible types of asset properties. + */ +@AllArgsConstructor +public enum PropertyType +{ + STRING(false), + INTEGER(false), + BOOLEAN(false), + CAPACITY(false), + CONDITION(true), + READWRITE(true), + BUILD(false), + ; + /// Set to `true` if the type is an enum, `false` otherwise. + public final boolean isEnum; + + /** + * Returns the name of the type, or "enum" if it is an enum type. + * + * @return The name of the type or "enum" if it is an enum. + */ + public String nameOrEnum() + { + return isEnum ? "enum" : name(); + } +} diff --git a/src/main/java/be/seeseepuff/pcinv/models/Build.java b/src/main/java/be/seeseepuff/pcinv/models/Build.java new file mode 100644 index 0000000..bd6893b --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/models/Build.java @@ -0,0 +1,50 @@ +package be.seeseepuff.pcinv.models; + +import jakarta.persistence.*; +import lombok.*; + +import java.util.List; + +@Getter +@Setter +@Builder +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Table( + name = "builds", + uniqueConstraints = @UniqueConstraint(columnNames = "name") +) +public class Build +{ + @Id + @GeneratedValue + private long id; + + /** + * Indicates whether this build is a meta build. + * A meta build is a build that does not represent a physical computer, + * but rather a collection of parts that can be used in other builds. + * + * It is used internally to represents parts that are explicitly not part of a build. + */ + private boolean meta; + + /** + * The name of the build. + */ + private String name; + + /** + * A description of the build. + * This can be used to provide additional information about the build. + */ + private String description; + + /** + * A list of parts that are included in the build. + */ + @OneToMany(mappedBy = "build") + @OrderBy("type, brand, model, qr") + private List parts; +} diff --git a/src/main/java/be/seeseepuff/pcinv/models/BuildInfo.java b/src/main/java/be/seeseepuff/pcinv/models/BuildInfo.java new file mode 100644 index 0000000..a15a1e3 --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/models/BuildInfo.java @@ -0,0 +1,20 @@ +package be.seeseepuff.pcinv.models; + +import be.seeseepuff.pcinv.meta.CapacityInfo; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class BuildInfo { + private long totalRam; + + /** + * Calculates the total RAM capacity of the build. + * + * @return A CapacityInfo object representing the total RAM capacity. + */ + public CapacityInfo getTotalRamCapacity() { + return CapacityInfo.of(totalRam); + } +} diff --git a/src/main/java/be/seeseepuff/pcinv/models/GenericAsset.java b/src/main/java/be/seeseepuff/pcinv/models/GenericAsset.java index 2cf7c2a..9b517f2 100644 --- a/src/main/java/be/seeseepuff/pcinv/models/GenericAsset.java +++ b/src/main/java/be/seeseepuff/pcinv/models/GenericAsset.java @@ -73,4 +73,9 @@ public class GenericAsset @OneToMany(mappedBy = "asset", cascade = CascadeType.ALL, orphanRemoval = true) private List workLog; + + @ManyToOne + @Property("Part of build") + @Description("Select which build this asset is placed in.") + private Build build; } diff --git a/src/main/java/be/seeseepuff/pcinv/repositories/BuildRepository.java b/src/main/java/be/seeseepuff/pcinv/repositories/BuildRepository.java new file mode 100644 index 0000000..957a45e --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/repositories/BuildRepository.java @@ -0,0 +1,17 @@ +package be.seeseepuff.pcinv.repositories; + +import be.seeseepuff.pcinv.models.Build; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface BuildRepository extends JpaRepository +{ + Build getBuildByNameAndMeta(String name, boolean meta); + + Build getBuildById(long id); + + List findAllByMeta(boolean meta); +} diff --git a/src/main/java/be/seeseepuff/pcinv/services/AssetService.java b/src/main/java/be/seeseepuff/pcinv/services/AssetService.java index bf07927..1ceb2cb 100644 --- a/src/main/java/be/seeseepuff/pcinv/services/AssetService.java +++ b/src/main/java/be/seeseepuff/pcinv/services/AssetService.java @@ -1,9 +1,6 @@ package be.seeseepuff.pcinv.services; -import be.seeseepuff.pcinv.meta.AssetDescriptor; -import be.seeseepuff.pcinv.meta.AssetDescriptors; -import be.seeseepuff.pcinv.meta.AssetInfo; -import be.seeseepuff.pcinv.meta.AssetProperty; +import be.seeseepuff.pcinv.meta.*; import be.seeseepuff.pcinv.models.Asset; import be.seeseepuff.pcinv.models.GenericAsset; import be.seeseepuff.pcinv.models.WorkLogEntry; @@ -31,6 +28,7 @@ public class AssetService { private final WorkLogRepository workLogRepository; private final Collection> repositories; private final EntityManager entityManager; + private final BuildService buildService; /** * Returns the count of all assets in the repository. @@ -225,7 +223,7 @@ public class AssetService { * @return The parsed value as an Object. */ private Object parseValue(AssetDescriptor descriptor, AssetProperty property, Map values) { - if (property.getType() == AssetProperty.Type.CAPACITY) { + if (property.getType() == PropertyType.CAPACITY) { var value = values.get(descriptor.asString(property) + "-value"); var unit = values.get(descriptor.asString(property) + "-unit"); if (value == null || value.isBlank() || unit == null || unit.isBlank()) { @@ -244,16 +242,22 @@ public class AssetService { return null; } - if (property.getType() == AssetProperty.Type.INTEGER) { + if (property.getType() == PropertyType.INTEGER) { return Integer.parseInt(stringValue); - } else if (property.getType() == AssetProperty.Type.STRING) { + } else if (property.getType() == PropertyType.STRING) { return stringValue; - } else if (property.getType() == AssetProperty.Type.BOOLEAN) { + } else if (property.getType() == PropertyType.BOOLEAN) { return switch (stringValue.toLowerCase()) { case "true" -> true; case "false" -> false; default -> null; }; + } else if (property.getType() == PropertyType.BUILD) { + var build = buildService.getBuildById(Integer.parseInt(stringValue)); + if (build == null) { + throw new IllegalArgumentException("Invalid build ID for property '" + property.getName() + "': " + stringValue); + } + return build; } else if (property.getType().isEnum) { for (var option : property.getOptions()) { if (option.getValue().equals(stringValue)) { diff --git a/src/main/java/be/seeseepuff/pcinv/services/BuildService.java b/src/main/java/be/seeseepuff/pcinv/services/BuildService.java new file mode 100644 index 0000000..e216bbe --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/services/BuildService.java @@ -0,0 +1,113 @@ +package be.seeseepuff.pcinv.services; + +import be.seeseepuff.pcinv.models.Build; +import be.seeseepuff.pcinv.models.BuildInfo; +import be.seeseepuff.pcinv.repositories.BuildRepository; +import be.seeseepuff.pcinv.repositories.RamRepository; +import jakarta.annotation.PostConstruct; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * A service that manages computer builds. + */ +@Service +@RequiredArgsConstructor +public class BuildService { + private final BuildRepository buildRepository; + private final RamRepository ramRepository; + + @PostConstruct + private void init() { + Build empty = buildRepository.getBuildByNameAndMeta("None", true); + if (empty == null) { + empty = Build.builder() + .name("None") + .meta(true) + .build(); + } + empty.setDescription("A meta build to hold unused parts."); + empty = buildRepository.save(empty); + } + + /** + * Gets a list of all computer builds, including meta builds. + * + * @return A list of all builds. + */ + public List getAllBuilds() { + return buildRepository.findAll(); + } + + /** + * Gets a list of all computer builds, excluding meta builds. + * + * @return A list of all builds. + */ + public List getAllRealBuilds() { + return buildRepository.findAllByMeta(false); + } + + /** + * Gets a build by its ID. + * + * @param id The ID of the build to retrieve. + * @return The build with the given ID, or null if not found. + */ + public Build getBuildById(long id) { + return buildRepository.getBuildById(id); + } + + /** + * Creates a new build with the given name and description. + * + * @param name The name of the build. + * @param description The description of the build. + * @return The created build. + */ + @Transactional + public Build createBuild(String name, String description) { + Build build = Build.builder() + .name(name) + .description(description) + .build(); + return buildRepository.saveAndFlush(build); + } + + /** + * Deletes a build by its ID. + * + * @param id The ID of the build to delete. + */ + @Transactional + public void deleteBuild(long id) { + Build build = buildRepository.getBuildById(id); + for (var part : build.getParts()) { + part.setBuild(null); + } + buildRepository.deleteById(id); + } + + /** + * Gets the build information for a given build ID. + * + * @param build The build object for which to retrieve the information. + * @return The BuildInfo object containing the build information. + * @throws IllegalArgumentException if the build with the given ID does not exist. + */ + public BuildInfo getBuildInfo(Build build) { + var buildInfo = new BuildInfo(); + for (var part : build.getParts()) { + if (part.getType().equals("ram")) { + var asset = ramRepository.findByAsset(part); + if (asset.getCapacity() != null) { + buildInfo.setTotalRam(buildInfo.getTotalRam() + asset.getCapacity()); + } + } + } + return buildInfo; + } +} diff --git a/src/main/resources/templates/build_view.html b/src/main/resources/templates/build_view.html new file mode 100644 index 0000000..6d0c816 --- /dev/null +++ b/src/main/resources/templates/build_view.html @@ -0,0 +1,24 @@ + +
+
    +
  • Name:
  • +
  • Description:
  • +
  • Part Count:
  • +
  • Total RAM:
  • +
+ + + + + + + + + + + + + +
QRTypeBrandModel
+
+ diff --git a/src/main/resources/templates/builds.html b/src/main/resources/templates/builds.html new file mode 100644 index 0000000..877b2b8 --- /dev/null +++ b/src/main/resources/templates/builds.html @@ -0,0 +1,28 @@ + +
+ + + + + + + + + + + + + + + + + + + + + +
NameDescriptionPart CountActions
+ Delete +
+
+ diff --git a/src/main/resources/templates/create_asset.html b/src/main/resources/templates/create_asset.html index 9997df4..b5b4ba1 100644 --- a/src/main/resources/templates/create_asset.html +++ b/src/main/resources/templates/create_asset.html @@ -8,6 +8,7 @@ + + - + @@ -27,9 +29,11 @@ + + + + + + Bad input type for diff --git a/src/main/resources/templates/fragments.html b/src/main/resources/templates/fragments.html index edde0fb..ad34ce8 100644 --- a/src/main/resources/templates/fragments.html +++ b/src/main/resources/templates/fragments.html @@ -10,6 +10,7 @@ Home Browse Create + Builds
diff --git a/src/main/resources/templates/view.html b/src/main/resources/templates/view.html index 655878c..241fa4d 100644 --- a/src/main/resources/templates/view.html +++ b/src/main/resources/templates/view.html @@ -7,7 +7,8 @@ - + +