feat(back-end front-end): shop improvements
Some checks failed
Build and Deploy / test-backend (push) Successful in 38s
Build and Deploy / test-frontend (push) Successful in 1m4s
Build and Deploy / build-and-push (push) Failing after 1m15s
Build and Deploy / deploy (push) Has been skipped

This commit is contained in:
2026-03-13 16:16:49 +01:00
parent fcdede2dd6
commit 00af9a9701
39 changed files with 1886 additions and 299 deletions

View File

@@ -94,6 +94,10 @@ public class OptionsController {
v.getId(), v.getId(),
v.getVariantDisplayName(), v.getVariantDisplayName(),
v.getColorName(), v.getColorName(),
v.getColorLabelIt(),
v.getColorLabelEn(),
v.getColorLabelDe(),
v.getColorLabelFr(),
resolveHexColor(v), resolveHexColor(v),
v.getFinishType() != null ? v.getFinishType() : "GLOSSY", v.getFinishType() != null ? v.getFinishType() : "GLOSSY",
v.getStockSpools() != null ? v.getStockSpools().doubleValue() : 0d, v.getStockSpools() != null ? v.getStockSpools().doubleValue() : 0d,

View File

@@ -12,6 +12,10 @@ public class AdminFilamentVariantDto {
private String materialTechnicalTypeLabel; private String materialTechnicalTypeLabel;
private String variantDisplayName; private String variantDisplayName;
private String colorName; private String colorName;
private String colorLabelIt;
private String colorLabelEn;
private String colorLabelDe;
private String colorLabelFr;
private String colorHex; private String colorHex;
private String finishType; private String finishType;
private String brand; private String brand;
@@ -89,6 +93,38 @@ public class AdminFilamentVariantDto {
this.colorName = colorName; this.colorName = colorName;
} }
public String getColorLabelIt() {
return colorLabelIt;
}
public void setColorLabelIt(String colorLabelIt) {
this.colorLabelIt = colorLabelIt;
}
public String getColorLabelEn() {
return colorLabelEn;
}
public void setColorLabelEn(String colorLabelEn) {
this.colorLabelEn = colorLabelEn;
}
public String getColorLabelDe() {
return colorLabelDe;
}
public void setColorLabelDe(String colorLabelDe) {
this.colorLabelDe = colorLabelDe;
}
public String getColorLabelFr() {
return colorLabelFr;
}
public void setColorLabelFr(String colorLabelFr) {
this.colorLabelFr = colorLabelFr;
}
public String getColorHex() { public String getColorHex() {
return colorHex; return colorHex;
} }

View File

@@ -10,9 +10,25 @@ public class AdminShopCategoryDto {
private String parentCategoryName; private String parentCategoryName;
private String slug; private String slug;
private String name; private String name;
private String nameIt;
private String nameEn;
private String nameDe;
private String nameFr;
private String description; private String description;
private String descriptionIt;
private String descriptionEn;
private String descriptionDe;
private String descriptionFr;
private String seoTitle; private String seoTitle;
private String seoTitleIt;
private String seoTitleEn;
private String seoTitleDe;
private String seoTitleFr;
private String seoDescription; private String seoDescription;
private String seoDescriptionIt;
private String seoDescriptionEn;
private String seoDescriptionDe;
private String seoDescriptionFr;
private String ogTitle; private String ogTitle;
private String ogDescription; private String ogDescription;
private Boolean indexable; private Boolean indexable;
@@ -69,6 +85,38 @@ public class AdminShopCategoryDto {
this.name = name; this.name = name;
} }
public String getNameIt() {
return nameIt;
}
public void setNameIt(String nameIt) {
this.nameIt = nameIt;
}
public String getNameEn() {
return nameEn;
}
public void setNameEn(String nameEn) {
this.nameEn = nameEn;
}
public String getNameDe() {
return nameDe;
}
public void setNameDe(String nameDe) {
this.nameDe = nameDe;
}
public String getNameFr() {
return nameFr;
}
public void setNameFr(String nameFr) {
this.nameFr = nameFr;
}
public String getDescription() { public String getDescription() {
return description; return description;
} }
@@ -77,6 +125,38 @@ public class AdminShopCategoryDto {
this.description = description; this.description = description;
} }
public String getDescriptionIt() {
return descriptionIt;
}
public void setDescriptionIt(String descriptionIt) {
this.descriptionIt = descriptionIt;
}
public String getDescriptionEn() {
return descriptionEn;
}
public void setDescriptionEn(String descriptionEn) {
this.descriptionEn = descriptionEn;
}
public String getDescriptionDe() {
return descriptionDe;
}
public void setDescriptionDe(String descriptionDe) {
this.descriptionDe = descriptionDe;
}
public String getDescriptionFr() {
return descriptionFr;
}
public void setDescriptionFr(String descriptionFr) {
this.descriptionFr = descriptionFr;
}
public String getSeoTitle() { public String getSeoTitle() {
return seoTitle; return seoTitle;
} }
@@ -85,6 +165,38 @@ public class AdminShopCategoryDto {
this.seoTitle = seoTitle; this.seoTitle = seoTitle;
} }
public String getSeoTitleIt() {
return seoTitleIt;
}
public void setSeoTitleIt(String seoTitleIt) {
this.seoTitleIt = seoTitleIt;
}
public String getSeoTitleEn() {
return seoTitleEn;
}
public void setSeoTitleEn(String seoTitleEn) {
this.seoTitleEn = seoTitleEn;
}
public String getSeoTitleDe() {
return seoTitleDe;
}
public void setSeoTitleDe(String seoTitleDe) {
this.seoTitleDe = seoTitleDe;
}
public String getSeoTitleFr() {
return seoTitleFr;
}
public void setSeoTitleFr(String seoTitleFr) {
this.seoTitleFr = seoTitleFr;
}
public String getSeoDescription() { public String getSeoDescription() {
return seoDescription; return seoDescription;
} }
@@ -93,6 +205,38 @@ public class AdminShopCategoryDto {
this.seoDescription = seoDescription; this.seoDescription = seoDescription;
} }
public String getSeoDescriptionIt() {
return seoDescriptionIt;
}
public void setSeoDescriptionIt(String seoDescriptionIt) {
this.seoDescriptionIt = seoDescriptionIt;
}
public String getSeoDescriptionEn() {
return seoDescriptionEn;
}
public void setSeoDescriptionEn(String seoDescriptionEn) {
this.seoDescriptionEn = seoDescriptionEn;
}
public String getSeoDescriptionDe() {
return seoDescriptionDe;
}
public void setSeoDescriptionDe(String seoDescriptionDe) {
this.seoDescriptionDe = seoDescriptionDe;
}
public String getSeoDescriptionFr() {
return seoDescriptionFr;
}
public void setSeoDescriptionFr(String seoDescriptionFr) {
this.seoDescriptionFr = seoDescriptionFr;
}
public String getOgTitle() { public String getOgTitle() {
return ogTitle; return ogTitle;
} }

View File

@@ -9,6 +9,10 @@ public class AdminShopProductVariantDto {
private String sku; private String sku;
private String variantLabel; private String variantLabel;
private String colorName; private String colorName;
private String colorLabelIt;
private String colorLabelEn;
private String colorLabelDe;
private String colorLabelFr;
private String colorHex; private String colorHex;
private String internalMaterialCode; private String internalMaterialCode;
private BigDecimal priceChf; private BigDecimal priceChf;
@@ -50,6 +54,38 @@ public class AdminShopProductVariantDto {
this.colorName = colorName; this.colorName = colorName;
} }
public String getColorLabelIt() {
return colorLabelIt;
}
public void setColorLabelIt(String colorLabelIt) {
this.colorLabelIt = colorLabelIt;
}
public String getColorLabelEn() {
return colorLabelEn;
}
public void setColorLabelEn(String colorLabelEn) {
this.colorLabelEn = colorLabelEn;
}
public String getColorLabelDe() {
return colorLabelDe;
}
public void setColorLabelDe(String colorLabelDe) {
this.colorLabelDe = colorLabelDe;
}
public String getColorLabelFr() {
return colorLabelFr;
}
public void setColorLabelFr(String colorLabelFr) {
this.colorLabelFr = colorLabelFr;
}
public String getColorHex() { public String getColorHex() {
return colorHex; return colorHex;
} }

View File

@@ -6,6 +6,10 @@ public class AdminUpsertFilamentVariantRequest {
private Long materialTypeId; private Long materialTypeId;
private String variantDisplayName; private String variantDisplayName;
private String colorName; private String colorName;
private String colorLabelIt;
private String colorLabelEn;
private String colorLabelDe;
private String colorLabelFr;
private String colorHex; private String colorHex;
private String finishType; private String finishType;
private String brand; private String brand;
@@ -40,6 +44,38 @@ public class AdminUpsertFilamentVariantRequest {
this.colorName = colorName; this.colorName = colorName;
} }
public String getColorLabelIt() {
return colorLabelIt;
}
public void setColorLabelIt(String colorLabelIt) {
this.colorLabelIt = colorLabelIt;
}
public String getColorLabelEn() {
return colorLabelEn;
}
public void setColorLabelEn(String colorLabelEn) {
this.colorLabelEn = colorLabelEn;
}
public String getColorLabelDe() {
return colorLabelDe;
}
public void setColorLabelDe(String colorLabelDe) {
this.colorLabelDe = colorLabelDe;
}
public String getColorLabelFr() {
return colorLabelFr;
}
public void setColorLabelFr(String colorLabelFr) {
this.colorLabelFr = colorLabelFr;
}
public String getColorHex() { public String getColorHex() {
return colorHex; return colorHex;
} }

View File

@@ -6,9 +6,25 @@ public class AdminUpsertShopCategoryRequest {
private UUID parentCategoryId; private UUID parentCategoryId;
private String slug; private String slug;
private String name; private String name;
private String nameIt;
private String nameEn;
private String nameDe;
private String nameFr;
private String description; private String description;
private String descriptionIt;
private String descriptionEn;
private String descriptionDe;
private String descriptionFr;
private String seoTitle; private String seoTitle;
private String seoTitleIt;
private String seoTitleEn;
private String seoTitleDe;
private String seoTitleFr;
private String seoDescription; private String seoDescription;
private String seoDescriptionIt;
private String seoDescriptionEn;
private String seoDescriptionDe;
private String seoDescriptionFr;
private String ogTitle; private String ogTitle;
private String ogDescription; private String ogDescription;
private Boolean indexable; private Boolean indexable;
@@ -39,6 +55,38 @@ public class AdminUpsertShopCategoryRequest {
this.name = name; this.name = name;
} }
public String getNameIt() {
return nameIt;
}
public void setNameIt(String nameIt) {
this.nameIt = nameIt;
}
public String getNameEn() {
return nameEn;
}
public void setNameEn(String nameEn) {
this.nameEn = nameEn;
}
public String getNameDe() {
return nameDe;
}
public void setNameDe(String nameDe) {
this.nameDe = nameDe;
}
public String getNameFr() {
return nameFr;
}
public void setNameFr(String nameFr) {
this.nameFr = nameFr;
}
public String getDescription() { public String getDescription() {
return description; return description;
} }
@@ -47,6 +95,38 @@ public class AdminUpsertShopCategoryRequest {
this.description = description; this.description = description;
} }
public String getDescriptionIt() {
return descriptionIt;
}
public void setDescriptionIt(String descriptionIt) {
this.descriptionIt = descriptionIt;
}
public String getDescriptionEn() {
return descriptionEn;
}
public void setDescriptionEn(String descriptionEn) {
this.descriptionEn = descriptionEn;
}
public String getDescriptionDe() {
return descriptionDe;
}
public void setDescriptionDe(String descriptionDe) {
this.descriptionDe = descriptionDe;
}
public String getDescriptionFr() {
return descriptionFr;
}
public void setDescriptionFr(String descriptionFr) {
this.descriptionFr = descriptionFr;
}
public String getSeoTitle() { public String getSeoTitle() {
return seoTitle; return seoTitle;
} }
@@ -55,6 +135,38 @@ public class AdminUpsertShopCategoryRequest {
this.seoTitle = seoTitle; this.seoTitle = seoTitle;
} }
public String getSeoTitleIt() {
return seoTitleIt;
}
public void setSeoTitleIt(String seoTitleIt) {
this.seoTitleIt = seoTitleIt;
}
public String getSeoTitleEn() {
return seoTitleEn;
}
public void setSeoTitleEn(String seoTitleEn) {
this.seoTitleEn = seoTitleEn;
}
public String getSeoTitleDe() {
return seoTitleDe;
}
public void setSeoTitleDe(String seoTitleDe) {
this.seoTitleDe = seoTitleDe;
}
public String getSeoTitleFr() {
return seoTitleFr;
}
public void setSeoTitleFr(String seoTitleFr) {
this.seoTitleFr = seoTitleFr;
}
public String getSeoDescription() { public String getSeoDescription() {
return seoDescription; return seoDescription;
} }
@@ -63,6 +175,38 @@ public class AdminUpsertShopCategoryRequest {
this.seoDescription = seoDescription; this.seoDescription = seoDescription;
} }
public String getSeoDescriptionIt() {
return seoDescriptionIt;
}
public void setSeoDescriptionIt(String seoDescriptionIt) {
this.seoDescriptionIt = seoDescriptionIt;
}
public String getSeoDescriptionEn() {
return seoDescriptionEn;
}
public void setSeoDescriptionEn(String seoDescriptionEn) {
this.seoDescriptionEn = seoDescriptionEn;
}
public String getSeoDescriptionDe() {
return seoDescriptionDe;
}
public void setSeoDescriptionDe(String seoDescriptionDe) {
this.seoDescriptionDe = seoDescriptionDe;
}
public String getSeoDescriptionFr() {
return seoDescriptionFr;
}
public void setSeoDescriptionFr(String seoDescriptionFr) {
this.seoDescriptionFr = seoDescriptionFr;
}
public String getOgTitle() { public String getOgTitle() {
return ogTitle; return ogTitle;
} }

View File

@@ -8,6 +8,10 @@ public class AdminUpsertShopProductVariantRequest {
private String sku; private String sku;
private String variantLabel; private String variantLabel;
private String colorName; private String colorName;
private String colorLabelIt;
private String colorLabelEn;
private String colorLabelDe;
private String colorLabelFr;
private String colorHex; private String colorHex;
private String internalMaterialCode; private String internalMaterialCode;
private BigDecimal priceChf; private BigDecimal priceChf;
@@ -47,6 +51,38 @@ public class AdminUpsertShopProductVariantRequest {
this.colorName = colorName; this.colorName = colorName;
} }
public String getColorLabelIt() {
return colorLabelIt;
}
public void setColorLabelIt(String colorLabelIt) {
this.colorLabelIt = colorLabelIt;
}
public String getColorLabelEn() {
return colorLabelEn;
}
public void setColorLabelEn(String colorLabelEn) {
this.colorLabelEn = colorLabelEn;
}
public String getColorLabelDe() {
return colorLabelDe;
}
public void setColorLabelDe(String colorLabelDe) {
this.colorLabelDe = colorLabelDe;
}
public String getColorLabelFr() {
return colorLabelFr;
}
public void setColorLabelFr(String colorLabelFr) {
this.colorLabelFr = colorLabelFr;
}
public String getColorHex() { public String getColorHex() {
return colorHex; return colorHex;
} }

View File

@@ -15,6 +15,10 @@ public record OptionsResponse(
Long id, Long id,
String name, String name,
String colorName, String colorName,
String colorLabelIt,
String colorLabelEn,
String colorLabelDe,
String colorLabelFr,
String hexColor, String hexColor,
String finishType, String finishType,
Double stockSpools, Double stockSpools,

View File

@@ -17,9 +17,17 @@ public class OrderItemDto {
private String shopProductName; private String shopProductName;
private String shopVariantLabel; private String shopVariantLabel;
private String shopVariantColorName; private String shopVariantColorName;
private String shopVariantColorLabelIt;
private String shopVariantColorLabelEn;
private String shopVariantColorLabelDe;
private String shopVariantColorLabelFr;
private String shopVariantColorHex; private String shopVariantColorHex;
private String filamentVariantDisplayName; private String filamentVariantDisplayName;
private String filamentColorName; private String filamentColorName;
private String filamentColorLabelIt;
private String filamentColorLabelEn;
private String filamentColorLabelDe;
private String filamentColorLabelFr;
private String filamentColorHex; private String filamentColorHex;
private String quality; private String quality;
private BigDecimal nozzleDiameterMm; private BigDecimal nozzleDiameterMm;
@@ -73,6 +81,18 @@ public class OrderItemDto {
public String getShopVariantColorName() { return shopVariantColorName; } public String getShopVariantColorName() { return shopVariantColorName; }
public void setShopVariantColorName(String shopVariantColorName) { this.shopVariantColorName = shopVariantColorName; } public void setShopVariantColorName(String shopVariantColorName) { this.shopVariantColorName = shopVariantColorName; }
public String getShopVariantColorLabelIt() { return shopVariantColorLabelIt; }
public void setShopVariantColorLabelIt(String shopVariantColorLabelIt) { this.shopVariantColorLabelIt = shopVariantColorLabelIt; }
public String getShopVariantColorLabelEn() { return shopVariantColorLabelEn; }
public void setShopVariantColorLabelEn(String shopVariantColorLabelEn) { this.shopVariantColorLabelEn = shopVariantColorLabelEn; }
public String getShopVariantColorLabelDe() { return shopVariantColorLabelDe; }
public void setShopVariantColorLabelDe(String shopVariantColorLabelDe) { this.shopVariantColorLabelDe = shopVariantColorLabelDe; }
public String getShopVariantColorLabelFr() { return shopVariantColorLabelFr; }
public void setShopVariantColorLabelFr(String shopVariantColorLabelFr) { this.shopVariantColorLabelFr = shopVariantColorLabelFr; }
public String getShopVariantColorHex() { return shopVariantColorHex; } public String getShopVariantColorHex() { return shopVariantColorHex; }
public void setShopVariantColorHex(String shopVariantColorHex) { this.shopVariantColorHex = shopVariantColorHex; } public void setShopVariantColorHex(String shopVariantColorHex) { this.shopVariantColorHex = shopVariantColorHex; }
@@ -82,6 +102,18 @@ public class OrderItemDto {
public String getFilamentColorName() { return filamentColorName; } public String getFilamentColorName() { return filamentColorName; }
public void setFilamentColorName(String filamentColorName) { this.filamentColorName = filamentColorName; } public void setFilamentColorName(String filamentColorName) { this.filamentColorName = filamentColorName; }
public String getFilamentColorLabelIt() { return filamentColorLabelIt; }
public void setFilamentColorLabelIt(String filamentColorLabelIt) { this.filamentColorLabelIt = filamentColorLabelIt; }
public String getFilamentColorLabelEn() { return filamentColorLabelEn; }
public void setFilamentColorLabelEn(String filamentColorLabelEn) { this.filamentColorLabelEn = filamentColorLabelEn; }
public String getFilamentColorLabelDe() { return filamentColorLabelDe; }
public void setFilamentColorLabelDe(String filamentColorLabelDe) { this.filamentColorLabelDe = filamentColorLabelDe; }
public String getFilamentColorLabelFr() { return filamentColorLabelFr; }
public void setFilamentColorLabelFr(String filamentColorLabelFr) { this.filamentColorLabelFr = filamentColorLabelFr; }
public String getFilamentColorHex() { return filamentColorHex; } public String getFilamentColorHex() { return filamentColorHex; }
public void setFilamentColorHex(String filamentColorHex) { this.filamentColorHex = filamentColorHex; } public void setFilamentColorHex(String filamentColorHex) { this.filamentColorHex = filamentColorHex; }

View File

@@ -8,6 +8,7 @@ public record ShopProductVariantOptionDto(
String sku, String sku,
String variantLabel, String variantLabel,
String colorName, String colorName,
String colorLabel,
String colorHex, String colorHex,
BigDecimal priceChf, BigDecimal priceChf,
Boolean isDefault Boolean isDefault

View File

@@ -24,6 +24,18 @@ public class FilamentVariant {
@Column(name = "color_name", nullable = false, length = Integer.MAX_VALUE) @Column(name = "color_name", nullable = false, length = Integer.MAX_VALUE)
private String colorName; private String colorName;
@Column(name = "color_label_it", length = Integer.MAX_VALUE)
private String colorLabelIt;
@Column(name = "color_label_en", length = Integer.MAX_VALUE)
private String colorLabelEn;
@Column(name = "color_label_de", length = Integer.MAX_VALUE)
private String colorLabelDe;
@Column(name = "color_label_fr", length = Integer.MAX_VALUE)
private String colorLabelFr;
@Column(name = "color_hex", length = Integer.MAX_VALUE) @Column(name = "color_hex", length = Integer.MAX_VALUE)
private String colorHex; private String colorHex;
@@ -93,6 +105,38 @@ public class FilamentVariant {
this.colorName = colorName; this.colorName = colorName;
} }
public String getColorLabelIt() {
return colorLabelIt;
}
public void setColorLabelIt(String colorLabelIt) {
this.colorLabelIt = colorLabelIt;
}
public String getColorLabelEn() {
return colorLabelEn;
}
public void setColorLabelEn(String colorLabelEn) {
this.colorLabelEn = colorLabelEn;
}
public String getColorLabelDe() {
return colorLabelDe;
}
public void setColorLabelDe(String colorLabelDe) {
this.colorLabelDe = colorLabelDe;
}
public String getColorLabelFr() {
return colorLabelFr;
}
public void setColorLabelFr(String colorLabelFr) {
this.colorLabelFr = colorLabelFr;
}
public String getColorHex() { public String getColorHex() {
return colorHex; return colorHex;
} }
@@ -173,4 +217,60 @@ public class FilamentVariant {
this.createdAt = createdAt; this.createdAt = createdAt;
} }
public String getColorLabelForLanguage(String language) {
return resolveLocalizedValue(
language,
colorName,
colorLabelIt,
colorLabelEn,
colorLabelDe,
colorLabelFr
);
}
private String resolveLocalizedValue(String language,
String fallback,
String valueIt,
String valueEn,
String valueDe,
String valueFr) {
String normalizedLanguage = normalizeLanguage(language);
String preferred = switch (normalizedLanguage) {
case "it" -> valueIt;
case "en" -> valueEn;
case "de" -> valueDe;
case "fr" -> valueFr;
default -> null;
};
String resolved = firstNonBlank(preferred, fallback);
if (resolved != null) {
return resolved;
}
return firstNonBlank(valueIt, valueEn, valueDe, valueFr);
}
private String normalizeLanguage(String language) {
if (language == null) {
return "";
}
String normalized = language.trim().toLowerCase();
int separatorIndex = normalized.indexOf('-');
if (separatorIndex > 0) {
normalized = normalized.substring(0, separatorIndex);
}
return normalized;
}
private String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (value != null && !value.isBlank()) {
return value;
}
}
return null;
}
} }

View File

@@ -15,6 +15,7 @@ import jakarta.persistence.Table;
import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.ColumnDefault;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@Entity @Entity
@@ -23,6 +24,8 @@ import java.util.UUID;
@Index(name = "ix_shop_category_active_sort", columnList = "is_active, sort_order") @Index(name = "ix_shop_category_active_sort", columnList = "is_active, sort_order")
}) })
public class ShopCategory { public class ShopCategory {
public static final List<String> SUPPORTED_LANGUAGES = List.of("it", "en", "de", "fr");
@Id @Id
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "shop_category_id", nullable = false) @Column(name = "shop_category_id", nullable = false)
@@ -38,15 +41,63 @@ public class ShopCategory {
@Column(name = "name", nullable = false, length = Integer.MAX_VALUE) @Column(name = "name", nullable = false, length = Integer.MAX_VALUE)
private String name; private String name;
@Column(name = "name_it", length = Integer.MAX_VALUE)
private String nameIt;
@Column(name = "name_en", length = Integer.MAX_VALUE)
private String nameEn;
@Column(name = "name_de", length = Integer.MAX_VALUE)
private String nameDe;
@Column(name = "name_fr", length = Integer.MAX_VALUE)
private String nameFr;
@Column(name = "description", length = Integer.MAX_VALUE) @Column(name = "description", length = Integer.MAX_VALUE)
private String description; private String description;
@Column(name = "description_it", length = Integer.MAX_VALUE)
private String descriptionIt;
@Column(name = "description_en", length = Integer.MAX_VALUE)
private String descriptionEn;
@Column(name = "description_de", length = Integer.MAX_VALUE)
private String descriptionDe;
@Column(name = "description_fr", length = Integer.MAX_VALUE)
private String descriptionFr;
@Column(name = "seo_title", length = Integer.MAX_VALUE) @Column(name = "seo_title", length = Integer.MAX_VALUE)
private String seoTitle; private String seoTitle;
@Column(name = "seo_title_it", length = Integer.MAX_VALUE)
private String seoTitleIt;
@Column(name = "seo_title_en", length = Integer.MAX_VALUE)
private String seoTitleEn;
@Column(name = "seo_title_de", length = Integer.MAX_VALUE)
private String seoTitleDe;
@Column(name = "seo_title_fr", length = Integer.MAX_VALUE)
private String seoTitleFr;
@Column(name = "seo_description", length = Integer.MAX_VALUE) @Column(name = "seo_description", length = Integer.MAX_VALUE)
private String seoDescription; private String seoDescription;
@Column(name = "seo_description_it", length = Integer.MAX_VALUE)
private String seoDescriptionIt;
@Column(name = "seo_description_en", length = Integer.MAX_VALUE)
private String seoDescriptionEn;
@Column(name = "seo_description_de", length = Integer.MAX_VALUE)
private String seoDescriptionDe;
@Column(name = "seo_description_fr", length = Integer.MAX_VALUE)
private String seoDescriptionFr;
@Column(name = "og_title", length = Integer.MAX_VALUE) @Column(name = "og_title", length = Integer.MAX_VALUE)
private String ogTitle; private String ogTitle;
@@ -139,6 +190,38 @@ public class ShopCategory {
this.name = name; this.name = name;
} }
public String getNameIt() {
return nameIt;
}
public void setNameIt(String nameIt) {
this.nameIt = nameIt;
}
public String getNameEn() {
return nameEn;
}
public void setNameEn(String nameEn) {
this.nameEn = nameEn;
}
public String getNameDe() {
return nameDe;
}
public void setNameDe(String nameDe) {
this.nameDe = nameDe;
}
public String getNameFr() {
return nameFr;
}
public void setNameFr(String nameFr) {
this.nameFr = nameFr;
}
public String getDescription() { public String getDescription() {
return description; return description;
} }
@@ -147,6 +230,38 @@ public class ShopCategory {
this.description = description; this.description = description;
} }
public String getDescriptionIt() {
return descriptionIt;
}
public void setDescriptionIt(String descriptionIt) {
this.descriptionIt = descriptionIt;
}
public String getDescriptionEn() {
return descriptionEn;
}
public void setDescriptionEn(String descriptionEn) {
this.descriptionEn = descriptionEn;
}
public String getDescriptionDe() {
return descriptionDe;
}
public void setDescriptionDe(String descriptionDe) {
this.descriptionDe = descriptionDe;
}
public String getDescriptionFr() {
return descriptionFr;
}
public void setDescriptionFr(String descriptionFr) {
this.descriptionFr = descriptionFr;
}
public String getSeoTitle() { public String getSeoTitle() {
return seoTitle; return seoTitle;
} }
@@ -155,6 +270,38 @@ public class ShopCategory {
this.seoTitle = seoTitle; this.seoTitle = seoTitle;
} }
public String getSeoTitleIt() {
return seoTitleIt;
}
public void setSeoTitleIt(String seoTitleIt) {
this.seoTitleIt = seoTitleIt;
}
public String getSeoTitleEn() {
return seoTitleEn;
}
public void setSeoTitleEn(String seoTitleEn) {
this.seoTitleEn = seoTitleEn;
}
public String getSeoTitleDe() {
return seoTitleDe;
}
public void setSeoTitleDe(String seoTitleDe) {
this.seoTitleDe = seoTitleDe;
}
public String getSeoTitleFr() {
return seoTitleFr;
}
public void setSeoTitleFr(String seoTitleFr) {
this.seoTitleFr = seoTitleFr;
}
public String getSeoDescription() { public String getSeoDescription() {
return seoDescription; return seoDescription;
} }
@@ -163,6 +310,38 @@ public class ShopCategory {
this.seoDescription = seoDescription; this.seoDescription = seoDescription;
} }
public String getSeoDescriptionIt() {
return seoDescriptionIt;
}
public void setSeoDescriptionIt(String seoDescriptionIt) {
this.seoDescriptionIt = seoDescriptionIt;
}
public String getSeoDescriptionEn() {
return seoDescriptionEn;
}
public void setSeoDescriptionEn(String seoDescriptionEn) {
this.seoDescriptionEn = seoDescriptionEn;
}
public String getSeoDescriptionDe() {
return seoDescriptionDe;
}
public void setSeoDescriptionDe(String seoDescriptionDe) {
this.seoDescriptionDe = seoDescriptionDe;
}
public String getSeoDescriptionFr() {
return seoDescriptionFr;
}
public void setSeoDescriptionFr(String seoDescriptionFr) {
this.seoDescriptionFr = seoDescriptionFr;
}
public String getOgTitle() { public String getOgTitle() {
return ogTitle; return ogTitle;
} }
@@ -218,4 +397,109 @@ public class ShopCategory {
public void setUpdatedAt(OffsetDateTime updatedAt) { public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt; this.updatedAt = updatedAt;
} }
public String getNameForLanguage(String language) {
return resolveLocalizedValue(language, name, nameIt, nameEn, nameDe, nameFr);
}
public void setNameForLanguage(String language, String value) {
switch (normalizeLanguage(language)) {
case "it" -> nameIt = value;
case "en" -> nameEn = value;
case "de" -> nameDe = value;
case "fr" -> nameFr = value;
default -> {
}
}
}
public String getDescriptionForLanguage(String language) {
return resolveLocalizedValue(language, description, descriptionIt, descriptionEn, descriptionDe, descriptionFr);
}
public void setDescriptionForLanguage(String language, String value) {
switch (normalizeLanguage(language)) {
case "it" -> descriptionIt = value;
case "en" -> descriptionEn = value;
case "de" -> descriptionDe = value;
case "fr" -> descriptionFr = value;
default -> {
}
}
}
public String getSeoTitleForLanguage(String language) {
return resolveLocalizedValue(language, seoTitle, seoTitleIt, seoTitleEn, seoTitleDe, seoTitleFr);
}
public void setSeoTitleForLanguage(String language, String value) {
switch (normalizeLanguage(language)) {
case "it" -> seoTitleIt = value;
case "en" -> seoTitleEn = value;
case "de" -> seoTitleDe = value;
case "fr" -> seoTitleFr = value;
default -> {
}
}
}
public String getSeoDescriptionForLanguage(String language) {
return resolveLocalizedValue(language, seoDescription, seoDescriptionIt, seoDescriptionEn, seoDescriptionDe, seoDescriptionFr);
}
public void setSeoDescriptionForLanguage(String language, String value) {
switch (normalizeLanguage(language)) {
case "it" -> seoDescriptionIt = value;
case "en" -> seoDescriptionEn = value;
case "de" -> seoDescriptionDe = value;
case "fr" -> seoDescriptionFr = value;
default -> {
}
}
}
private String resolveLocalizedValue(String language,
String fallback,
String valueIt,
String valueEn,
String valueDe,
String valueFr) {
String normalizedLanguage = normalizeLanguage(language);
String preferred = switch (normalizedLanguage) {
case "it" -> valueIt;
case "en" -> valueEn;
case "de" -> valueDe;
case "fr" -> valueFr;
default -> null;
};
String resolved = firstNonBlank(preferred, fallback);
if (resolved != null) {
return resolved;
}
return firstNonBlank(valueIt, valueEn, valueDe, valueFr);
}
private String normalizeLanguage(String language) {
if (language == null) {
return "";
}
String normalized = language.trim().toLowerCase();
int separatorIndex = normalized.indexOf('-');
if (separatorIndex > 0) {
normalized = normalized.substring(0, separatorIndex);
}
return normalized;
}
private String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (value != null && !value.isBlank()) {
return value;
}
}
return null;
}
} }

View File

@@ -42,6 +42,18 @@ public class ShopProductVariant {
@Column(name = "color_name", nullable = false, length = Integer.MAX_VALUE) @Column(name = "color_name", nullable = false, length = Integer.MAX_VALUE)
private String colorName; private String colorName;
@Column(name = "color_label_it", length = Integer.MAX_VALUE)
private String colorLabelIt;
@Column(name = "color_label_en", length = Integer.MAX_VALUE)
private String colorLabelEn;
@Column(name = "color_label_de", length = Integer.MAX_VALUE)
private String colorLabelDe;
@Column(name = "color_label_fr", length = Integer.MAX_VALUE)
private String colorLabelFr;
@Column(name = "color_hex", length = Integer.MAX_VALUE) @Column(name = "color_hex", length = Integer.MAX_VALUE)
private String colorHex; private String colorHex;
@@ -152,6 +164,38 @@ public class ShopProductVariant {
this.colorName = colorName; this.colorName = colorName;
} }
public String getColorLabelIt() {
return colorLabelIt;
}
public void setColorLabelIt(String colorLabelIt) {
this.colorLabelIt = colorLabelIt;
}
public String getColorLabelEn() {
return colorLabelEn;
}
public void setColorLabelEn(String colorLabelEn) {
this.colorLabelEn = colorLabelEn;
}
public String getColorLabelDe() {
return colorLabelDe;
}
public void setColorLabelDe(String colorLabelDe) {
this.colorLabelDe = colorLabelDe;
}
public String getColorLabelFr() {
return colorLabelFr;
}
public void setColorLabelFr(String colorLabelFr) {
this.colorLabelFr = colorLabelFr;
}
public String getColorHex() { public String getColorHex() {
return colorHex; return colorHex;
} }
@@ -215,4 +259,60 @@ public class ShopProductVariant {
public void setUpdatedAt(OffsetDateTime updatedAt) { public void setUpdatedAt(OffsetDateTime updatedAt) {
this.updatedAt = updatedAt; this.updatedAt = updatedAt;
} }
public String getColorLabelForLanguage(String language) {
return resolveLocalizedValue(
language,
colorName,
colorLabelIt,
colorLabelEn,
colorLabelDe,
colorLabelFr
);
}
private String resolveLocalizedValue(String language,
String fallback,
String valueIt,
String valueEn,
String valueDe,
String valueFr) {
String normalizedLanguage = normalizeLanguage(language);
String preferred = switch (normalizedLanguage) {
case "it" -> valueIt;
case "en" -> valueEn;
case "de" -> valueDe;
case "fr" -> valueFr;
default -> null;
};
String resolved = firstNonBlank(preferred, fallback);
if (resolved != null) {
return resolved;
}
return firstNonBlank(valueIt, valueEn, valueDe, valueFr);
}
private String normalizeLanguage(String language) {
if (language == null) {
return "";
}
String normalized = language.trim().toLowerCase();
int separatorIndex = normalized.indexOf('-');
if (separatorIndex > 0) {
normalized = normalized.substring(0, separatorIndex);
}
return normalized;
}
private String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (value != null && !value.isBlank()) {
return value;
}
}
return null;
}
} }

View File

@@ -161,10 +161,21 @@ public class AdminFilamentControllerService {
String normalizedColorHex = normalizeAndValidateColorHex(payload.getColorHex()); String normalizedColorHex = normalizeAndValidateColorHex(payload.getColorHex());
String normalizedFinishType = normalizeAndValidateFinishType(payload.getFinishType(), payload.getIsMatte()); String normalizedFinishType = normalizeAndValidateFinishType(payload.getFinishType(), payload.getIsMatte());
String normalizedBrand = normalizeOptional(payload.getBrand()); String normalizedBrand = normalizeOptional(payload.getBrand());
String fallbackColorLabel = firstNonBlank(
normalizeOptional(payload.getColorLabelIt()),
normalizeOptional(payload.getColorLabelEn()),
normalizeOptional(payload.getColorLabelDe()),
normalizeOptional(payload.getColorLabelFr()),
normalizedColorName
);
variant.setFilamentMaterialType(material); variant.setFilamentMaterialType(material);
variant.setVariantDisplayName(normalizedDisplayName); variant.setVariantDisplayName(normalizedDisplayName);
variant.setColorName(normalizedColorName); variant.setColorName(normalizedColorName);
variant.setColorLabelIt(firstNonBlank(normalizeOptional(payload.getColorLabelIt()), fallbackColorLabel));
variant.setColorLabelEn(firstNonBlank(normalizeOptional(payload.getColorLabelEn()), fallbackColorLabel));
variant.setColorLabelDe(firstNonBlank(normalizeOptional(payload.getColorLabelDe()), fallbackColorLabel));
variant.setColorLabelFr(firstNonBlank(normalizeOptional(payload.getColorLabelFr()), fallbackColorLabel));
variant.setColorHex(normalizedColorHex); variant.setColorHex(normalizedColorHex);
variant.setFinishType(normalizedFinishType); variant.setFinishType(normalizedFinishType);
variant.setBrand(normalizedBrand); variant.setBrand(normalizedBrand);
@@ -226,6 +237,18 @@ public class AdminFilamentControllerService {
return normalized.isBlank() ? null : normalized; return normalized.isBlank() ? null : normalized;
} }
private String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (value != null && !value.isBlank()) {
return value;
}
}
return null;
}
private FilamentMaterialType validateAndResolveMaterial(AdminUpsertFilamentVariantRequest payload) { private FilamentMaterialType validateAndResolveMaterial(AdminUpsertFilamentVariantRequest payload) {
if (payload == null || payload.getMaterialTypeId() == null) { if (payload == null || payload.getMaterialTypeId() == null) {
throw new ResponseStatusException(BAD_REQUEST, "Material type id is required"); throw new ResponseStatusException(BAD_REQUEST, "Material type id is required");
@@ -306,6 +329,10 @@ public class AdminFilamentControllerService {
dto.setVariantDisplayName(variant.getVariantDisplayName()); dto.setVariantDisplayName(variant.getVariantDisplayName());
dto.setColorName(variant.getColorName()); dto.setColorName(variant.getColorName());
dto.setColorLabelIt(variant.getColorLabelIt());
dto.setColorLabelEn(variant.getColorLabelEn());
dto.setColorLabelDe(variant.getColorLabelDe());
dto.setColorLabelFr(variant.getColorLabelFr());
dto.setColorHex(variant.getColorHex()); dto.setColorHex(variant.getColorHex());
dto.setFinishType(variant.getFinishType()); dto.setFinishType(variant.getFinishType());
dto.setBrand(variant.getBrand()); dto.setBrand(variant.getBrand());

View File

@@ -67,13 +67,13 @@ public class AdminShopCategoryControllerService {
@Transactional @Transactional
public AdminShopCategoryDto createCategory(AdminUpsertShopCategoryRequest payload) { public AdminShopCategoryDto createCategory(AdminUpsertShopCategoryRequest payload) {
ensurePayload(payload); ensurePayload(payload);
String normalizedName = normalizeRequiredName(payload.getName()); LocalizedCategoryContent localizedContent = normalizeLocalizedCategoryContent(payload);
String normalizedSlug = normalizeAndValidateSlug(payload.getSlug(), normalizedName); String normalizedSlug = normalizeAndValidateSlug(payload.getSlug(), localizedContent.defaultName());
ensureSlugAvailable(normalizedSlug, null); ensureSlugAvailable(normalizedSlug, null);
ShopCategory category = new ShopCategory(); ShopCategory category = new ShopCategory();
category.setCreatedAt(OffsetDateTime.now()); category.setCreatedAt(OffsetDateTime.now());
applyPayload(category, payload, normalizedName, normalizedSlug, null); applyPayload(category, payload, localizedContent, normalizedSlug, null);
ShopCategory saved = shopCategoryRepository.save(category); ShopCategory saved = shopCategoryRepository.save(category);
return getCategory(saved.getId()); return getCategory(saved.getId());
@@ -86,11 +86,11 @@ public class AdminShopCategoryControllerService {
ShopCategory category = shopCategoryRepository.findById(categoryId) ShopCategory category = shopCategoryRepository.findById(categoryId)
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Shop category not found")); .orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Shop category not found"));
String normalizedName = normalizeRequiredName(payload.getName()); LocalizedCategoryContent localizedContent = normalizeLocalizedCategoryContent(payload);
String normalizedSlug = normalizeAndValidateSlug(payload.getSlug(), normalizedName); String normalizedSlug = normalizeAndValidateSlug(payload.getSlug(), localizedContent.defaultName());
ensureSlugAvailable(normalizedSlug, category.getId()); ensureSlugAvailable(normalizedSlug, category.getId());
applyPayload(category, payload, normalizedName, normalizedSlug, category.getId()); applyPayload(category, payload, localizedContent, normalizedSlug, category.getId());
ShopCategory saved = shopCategoryRepository.save(category); ShopCategory saved = shopCategoryRepository.save(category);
return getCategory(saved.getId()); return getCategory(saved.getId());
} }
@@ -112,17 +112,33 @@ public class AdminShopCategoryControllerService {
private void applyPayload(ShopCategory category, private void applyPayload(ShopCategory category,
AdminUpsertShopCategoryRequest payload, AdminUpsertShopCategoryRequest payload,
String normalizedName, LocalizedCategoryContent localizedContent,
String normalizedSlug, String normalizedSlug,
UUID currentCategoryId) { UUID currentCategoryId) {
ShopCategory parentCategory = resolveParentCategory(payload.getParentCategoryId(), currentCategoryId); ShopCategory parentCategory = resolveParentCategory(payload.getParentCategoryId(), currentCategoryId);
category.setParentCategory(parentCategory); category.setParentCategory(parentCategory);
category.setSlug(normalizedSlug); category.setSlug(normalizedSlug);
category.setName(normalizedName); category.setName(localizedContent.defaultName());
category.setDescription(normalizeOptional(payload.getDescription())); category.setNameIt(localizedContent.names().get("it"));
category.setSeoTitle(normalizeOptional(payload.getSeoTitle())); category.setNameEn(localizedContent.names().get("en"));
category.setSeoDescription(normalizeOptional(payload.getSeoDescription())); category.setNameDe(localizedContent.names().get("de"));
category.setNameFr(localizedContent.names().get("fr"));
category.setDescription(localizedContent.defaultDescription());
category.setDescriptionIt(localizedContent.descriptions().get("it"));
category.setDescriptionEn(localizedContent.descriptions().get("en"));
category.setDescriptionDe(localizedContent.descriptions().get("de"));
category.setDescriptionFr(localizedContent.descriptions().get("fr"));
category.setSeoTitle(localizedContent.defaultSeoTitle());
category.setSeoTitleIt(localizedContent.seoTitles().get("it"));
category.setSeoTitleEn(localizedContent.seoTitles().get("en"));
category.setSeoTitleDe(localizedContent.seoTitles().get("de"));
category.setSeoTitleFr(localizedContent.seoTitles().get("fr"));
category.setSeoDescription(localizedContent.defaultSeoDescription());
category.setSeoDescriptionIt(localizedContent.seoDescriptions().get("it"));
category.setSeoDescriptionEn(localizedContent.seoDescriptions().get("en"));
category.setSeoDescriptionDe(localizedContent.seoDescriptions().get("de"));
category.setSeoDescriptionFr(localizedContent.seoDescriptions().get("fr"));
category.setOgTitle(normalizeOptional(payload.getOgTitle())); category.setOgTitle(normalizeOptional(payload.getOgTitle()));
category.setOgDescription(normalizeOptional(payload.getOgDescription())); category.setOgDescription(normalizeOptional(payload.getOgDescription()));
category.setIndexable(payload.getIndexable() == null || payload.getIndexable()); category.setIndexable(payload.getIndexable() == null || payload.getIndexable());
@@ -161,14 +177,6 @@ public class AdminShopCategoryControllerService {
} }
} }
private String normalizeRequiredName(String name) {
String normalized = normalizeOptional(name);
if (normalized == null) {
throw new ResponseStatusException(BAD_REQUEST, "Category name is required");
}
return normalized;
}
private String normalizeAndValidateSlug(String slug, String fallbackName) { private String normalizeAndValidateSlug(String slug, String fallbackName) {
String source = normalizeOptional(slug); String source = normalizeOptional(slug);
if (source == null) { if (source == null) {
@@ -203,6 +211,103 @@ public class AdminShopCategoryControllerService {
return normalized.isBlank() ? null : normalized; return normalized.isBlank() ? null : normalized;
} }
private String normalizeRequired(String value, String message) {
String normalized = normalizeOptional(value);
if (normalized == null) {
throw new ResponseStatusException(BAD_REQUEST, message);
}
return normalized;
}
private LocalizedCategoryContent normalizeLocalizedCategoryContent(AdminUpsertShopCategoryRequest payload) {
String legacyName = normalizeOptional(payload.getName());
String fallbackName = firstNonBlank(
legacyName,
normalizeOptional(payload.getNameIt()),
normalizeOptional(payload.getNameEn()),
normalizeOptional(payload.getNameDe()),
normalizeOptional(payload.getNameFr())
);
if (fallbackName == null) {
throw new ResponseStatusException(BAD_REQUEST, "Category name is required");
}
Map<String, String> names = new LinkedHashMap<>();
names.put("it", normalizeRequired(firstNonBlank(normalizeOptional(payload.getNameIt()), fallbackName), "Italian category name is required"));
names.put("en", normalizeRequired(firstNonBlank(normalizeOptional(payload.getNameEn()), fallbackName), "English category name is required"));
names.put("de", normalizeRequired(firstNonBlank(normalizeOptional(payload.getNameDe()), fallbackName), "German category name is required"));
names.put("fr", normalizeRequired(firstNonBlank(normalizeOptional(payload.getNameFr()), fallbackName), "French category name is required"));
String fallbackDescription = firstNonBlank(
normalizeOptional(payload.getDescription()),
normalizeOptional(payload.getDescriptionIt()),
normalizeOptional(payload.getDescriptionEn()),
normalizeOptional(payload.getDescriptionDe()),
normalizeOptional(payload.getDescriptionFr())
);
Map<String, String> descriptions = new LinkedHashMap<>();
descriptions.put("it", firstNonBlank(normalizeOptional(payload.getDescriptionIt()), fallbackDescription));
descriptions.put("en", firstNonBlank(normalizeOptional(payload.getDescriptionEn()), fallbackDescription));
descriptions.put("de", firstNonBlank(normalizeOptional(payload.getDescriptionDe()), fallbackDescription));
descriptions.put("fr", firstNonBlank(normalizeOptional(payload.getDescriptionFr()), fallbackDescription));
String fallbackSeoTitle = firstNonBlank(
normalizeOptional(payload.getSeoTitle()),
normalizeOptional(payload.getSeoTitleIt()),
normalizeOptional(payload.getSeoTitleEn()),
normalizeOptional(payload.getSeoTitleDe()),
normalizeOptional(payload.getSeoTitleFr())
);
Map<String, String> seoTitles = new LinkedHashMap<>();
seoTitles.put("it", firstNonBlank(normalizeOptional(payload.getSeoTitleIt()), fallbackSeoTitle));
seoTitles.put("en", firstNonBlank(normalizeOptional(payload.getSeoTitleEn()), fallbackSeoTitle));
seoTitles.put("de", firstNonBlank(normalizeOptional(payload.getSeoTitleDe()), fallbackSeoTitle));
seoTitles.put("fr", firstNonBlank(normalizeOptional(payload.getSeoTitleFr()), fallbackSeoTitle));
String fallbackSeoDescription = firstNonBlank(
normalizeOptional(payload.getSeoDescription()),
normalizeOptional(payload.getSeoDescriptionIt()),
normalizeOptional(payload.getSeoDescriptionEn()),
normalizeOptional(payload.getSeoDescriptionDe()),
normalizeOptional(payload.getSeoDescriptionFr())
);
Map<String, String> seoDescriptions = new LinkedHashMap<>();
seoDescriptions.put("it", validateSeoDescriptionLength(firstNonBlank(normalizeOptional(payload.getSeoDescriptionIt()), fallbackSeoDescription), "Italian"));
seoDescriptions.put("en", validateSeoDescriptionLength(firstNonBlank(normalizeOptional(payload.getSeoDescriptionEn()), fallbackSeoDescription), "English"));
seoDescriptions.put("de", validateSeoDescriptionLength(firstNonBlank(normalizeOptional(payload.getSeoDescriptionDe()), fallbackSeoDescription), "German"));
seoDescriptions.put("fr", validateSeoDescriptionLength(firstNonBlank(normalizeOptional(payload.getSeoDescriptionFr()), fallbackSeoDescription), "French"));
return new LocalizedCategoryContent(
names.get("it"),
firstNonBlank(descriptions.get("it"), fallbackDescription),
firstNonBlank(seoTitles.get("it"), fallbackSeoTitle),
firstNonBlank(seoDescriptions.get("it"), fallbackSeoDescription),
names,
descriptions,
seoTitles,
seoDescriptions
);
}
private String validateSeoDescriptionLength(String value, String languageLabel) {
if (value != null && value.length() > 160) {
throw new ResponseStatusException(BAD_REQUEST, languageLabel + " SEO description must be at most 160 characters");
}
return value;
}
private String firstNonBlank(String... values) {
if (values == null) {
return null;
}
for (String value : values) {
if (value != null && !value.isBlank()) {
return value;
}
}
return null;
}
private CategoryContext buildContext() { private CategoryContext buildContext() {
List<ShopCategory> categories = shopCategoryRepository.findAllByOrderBySortOrderAscNameAsc(); List<ShopCategory> categories = shopCategoryRepository.findAllByOrderBySortOrderAscNameAsc();
List<ShopProduct> products = shopProductRepository.findAll(); List<ShopProduct> products = shopProductRepository.findAll();
@@ -278,9 +383,25 @@ public class AdminShopCategoryControllerService {
dto.setParentCategoryName(category.getParentCategory() != null ? category.getParentCategory().getName() : null); dto.setParentCategoryName(category.getParentCategory() != null ? category.getParentCategory().getName() : null);
dto.setSlug(category.getSlug()); dto.setSlug(category.getSlug());
dto.setName(category.getName()); dto.setName(category.getName());
dto.setNameIt(category.getNameIt());
dto.setNameEn(category.getNameEn());
dto.setNameDe(category.getNameDe());
dto.setNameFr(category.getNameFr());
dto.setDescription(category.getDescription()); dto.setDescription(category.getDescription());
dto.setDescriptionIt(category.getDescriptionIt());
dto.setDescriptionEn(category.getDescriptionEn());
dto.setDescriptionDe(category.getDescriptionDe());
dto.setDescriptionFr(category.getDescriptionFr());
dto.setSeoTitle(category.getSeoTitle()); dto.setSeoTitle(category.getSeoTitle());
dto.setSeoTitleIt(category.getSeoTitleIt());
dto.setSeoTitleEn(category.getSeoTitleEn());
dto.setSeoTitleDe(category.getSeoTitleDe());
dto.setSeoTitleFr(category.getSeoTitleFr());
dto.setSeoDescription(category.getSeoDescription()); dto.setSeoDescription(category.getSeoDescription());
dto.setSeoDescriptionIt(category.getSeoDescriptionIt());
dto.setSeoDescriptionEn(category.getSeoDescriptionEn());
dto.setSeoDescriptionDe(category.getSeoDescriptionDe());
dto.setSeoDescriptionFr(category.getSeoDescriptionFr());
dto.setOgTitle(category.getOgTitle()); dto.setOgTitle(category.getOgTitle());
dto.setOgDescription(category.getOgDescription()); dto.setOgDescription(category.getOgDescription());
dto.setIndexable(category.getIndexable()); dto.setIndexable(category.getIndexable());
@@ -331,4 +452,16 @@ public class AdminShopCategoryControllerService {
Map<UUID, Integer> descendantProductCounts Map<UUID, Integer> descendantProductCounts
) { ) {
} }
private record LocalizedCategoryContent(
String defaultName,
String defaultDescription,
String defaultSeoTitle,
String defaultSeoDescription,
Map<String, String> names,
Map<String, String> descriptions,
Map<String, String> seoTitles,
Map<String, String> seoDescriptions
) {
}
} }

View File

@@ -353,6 +353,13 @@ public class AdminShopProductControllerService {
String normalizedColorName = normalizeRequired(payload.getColorName(), "Variant colorName is required"); String normalizedColorName = normalizeRequired(payload.getColorName(), "Variant colorName is required");
String normalizedVariantLabel = normalizeOptional(payload.getVariantLabel()); String normalizedVariantLabel = normalizeOptional(payload.getVariantLabel());
String normalizedSku = normalizeOptional(payload.getSku()); String normalizedSku = normalizeOptional(payload.getSku());
String fallbackColorLabel = firstNonBlank(
normalizeOptional(payload.getColorLabelIt()),
normalizeOptional(payload.getColorLabelEn()),
normalizeOptional(payload.getColorLabelDe()),
normalizeOptional(payload.getColorLabelFr()),
normalizedColorName
);
String normalizedMaterialCode = normalizeRequired( String normalizedMaterialCode = normalizeRequired(
payload.getInternalMaterialCode(), payload.getInternalMaterialCode(),
"Variant internalMaterialCode is required" "Variant internalMaterialCode is required"
@@ -380,6 +387,10 @@ public class AdminShopProductControllerService {
variant.setSku(normalizedSku); variant.setSku(normalizedSku);
variant.setVariantLabel(normalizedVariantLabel != null ? normalizedVariantLabel : normalizedColorName); variant.setVariantLabel(normalizedVariantLabel != null ? normalizedVariantLabel : normalizedColorName);
variant.setColorName(normalizedColorName); variant.setColorName(normalizedColorName);
variant.setColorLabelIt(firstNonBlank(normalizeOptional(payload.getColorLabelIt()), fallbackColorLabel));
variant.setColorLabelEn(firstNonBlank(normalizeOptional(payload.getColorLabelEn()), fallbackColorLabel));
variant.setColorLabelDe(firstNonBlank(normalizeOptional(payload.getColorLabelDe()), fallbackColorLabel));
variant.setColorLabelFr(firstNonBlank(normalizeOptional(payload.getColorLabelFr()), fallbackColorLabel));
variant.setColorHex(normalizeColorHex(payload.getColorHex())); variant.setColorHex(normalizeColorHex(payload.getColorHex()));
variant.setInternalMaterialCode(normalizedMaterialCode); variant.setInternalMaterialCode(normalizedMaterialCode);
variant.setPriceChf(price); variant.setPriceChf(price);
@@ -531,6 +542,10 @@ public class AdminShopProductControllerService {
dto.setSku(variant.getSku()); dto.setSku(variant.getSku());
dto.setVariantLabel(variant.getVariantLabel()); dto.setVariantLabel(variant.getVariantLabel());
dto.setColorName(variant.getColorName()); dto.setColorName(variant.getColorName());
dto.setColorLabelIt(variant.getColorLabelIt());
dto.setColorLabelEn(variant.getColorLabelEn());
dto.setColorLabelDe(variant.getColorLabelDe());
dto.setColorLabelFr(variant.getColorLabelFr());
dto.setColorHex(variant.getColorHex()); dto.setColorHex(variant.getColorHex());
dto.setInternalMaterialCode(variant.getInternalMaterialCode()); dto.setInternalMaterialCode(variant.getInternalMaterialCode());
dto.setPriceChf(variant.getPriceChf()); dto.setPriceChf(variant.getPriceChf());

View File

@@ -280,11 +280,19 @@ public class AdminOrderControllerService {
itemDto.setShopProductName(item.getShopProductName()); itemDto.setShopProductName(item.getShopProductName());
itemDto.setShopVariantLabel(item.getShopVariantLabel()); itemDto.setShopVariantLabel(item.getShopVariantLabel());
itemDto.setShopVariantColorName(item.getShopVariantColorName()); itemDto.setShopVariantColorName(item.getShopVariantColorName());
itemDto.setShopVariantColorLabelIt(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelIt() : null);
itemDto.setShopVariantColorLabelEn(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelEn() : null);
itemDto.setShopVariantColorLabelDe(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelDe() : null);
itemDto.setShopVariantColorLabelFr(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelFr() : null);
itemDto.setShopVariantColorHex(item.getShopVariantColorHex()); itemDto.setShopVariantColorHex(item.getShopVariantColorHex());
if (item.getFilamentVariant() != null) { if (item.getFilamentVariant() != null) {
itemDto.setFilamentVariantId(item.getFilamentVariant().getId()); itemDto.setFilamentVariantId(item.getFilamentVariant().getId());
itemDto.setFilamentVariantDisplayName(item.getFilamentVariant().getVariantDisplayName()); itemDto.setFilamentVariantDisplayName(item.getFilamentVariant().getVariantDisplayName());
itemDto.setFilamentColorName(item.getFilamentVariant().getColorName()); itemDto.setFilamentColorName(item.getFilamentVariant().getColorName());
itemDto.setFilamentColorLabelIt(item.getFilamentVariant().getColorLabelIt());
itemDto.setFilamentColorLabelEn(item.getFilamentVariant().getColorLabelEn());
itemDto.setFilamentColorLabelDe(item.getFilamentVariant().getColorLabelDe());
itemDto.setFilamentColorLabelFr(item.getFilamentVariant().getColorLabelFr());
itemDto.setFilamentColorHex(item.getFilamentVariant().getColorHex()); itemDto.setFilamentColorHex(item.getFilamentVariant().getColorHex());
} }
itemDto.setQuality(item.getQuality()); itemDto.setQuality(item.getQuality());

View File

@@ -334,11 +334,19 @@ public class OrderControllerService {
itemDto.setShopProductName(item.getShopProductName()); itemDto.setShopProductName(item.getShopProductName());
itemDto.setShopVariantLabel(item.getShopVariantLabel()); itemDto.setShopVariantLabel(item.getShopVariantLabel());
itemDto.setShopVariantColorName(item.getShopVariantColorName()); itemDto.setShopVariantColorName(item.getShopVariantColorName());
itemDto.setShopVariantColorLabelIt(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelIt() : null);
itemDto.setShopVariantColorLabelEn(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelEn() : null);
itemDto.setShopVariantColorLabelDe(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelDe() : null);
itemDto.setShopVariantColorLabelFr(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelFr() : null);
itemDto.setShopVariantColorHex(item.getShopVariantColorHex()); itemDto.setShopVariantColorHex(item.getShopVariantColorHex());
if (item.getFilamentVariant() != null) { if (item.getFilamentVariant() != null) {
itemDto.setFilamentVariantId(item.getFilamentVariant().getId()); itemDto.setFilamentVariantId(item.getFilamentVariant().getId());
itemDto.setFilamentVariantDisplayName(item.getFilamentVariant().getVariantDisplayName()); itemDto.setFilamentVariantDisplayName(item.getFilamentVariant().getVariantDisplayName());
itemDto.setFilamentColorName(item.getFilamentVariant().getColorName()); itemDto.setFilamentColorName(item.getFilamentVariant().getColorName());
itemDto.setFilamentColorLabelIt(item.getFilamentVariant().getColorLabelIt());
itemDto.setFilamentColorLabelEn(item.getFilamentVariant().getColorLabelEn());
itemDto.setFilamentColorLabelDe(item.getFilamentVariant().getColorLabelDe());
itemDto.setFilamentColorLabelFr(item.getFilamentVariant().getColorLabelFr());
itemDto.setFilamentColorHex(item.getFilamentVariant().getColorHex()); itemDto.setFilamentColorHex(item.getFilamentVariant().getColorHex());
} }
itemDto.setQuality(item.getQuality()); itemDto.setQuality(item.getQuality());

View File

@@ -81,7 +81,15 @@ public class QuoteSessionResponseAssembler {
dto.put("shopProductName", item.getShopProductName()); dto.put("shopProductName", item.getShopProductName());
dto.put("shopVariantLabel", item.getShopVariantLabel()); dto.put("shopVariantLabel", item.getShopVariantLabel());
dto.put("shopVariantColorName", item.getShopVariantColorName()); dto.put("shopVariantColorName", item.getShopVariantColorName());
dto.put("shopVariantColorLabelIt", item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelIt() : null);
dto.put("shopVariantColorLabelEn", item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelEn() : null);
dto.put("shopVariantColorLabelDe", item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelDe() : null);
dto.put("shopVariantColorLabelFr", item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelFr() : null);
dto.put("shopVariantColorHex", item.getShopVariantColorHex()); dto.put("shopVariantColorHex", item.getShopVariantColorHex());
dto.put("filamentColorLabelIt", item.getFilamentVariant() != null ? item.getFilamentVariant().getColorLabelIt() : null);
dto.put("filamentColorLabelEn", item.getFilamentVariant() != null ? item.getFilamentVariant().getColorLabelEn() : null);
dto.put("filamentColorLabelDe", item.getFilamentVariant() != null ? item.getFilamentVariant().getColorLabelDe() : null);
dto.put("filamentColorLabelFr", item.getFilamentVariant() != null ? item.getFilamentVariant().getColorLabelFr() : null);
dto.put("materialCode", item.getMaterialCode()); dto.put("materialCode", item.getMaterialCode());
dto.put("quality", item.getQuality()); dto.put("quality", item.getQuality());
dto.put("nozzleDiameterMm", item.getNozzleDiameterMm()); dto.put("nozzleDiameterMm", item.getNozzleDiameterMm());

View File

@@ -71,7 +71,7 @@ public class PublicShopCatalogService {
public List<ShopCategoryTreeDto> getCategories(String language) { public List<ShopCategoryTreeDto> getCategories(String language) {
CategoryContext categoryContext = loadCategoryContext(language); CategoryContext categoryContext = loadCategoryContext(language);
return buildCategoryTree(null, categoryContext); return buildCategoryTree(null, categoryContext, language);
} }
public ShopCategoryDetailDto getCategory(String slug, String language) { public ShopCategoryDetailDto getCategory(String slug, String language) {
@@ -83,7 +83,7 @@ public class PublicShopCatalogService {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Category not found"); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Category not found");
} }
return buildCategoryDetail(category, categoryContext); return buildCategoryDetail(category, categoryContext, language);
} }
public ShopProductCatalogResponseDto getProductCatalog(String categorySlug, Boolean featuredOnly, String language) { public ShopProductCatalogResponseDto getProductCatalog(String categorySlug, Boolean featuredOnly, String language) {
@@ -114,7 +114,7 @@ public class PublicShopCatalogService {
.toList(); .toList();
ShopCategoryDetailDto selectedCategoryDetail = selectedCategory != null ShopCategoryDetailDto selectedCategoryDetail = selectedCategory != null
? buildCategoryDetail(selectedCategory, categoryContext) ? buildCategoryDetail(selectedCategory, categoryContext, language)
: null; : null;
return new ShopProductCatalogResponseDto( return new ShopProductCatalogResponseDto(
@@ -316,53 +316,63 @@ public class PublicShopCatalogService {
return total; return total;
} }
private List<ShopCategoryTreeDto> buildCategoryTree(UUID parentId, CategoryContext categoryContext) { private List<ShopCategoryTreeDto> buildCategoryTree(UUID parentId,
CategoryContext categoryContext,
String language) {
return categoryContext.childrenByParentId().getOrDefault(parentId, List.of()).stream() return categoryContext.childrenByParentId().getOrDefault(parentId, List.of()).stream()
.map(category -> new ShopCategoryTreeDto( .map(category -> new ShopCategoryTreeDto(
category.getId(), category.getId(),
category.getParentCategory() != null ? category.getParentCategory().getId() : null, category.getParentCategory() != null ? category.getParentCategory().getId() : null,
category.getSlug(), category.getSlug(),
category.getName(), category.getNameForLanguage(language),
category.getDescription(), category.getDescriptionForLanguage(language),
category.getSeoTitle(), category.getSeoTitleForLanguage(language),
category.getSeoDescription(), category.getSeoDescriptionForLanguage(language),
category.getOgTitle(), category.getOgTitle(),
category.getOgDescription(), category.getOgDescription(),
category.getIndexable(), category.getIndexable(),
category.getSortOrder(), category.getSortOrder(),
categoryContext.descendantProductCounts().getOrDefault(category.getId(), 0), categoryContext.descendantProductCounts().getOrDefault(category.getId(), 0),
selectPrimaryMedia(categoryContext.categoryMediaBySlug().get(categoryMediaUsageKey(category))), selectPrimaryMedia(categoryContext.categoryMediaBySlug().get(categoryMediaUsageKey(category))),
buildCategoryTree(category.getId(), categoryContext) buildCategoryTree(category.getId(), categoryContext, language)
)) ))
.toList(); .toList();
} }
private ShopCategoryDetailDto buildCategoryDetail(ShopCategory category, CategoryContext categoryContext) { private ShopCategoryDetailDto buildCategoryDetail(ShopCategory category,
CategoryContext categoryContext,
String language) {
List<PublicMediaUsageDto> images = categoryContext.categoryMediaBySlug().getOrDefault(categoryMediaUsageKey(category), List.of()); List<PublicMediaUsageDto> images = categoryContext.categoryMediaBySlug().getOrDefault(categoryMediaUsageKey(category), List.of());
String localizedSeoTitle = category.getSeoTitleForLanguage(language);
String localizedSeoDescription = category.getSeoDescriptionForLanguage(language);
return new ShopCategoryDetailDto( return new ShopCategoryDetailDto(
category.getId(), category.getId(),
category.getSlug(), category.getSlug(),
category.getName(), category.getNameForLanguage(language),
category.getDescription(), category.getDescriptionForLanguage(language),
category.getSeoTitle(), localizedSeoTitle,
category.getSeoDescription(), localizedSeoDescription,
category.getOgTitle(), category.getOgTitle(),
category.getOgDescription(), category.getOgDescription(),
category.getIndexable(), category.getIndexable(),
category.getSortOrder(), category.getSortOrder(),
categoryContext.descendantProductCounts().getOrDefault(category.getId(), 0), categoryContext.descendantProductCounts().getOrDefault(category.getId(), 0),
buildCategoryBreadcrumbs(category), buildCategoryBreadcrumbs(category, language),
selectPrimaryMedia(images), selectPrimaryMedia(images),
images, images,
buildCategoryTree(category.getId(), categoryContext) buildCategoryTree(category.getId(), categoryContext, language)
); );
} }
private List<ShopCategoryRefDto> buildCategoryBreadcrumbs(ShopCategory category) { private List<ShopCategoryRefDto> buildCategoryBreadcrumbs(ShopCategory category, String language) {
List<ShopCategoryRefDto> breadcrumbs = new ArrayList<>(); List<ShopCategoryRefDto> breadcrumbs = new ArrayList<>();
ShopCategory current = category; ShopCategory current = category;
while (current != null) { while (current != null) {
breadcrumbs.add(new ShopCategoryRefDto(current.getId(), current.getSlug(), current.getName())); breadcrumbs.add(new ShopCategoryRefDto(
current.getId(),
current.getSlug(),
current.getNameForLanguage(language)
));
current = current.getParentCategory(); current = current.getParentCategory();
} }
java.util.Collections.reverse(breadcrumbs); java.util.Collections.reverse(breadcrumbs);
@@ -399,11 +409,11 @@ public class PublicShopCatalogService {
new ShopCategoryRefDto( new ShopCategoryRefDto(
entry.product().getCategory().getId(), entry.product().getCategory().getId(),
entry.product().getCategory().getSlug(), entry.product().getCategory().getSlug(),
entry.product().getCategory().getName() entry.product().getCategory().getNameForLanguage(language)
), ),
resolvePriceFrom(entry.variants()), resolvePriceFrom(entry.variants()),
resolvePriceTo(entry.variants()), resolvePriceTo(entry.variants()),
toVariantDto(entry.defaultVariant(), entry.defaultVariant(), variantColorHexByMaterialAndColor), toVariantDto(entry.defaultVariant(), entry.defaultVariant(), variantColorHexByMaterialAndColor, language),
selectPrimaryMedia(images), selectPrimaryMedia(images),
toProductModelDto(entry) toProductModelDto(entry)
); );
@@ -432,14 +442,14 @@ public class PublicShopCatalogService {
new ShopCategoryRefDto( new ShopCategoryRefDto(
entry.product().getCategory().getId(), entry.product().getCategory().getId(),
entry.product().getCategory().getSlug(), entry.product().getCategory().getSlug(),
entry.product().getCategory().getName() entry.product().getCategory().getNameForLanguage(language)
), ),
buildCategoryBreadcrumbs(entry.product().getCategory()), buildCategoryBreadcrumbs(entry.product().getCategory(), language),
resolvePriceFrom(entry.variants()), resolvePriceFrom(entry.variants()),
resolvePriceTo(entry.variants()), resolvePriceTo(entry.variants()),
toVariantDto(entry.defaultVariant(), entry.defaultVariant(), variantColorHexByMaterialAndColor), toVariantDto(entry.defaultVariant(), entry.defaultVariant(), variantColorHexByMaterialAndColor, language),
entry.variants().stream() entry.variants().stream()
.map(variant -> toVariantDto(variant, entry.defaultVariant(), variantColorHexByMaterialAndColor)) .map(variant -> toVariantDto(variant, entry.defaultVariant(), variantColorHexByMaterialAndColor, language))
.toList(), .toList(),
selectPrimaryMedia(images), selectPrimaryMedia(images),
images, images,
@@ -449,7 +459,8 @@ public class PublicShopCatalogService {
private ShopProductVariantOptionDto toVariantDto(ShopProductVariant variant, private ShopProductVariantOptionDto toVariantDto(ShopProductVariant variant,
ShopProductVariant defaultVariant, ShopProductVariant defaultVariant,
Map<String, String> variantColorHexByMaterialAndColor) { Map<String, String> variantColorHexByMaterialAndColor,
String language) {
if (variant == null) { if (variant == null) {
return null; return null;
} }
@@ -463,6 +474,7 @@ public class PublicShopCatalogService {
variant.getSku(), variant.getSku(),
variant.getVariantLabel(), variant.getVariantLabel(),
variant.getColorName(), variant.getColorName(),
variant.getColorLabelForLanguage(language),
colorHex, colorHex,
variant.getPriceChf(), variant.getPriceChf(),
defaultVariant != null && Objects.equals(defaultVariant.getId(), variant.getId()) defaultVariant != null && Objects.equals(defaultVariant.getId(), variant.getId())

View File

@@ -0,0 +1,55 @@
package com.printcalculator.entity;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ShopCategoryTest {
@Test
void localizedAccessorsShouldReturnLanguageSpecificValues() {
ShopCategory category = new ShopCategory();
category.setName("Desk accessories");
category.setNameIt("Accessori da scrivania");
category.setNameEn("Desk accessories");
category.setNameDe("Schreibtischzubehor");
category.setNameFr("Accessoires de bureau");
category.setDescription("Legacy description");
category.setDescriptionIt("Organizer e accessori stampati per la scrivania.");
category.setDescriptionEn("Printed desk organizers and accessories.");
category.setDescriptionDe("Gedruckte Organizer und Zubehor fur den Schreibtisch.");
category.setDescriptionFr("Accessoires et organiseurs imprimes pour le bureau.");
category.setSeoTitle("Legacy SEO title");
category.setSeoTitleIt("Accessori da scrivania stampati in 3D");
category.setSeoTitleEn("3D printed desk accessories");
category.setSeoTitleDe("3D-gedruckte Schreibtischaccessoires");
category.setSeoTitleFr("Accessoires de bureau imprimes en 3D");
category.setSeoDescription("Legacy SEO description");
category.setSeoDescriptionIt("Accessori da scrivania personalizzati e funzionali.");
category.setSeoDescriptionEn("Functional custom desk accessories.");
category.setSeoDescriptionDe("Funktionale personalisierte Schreibtischaccessoires.");
category.setSeoDescriptionFr("Accessoires de bureau fonctionnels et personnalises.");
assertEquals("Accessori da scrivania", category.getNameForLanguage("it"));
assertEquals("Desk accessories", category.getNameForLanguage("en"));
assertEquals("Schreibtischzubehor", category.getNameForLanguage("de"));
assertEquals("Accessoires de bureau", category.getNameForLanguage("fr"));
assertEquals("Gedruckte Organizer und Zubehor fur den Schreibtisch.", category.getDescriptionForLanguage("de"));
assertEquals("3D printed desk accessories", category.getSeoTitleForLanguage("en"));
assertEquals("Accessoires de bureau fonctionnels et personnalises.", category.getSeoDescriptionForLanguage("fr"));
}
@Test
void localizedAccessorsShouldFallbackToLegacyValues() {
ShopCategory category = new ShopCategory();
category.setName("Desk accessories");
category.setDescription("Printed desk organizers and accessories.");
category.setSeoTitle("3D printed desk accessories");
category.setSeoDescription("Functional custom desk accessories.");
assertEquals("Desk accessories", category.getNameForLanguage("it"));
assertEquals("Printed desk organizers and accessories.", category.getDescriptionForLanguage("de"));
assertEquals("3D printed desk accessories", category.getSeoTitleForLanguage("fr-CH"));
assertEquals("Functional custom desk accessories.", category.getSeoDescriptionForLanguage("en-US"));
}
}

116
db.sql
View File

@@ -44,6 +44,10 @@ create table filament_variant
variant_display_name text not null, -- es: "PLA Nero Opaco BrandX" variant_display_name text not null, -- es: "PLA Nero Opaco BrandX"
color_name text not null, -- Nero, Bianco, ecc. color_name text not null, -- Nero, Bianco, ecc.
color_label_it text,
color_label_en text,
color_label_de text,
color_label_fr text,
color_hex text, color_hex text,
finish_type text not null default 'GLOSSY' finish_type text not null default 'GLOSSY'
check (finish_type in ('GLOSSY', 'MATTE', 'MARBLE', 'SILK', 'TRANSLUCENT', 'SPECIAL')), check (finish_type in ('GLOSSY', 'MATTE', 'MARBLE', 'SILK', 'TRANSLUCENT', 'SPECIAL')),
@@ -70,6 +74,22 @@ select filament_variant_id,
(stock_spools * spool_net_kg) as stock_kg (stock_spools * spool_net_kg) as stock_kg
from filament_variant; from filament_variant;
alter table filament_variant
add column if not exists color_label_it text,
add column if not exists color_label_en text,
add column if not exists color_label_de text,
add column if not exists color_label_fr text;
update filament_variant
set color_label_it = coalesce(nullif(btrim(color_label_it), ''), color_name),
color_label_en = coalesce(nullif(btrim(color_label_en), ''), color_name),
color_label_de = coalesce(nullif(btrim(color_label_de), ''), color_name),
color_label_fr = coalesce(nullif(btrim(color_label_fr), ''), color_name)
where nullif(btrim(color_label_it), '') is null
or nullif(btrim(color_label_en), '') is null
or nullif(btrim(color_label_de), '') is null
or nullif(btrim(color_label_fr), '') is null;
create table printer_machine_profile create table printer_machine_profile
( (
printer_machine_profile_id bigserial primary key, printer_machine_profile_id bigserial primary key,
@@ -1013,9 +1033,25 @@ CREATE TABLE IF NOT EXISTS shop_category
parent_category_id uuid REFERENCES shop_category (shop_category_id) ON DELETE SET NULL, parent_category_id uuid REFERENCES shop_category (shop_category_id) ON DELETE SET NULL,
slug text NOT NULL UNIQUE, slug text NOT NULL UNIQUE,
name text NOT NULL, name text NOT NULL,
name_it text,
name_en text,
name_de text,
name_fr text,
description text, description text,
description_it text,
description_en text,
description_de text,
description_fr text,
seo_title text, seo_title text,
seo_title_it text,
seo_title_en text,
seo_title_de text,
seo_title_fr text,
seo_description text, seo_description text,
seo_description_it text,
seo_description_en text,
seo_description_de text,
seo_description_fr text,
og_title text, og_title text,
og_description text, og_description text,
indexable boolean NOT NULL DEFAULT true, indexable boolean NOT NULL DEFAULT true,
@@ -1034,6 +1070,66 @@ CREATE INDEX IF NOT EXISTS ix_shop_category_parent_sort
CREATE INDEX IF NOT EXISTS ix_shop_category_active_sort CREATE INDEX IF NOT EXISTS ix_shop_category_active_sort
ON shop_category (is_active, sort_order, created_at DESC); ON shop_category (is_active, sort_order, created_at DESC);
ALTER TABLE shop_category
ADD COLUMN IF NOT EXISTS name_it text,
ADD COLUMN IF NOT EXISTS name_en text,
ADD COLUMN IF NOT EXISTS name_de text,
ADD COLUMN IF NOT EXISTS name_fr text,
ADD COLUMN IF NOT EXISTS description_it text,
ADD COLUMN IF NOT EXISTS description_en text,
ADD COLUMN IF NOT EXISTS description_de text,
ADD COLUMN IF NOT EXISTS description_fr text,
ADD COLUMN IF NOT EXISTS seo_title_it text,
ADD COLUMN IF NOT EXISTS seo_title_en text,
ADD COLUMN IF NOT EXISTS seo_title_de text,
ADD COLUMN IF NOT EXISTS seo_title_fr text,
ADD COLUMN IF NOT EXISTS seo_description_it text,
ADD COLUMN IF NOT EXISTS seo_description_en text,
ADD COLUMN IF NOT EXISTS seo_description_de text,
ADD COLUMN IF NOT EXISTS seo_description_fr text;
UPDATE shop_category
SET
name_it = COALESCE(NULLIF(btrim(name_it), ''), name),
name_en = COALESCE(NULLIF(btrim(name_en), ''), name),
name_de = COALESCE(NULLIF(btrim(name_de), ''), name),
name_fr = COALESCE(NULLIF(btrim(name_fr), ''), name),
description_it = COALESCE(NULLIF(btrim(description_it), ''), description),
description_en = COALESCE(NULLIF(btrim(description_en), ''), description),
description_de = COALESCE(NULLIF(btrim(description_de), ''), description),
description_fr = COALESCE(NULLIF(btrim(description_fr), ''), description),
seo_title_it = COALESCE(NULLIF(btrim(seo_title_it), ''), seo_title),
seo_title_en = COALESCE(NULLIF(btrim(seo_title_en), ''), seo_title),
seo_title_de = COALESCE(NULLIF(btrim(seo_title_de), ''), seo_title),
seo_title_fr = COALESCE(NULLIF(btrim(seo_title_fr), ''), seo_title),
seo_description_it = COALESCE(NULLIF(btrim(seo_description_it), ''), seo_description),
seo_description_en = COALESCE(NULLIF(btrim(seo_description_en), ''), seo_description),
seo_description_de = COALESCE(NULLIF(btrim(seo_description_de), ''), seo_description),
seo_description_fr = COALESCE(NULLIF(btrim(seo_description_fr), ''), seo_description)
WHERE
NULLIF(btrim(name_it), '') IS NULL
OR NULLIF(btrim(name_en), '') IS NULL
OR NULLIF(btrim(name_de), '') IS NULL
OR NULLIF(btrim(name_fr), '') IS NULL
OR (description IS NOT NULL AND (
NULLIF(btrim(description_it), '') IS NULL
OR NULLIF(btrim(description_en), '') IS NULL
OR NULLIF(btrim(description_de), '') IS NULL
OR NULLIF(btrim(description_fr), '') IS NULL
))
OR (seo_title IS NOT NULL AND (
NULLIF(btrim(seo_title_it), '') IS NULL
OR NULLIF(btrim(seo_title_en), '') IS NULL
OR NULLIF(btrim(seo_title_de), '') IS NULL
OR NULLIF(btrim(seo_title_fr), '') IS NULL
))
OR (seo_description IS NOT NULL AND (
NULLIF(btrim(seo_description_it), '') IS NULL
OR NULLIF(btrim(seo_description_en), '') IS NULL
OR NULLIF(btrim(seo_description_de), '') IS NULL
OR NULLIF(btrim(seo_description_fr), '') IS NULL
));
CREATE TABLE IF NOT EXISTS shop_product CREATE TABLE IF NOT EXISTS shop_product
( (
shop_product_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), shop_product_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -1165,6 +1261,10 @@ CREATE TABLE IF NOT EXISTS shop_product_variant
sku text UNIQUE, sku text UNIQUE,
variant_label text NOT NULL, variant_label text NOT NULL,
color_name text NOT NULL, color_name text NOT NULL,
color_label_it text,
color_label_en text,
color_label_de text,
color_label_fr text,
color_hex text, color_hex text,
internal_material_code text NOT NULL, internal_material_code text NOT NULL,
price_chf numeric(12, 2) NOT NULL DEFAULT 0.00 CHECK (price_chf >= 0), price_chf numeric(12, 2) NOT NULL DEFAULT 0.00 CHECK (price_chf >= 0),
@@ -1181,6 +1281,22 @@ CREATE INDEX IF NOT EXISTS ix_shop_product_variant_product_active_sort
CREATE INDEX IF NOT EXISTS ix_shop_product_variant_sku CREATE INDEX IF NOT EXISTS ix_shop_product_variant_sku
ON shop_product_variant (sku); ON shop_product_variant (sku);
ALTER TABLE shop_product_variant
ADD COLUMN IF NOT EXISTS color_label_it text,
ADD COLUMN IF NOT EXISTS color_label_en text,
ADD COLUMN IF NOT EXISTS color_label_de text,
ADD COLUMN IF NOT EXISTS color_label_fr text;
UPDATE shop_product_variant
SET color_label_it = COALESCE(NULLIF(btrim(color_label_it), ''), color_name),
color_label_en = COALESCE(NULLIF(btrim(color_label_en), ''), color_name),
color_label_de = COALESCE(NULLIF(btrim(color_label_de), ''), color_name),
color_label_fr = COALESCE(NULLIF(btrim(color_label_fr), ''), color_name)
WHERE NULLIF(btrim(color_label_it), '') IS NULL
OR NULLIF(btrim(color_label_en), '') IS NULL
OR NULLIF(btrim(color_label_de), '') IS NULL
OR NULLIF(btrim(color_label_fr), '') IS NULL;
CREATE TABLE IF NOT EXISTS shop_product_model_asset CREATE TABLE IF NOT EXISTS shop_product_model_asset
( (
shop_product_model_asset_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), shop_product_model_asset_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),

View File

@@ -40,93 +40,6 @@ export const PRODUCT_COLORS: ColorCategory[] = [
}, },
]; ];
const COLOR_HEX_BY_TRANSLATION_KEY: Record<string, string> = {
...Object.fromEntries(
PRODUCT_COLORS.flatMap((category) =>
category.colors.map((color) => [color.label, color.hex] as const),
),
),
'COLOR.NAME.ORANGE': '#f5a623',
'COLOR.NAME.GRAY': '#b7b7b7',
'COLOR.NAME.LIGHT_GRAY': '#d8dadd',
'COLOR.NAME.DARK_GRAY': '#4f4f4f',
'COLOR.NAME.PURPLE': '#7b1fa2',
'COLOR.NAME.BEIGE': '#d4c09a',
'COLOR.NAME.SAND_BEIGE': '#d7c2a0',
};
const COLOR_TRANSLATION_KEY_BY_VALUE: Record<string, string> = {
black: 'COLOR.NAME.BLACK',
nero: 'COLOR.NAME.BLACK',
noir: 'COLOR.NAME.BLACK',
schwarz: 'COLOR.NAME.BLACK',
white: 'COLOR.NAME.WHITE',
bianco: 'COLOR.NAME.WHITE',
blanc: 'COLOR.NAME.WHITE',
weiss: 'COLOR.NAME.WHITE',
red: 'COLOR.NAME.RED',
rosso: 'COLOR.NAME.RED',
rouge: 'COLOR.NAME.RED',
rot: 'COLOR.NAME.RED',
blue: 'COLOR.NAME.BLUE',
blu: 'COLOR.NAME.BLUE',
bleu: 'COLOR.NAME.BLUE',
blau: 'COLOR.NAME.BLUE',
green: 'COLOR.NAME.GREEN',
verde: 'COLOR.NAME.GREEN',
vert: 'COLOR.NAME.GREEN',
grun: 'COLOR.NAME.GREEN',
yellow: 'COLOR.NAME.YELLOW',
giallo: 'COLOR.NAME.YELLOW',
jaune: 'COLOR.NAME.YELLOW',
gelb: 'COLOR.NAME.YELLOW',
orange: 'COLOR.NAME.ORANGE',
arancione: 'COLOR.NAME.ORANGE',
naranja: 'COLOR.NAME.ORANGE',
gris: 'COLOR.NAME.GRAY',
gray: 'COLOR.NAME.GRAY',
grey: 'COLOR.NAME.GRAY',
grigio: 'COLOR.NAME.GRAY',
grau: 'COLOR.NAME.GRAY',
'light gray': 'COLOR.NAME.LIGHT_GRAY',
'light grey': 'COLOR.NAME.LIGHT_GRAY',
'grigio chiaro': 'COLOR.NAME.LIGHT_GRAY',
'gris clair': 'COLOR.NAME.LIGHT_GRAY',
hellgrau: 'COLOR.NAME.LIGHT_GRAY',
'dark gray': 'COLOR.NAME.DARK_GRAY',
'dark grey': 'COLOR.NAME.DARK_GRAY',
'grigio scuro': 'COLOR.NAME.DARK_GRAY',
'gris fonce': 'COLOR.NAME.DARK_GRAY',
dunkelgrau: 'COLOR.NAME.DARK_GRAY',
purple: 'COLOR.NAME.PURPLE',
violet: 'COLOR.NAME.PURPLE',
viola: 'COLOR.NAME.PURPLE',
lila: 'COLOR.NAME.PURPLE',
beige: 'COLOR.NAME.BEIGE',
'sand beige': 'COLOR.NAME.SAND_BEIGE',
'beige sabbia': 'COLOR.NAME.SAND_BEIGE',
'beige sable': 'COLOR.NAME.SAND_BEIGE',
sandbeige: 'COLOR.NAME.SAND_BEIGE',
'matte black': 'COLOR.NAME.MATTE_BLACK',
'black matte': 'COLOR.NAME.MATTE_BLACK',
'nero opaco': 'COLOR.NAME.MATTE_BLACK',
'noir mat': 'COLOR.NAME.MATTE_BLACK',
'matt schwarz': 'COLOR.NAME.MATTE_BLACK',
'schwarz matt': 'COLOR.NAME.MATTE_BLACK',
'matte white': 'COLOR.NAME.MATTE_WHITE',
'white matte': 'COLOR.NAME.MATTE_WHITE',
'bianco opaco': 'COLOR.NAME.MATTE_WHITE',
'blanc mat': 'COLOR.NAME.MATTE_WHITE',
'matt weiss': 'COLOR.NAME.MATTE_WHITE',
'weiss matt': 'COLOR.NAME.MATTE_WHITE',
'matte gray': 'COLOR.NAME.MATTE_GRAY',
'matte grey': 'COLOR.NAME.MATTE_GRAY',
'grigio opaco': 'COLOR.NAME.MATTE_GRAY',
'gris mat': 'COLOR.NAME.MATTE_GRAY',
'matt grau': 'COLOR.NAME.MATTE_GRAY',
'grau matt': 'COLOR.NAME.MATTE_GRAY',
};
export function normalizeColorValue(value: string | null | undefined): string { export function normalizeColorValue(value: string | null | undefined): string {
return String(value ?? '') return String(value ?? '')
.trim() .trim()
@@ -138,30 +51,7 @@ export function normalizeColorValue(value: string | null | undefined): string {
.replace(/\s+/g, ' '); .replace(/\s+/g, ' ');
} }
export function getColorTranslationKey(
value: string | null | undefined,
): string | null {
const normalized = normalizeColorValue(value);
return normalized ? COLOR_TRANSLATION_KEY_BY_VALUE[normalized] ?? null : null;
}
export function getColorLabelToken(
value: string | null | undefined,
): string | null {
const raw = String(value ?? '').trim();
if (!raw) {
return null;
}
return getColorTranslationKey(raw) ?? raw;
}
export function findColorHex(value: string | null | undefined): string | null { export function findColorHex(value: string | null | undefined): string | null {
const translationKey = getColorTranslationKey(value);
if (translationKey) {
return COLOR_HEX_BY_TRANSLATION_KEY[translationKey] ?? null;
}
const normalized = normalizeColorValue(value); const normalized = normalizeColorValue(value);
if (!normalized) { if (!normalized) {
return null; return null;
@@ -179,6 +69,52 @@ export function findColorHex(value: string | null | undefined): string | null {
return null; return null;
} }
export interface LocalizedColorLabelSet {
fallback?: string | null;
it?: string | null;
en?: string | null;
de?: string | null;
fr?: string | null;
}
export function resolveLocalizedColorLabel(
language: string | null | undefined,
labels: LocalizedColorLabelSet,
): string | null {
const normalizedLanguage = String(language ?? '')
.trim()
.toLowerCase()
.split('-')[0];
const preferred =
normalizedLanguage === 'it'
? labels.it
: normalizedLanguage === 'en'
? labels.en
: normalizedLanguage === 'de'
? labels.de
: normalizedLanguage === 'fr'
? labels.fr
: null;
return (
firstNonBlank(preferred, labels.fallback) ??
firstNonBlank(labels.it, labels.en, labels.de, labels.fr)
);
}
function firstNonBlank(
...values: Array<string | null | undefined>
): string | null {
for (const value of values) {
const normalized = String(value ?? '').trim();
if (normalized) {
return normalized;
}
}
return null;
}
export function getColorHex(value: string): string { export function getColorHex(value: string): string {
return findColorHex(value) ?? DEFAULT_BRAND_COLOR; return findColorHex(value) ?? DEFAULT_BRAND_COLOR;
} }

View File

@@ -17,7 +17,7 @@ import {
import { finalize } from 'rxjs'; import { finalize } from 'rxjs';
import { import {
findColorHex, findColorHex,
getColorLabelToken, resolveLocalizedColorLabel,
} from '../constants/colors.const'; } from '../constants/colors.const';
@Component({ @Component({
@@ -147,15 +147,20 @@ export class NavbarComponent {
} }
cartItemVariant(item: ShopCartItem): string | null { cartItemVariant(item: ShopCartItem): string | null {
return ( return item.shopVariantLabel || this.cartItemColor(item);
item.shopVariantLabel || getColorLabelToken(item.shopVariantColorName)
);
} }
cartItemColor(item: ShopCartItem): string | null { cartItemColor(item: ShopCartItem): string | null {
return ( return (
getColorLabelToken(item.shopVariantColorName) ?? resolveLocalizedColorLabel(this.langService.selectedLang(), {
getColorLabelToken(item.colorCode) fallback: item.shopVariantColorName ?? item.colorCode,
it: item.shopVariantColorLabelIt,
en: item.shopVariantColorLabelEn,
de: item.shopVariantColorLabelDe,
fr: item.shopVariantColorLabelFr,
}) ??
item.shopVariantColorName ??
item.colorCode
); );
} }

View File

@@ -101,6 +101,22 @@
placeholder="Nero, Bianco..." placeholder="Nero, Bianco..."
/> />
</label> </label>
<label class="form-field">
<span>Label IT</span>
<input type="text" [(ngModel)]="newVariant.colorLabelIt" />
</label>
<label class="form-field">
<span>Label EN</span>
<input type="text" [(ngModel)]="newVariant.colorLabelEn" />
</label>
<label class="form-field">
<span>Label DE</span>
<input type="text" [(ngModel)]="newVariant.colorLabelDe" />
</label>
<label class="form-field">
<span>Label FR</span>
<input type="text" [(ngModel)]="newVariant.colorLabelFr" />
</label>
<label class="form-field"> <label class="form-field">
<span>Hex colore</span> <span>Hex colore</span>
<input <input
@@ -229,7 +245,7 @@
class="color-dot" class="color-dot"
[style.background-color]="getVariantColorHex(variant)" [style.background-color]="getVariantColorHex(variant)"
></span> ></span>
{{ variant.colorName || "N/D" }} {{ variant.colorLabelIt || variant.colorName || "N/D" }}
</span> </span>
<span <span
>Stock spools: >Stock spools:
@@ -290,6 +306,22 @@
<span>Colore</span> <span>Colore</span>
<input type="text" [(ngModel)]="variant.colorName" /> <input type="text" [(ngModel)]="variant.colorName" />
</label> </label>
<label class="form-field">
<span>Label IT</span>
<input type="text" [(ngModel)]="variant.colorLabelIt" />
</label>
<label class="form-field">
<span>Label EN</span>
<input type="text" [(ngModel)]="variant.colorLabelEn" />
</label>
<label class="form-field">
<span>Label DE</span>
<input type="text" [(ngModel)]="variant.colorLabelDe" />
</label>
<label class="form-field">
<span>Label FR</span>
<input type="text" [(ngModel)]="variant.colorLabelFr" />
</label>
<label class="form-field"> <label class="form-field">
<span>Hex colore</span> <span>Hex colore</span>
<input type="text" [(ngModel)]="variant.colorHex" /> <input type="text" [(ngModel)]="variant.colorHex" />

View File

@@ -47,6 +47,10 @@ export class AdminFilamentStockComponent implements OnInit {
materialTypeId: 0, materialTypeId: 0,
variantDisplayName: '', variantDisplayName: '',
colorName: '', colorName: '',
colorLabelIt: '',
colorLabelEn: '',
colorLabelDe: '',
colorLabelFr: '',
colorHex: '', colorHex: '',
finishType: 'GLOSSY', finishType: 'GLOSSY',
brand: '', brand: '',
@@ -206,6 +210,10 @@ export class AdminFilamentStockComponent implements OnInit {
this.newVariant.materialTypeId || this.materials[0]?.id || 0, this.newVariant.materialTypeId || this.materials[0]?.id || 0,
variantDisplayName: '', variantDisplayName: '',
colorName: '', colorName: '',
colorLabelIt: '',
colorLabelEn: '',
colorLabelDe: '',
colorLabelFr: '',
colorHex: '', colorHex: '',
finishType: 'GLOSSY', finishType: 'GLOSSY',
brand: '', brand: '',
@@ -359,6 +367,10 @@ export class AdminFilamentStockComponent implements OnInit {
materialTypeId: Number(source.materialTypeId), materialTypeId: Number(source.materialTypeId),
variantDisplayName: (source.variantDisplayName || '').trim(), variantDisplayName: (source.variantDisplayName || '').trim(),
colorName: (source.colorName || '').trim(), colorName: (source.colorName || '').trim(),
colorLabelIt: (source.colorLabelIt || '').trim() || undefined,
colorLabelEn: (source.colorLabelEn || '').trim() || undefined,
colorLabelDe: (source.colorLabelDe || '').trim() || undefined,
colorLabelFr: (source.colorLabelFr || '').trim() || undefined,
colorHex: (source.colorHex || '').trim() || undefined, colorHex: (source.colorHex || '').trim() || undefined,
finishType: (source.finishType || 'GLOSSY').trim().toUpperCase(), finishType: (source.finishType || 'GLOSSY').trim().toUpperCase(),
brand: (source.brand || '').trim() || undefined, brand: (source.brand || '').trim() || undefined,

View File

@@ -206,27 +206,16 @@
/> />
</label> </label>
<label class="ui-form-field">
<span class="ui-form-caption">Nome categoria</span>
<input
class="ui-form-control"
type="text"
[(ngModel)]="categoryForm.name"
name="categoryName"
placeholder="Desk accessories"
/>
</label>
<label class="ui-form-field"> <label class="ui-form-field">
<span class="ui-form-caption">Slug</span> <span class="ui-form-caption">Slug</span>
<div class="input-with-action"> <div class="input-with-action">
<input <input
class="ui-form-control" class="ui-form-control"
type="text" type="text"
[(ngModel)]="categoryForm.slug" [(ngModel)]="categoryForm.slug"
name="categorySlug" name="categorySlug"
placeholder="desk-accessories" placeholder="desk-accessories"
/> />
<button <button
type="button" type="button"
class="ui-button ui-button--ghost" class="ui-button ui-button--ghost"
@@ -237,36 +226,6 @@
</div> </div>
</label> </label>
<label class="ui-form-field form-field--wide">
<span class="ui-form-caption">Descrizione</span>
<textarea
class="ui-form-control textarea-control"
[(ngModel)]="categoryForm.description"
name="categoryDescription"
rows="3"
></textarea>
</label>
<label class="ui-form-field">
<span class="ui-form-caption">SEO title</span>
<input
class="ui-form-control"
type="text"
[(ngModel)]="categoryForm.seoTitle"
name="categorySeoTitle"
/>
</label>
<label class="ui-form-field">
<span class="ui-form-caption">SEO description</span>
<input
class="ui-form-control"
type="text"
[(ngModel)]="categoryForm.seoDescription"
name="categorySeoDescription"
/>
</label>
<label class="ui-form-field"> <label class="ui-form-field">
<span class="ui-form-caption">OG title</span> <span class="ui-form-caption">OG title</span>
<input <input
@@ -288,6 +247,141 @@
</label> </label>
</div> </div>
<div class="ui-language-toolbar">
<div class="ui-language-toolbar__copy">
<span>Lingua contenuti categoria</span>
<p>IT / EN / DE / FR</p>
</div>
<div class="ui-language-toolbar__toggle">
<button
*ngFor="let language of shopLanguages"
type="button"
class="ui-language-toolbar__button image-language-button"
[class.active]="activeContentLanguage === language"
[class.complete]="isCategoryContentLanguageComplete(language)"
[class.incomplete]="
isCategoryContentLanguageIncomplete(language)
"
[class.empty]="!isCategoryContentLanguageStarted(language)"
(click)="setActiveContentLanguage(language)"
>
<span class="image-language-button__label">
{{ languageLabels[language] }}
</span>
<span
class="image-language-button__state"
*ngIf="isCategoryContentLanguageComplete(language)"
>
OK
</span>
<span
class="image-language-button__state"
*ngIf="isCategoryContentLanguageIncomplete(language)"
>
...
</span>
</button>
</div>
</div>
<div class="ui-form-grid ui-form-grid--two">
<label class="ui-form-field">
<span class="ui-form-caption">
Nome categoria {{ languageLabels[activeContentLanguage] }}
</span>
<input
class="ui-form-control"
type="text"
[(ngModel)]="categoryForm.names[activeContentLanguage]"
[name]="'category-name-' + activeContentLanguage"
placeholder="Desk accessories"
/>
</label>
<label class="ui-form-field form-field--wide">
<span class="ui-form-caption">
Descrizione {{ languageLabels[activeContentLanguage] }}
</span>
<textarea
class="ui-form-control textarea-control"
[(ngModel)]="categoryForm.descriptions[activeContentLanguage]"
[name]="'category-description-' + activeContentLanguage"
rows="3"
></textarea>
</label>
</div>
<div class="ui-language-toolbar">
<div class="ui-language-toolbar__copy">
<span>Lingua SEO categoria</span>
<p>Stessa lingua attiva dell'editor</p>
</div>
<div class="ui-language-toolbar__toggle">
<button
*ngFor="let language of shopLanguages"
type="button"
class="ui-language-toolbar__button image-language-button"
[class.active]="activeContentLanguage === language"
[class.complete]="isCategorySeoLanguageComplete(language)"
[class.incomplete]="isCategorySeoLanguageIncomplete(language)"
[class.empty]="!isCategorySeoLanguageStarted(language)"
(click)="setActiveContentLanguage(language)"
>
<span class="image-language-button__label">
{{ languageLabels[language] }}
</span>
<span
class="image-language-button__state"
*ngIf="isCategorySeoLanguageComplete(language)"
>
OK
</span>
<span
class="image-language-button__state"
*ngIf="isCategorySeoLanguageIncomplete(language)"
>
...
</span>
</button>
</div>
</div>
<div class="ui-form-grid ui-form-grid--two">
<label class="ui-form-field">
<span class="ui-form-caption">
SEO title {{ languageLabels[activeContentLanguage] }}
</span>
<input
class="ui-form-control"
type="text"
[(ngModel)]="categoryForm.seoTitles[activeContentLanguage]"
[name]="'category-seo-title-' + activeContentLanguage"
/>
</label>
<label class="ui-form-field form-field--wide">
<span class="ui-form-caption">
SEO description {{ languageLabels[activeContentLanguage] }}
</span>
<textarea
class="ui-form-control"
[(ngModel)]="
categoryForm.seoDescriptions[activeContentLanguage]
"
[name]="'category-seo-description-' + activeContentLanguage"
rows="3"
></textarea>
<span
class="seo-counter"
[class.seo-counter--danger]="
categorySeoDescriptionLength(activeContentLanguage) > 160
"
>
{{ categorySeoDescriptionLength(activeContentLanguage) }}/160
</span>
</label>
</div>
<div class="toggle-row"> <div class="toggle-row">
<label class="ui-checkbox"> <label class="ui-checkbox">
<input <input

View File

@@ -41,10 +41,10 @@ interface CategoryFormState {
id: string | null; id: string | null;
parentCategoryId: string | null; parentCategoryId: string | null;
slug: string; slug: string;
name: string; names: Record<ShopLanguage, string>;
description: string; descriptions: Record<ShopLanguage, string>;
seoTitle: string; seoTitles: Record<ShopLanguage, string>;
seoDescription: string; seoDescriptions: Record<ShopLanguage, string>;
ogTitle: string; ogTitle: string;
ogDescription: string; ogDescription: string;
indexable: boolean; indexable: boolean;
@@ -554,7 +554,10 @@ export class AdminShopComponent implements OnInit, OnDestroy {
} }
slugifyCategoryFromName(): void { slugifyCategoryFromName(): void {
this.categoryForm.slug = this.slugify(this.categoryForm.name); const source =
this.categoryForm.names[this.activeContentLanguage] ||
this.categoryForm.names['it'];
this.categoryForm.slug = this.slugify(source);
} }
setActiveContentLanguage(language: ShopLanguage): void { setActiveContentLanguage(language: ShopLanguage): void {
@@ -603,6 +606,45 @@ export class AdminShopComponent implements OnInit, OnDestroy {
); );
} }
isCategoryContentLanguageComplete(language: ShopLanguage): boolean {
return !!this.categoryForm.names[language].trim();
}
isCategoryContentLanguageStarted(language: ShopLanguage): boolean {
return (
!!this.categoryForm.names[language].trim() ||
!!this.categoryForm.descriptions[language].trim()
);
}
isCategoryContentLanguageIncomplete(language: ShopLanguage): boolean {
return (
this.isCategoryContentLanguageStarted(language) &&
!this.isCategoryContentLanguageComplete(language)
);
}
isCategorySeoLanguageComplete(language: ShopLanguage): boolean {
return (
!!this.categoryForm.seoTitles[language].trim() &&
!!this.categoryForm.seoDescriptions[language].trim()
);
}
isCategorySeoLanguageStarted(language: ShopLanguage): boolean {
return (
!!this.categoryForm.seoTitles[language].trim() ||
!!this.categoryForm.seoDescriptions[language].trim()
);
}
isCategorySeoLanguageIncomplete(language: ShopLanguage): boolean {
return (
this.isCategorySeoLanguageStarted(language) &&
!this.isCategorySeoLanguageComplete(language)
);
}
preventRichTextToolbarMouseDown(event: MouseEvent): void { preventRichTextToolbarMouseDown(event: MouseEvent): void {
event.preventDefault(); event.preventDefault();
} }
@@ -1228,10 +1270,10 @@ export class AdminShopComponent implements OnInit, OnDestroy {
id: null, id: null,
parentCategoryId: null, parentCategoryId: null,
slug: '', slug: '',
name: '', names: this.createEmptyLocalizedTextRecord(),
description: '', descriptions: this.createEmptyLocalizedTextRecord(),
seoTitle: '', seoTitles: this.createEmptyLocalizedTextRecord(),
seoDescription: '', seoDescriptions: this.createEmptyLocalizedTextRecord(),
ogTitle: '', ogTitle: '',
ogDescription: '', ogDescription: '',
indexable: true, indexable: true,
@@ -1241,6 +1283,7 @@ export class AdminShopComponent implements OnInit, OnDestroy {
} }
private resetCategoryForm(): void { private resetCategoryForm(): void {
this.activeContentLanguage = 'it';
Object.assign(this.categoryForm, this.createEmptyCategoryForm()); Object.assign(this.categoryForm, this.createEmptyCategoryForm());
} }
@@ -1249,10 +1292,30 @@ export class AdminShopComponent implements OnInit, OnDestroy {
id: category.id, id: category.id,
parentCategoryId: category.parentCategoryId, parentCategoryId: category.parentCategoryId,
slug: category.slug ?? '', slug: category.slug ?? '',
name: category.name ?? '', names: {
description: category.description ?? '', it: category.nameIt ?? category.name ?? '',
seoTitle: category.seoTitle ?? '', en: category.nameEn ?? category.name ?? '',
seoDescription: category.seoDescription ?? '', de: category.nameDe ?? category.name ?? '',
fr: category.nameFr ?? category.name ?? '',
},
descriptions: {
it: category.descriptionIt ?? category.description ?? '',
en: category.descriptionEn ?? category.description ?? '',
de: category.descriptionDe ?? category.description ?? '',
fr: category.descriptionFr ?? category.description ?? '',
},
seoTitles: {
it: category.seoTitleIt ?? category.seoTitle ?? '',
en: category.seoTitleEn ?? category.seoTitle ?? '',
de: category.seoTitleDe ?? category.seoTitle ?? '',
fr: category.seoTitleFr ?? category.seoTitle ?? '',
},
seoDescriptions: {
it: category.seoDescriptionIt ?? category.seoDescription ?? '',
en: category.seoDescriptionEn ?? category.seoDescription ?? '',
de: category.seoDescriptionDe ?? category.seoDescription ?? '',
fr: category.seoDescriptionFr ?? category.seoDescription ?? '',
},
ogTitle: category.ogTitle ?? '', ogTitle: category.ogTitle ?? '',
ogDescription: category.ogDescription ?? '', ogDescription: category.ogDescription ?? '',
indexable: category.indexable, indexable: category.indexable,
@@ -1265,10 +1328,34 @@ export class AdminShopComponent implements OnInit, OnDestroy {
return { return {
parentCategoryId: this.categoryForm.parentCategoryId || null, parentCategoryId: this.categoryForm.parentCategoryId || null,
slug: this.categoryForm.slug.trim(), slug: this.categoryForm.slug.trim(),
name: this.categoryForm.name.trim(), name: this.categoryForm.names['it'].trim(),
description: this.categoryForm.description.trim(), nameIt: this.categoryForm.names['it'].trim(),
seoTitle: this.categoryForm.seoTitle.trim(), nameEn: this.categoryForm.names['en'].trim(),
seoDescription: this.categoryForm.seoDescription.trim(), nameDe: this.categoryForm.names['de'].trim(),
nameFr: this.categoryForm.names['fr'].trim(),
description: this.optionalValue(this.categoryForm.descriptions['it']),
descriptionIt: this.optionalValue(this.categoryForm.descriptions['it']),
descriptionEn: this.optionalValue(this.categoryForm.descriptions['en']),
descriptionDe: this.optionalValue(this.categoryForm.descriptions['de']),
descriptionFr: this.optionalValue(this.categoryForm.descriptions['fr']),
seoTitle: this.optionalValue(this.categoryForm.seoTitles['it']),
seoTitleIt: this.optionalValue(this.categoryForm.seoTitles['it']),
seoTitleEn: this.optionalValue(this.categoryForm.seoTitles['en']),
seoTitleDe: this.optionalValue(this.categoryForm.seoTitles['de']),
seoTitleFr: this.optionalValue(this.categoryForm.seoTitles['fr']),
seoDescription: this.optionalValue(this.categoryForm.seoDescriptions['it']),
seoDescriptionIt: this.optionalValue(
this.categoryForm.seoDescriptions['it'],
),
seoDescriptionEn: this.optionalValue(
this.categoryForm.seoDescriptions['en'],
),
seoDescriptionDe: this.optionalValue(
this.categoryForm.seoDescriptions['de'],
),
seoDescriptionFr: this.optionalValue(
this.categoryForm.seoDescriptions['fr'],
),
ogTitle: this.categoryForm.ogTitle.trim(), ogTitle: this.categoryForm.ogTitle.trim(),
ogDescription: this.categoryForm.ogDescription.trim(), ogDescription: this.categoryForm.ogDescription.trim(),
indexable: this.categoryForm.indexable, indexable: this.categoryForm.indexable,
@@ -1278,12 +1365,19 @@ export class AdminShopComponent implements OnInit, OnDestroy {
} }
private validateCategoryForm(): string | null { private validateCategoryForm(): string | null {
if (!this.categoryForm.name.trim()) { for (const language of this.shopLanguages) {
return 'Il nome categoria è obbligatorio.'; if (!this.categoryForm.names[language].trim()) {
return `Il nome categoria ${this.languageLabels[language]} è obbligatorio.`;
}
} }
if (!this.categoryForm.slug.trim()) { if (!this.categoryForm.slug.trim()) {
return 'Lo slug categoria è obbligatorio.'; return 'Lo slug categoria è obbligatorio.';
} }
for (const language of this.shopLanguages) {
if (this.categoryForm.seoDescriptions[language].trim().length > 160) {
return `La SEO description categoria ${this.languageLabels[language]} deve avere massimo 160 caratteri.`;
}
}
return null; return null;
} }
@@ -1616,6 +1710,10 @@ export class AdminShopComponent implements OnInit, OnDestroy {
sku: this.optionalValue(existingVariant?.sku ?? ''), sku: this.optionalValue(existingVariant?.sku ?? ''),
variantLabel: materialCode, variantLabel: materialCode,
colorName: stockVariant.colorName.trim(), colorName: stockVariant.colorName.trim(),
colorLabelIt: this.optionalValue(stockVariant.colorLabelIt ?? ''),
colorLabelEn: this.optionalValue(stockVariant.colorLabelEn ?? ''),
colorLabelDe: this.optionalValue(stockVariant.colorLabelDe ?? ''),
colorLabelFr: this.optionalValue(stockVariant.colorLabelFr ?? ''),
colorHex: this.optionalValue( colorHex: this.optionalValue(
stockVariant.colorHex ?? '', stockVariant.colorHex ?? '',
)?.toUpperCase(), )?.toUpperCase(),
@@ -1714,7 +1812,7 @@ export class AdminShopComponent implements OnInit, OnDestroy {
} }
private stockVariantLabel(variant: AdminFilamentVariant): string { private stockVariantLabel(variant: AdminFilamentVariant): string {
const colorName = variant.colorName.trim(); const colorName = (variant.colorLabelIt || variant.colorName).trim();
const variantDisplayName = variant.variantDisplayName.trim(); const variantDisplayName = variant.variantDisplayName.trim();
if ( if (
variantDisplayName && variantDisplayName &&
@@ -2193,6 +2291,19 @@ export class AdminShopComponent implements OnInit, OnDestroy {
return this.productForm.seoDescriptions[language].trim().length; return this.productForm.seoDescriptions[language].trim().length;
} }
categorySeoDescriptionLength(language: ShopLanguage): number {
return this.categoryForm.seoDescriptions[language].trim().length;
}
private createEmptyLocalizedTextRecord(): Record<ShopLanguage, string> {
return {
it: '',
en: '',
de: '',
fr: '',
};
}
private slugify(source: string): string { private slugify(source: string): string {
return source return source
.normalize('NFD') .normalize('NFD')

View File

@@ -32,6 +32,10 @@ export interface AdminFilamentVariant {
materialTechnicalTypeLabel?: string; materialTechnicalTypeLabel?: string;
variantDisplayName: string; variantDisplayName: string;
colorName: string; colorName: string;
colorLabelIt: string;
colorLabelEn: string;
colorLabelDe: string;
colorLabelFr: string;
colorHex?: string; colorHex?: string;
finishType?: string; finishType?: string;
brand?: string; brand?: string;
@@ -57,6 +61,10 @@ export interface AdminUpsertFilamentVariantPayload {
materialTypeId: number; materialTypeId: number;
variantDisplayName: string; variantDisplayName: string;
colorName: string; colorName: string;
colorLabelIt?: string;
colorLabelEn?: string;
colorLabelDe?: string;
colorLabelFr?: string;
colorHex?: string; colorHex?: string;
finishType?: string; finishType?: string;
brand?: string; brand?: string;

View File

@@ -30,9 +30,25 @@ export interface AdminShopCategory {
parentCategoryName: string | null; parentCategoryName: string | null;
slug: string; slug: string;
name: string; name: string;
nameIt: string;
nameEn: string;
nameDe: string;
nameFr: string;
description: string | null; description: string | null;
descriptionIt: string | null;
descriptionEn: string | null;
descriptionDe: string | null;
descriptionFr: string | null;
seoTitle: string | null; seoTitle: string | null;
seoTitleIt: string | null;
seoTitleEn: string | null;
seoTitleDe: string | null;
seoTitleFr: string | null;
seoDescription: string | null; seoDescription: string | null;
seoDescriptionIt: string | null;
seoDescriptionEn: string | null;
seoDescriptionDe: string | null;
seoDescriptionFr: string | null;
ogTitle: string | null; ogTitle: string | null;
ogDescription: string | null; ogDescription: string | null;
indexable: boolean; indexable: boolean;
@@ -54,9 +70,25 @@ export interface AdminUpsertShopCategoryPayload {
parentCategoryId?: string | null; parentCategoryId?: string | null;
slug: string; slug: string;
name: string; name: string;
nameIt: string;
nameEn: string;
nameDe: string;
nameFr: string;
description?: string; description?: string;
descriptionIt?: string;
descriptionEn?: string;
descriptionDe?: string;
descriptionFr?: string;
seoTitle?: string; seoTitle?: string;
seoTitleIt?: string;
seoTitleEn?: string;
seoTitleDe?: string;
seoTitleFr?: string;
seoDescription?: string; seoDescription?: string;
seoDescriptionIt?: string;
seoDescriptionEn?: string;
seoDescriptionDe?: string;
seoDescriptionFr?: string;
ogTitle?: string; ogTitle?: string;
ogDescription?: string; ogDescription?: string;
indexable: boolean; indexable: boolean;
@@ -69,6 +101,10 @@ export interface AdminShopProductVariant {
sku: string | null; sku: string | null;
variantLabel: string; variantLabel: string;
colorName: string; colorName: string;
colorLabelIt: string;
colorLabelEn: string;
colorLabelDe: string;
colorLabelFr: string;
colorHex: string | null; colorHex: string | null;
internalMaterialCode: string; internalMaterialCode: string;
priceChf: number; priceChf: number;
@@ -170,6 +206,10 @@ export interface AdminUpsertShopProductVariantPayload {
sku?: string; sku?: string;
variantLabel?: string; variantLabel?: string;
colorName: string; colorName: string;
colorLabelIt?: string;
colorLabelEn?: string;
colorLabelDe?: string;
colorLabelFr?: string;
colorHex?: string; colorHex?: string;
internalMaterialCode: string; internalMaterialCode: string;
priceChf: number; priceChf: number;

View File

@@ -75,6 +75,10 @@ export interface VariantOption {
id: number; id: number;
name: string; name: string;
colorName: string; colorName: string;
colorLabelIt?: string;
colorLabelEn?: string;
colorLabelDe?: string;
colorLabelFr?: string;
hexColor: string; hexColor: string;
finishType: string; finishType: string;
stockSpools: number; stockSpools: number;

View File

@@ -25,8 +25,8 @@ import { StlViewerComponent } from '../../shared/components/stl-viewer/stl-viewe
import { import {
findColorHex, findColorHex,
getColorHex, getColorHex,
getColorLabelToken,
normalizeColorValue, normalizeColorValue,
resolveLocalizedColorLabel,
} from '../../core/constants/colors.const'; } from '../../core/constants/colors.const';
@Component({ @Component({
@@ -257,7 +257,7 @@ export class CheckoutComponent implements OnInit {
if (variantLabel) { if (variantLabel) {
return variantLabel; return variantLabel;
} }
return getColorLabelToken(item?.shopVariantColorName); return this.localizedShopColorLabel(item);
} }
showItemMaterial(item: any): boolean { showItemMaterial(item: any): boolean {
@@ -286,12 +286,7 @@ export class CheckoutComponent implements OnInit {
} }
itemColorLabel(item: any): string { itemColorLabel(item: any): string {
const shopColor = String(item?.shopVariantColorName ?? '').trim(); return this.localizedShopColorLabel(item) || String(item?.colorCode ?? '-');
if (shopColor) {
return getColorLabelToken(shopColor) ?? '-';
}
const raw = String(item?.colorCode ?? '').trim();
return getColorLabelToken(raw) ?? '-';
} }
itemColorSwatch(item: any): string { itemColorSwatch(item: any): string {
@@ -335,6 +330,16 @@ export class CheckoutComponent implements OnInit {
return !!this.previewLoading()[id]; return !!this.previewLoading()[id];
} }
private localizedShopColorLabel(item: any): string | null {
return resolveLocalizedColorLabel(this.languageService.selectedLang(), {
fallback: item?.shopVariantColorName ?? item?.colorCode,
it: item?.shopVariantColorLabelIt,
en: item?.shopVariantColorLabelEn,
de: item?.shopVariantColorLabelDe,
fr: item?.shopVariantColorLabelFr,
});
}
hasPreviewError(item: any): boolean { hasPreviewError(item: any): boolean {
const id = String(item?.id ?? ''); const id = String(item?.id ?? '');
if (!id) { if (!id) {

View File

@@ -8,7 +8,7 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { import {
findColorHex, findColorHex,
getColorLabelToken, resolveLocalizedColorLabel,
} from '../../core/constants/colors.const'; } from '../../core/constants/colors.const';
import { import {
PriceBreakdownComponent, PriceBreakdownComponent,
@@ -29,9 +29,17 @@ interface PublicOrderItem {
shopProductName?: string; shopProductName?: string;
shopVariantLabel?: string; shopVariantLabel?: string;
shopVariantColorName?: string; shopVariantColorName?: string;
shopVariantColorLabelIt?: string;
shopVariantColorLabelEn?: string;
shopVariantColorLabelDe?: string;
shopVariantColorLabelFr?: string;
shopVariantColorHex?: string; shopVariantColorHex?: string;
filamentVariantDisplayName?: string; filamentVariantDisplayName?: string;
filamentColorName?: string; filamentColorName?: string;
filamentColorLabelIt?: string;
filamentColorLabelEn?: string;
filamentColorLabelDe?: string;
filamentColorLabelFr?: string;
filamentColorHex?: string; filamentColorHex?: string;
quality?: string; quality?: string;
nozzleDiameterMm?: number; nozzleDiameterMm?: number;
@@ -282,26 +290,14 @@ export class OrderComponent implements OnInit {
return variantLabel; return variantLabel;
} }
return getColorLabelToken(item?.shopVariantColorName); return this.localizedColorLabel(item, 'shop');
} }
itemColorLabel(item: PublicOrderItem): string { itemColorLabel(item: PublicOrderItem): string {
const shopColor = String(item?.shopVariantColorName ?? '').trim();
if (shopColor) {
return getColorLabelToken(shopColor) ?? this.translate.instant('ORDER.NOT_AVAILABLE');
}
const filamentColor = String(item?.filamentColorName ?? '').trim();
if (filamentColor) {
return (
getColorLabelToken(filamentColor) ??
this.translate.instant('ORDER.NOT_AVAILABLE')
);
}
const rawColor = String(item?.colorCode ?? '').trim();
return ( return (
getColorLabelToken(rawColor) ?? this.localizedColorLabel(item, 'shop') ||
this.localizedColorLabel(item, 'filament') ||
String(item?.colorCode ?? '').trim() ||
this.translate.instant('ORDER.NOT_AVAILABLE') this.translate.instant('ORDER.NOT_AVAILABLE')
); );
} }
@@ -333,6 +329,29 @@ export class OrderComponent implements OnInit {
return !this.isShopItem(item); return !this.isShopItem(item);
} }
private localizedColorLabel(
item: PublicOrderItem,
source: 'shop' | 'filament',
): string | null {
if (source === 'shop') {
return resolveLocalizedColorLabel(this.translate.currentLang, {
fallback: item.shopVariantColorName,
it: item.shopVariantColorLabelIt,
en: item.shopVariantColorLabelEn,
de: item.shopVariantColorLabelDe,
fr: item.shopVariantColorLabelFr,
});
}
return resolveLocalizedColorLabel(this.translate.currentLang, {
fallback: item.filamentColorName ?? item.colorCode,
it: item.filamentColorLabelIt,
en: item.filamentColorLabelEn,
de: item.filamentColorLabelDe,
fr: item.filamentColorLabelFr,
});
}
orderKind(order: PublicOrder | null): 'SHOP' | 'CALCULATOR' | 'MIXED' { orderKind(order: PublicOrder | null): 'SHOP' | 'CALCULATOR' | 'MIXED' {
const items = order?.items ?? []; const items = order?.items ?? [];
const hasShop = items.some((item) => this.isShopItem(item)); const hasShop = items.some((item) => this.isShopItem(item));

View File

@@ -28,10 +28,7 @@
<div class="detail-grid"> <div class="detail-grid">
<section class="visual-column"> <section class="visual-column">
<div <div class="hero-media">
class="hero-media"
[class.hero-media--portrait]="selectedImageIsPortrait()"
>
@if (galleryImages().length > 1) { @if (galleryImages().length > 1) {
<button <button
type="button" type="button"
@@ -56,7 +53,6 @@
[src]="imageUrl" [src]="imageUrl"
[alt]="selectedImage().altText || p.name" [alt]="selectedImage().altText || p.name"
class="hero-image" class="hero-image"
(load)="onHeroImageLoad($event)"
/> />
} @else { } @else {
<div class="image-fallback"> <div class="image-fallback">

View File

@@ -107,10 +107,6 @@
background: #f2eee5; background: #f2eee5;
} }
.hero-media--portrait .hero-image {
object-fit: contain;
}
.image-fallback { .image-fallback {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@@ -18,7 +18,6 @@ import { LanguageService } from '../../core/services/language.service';
import { import {
findColorHex, findColorHex,
getColorHex, getColorHex,
getColorLabelToken,
} from '../../core/constants/colors.const'; } from '../../core/constants/colors.const';
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component'; import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
import { AppCardComponent } from '../../shared/components/app-card/app-card.component'; import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
@@ -78,9 +77,6 @@ export class ProductDetailComponent {
readonly product = signal<ShopProductDetail | null>(null); readonly product = signal<ShopProductDetail | null>(null);
readonly selectedVariantId = signal<string | null>(null); readonly selectedVariantId = signal<string | null>(null);
readonly selectedImageAssetId = signal<string | null>(null); readonly selectedImageAssetId = signal<string | null>(null);
readonly selectedImageOrientation = signal<
'portrait' | 'landscape' | 'square' | null
>(null);
readonly quantity = signal(1); readonly quantity = signal(1);
readonly isAddingToCart = signal(false); readonly isAddingToCart = signal(false);
readonly addSuccess = signal(false); readonly addSuccess = signal(false);
@@ -198,9 +194,6 @@ export class ProductDetailComponent {
readonly selectedVariantCartQuantity = computed(() => readonly selectedVariantCartQuantity = computed(() =>
this.shopService.quantityForVariant(this.selectedVariant()?.id), this.shopService.quantityForVariant(this.selectedVariant()?.id),
); );
readonly selectedImageIsPortrait = computed(
() => this.selectedImageOrientation() === 'portrait',
);
constructor() { constructor() {
if (!this.shopService.cartLoaded()) { if (!this.shopService.cartLoaded()) {
@@ -315,25 +308,6 @@ export class ProductDetailComponent {
this.setSelectedImageAssetId(images[nextIndex].mediaAssetId); this.setSelectedImageAssetId(images[nextIndex].mediaAssetId);
} }
onHeroImageLoad(event: Event): void {
const target = event.target;
if (!(target instanceof HTMLImageElement)) {
return;
}
if (target.naturalHeight > target.naturalWidth) {
this.selectedImageOrientation.set('portrait');
return;
}
if (target.naturalWidth > target.naturalHeight) {
this.selectedImageOrientation.set('landscape');
return;
}
this.selectedImageOrientation.set('square');
}
selectVariant(variant: ShopProductVariantOption): void { selectVariant(variant: ShopProductVariantOption): void {
this.selectedVariantId.set(variant.id); this.selectedVariantId.set(variant.id);
this.selectedMaterialKey.set(this.materialKeyForVariant(variant)); this.selectedMaterialKey.set(this.materialKeyForVariant(variant));
@@ -407,9 +381,7 @@ export class ProductDetailComponent {
} }
colorLabel(variant: ShopProductVariantOption): string { colorLabel(variant: ShopProductVariantOption): string {
return ( return variant.colorLabel || variant.colorName || variant.variantLabel || '-';
getColorLabelToken(variant.colorName || variant.variantLabel) ?? '-'
);
} }
colorHex(variant: ShopProductVariantOption | null | undefined): string { colorHex(variant: ShopProductVariantOption | null | undefined): string {
@@ -512,7 +484,6 @@ export class ProductDetailComponent {
private setSelectedImageAssetId(mediaAssetId: string | null): void { private setSelectedImageAssetId(mediaAssetId: string | null): void {
this.selectedImageAssetId.set(mediaAssetId); this.selectedImageAssetId.set(mediaAssetId);
this.selectedImageOrientation.set(null);
} }
private normalizeHexColor(value: string | null | undefined): string | null { private normalizeHexColor(value: string | null | undefined): string | null {

View File

@@ -55,6 +55,7 @@ export interface ShopProductVariantOption {
sku: string | null; sku: string | null;
variantLabel: string | null; variantLabel: string | null;
colorName: string | null; colorName: string | null;
colorLabel: string | null;
colorHex: string | null; colorHex: string | null;
priceChf: number; priceChf: number;
isDefault: boolean; isDefault: boolean;
@@ -138,6 +139,10 @@ export interface ShopCartItem {
shopProductName: string | null; shopProductName: string | null;
shopVariantLabel: string | null; shopVariantLabel: string | null;
shopVariantColorName: string | null; shopVariantColorName: string | null;
shopVariantColorLabelIt?: string | null;
shopVariantColorLabelEn?: string | null;
shopVariantColorLabelDe?: string | null;
shopVariantColorLabelFr?: string | null;
shopVariantColorHex: string | null; shopVariantColorHex: string | null;
materialCode: string | null; materialCode: string | null;
quality: string | null; quality: string | null;

View File

@@ -24,7 +24,7 @@ import { SeoService } from '../../core/services/seo.service';
import { LanguageService } from '../../core/services/language.service'; import { LanguageService } from '../../core/services/language.service';
import { import {
findColorHex, findColorHex,
getColorLabelToken, resolveLocalizedColorLabel,
} from '../../core/constants/colors.const'; } from '../../core/constants/colors.const';
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component'; import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
import { AppCardComponent } from '../../shared/components/app-card/app-card.component'; import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
@@ -161,15 +161,20 @@ export class ShopPageComponent {
} }
cartItemVariant(item: ShopCartItem): string | null { cartItemVariant(item: ShopCartItem): string | null {
return ( return item.shopVariantLabel || this.cartItemColor(item);
item.shopVariantLabel || getColorLabelToken(item.shopVariantColorName)
);
} }
cartItemColor(item: ShopCartItem): string | null { cartItemColor(item: ShopCartItem): string | null {
return ( return (
getColorLabelToken(item.shopVariantColorName) ?? resolveLocalizedColorLabel(this.languageService.selectedLang(), {
getColorLabelToken(item.colorCode) fallback: item.shopVariantColorName ?? item.colorCode,
it: item.shopVariantColorLabelIt,
en: item.shopVariantColorLabelEn,
de: item.shopVariantColorLabelDe,
fr: item.shopVariantColorLabelFr,
}) ??
item.shopVariantColorName ??
item.colorCode
); );
} }

View File

@@ -1,14 +1,15 @@
import { Component, input, output, signal, computed } from '@angular/core'; import { Component, input, output, signal, computed, inject } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { import {
PRODUCT_COLORS, PRODUCT_COLORS,
getColorHex, getColorHex,
getColorLabelToken,
ColorCategory, ColorCategory,
ColorOption, ColorOption,
resolveLocalizedColorLabel,
} from '../../../core/constants/colors.const'; } from '../../../core/constants/colors.const';
import { VariantOption } from '../../../features/calculator/services/quote-estimator.service'; import { VariantOption } from '../../../features/calculator/services/quote-estimator.service';
import { LanguageService } from '../../../core/services/language.service';
@Component({ @Component({
selector: 'app-color-selector', selector: 'app-color-selector',
@@ -18,6 +19,7 @@ import { VariantOption } from '../../../features/calculator/services/quote-estim
styleUrl: './color-selector.component.scss', styleUrl: './color-selector.component.scss',
}) })
export class ColorSelectorComponent { export class ColorSelectorComponent {
private readonly languageService = inject(LanguageService);
selectedColor = input<string>('Black'); selectedColor = input<string>('Black');
selectedVariantId = input<number | null>(null); selectedVariantId = input<number | null>(null);
variants = input<VariantOption[]>([]); variants = input<VariantOption[]>([]);
@@ -33,7 +35,14 @@ export class ColorSelectorComponent {
const finish = v.finishType || 'AVAILABLE_COLORS'; const finish = v.finishType || 'AVAILABLE_COLORS';
const bucket = byFinish.get(finish) || []; const bucket = byFinish.get(finish) || [];
bucket.push({ bucket.push({
label: getColorLabelToken(v.colorName) ?? v.colorName, label:
resolveLocalizedColorLabel(this.languageService.selectedLang(), {
fallback: v.colorName,
it: v.colorLabelIt,
en: v.colorLabelEn,
de: v.colorLabelDe,
fr: v.colorLabelFr,
}) ?? v.colorName,
value: v.colorName, value: v.colorName,
hex: v.hexColor, hex: v.hexColor,
variantId: v.id, variantId: v.id,