diff --git a/src/main/java/be/seeseepuff/pcinv/controllers/WebController.java b/src/main/java/be/seeseepuff/pcinv/controllers/WebController.java index 2c9aa7b..ba863b2 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.services.AssetService; import lombok.RequiredArgsConstructor; +import org.springframework.data.repository.query.Param; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -26,8 +27,14 @@ public class WebController { private static final String DESCRIPTOR = "descriptor"; /// The name of the model attribute that holds the list of assets. private static final String ASSETS = "assets"; + /// The name of the model attribute that holds the asset being viewed or edited. + private static final String ASSET = "asset"; /// The name of the model attribute that holds a list of all properties of all descriptors. private static final String PROPERTIES = "properties"; + /// The name of the model attribute that holds the action to be performed. + private static final String ACTION = "action"; + /// The name of the model attribute that holds the current time in milliseconds. + private static final String TIME = "time"; private final AssetService assetService; @@ -36,6 +43,7 @@ public class WebController { */ @GetMapping("/") public String index(Model model) { + model.addAttribute(TIME, System.currentTimeMillis()); model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors()); model.addAttribute("asset_count", assetService.countAssets()); return "index"; @@ -46,6 +54,7 @@ public class WebController { */ @GetMapping("/browse") public String browse(Model model) { + model.addAttribute(TIME, System.currentTimeMillis()); model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors()); return "browse"; } @@ -60,6 +69,7 @@ public class WebController { */ @GetMapping("/browse/{type}") public String browseType(Model model, @PathVariable String type) { + model.addAttribute(TIME, System.currentTimeMillis()); var tree = assetService.getAssetDescriptorTree(type); model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(type)); model.addAttribute(DESCRIPTORS, tree); @@ -76,20 +86,62 @@ public class WebController { */ @GetMapping("/view/{qr}") public String view(Model model, @PathVariable long qr) { + model.addAttribute(TIME, System.currentTimeMillis()); + model.addAttribute(ACTION, "view"); + return renderView(model, qr); + } + + /** + * Show a page asking if the user wants to delete an asset. + * + * @param qr The QR code of the asset to delete. + */ + @GetMapping("/delete/{qr}") + public String delete(Model model, @PathVariable long qr, @Param("confirm") boolean confirm) { + model.addAttribute(TIME, System.currentTimeMillis()); + model.addAttribute(ACTION, "delete"); + if (confirm) { + var asset = assetService.getAssetByQr(qr); + if (asset == null) { + return "redirect:/"; + } + var type = asset.getAsset().getType(); + assetService.deleteAsset(qr); + return "redirect:/browse/" + type; + } + return renderView(model, qr); + } + + /** + * Renders the view for an asset based on its QR code. + * If the asset does not exist, it redirects to the index page. + * + * @param model The model to add attributes to. + * @param qr The QR code of the asset to view. + * @return The view name for viewing the asset. + */ + private String renderView(Model model, long qr) { var asset = assetService.getAssetByQr(qr); if (asset == null) { return "redirect:/"; } - model.addAttribute("asset", asset); + model.addAttribute(ASSET, asset); model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(asset.getAsset().getType())); + model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(asset.getAsset().getType())); return "view"; } + @GetMapping("/search") + public String search(@Param("qr") long qr) { + return "redirect:/view/" + qr; + } + /** * Shows a view where the user can create the type of asset to create. */ @GetMapping("/create") public String create(Model model) { + model.addAttribute(TIME, System.currentTimeMillis()); model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptors()); return "create_select"; } @@ -101,11 +153,51 @@ public class WebController { */ @GetMapping("/create/{type}") public String createType(Model model, @PathVariable String type) { + model.addAttribute(TIME, System.currentTimeMillis()); + model.addAttribute(ACTION, "create"); model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(type)); model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(type)); return "create_asset"; } + /** + * Shows a view where the user can edit an existing asset. + * + * @param qr The QR code of the asset to edit. + */ + @GetMapping("/edit/{qr}") + public String edit(Model model, @PathVariable long qr) { + model.addAttribute(TIME, System.currentTimeMillis()); + var asset = assetService.getAssetByQr(qr); + if (asset == null) { + throw new RuntimeException("Asset not found"); + } + String assetType = asset.getAsset().getType(); + model.addAttribute(ACTION, "edit"); + model.addAttribute(ASSET, asset); + model.addAttribute(DESCRIPTORS, assetService.getAssetDescriptorTree(assetType)); + model.addAttribute(DESCRIPTOR, assetService.getAssetDescriptor(assetType)); + return "create_asset"; + } + + /** + * Actually edits an asset based on the form data submitted. + * + * @param qr The QR code of the asset to edit. + */ + @PostMapping("/edit/{qr}") + public String editPost(Model model, @PathVariable long qr, @RequestBody MultiValueMap formData) { + model.addAttribute(TIME, System.currentTimeMillis()); + var formMap = new HashMap(); + formData.forEach((key, values) -> { + if (!values.isEmpty()) { + formMap.put(key, values.getFirst()); + } + }); + var asset = assetService.editAsset(qr, formMap); + return "redirect:/view/" + asset.getQr(); + } + /** * Handles the form submission for creating an asset. * @@ -118,6 +210,7 @@ public class WebController { consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE ) public String createTypePost(Model model, @PathVariable String type, @RequestBody MultiValueMap formData) { + model.addAttribute(TIME, System.currentTimeMillis()); var formMap = new HashMap(); formData.forEach((key, values) -> { if (!values.isEmpty()) { diff --git a/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java b/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java index 7defda0..8e40f90 100644 --- a/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java +++ b/src/main/java/be/seeseepuff/pcinv/meta/AssetProperty.java @@ -41,6 +41,8 @@ public class AssetProperty { private final BiConsumer setter; /// A getter function that can be used to get the value of the property from an asset. private final Function getter; + /// Whether the property is an input list. + private final boolean inputList; /** * Enum representing the possible types of asset properties. @@ -84,6 +86,7 @@ public class AssetProperty { .displayName(annotation.value()) .type(type) .required(annotation.required()) + .inputList(property.isAnnotationPresent(InputList.class)) .setter((obj, value) -> { try { property.setAccessible(true); @@ -167,7 +170,11 @@ public class AssetProperty { * @param asset The asset to get the property value from. * @return The value of the property. */ - public Object getValue(Object asset) { + @Nullable + public Object getValue(@Nullable Object asset) { + if (asset == null) { + return null; + } return getter.apply(asset); } @@ -176,7 +183,11 @@ public class AssetProperty { * * @return The rendered value as a string. */ - public String renderValue(Object asset) { + @Nullable + public String renderValue(@Nullable Object asset) { + if (asset == null) { + return null; + } var value = getValue(asset); if (value == null) { return "Unknown"; diff --git a/src/main/java/be/seeseepuff/pcinv/meta/InputList.java b/src/main/java/be/seeseepuff/pcinv/meta/InputList.java new file mode 100644 index 0000000..333fa7c --- /dev/null +++ b/src/main/java/be/seeseepuff/pcinv/meta/InputList.java @@ -0,0 +1,15 @@ +package be.seeseepuff.pcinv.meta; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that any form for the given field should be rendered as a dropdown + * list with optional manual text input. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface InputList { +} diff --git a/src/main/java/be/seeseepuff/pcinv/models/GenericAsset.java b/src/main/java/be/seeseepuff/pcinv/models/GenericAsset.java index 7255c49..ef170de 100644 --- a/src/main/java/be/seeseepuff/pcinv/models/GenericAsset.java +++ b/src/main/java/be/seeseepuff/pcinv/models/GenericAsset.java @@ -1,11 +1,9 @@ package be.seeseepuff.pcinv.models; import be.seeseepuff.pcinv.meta.AssetInfo; +import be.seeseepuff.pcinv.meta.InputList; import be.seeseepuff.pcinv.meta.Property; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.Table; +import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; @@ -21,7 +19,10 @@ import lombok.Setter; type = "asset", isVisible = false ) -@Table(name = "assets") +@Table( + name = "assets", + uniqueConstraints = @UniqueConstraint(columnNames = "qr") +) public class GenericAsset { public static final String TYPE = "asset"; @@ -38,6 +39,7 @@ public class GenericAsset /// The brand of the asset. @Property("Brand") + @InputList private String brand; /// The model of the asset diff --git a/src/main/java/be/seeseepuff/pcinv/repositories/AssetRepository.java b/src/main/java/be/seeseepuff/pcinv/repositories/AssetRepository.java index 36fb356..294ac9d 100644 --- a/src/main/java/be/seeseepuff/pcinv/repositories/AssetRepository.java +++ b/src/main/java/be/seeseepuff/pcinv/repositories/AssetRepository.java @@ -21,9 +21,11 @@ public interface AssetRepository { T findByAsset(GenericAsset asset); + void deleteByAsset(GenericAsset asset); + + void flush(); + List findAll(); Class getAssetType(); - - long count(); } diff --git a/src/main/java/be/seeseepuff/pcinv/services/AssetService.java b/src/main/java/be/seeseepuff/pcinv/services/AssetService.java index d1c56a0..88bd8e2 100644 --- a/src/main/java/be/seeseepuff/pcinv/services/AssetService.java +++ b/src/main/java/be/seeseepuff/pcinv/services/AssetService.java @@ -125,6 +125,31 @@ public class AssetService { return asset; } + /** + * Edits an existing asset with the provided form data. + * + * @param qr The QR code of the asset to edit. + * @param formData The form data containing the updated asset properties. + * @return The updated asset. + */ + @Transactional + public Asset editAsset(long qr, Map formData) { + var genericAsset = genericRepository.findByQr(qr); + if (genericAsset == null) { + throw new IllegalArgumentException("No asset found with QR code: " + qr); + } + + var assetType = genericAsset.getType(); + var assetDescriptor = getAssetDescriptor(assetType); + var asset = getRepositoryFor(assetType).findByAsset(genericAsset); + + fillIn(genericAsset, getAssetDescriptor(GenericAsset.TYPE), formData); + fillIn(asset, assetDescriptor, formData); + + genericRepository.saveAndFlush(genericAsset); + return getRepositoryFor(assetType).saveAndFlushAsset(asset); + } + /** * Gets the asset repository for the specified type. * @@ -148,11 +173,10 @@ public class AssetService { private void fillIn(Object asset, AssetDescriptor assetDescriptor, Map formData) { for (var property : assetDescriptor.getProperties()) { var value = parseValue(assetDescriptor, property, formData); - if (value != null) { - property.setValue(asset, value); - } else if (property.isRequired()) { + if (value == null && property.isRequired()) { throw new IllegalArgumentException("Property '" + property.getName() + "' is required but not provided."); } + property.setValue(asset, value); } } @@ -199,4 +223,18 @@ public class AssetService { throw new IllegalArgumentException("Unsupported property type: " + property.getType()); } } + + @Transactional + public void deleteAsset(long qr) { + var genericAsset = genericRepository.findByQr(qr); + if (genericAsset == null) { + throw new IllegalArgumentException("No asset found with QR code: " + qr); + } + var assetType = genericAsset.getType(); + var assetRepository = getRepositoryFor(assetType); + assetRepository.deleteByAsset(genericAsset); + assetRepository.flush(); + genericRepository.delete(genericAsset); + genericRepository.flush(); + } } diff --git a/src/main/resources/templates/browse_type.html b/src/main/resources/templates/browse_type.html index 4193c60..ad0420f 100644 --- a/src/main/resources/templates/browse_type.html +++ b/src/main/resources/templates/browse_type.html @@ -1,15 +1,19 @@
There are in the database. - +
- +
Actions
+ + + - Edit + View + Edit
diff --git a/src/main/resources/templates/create_asset.html b/src/main/resources/templates/create_asset.html index 713773c..90cec0f 100644 --- a/src/main/resources/templates/create_asset.html +++ b/src/main/resources/templates/create_asset.html @@ -1,30 +1,30 @@
Create a -
+

- +
- +
- - + + Bad input type for @@ -33,7 +33,8 @@

- + +

diff --git a/src/main/resources/templates/fragments.html b/src/main/resources/templates/fragments.html index 86928ef..edde0fb 100644 --- a/src/main/resources/templates/fragments.html +++ b/src/main/resources/templates/fragments.html @@ -13,5 +13,9 @@
+
+

+ Rendered in 25ms on . +

diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index f528dd7..e170565 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -1,5 +1,12 @@

This system holds 5 assets.

+
+ + +
diff --git a/src/main/resources/templates/view.html b/src/main/resources/templates/view.html index 832ab0a..a6075eb 100644 --- a/src/main/resources/templates/view.html +++ b/src/main/resources/templates/view.html @@ -1,14 +1,26 @@
- View device details +

Details of Hard Disk Drive 21

+

Are you sure you want to delete Hard Disk Drive 21

- +
- +
+

+

+ +