From 415d347f53acffbd518cdb68d093080f9b3e3caf Mon Sep 17 00:00:00 2001 From: Robert van Dijk Date: Sat, 30 May 2026 00:43:26 +0200 Subject: [PATCH 1/2] Move LED config from user to confetti LED color and pattern are now stored on the Confetti entity rather than the User. Removes the standalone LED page, adds LED fields to the confetti create/edit forms, and restricts LED API endpoints to the ledstrip scope. Confetti table and user picker now show stacked color dots. Co-Authored-By: Claude Sonnet 4.6 --- .../AdminConfettiCreateController.java | 17 ++- .../AdminConfettiDetailController.java | 16 ++- .../admin/service/AdminConfettiService.java | 17 ++- .../admin/service/ConfettiFormParser.java | 68 ++++++--- .../controller/LedStripController.java | 14 +- .../ch/wisv/chpay/core/model/Confetti.java | 21 ++- .../java/ch/wisv/chpay/core/model/User.java | 17 --- .../customer/controller/LedController.java | 81 ----------- ...260530_1__Remove_user_led_preferences.java | 22 +++ .../V20260530_2__Add_led_to_confetti.java | 42 ++++++ .../resources/static/js/confetti-admin.js | 85 +++++++---- .../templates/admin-confetti-create.html | 2 +- .../templates/admin-confetti-table.html | 17 ++- .../resources/templates/admin-confetti.html | 2 +- src/main/resources/templates/confetti.html | 14 ++ .../templates/fragments/confetti-form.html | 133 +++++++++++++----- .../resources/templates/layouts/layout.html | 6 - src/main/resources/templates/led.html | 100 ------------- 18 files changed, 370 insertions(+), 304 deletions(-) delete mode 100644 src/main/java/ch/wisv/chpay/customer/controller/LedController.java create mode 100644 src/main/java/db/migration/V20260530_1__Remove_user_led_preferences.java create mode 100644 src/main/java/db/migration/V20260530_2__Add_led_to_confetti.java delete mode 100644 src/main/resources/templates/led.html diff --git a/src/main/java/ch/wisv/chpay/admin/controller/AdminConfettiCreateController.java b/src/main/java/ch/wisv/chpay/admin/controller/AdminConfettiCreateController.java index 3de85e6..af93195 100644 --- a/src/main/java/ch/wisv/chpay/admin/controller/AdminConfettiCreateController.java +++ b/src/main/java/ch/wisv/chpay/admin/controller/AdminConfettiCreateController.java @@ -4,6 +4,7 @@ import ch.wisv.chpay.admin.service.ConfettiFormParser; import ch.wisv.chpay.admin.service.ConfettiFormParser.ConfettiFormResult; import ch.wisv.chpay.core.model.Confetti; +import ch.wisv.chpay.core.model.LedPattern; import ch.wisv.chpay.core.service.NotificationService; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; @@ -38,7 +39,7 @@ public AdminConfettiCreateController( @GetMapping public String showCreateForm(Model model) { model.addAttribute("confettiName", ""); - model.addAttribute("colorsCsv", ""); + model.addAttribute("colorValues", List.of("#ff0000")); model.addAttribute("scalarValue", Confetti.DEFAULT_SCALAR); model.addAttribute("minTransactionsValue", 0); model.addAttribute("groupValue", ""); @@ -47,6 +48,8 @@ public String showCreateForm(Model model) { model.addAttribute("defaultValue", false); model.addAttribute("shapeTypes", List.of("SQUARE")); model.addAttribute("shapeValues", List.of("")); + model.addAttribute("ledColorValue", "#ffffff"); + model.addAttribute("ledPatternValue", LedPattern.oplopen.name()); model.addAttribute(MODEL_ATTR_URL_PAGE, "adminConfetti"); return "admin-confetti-create"; } @@ -54,7 +57,7 @@ public String showCreateForm(Model model) { @PostMapping public String createConfetti( @RequestParam("name") String name, - @RequestParam("colors") String colorsInput, + @RequestParam(value = "colors", required = false) List colorsInput, @RequestParam("scalar") String scalarInput, @RequestParam("minTransactions") String minTransactionsInput, @RequestParam("group") String groupInput, @@ -63,6 +66,8 @@ public String createConfetti( @RequestParam(value = "isDefault", defaultValue = "false") boolean isDefault, @RequestParam(value = "shapeType", required = false) List shapeTypes, @RequestParam(value = "shapeValue", required = false) List shapeValues, + @RequestParam("ledColor") String ledColor, + @RequestParam("ledPattern") String ledPattern, RedirectAttributes redirectAttributes) { ConfettiFormResult result = @@ -76,7 +81,9 @@ public String createConfetti( hidden, isDefault, shapeTypes, - shapeValues); + shapeValues, + ledColor, + ledPattern); if (!result.isValid()) { notificationService.addErrorMessage(redirectAttributes, result.getErrorMessage()); return "redirect:/admin/confetti/new"; @@ -92,7 +99,9 @@ public String createConfetti( result.getGroup(), result.isGroupStartsWith(), result.isHidden(), - result.isDefault()); + result.isDefault(), + result.getLedColor(), + result.getLedPattern()); notificationService.addSuccessMessage(redirectAttributes, "Confetti created successfully"); return "redirect:/admin/confetti/" + confetti.getId(); } diff --git a/src/main/java/ch/wisv/chpay/admin/controller/AdminConfettiDetailController.java b/src/main/java/ch/wisv/chpay/admin/controller/AdminConfettiDetailController.java index f0f8edb..d8527ec 100644 --- a/src/main/java/ch/wisv/chpay/admin/controller/AdminConfettiDetailController.java +++ b/src/main/java/ch/wisv/chpay/admin/controller/AdminConfettiDetailController.java @@ -49,7 +49,7 @@ public String showConfetti(@PathVariable("id") UUID id, Model model, RedirectAtt Confetti current = confetti.get(); model.addAttribute(MODEL_ATTR_CONFETTI, current); model.addAttribute("confettiUserCount", adminConfettiService.getUsageCount(current)); - model.addAttribute("colorsCsv", String.join(", ", current.getColors())); + model.addAttribute("colorValues", current.getColors()); model.addAttribute("scalarValue", current.getScalar()); model.addAttribute("minTransactionsValue", current.getMinimumTransactions()); model.addAttribute("groupValue", current.getGroup()); @@ -65,6 +65,8 @@ public String showConfetti(@PathVariable("id") UUID id, Model model, RedirectAtt .map(shape -> shape.getValue() == null ? "" : shape.getValue().trim()) .toList()); + model.addAttribute("ledColorValue", current.getLedColor()); + model.addAttribute("ledPatternValue", current.getLedPattern().name()); model.addAttribute(MODEL_ATTR_URL_PAGE, "adminConfetti"); return "admin-confetti"; } @@ -73,7 +75,7 @@ public String showConfetti(@PathVariable("id") UUID id, Model model, RedirectAtt public String updateConfetti( @PathVariable("id") UUID id, @RequestParam("name") String name, - @RequestParam("colors") String colorsInput, + @RequestParam(value = "colors", required = false) List colorsInput, @RequestParam("scalar") String scalarInput, @RequestParam("minTransactions") String minTransactionsInput, @RequestParam("group") String groupInput, @@ -82,6 +84,8 @@ public String updateConfetti( @RequestParam(value = "isDefault", defaultValue = "false") boolean isDefault, @RequestParam(value = "shapeType", required = false) List shapeTypes, @RequestParam(value = "shapeValue", required = false) List shapeValues, + @RequestParam("ledColor") String ledColor, + @RequestParam("ledPattern") String ledPattern, RedirectAttributes redirectAttributes) { Optional confetti = adminConfettiService.getById(id); @@ -101,7 +105,9 @@ public String updateConfetti( hidden, isDefault, shapeTypes, - shapeValues); + shapeValues, + ledColor, + ledPattern); if (!result.isValid()) { notificationService.addErrorMessage(redirectAttributes, result.getErrorMessage()); return "redirect:/admin/confetti/" + id; @@ -118,7 +124,9 @@ public String updateConfetti( result.getGroup(), result.isGroupStartsWith(), result.isHidden(), - result.isDefault()); + result.isDefault(), + result.getLedColor(), + result.getLedPattern()); } catch (IllegalStateException ex) { notificationService.addErrorMessage(redirectAttributes, ex.getMessage()); return "redirect:/admin/confetti/" + id; diff --git a/src/main/java/ch/wisv/chpay/admin/service/AdminConfettiService.java b/src/main/java/ch/wisv/chpay/admin/service/AdminConfettiService.java index 260a91f..015e117 100644 --- a/src/main/java/ch/wisv/chpay/admin/service/AdminConfettiService.java +++ b/src/main/java/ch/wisv/chpay/admin/service/AdminConfettiService.java @@ -2,6 +2,7 @@ import ch.wisv.chpay.core.model.Confetti; import ch.wisv.chpay.core.model.ConfettiShape; +import ch.wisv.chpay.core.model.LedPattern; import ch.wisv.chpay.core.repository.ConfettiRepository; import ch.wisv.chpay.core.repository.ConfettiUsageCount; import ch.wisv.chpay.core.repository.UserRepository; @@ -78,7 +79,9 @@ public Confetti create( String group, boolean groupStartsWith, boolean hidden, - boolean isDefault) { + boolean isDefault, + String ledColor, + LedPattern ledPattern) { boolean shouldBeDefault = isDefault || confettiRepository.countByDefaultConfettiTrue() == 0; Confetti confetti = new Confetti( @@ -90,7 +93,9 @@ public Confetti create( group, groupStartsWith, hidden, - shouldBeDefault); + shouldBeDefault, + ledColor, + ledPattern); Confetti saved = confettiRepository.save(confetti); enforceSingleDefault(saved.getId()); return saved; @@ -108,7 +113,9 @@ public Confetti update( String group, boolean groupStartsWith, boolean hidden, - boolean isDefault) { + boolean isDefault, + String ledColor, + LedPattern ledPattern) { if (!isDefault && confetti.isDefaultConfetti() && confettiRepository.countByDefaultConfettiTrue() == 1) { @@ -123,7 +130,9 @@ public Confetti update( group, groupStartsWith, hidden, - isDefault); + isDefault, + ledColor, + ledPattern); Confetti saved = confettiRepository.save(confetti); enforceSingleDefault(saved.getId()); return saved; diff --git a/src/main/java/ch/wisv/chpay/admin/service/ConfettiFormParser.java b/src/main/java/ch/wisv/chpay/admin/service/ConfettiFormParser.java index 8b8aafa..7b1def1 100644 --- a/src/main/java/ch/wisv/chpay/admin/service/ConfettiFormParser.java +++ b/src/main/java/ch/wisv/chpay/admin/service/ConfettiFormParser.java @@ -2,8 +2,8 @@ import ch.wisv.chpay.core.model.ConfettiShape; import ch.wisv.chpay.core.model.ConfettiShapeType; +import ch.wisv.chpay.core.model.LedPattern; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -15,7 +15,7 @@ public class ConfettiFormParser { public ConfettiFormResult parse( String name, - String colorsInput, + List colorsInput, String scalarInput, String minimumTransactionsInput, String groupInput, @@ -23,14 +23,22 @@ public ConfettiFormResult parse( boolean hidden, boolean isDefault, List shapeTypes, - List shapeValues) { + List shapeValues, + String ledColorInput, + String ledPatternInput) { String trimmedName = name == null ? "" : name.trim(); if (trimmedName.isEmpty()) { return ConfettiFormResult.error("Name is required"); } - List colors = parseColors(colorsInput); + List colors = + colorsInput == null + ? List.of() + : colorsInput.stream() + .map(String::trim) + .filter(s -> !s.isEmpty()) + .collect(Collectors.toList()); if (colors.isEmpty() || colors.stream().anyMatch(color -> !isValidColor(color))) { return ConfettiFormResult.error("Please provide at least one valid hex color (e.g. #FF0000)"); } @@ -57,6 +65,18 @@ public ConfettiFormResult parse( return ConfettiFormResult.error("Add at least one shape"); } + String ledColor = ledColorInput == null ? "" : ledColorInput.trim(); + if (!isValidLedColor(ledColor)) { + return ConfettiFormResult.error("Please provide a valid LED color (e.g. #FF0000)"); + } + + LedPattern ledPattern; + try { + ledPattern = LedPattern.valueOf(ledPatternInput == null ? "" : ledPatternInput.trim()); + } catch (IllegalArgumentException e) { + return ConfettiFormResult.error("Please select a valid LED pattern"); + } + return ConfettiFormResult.success( trimmedName, colors, @@ -66,23 +86,19 @@ public ConfettiFormResult parse( normalizeGroup(groupInput), normalizeGroupStartsWith(groupStartsWith, groupInput), hidden, - isDefault); - } - - private List parseColors(String colorsInput) { - if (colorsInput == null) { - return List.of(); - } - return Arrays.stream(colorsInput.split(",")) - .map(String::trim) - .filter(value -> !value.isEmpty()) - .collect(Collectors.toList()); + isDefault, + ledColor, + ledPattern); } private boolean isValidColor(String color) { return color.matches("^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"); } + private boolean isValidLedColor(String color) { + return color.matches("^#[0-9a-fA-F]{6}$"); + } + private ScalarParseResult parseScalar(String scalarInput) { if (scalarInput == null || scalarInput.trim().isEmpty()) { return ScalarParseResult.invalid(); @@ -197,6 +213,8 @@ public static final class ConfettiFormResult { private final boolean groupStartsWith; private final boolean hidden; private final boolean isDefault; + private final String ledColor; + private final LedPattern ledPattern; private final String errorMessage; private ConfettiFormResult( @@ -209,6 +227,8 @@ private ConfettiFormResult( boolean groupStartsWith, boolean hidden, boolean isDefault, + String ledColor, + LedPattern ledPattern, String errorMessage) { this.name = name; this.colors = colors; @@ -219,6 +239,8 @@ private ConfettiFormResult( this.groupStartsWith = groupStartsWith; this.hidden = hidden; this.isDefault = isDefault; + this.ledColor = ledColor; + this.ledPattern = ledPattern; this.errorMessage = errorMessage; } @@ -231,7 +253,9 @@ public static ConfettiFormResult success( String group, boolean groupStartsWith, boolean hidden, - boolean isDefault) { + boolean isDefault, + String ledColor, + LedPattern ledPattern) { return new ConfettiFormResult( name, colors, @@ -242,12 +266,14 @@ public static ConfettiFormResult success( groupStartsWith, hidden, isDefault, + ledColor, + ledPattern, null); } public static ConfettiFormResult error(String message) { return new ConfettiFormResult( - null, List.of(), List.of(), 0.0, 0, null, false, false, false, message); + null, List.of(), List.of(), 0.0, 0, null, false, false, false, null, null, message); } public boolean isValid() { @@ -290,6 +316,14 @@ public boolean isDefault() { return isDefault; } + public String getLedColor() { + return ledColor; + } + + public LedPattern getLedPattern() { + return ledPattern; + } + public String getErrorMessage() { return errorMessage; } diff --git a/src/main/java/ch/wisv/chpay/api/ledstrip/controller/LedStripController.java b/src/main/java/ch/wisv/chpay/api/ledstrip/controller/LedStripController.java index 56159bf..a9a8ddd 100644 --- a/src/main/java/ch/wisv/chpay/api/ledstrip/controller/LedStripController.java +++ b/src/main/java/ch/wisv/chpay/api/ledstrip/controller/LedStripController.java @@ -1,5 +1,6 @@ package ch.wisv.chpay.api.ledstrip.controller; +import ch.wisv.chpay.core.model.Confetti; import ch.wisv.chpay.core.model.LedPattern; import ch.wisv.chpay.core.model.User; import ch.wisv.chpay.core.model.transaction.Transaction; @@ -53,10 +54,15 @@ public ResponseEntity getLatestTransaction() { .map( t -> { User user = t.getUser(); - Integer r = user != null ? user.getLedR() : null; - Integer g = user != null ? user.getLedG() : null; - Integer b = user != null ? user.getLedB() : null; - LedPattern ledPattern = user != null ? user.getLedPattern() : null; + Confetti confetti = user != null ? user.getConfetti() : null; + String ledColor = confetti != null ? confetti.getLedColor() : null; + LedPattern ledPattern = confetti != null ? confetti.getLedPattern() : null; + Integer r = null, g = null, b = null; + if (ledColor != null && ledColor.length() == 7) { + r = Integer.parseInt(ledColor.substring(1, 3), 16); + g = Integer.parseInt(ledColor.substring(3, 5), 16); + b = Integer.parseInt(ledColor.substring(5, 7), 16); + } String pattern = ledPattern != null ? ledPattern.name() : null; return ResponseEntity.ok( new LatestTransactionResponse( diff --git a/src/main/java/ch/wisv/chpay/core/model/Confetti.java b/src/main/java/ch/wisv/chpay/core/model/Confetti.java index f858302..0d30caa 100644 --- a/src/main/java/ch/wisv/chpay/core/model/Confetti.java +++ b/src/main/java/ch/wisv/chpay/core/model/Confetti.java @@ -57,6 +57,15 @@ public class Confetti { @Column(nullable = false) private LocalDateTime createdAt; + @Setter + @Column(name = "led_color", nullable = false, length = 7) + private String ledColor; + + @Setter + @Enumerated(EnumType.STRING) + @Column(name = "led_pattern", nullable = false) + private LedPattern ledPattern; + public Confetti( String name, List shapes, @@ -66,7 +75,9 @@ public Confetti( String group, boolean groupStartsWith, boolean hidden, - boolean defaultConfetti) { + boolean defaultConfetti, + String ledColor, + LedPattern ledPattern) { this.name = name; this.colors.addAll(colors); this.shapes.addAll(shapes); @@ -77,6 +88,8 @@ public Confetti( this.hidden = hidden; this.defaultConfetti = defaultConfetti; this.createdAt = LocalDateTime.now(); + this.ledColor = ledColor; + this.ledPattern = ledPattern; } public void updateDefinition( @@ -88,7 +101,9 @@ public void updateDefinition( String group, boolean groupStartsWith, boolean hidden, - boolean defaultConfetti) { + boolean defaultConfetti, + String ledColor, + LedPattern ledPattern) { this.name = name; this.shapes.clear(); this.shapes.addAll(shapes); @@ -100,6 +115,8 @@ public void updateDefinition( this.groupStartsWith = normalizeGroupStartsWith(groupStartsWith, this.group); this.hidden = hidden; this.defaultConfetti = defaultConfetti; + this.ledColor = ledColor; + this.ledPattern = ledPattern; } public boolean hasShapeType(ConfettiShapeType type) { diff --git a/src/main/java/ch/wisv/chpay/core/model/User.java b/src/main/java/ch/wisv/chpay/core/model/User.java index 425c0d0..37d2628 100644 --- a/src/main/java/ch/wisv/chpay/core/model/User.java +++ b/src/main/java/ch/wisv/chpay/core/model/User.java @@ -42,23 +42,6 @@ public class User { @JoinColumn(name = "confetti_id") private Confetti confetti; - @Setter - @Column(name = "led_r") - private Integer ledR; - - @Setter - @Column(name = "led_g") - private Integer ledG; - - @Setter - @Column(name = "led_b") - private Integer ledB; - - @Setter - @Enumerated(EnumType.STRING) - @Column(name = "led_pattern") - private LedPattern ledPattern; - @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "user_groups", joinColumns = @JoinColumn(name = "user_id")) @Column(name = "group_name", nullable = false, length = 255) diff --git a/src/main/java/ch/wisv/chpay/customer/controller/LedController.java b/src/main/java/ch/wisv/chpay/customer/controller/LedController.java deleted file mode 100644 index d145919..0000000 --- a/src/main/java/ch/wisv/chpay/customer/controller/LedController.java +++ /dev/null @@ -1,81 +0,0 @@ -package ch.wisv.chpay.customer.controller; - -import ch.wisv.chpay.core.model.LedPattern; -import ch.wisv.chpay.core.model.User; -import ch.wisv.chpay.core.repository.UserRepository; -import ch.wisv.chpay.core.service.NotificationService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.stereotype.Controller; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.servlet.mvc.support.RedirectAttributes; - -@Controller -@RequestMapping("/led") -public class LedController extends CustomerController { - - private final UserRepository userRepository; - private final NotificationService notificationService; - - @Autowired - public LedController(UserRepository userRepository, NotificationService notificationService) { - this.userRepository = userRepository; - this.notificationService = notificationService; - } - - @PreAuthorize("hasAnyRole('USER', 'BANNED')") - @GetMapping - public String showLedPage(@ModelAttribute("currentUser") User currentUser, Model model) { - if (currentUser == null) { - return "redirect:/login"; - } - - model.addAttribute("patterns", LedPattern.values()); - model.addAttribute(MODEL_ATTR_URL_PAGE, "led"); - return "led"; - } - - @PreAuthorize("hasAnyRole('USER', 'BANNED')") - @PostMapping("/save") - @Transactional - public String saveLedPreferences( - @RequestParam("r") int r, - @RequestParam("g") int g, - @RequestParam("b") int b, - @RequestParam("pattern") String pattern, - @ModelAttribute("currentUser") User currentUser, - RedirectAttributes redirectAttributes) { - if (currentUser == null) { - return "redirect:/login"; - } - - if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) { - notificationService.addErrorMessage(redirectAttributes, "Invalid color values"); - return "redirect:/led"; - } - - LedPattern ledPattern; - try { - ledPattern = LedPattern.valueOf(pattern); - } catch (IllegalArgumentException e) { - notificationService.addErrorMessage(redirectAttributes, "Invalid pattern"); - return "redirect:/led"; - } - - User user = userRepository.findAndLockByOpenID(currentUser.getOpenID()).orElse(currentUser); - user.setLedR(r); - user.setLedG(g); - user.setLedB(b); - user.setLedPattern(ledPattern); - userRepository.save(user); - - notificationService.addSuccessMessage(redirectAttributes, "LED preferences saved"); - return "redirect:/led"; - } -} diff --git a/src/main/java/db/migration/V20260530_1__Remove_user_led_preferences.java b/src/main/java/db/migration/V20260530_1__Remove_user_led_preferences.java new file mode 100644 index 0000000..62a308d --- /dev/null +++ b/src/main/java/db/migration/V20260530_1__Remove_user_led_preferences.java @@ -0,0 +1,22 @@ +package db.migration; + +import java.sql.Statement; +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; + +public class V20260530_1__Remove_user_led_preferences extends BaseJavaMigration { + + @Override + public void migrate(Context context) throws Exception { + try (Statement stmt = context.getConnection().createStatement()) { + stmt.execute( + """ + ALTER TABLE users + DROP COLUMN IF EXISTS led_r, + DROP COLUMN IF EXISTS led_g, + DROP COLUMN IF EXISTS led_b, + DROP COLUMN IF EXISTS led_pattern + """); + } + } +} diff --git a/src/main/java/db/migration/V20260530_2__Add_led_to_confetti.java b/src/main/java/db/migration/V20260530_2__Add_led_to_confetti.java new file mode 100644 index 0000000..b185550 --- /dev/null +++ b/src/main/java/db/migration/V20260530_2__Add_led_to_confetti.java @@ -0,0 +1,42 @@ +package db.migration; + +import java.sql.Statement; +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; + +public class V20260530_2__Add_led_to_confetti extends BaseJavaMigration { + + @Override + public void migrate(Context context) throws Exception { + try (Statement stmt = context.getConnection().createStatement()) { + stmt.execute( + """ + ALTER TABLE confetti + ADD COLUMN IF NOT EXISTS led_color VARCHAR(7) NOT NULL DEFAULT '#ffffff', + ADD COLUMN IF NOT EXISTS led_pattern VARCHAR(32) NOT NULL DEFAULT 'oplopen' + """); + + stmt.execute( + """ + UPDATE confetti c + SET led_color = ( + SELECT cc.color + FROM confetti_colors cc + WHERE cc.confetti_id = c.id + ORDER BY cc.ctid + LIMIT 1 + ) + WHERE EXISTS ( + SELECT 1 FROM confetti_colors cc WHERE cc.confetti_id = c.id + ) + """); + + stmt.execute( + """ + ALTER TABLE confetti + ALTER COLUMN led_color DROP DEFAULT, + ALTER COLUMN led_pattern DROP DEFAULT + """); + } + } +} diff --git a/src/main/resources/static/js/confetti-admin.js b/src/main/resources/static/js/confetti-admin.js index c746716..728ed89 100644 --- a/src/main/resources/static/js/confetti-admin.js +++ b/src/main/resources/static/js/confetti-admin.js @@ -5,12 +5,6 @@ document.addEventListener('DOMContentLoaded', function () { const TEXT_PLACEHOLDER = 'Text for confetti shape'; const DEFAULT_PLACEHOLDER = 'Value'; - const parseColors = (value) => value.split(',') - .map(item => item.trim()) - .filter(item => item.length > 0); - - const isValidColor = (value) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value); - const getShapeWrapper = (form) => form.querySelector('[data-confetti-shape-list]'); const getShapeRows = (form) => { @@ -19,6 +13,42 @@ document.addEventListener('DOMContentLoaded', function () { return Array.from(wrapper.querySelectorAll('[data-confetti-shape-row]')); }; + const getColorWrapper = (form) => form.querySelector('[data-confetti-color-list]'); + + const getColorRows = (form) => { + const wrapper = getColorWrapper(form); + if (!wrapper) return []; + return Array.from(wrapper.querySelectorAll('[data-confetti-color-row]')); + }; + + const initializeColorRows = (form) => { + const wrapper = getColorWrapper(form); + if (!wrapper) return; + wrapper.querySelectorAll('[data-confetti-color-row]').forEach(row => { + if (row.hasAttribute('data-confetti-color-template')) { + row.removeAttribute('data-confetti-color-template'); + row.removeAttribute('id'); + row.classList.remove('hidden'); + row.querySelectorAll('[disabled]').forEach(el => { el.disabled = false; }); + } + const picker = row.querySelector('input[type="color"]'); + const hex = row.querySelector('[data-confetti-color-hex]'); + if (picker && hex) { + hex.textContent = picker.value; + picker.addEventListener('input', () => { hex.textContent = picker.value; }); + } + }); + }; + + const validateColors = (form) => { + const colorSelection = form.querySelector('#colorSelection'); + if (!colorSelection) return true; + const isValid = getColorRows(form).length > 0; + colorSelection.setCustomValidity(isValid ? '' : 'Add at least one color'); + colorSelection.value = isValid ? 'ok' : ''; + return isValid; + }; + const updateShapeRow = (row) => { const select = row.querySelector('[data-confetti-shape-type]'); const input = row.querySelector('[data-confetti-shape-value]'); @@ -80,7 +110,6 @@ document.addEventListener('DOMContentLoaded', function () { forms.forEach(form => { const nameInput = form.querySelector('#name'); - const colorsInput = form.querySelector('#colors'); const scalarInput = form.querySelector('#scalar'); const minTransactionsInput = form.querySelector('#minTransactions'); @@ -91,14 +120,6 @@ document.addEventListener('DOMContentLoaded', function () { return isValid; }; - const validateColors = () => { - if (!colorsInput) return true; - const colors = parseColors(colorsInput.value); - const isValid = colors.length > 0 && colors.every(isValidColor); - colorsInput.setCustomValidity(isValid ? '' : 'Enter at least one valid hex color'); - return isValid; - }; - const validateScalar = () => { if (!scalarInput) return true; const value = Number.parseFloat(scalarInput.value); @@ -115,13 +136,22 @@ document.addEventListener('DOMContentLoaded', function () { return isValid; }; + const ledColorInput = form.querySelector('#ledColor'); + const ledColorHex = form.querySelector('#led-color-hex'); + if (ledColorInput && ledColorHex) { + ledColorInput.addEventListener('input', () => { + ledColorHex.textContent = ledColorInput.value; + }); + } + initializeShapeRows(form); validateShapes(form); + initializeColorRows(form); + validateColors(form); validateScalar(); validateMinTransactions(); nameInput?.addEventListener('input', validateName); - colorsInput?.addEventListener('input', validateColors); scalarInput?.addEventListener('input', validateScalar); minTransactionsInput?.addEventListener('input', validateMinTransactions); @@ -145,38 +175,43 @@ document.addEventListener('DOMContentLoaded', function () { } }); - const copyTrigger = form.querySelector('[data-copy-markup]'); - copyTrigger?.addEventListener('click', () => { + form.querySelector('[data-confetti-add-shape]')?.addEventListener('click', () => { setTimeout(() => { initializeShapeRows(form); validateShapes(form); }, 0); }); + form.querySelector('[data-confetti-add-color]')?.addEventListener('click', () => { + setTimeout(() => { + initializeColorRows(form); + validateColors(form); + }, 0); + }); + form.addEventListener('click', (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; - if (target.closest('[data-copy-markup-delete-item]')) { - setTimeout(() => { - validateShapes(form); - }, 0); + if (target.closest('[data-confetti-remove-shape]')) { + setTimeout(() => { validateShapes(form); }, 0); + } + if (target.closest('[data-confetti-remove-color]')) { + setTimeout(() => { validateColors(form); }, 0); } }); form.addEventListener('submit', (event) => { const nameValid = validateName(); - const colorsValid = validateColors(); const scalarValid = validateScalar(); const minTransactionsValid = validateMinTransactions(); const shapesValid = validateShapes(form); + const colorsValid = validateColors(form); if (!nameValid || !colorsValid || !scalarValid || !minTransactionsValid || !shapesValid) { event.preventDefault(); event.stopPropagation(); if (!nameValid && nameInput) { nameInput.focus(); - } else if (!colorsValid && colorsInput) { - colorsInput.focus(); } else if (!scalarValid && scalarInput) { scalarInput.focus(); } else if (!minTransactionsValid && minTransactionsInput) { diff --git a/src/main/resources/templates/admin-confetti-create.html b/src/main/resources/templates/admin-confetti-create.html index 769aebf..fabf8e3 100644 --- a/src/main/resources/templates/admin-confetti-create.html +++ b/src/main/resources/templates/admin-confetti-create.html @@ -43,7 +43,7 @@

Confetti Configuration

-
+
diff --git a/src/main/resources/templates/admin-confetti-table.html b/src/main/resources/templates/admin-confetti-table.html index 5ac37e6..cd4105c 100644 --- a/src/main/resources/templates/admin-confetti-table.html +++ b/src/main/resources/templates/admin-confetti-table.html @@ -29,11 +29,11 @@

Confetti ConfigurationsConfetti Configurations

+ Colors Actions ID @@ -141,6 +142,18 @@

Confetti Configurations + + +
+
+
+
+ +

-
+
diff --git a/src/main/resources/templates/confetti.html b/src/main/resources/templates/confetti.html index 62fc1e9..3c0d0c5 100644 --- a/src/main/resources/templates/confetti.html +++ b/src/main/resources/templates/confetti.html @@ -23,6 +23,20 @@

Confetti

Confetti

+ +
+
+
+
+
+
+
+
+

diff --git a/src/main/resources/templates/fragments/confetti-form.html b/src/main/resources/templates/fragments/confetti-form.html index d02e750..a404048 100644 --- a/src/main/resources/templates/fragments/confetti-form.html +++ b/src/main/resources/templates/fragments/confetti-form.html @@ -1,5 +1,8 @@ -

- +
+ + +

General

+
Please provide a name
-
-
+
- -
- - - Enter at least one valid hex color -
+ +

Particle configuration

- -
- - - Enter a scalar greater than 0 -
- - +
@@ -190,4 +163,92 @@ Add at least one shape and provide values for Path/Text shapes
+ + +
+ +
+
+ + #ff0000 + +
+
+ + #ffffff + +
+
+ +

+ +

+ + Add at least one color +
+ + +
+ + + Enter a scalar greater than 0 +
+ +
+ + +

LED configuration

+ +
+ +
+ + #ffffff +
+
+ +
+ + +
+
diff --git a/src/main/resources/templates/layouts/layout.html b/src/main/resources/templates/layouts/layout.html index ee8d80b..0b96f18 100644 --- a/src/main/resources/templates/layouts/layout.html +++ b/src/main/resources/templates/layouts/layout.html @@ -75,12 +75,6 @@
Confetti - -
  • - - - LED -
  • -
    - -
    - - - - - - - - - From b488c8370562810e8590edf101a7e970f1f7fb0b Mon Sep 17 00:00:00 2001 From: Robert van Dijk Date: Sat, 30 May 2026 01:03:02 +0200 Subject: [PATCH 2/2] Update confetti test --- .../chpay/core/service/ConfettiEligibilityServiceTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/java/ch/wisv/chpay/core/service/ConfettiEligibilityServiceTest.java b/src/test/java/ch/wisv/chpay/core/service/ConfettiEligibilityServiceTest.java index c3f6023..902858b 100644 --- a/src/test/java/ch/wisv/chpay/core/service/ConfettiEligibilityServiceTest.java +++ b/src/test/java/ch/wisv/chpay/core/service/ConfettiEligibilityServiceTest.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.when; import ch.wisv.chpay.core.model.Confetti; +import ch.wisv.chpay.core.model.LedPattern; import ch.wisv.chpay.core.model.User; import ch.wisv.chpay.core.model.transaction.Transaction.TransactionStatus; import ch.wisv.chpay.core.model.transaction.Transaction.TransactionType; @@ -175,7 +176,9 @@ private Confetti buildConfetti( group, groupStartsWith, false, - isDefault); + isDefault, + "#ffffff", + LedPattern.oplopen); } private User buildUserWithId() {