feat(front-end): back-office desing improvements
This commit is contained in:
@@ -12,8 +12,20 @@ public class AdminShopProductDto {
|
||||
private String categorySlug;
|
||||
private String slug;
|
||||
private String name;
|
||||
private String nameIt;
|
||||
private String nameEn;
|
||||
private String nameDe;
|
||||
private String nameFr;
|
||||
private String excerpt;
|
||||
private String excerptIt;
|
||||
private String excerptEn;
|
||||
private String excerptDe;
|
||||
private String excerptFr;
|
||||
private String description;
|
||||
private String descriptionIt;
|
||||
private String descriptionEn;
|
||||
private String descriptionDe;
|
||||
private String descriptionFr;
|
||||
private String seoTitle;
|
||||
private String seoDescription;
|
||||
private String ogTitle;
|
||||
@@ -83,6 +95,38 @@ public class AdminShopProductDto {
|
||||
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 getExcerpt() {
|
||||
return excerpt;
|
||||
}
|
||||
@@ -91,6 +135,38 @@ public class AdminShopProductDto {
|
||||
this.excerpt = excerpt;
|
||||
}
|
||||
|
||||
public String getExcerptIt() {
|
||||
return excerptIt;
|
||||
}
|
||||
|
||||
public void setExcerptIt(String excerptIt) {
|
||||
this.excerptIt = excerptIt;
|
||||
}
|
||||
|
||||
public String getExcerptEn() {
|
||||
return excerptEn;
|
||||
}
|
||||
|
||||
public void setExcerptEn(String excerptEn) {
|
||||
this.excerptEn = excerptEn;
|
||||
}
|
||||
|
||||
public String getExcerptDe() {
|
||||
return excerptDe;
|
||||
}
|
||||
|
||||
public void setExcerptDe(String excerptDe) {
|
||||
this.excerptDe = excerptDe;
|
||||
}
|
||||
|
||||
public String getExcerptFr() {
|
||||
return excerptFr;
|
||||
}
|
||||
|
||||
public void setExcerptFr(String excerptFr) {
|
||||
this.excerptFr = excerptFr;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
@@ -99,6 +175,38 @@ public class AdminShopProductDto {
|
||||
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() {
|
||||
return seoTitle;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,20 @@ public class AdminUpsertShopProductRequest {
|
||||
private UUID categoryId;
|
||||
private String slug;
|
||||
private String name;
|
||||
private String nameIt;
|
||||
private String nameEn;
|
||||
private String nameDe;
|
||||
private String nameFr;
|
||||
private String excerpt;
|
||||
private String excerptIt;
|
||||
private String excerptEn;
|
||||
private String excerptDe;
|
||||
private String excerptFr;
|
||||
private String description;
|
||||
private String descriptionIt;
|
||||
private String descriptionEn;
|
||||
private String descriptionDe;
|
||||
private String descriptionFr;
|
||||
private String seoTitle;
|
||||
private String seoDescription;
|
||||
private String ogTitle;
|
||||
@@ -43,6 +55,38 @@ public class AdminUpsertShopProductRequest {
|
||||
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 getExcerpt() {
|
||||
return excerpt;
|
||||
}
|
||||
@@ -51,6 +95,38 @@ public class AdminUpsertShopProductRequest {
|
||||
this.excerpt = excerpt;
|
||||
}
|
||||
|
||||
public String getExcerptIt() {
|
||||
return excerptIt;
|
||||
}
|
||||
|
||||
public void setExcerptIt(String excerptIt) {
|
||||
this.excerptIt = excerptIt;
|
||||
}
|
||||
|
||||
public String getExcerptEn() {
|
||||
return excerptEn;
|
||||
}
|
||||
|
||||
public void setExcerptEn(String excerptEn) {
|
||||
this.excerptEn = excerptEn;
|
||||
}
|
||||
|
||||
public String getExcerptDe() {
|
||||
return excerptDe;
|
||||
}
|
||||
|
||||
public void setExcerptDe(String excerptDe) {
|
||||
this.excerptDe = excerptDe;
|
||||
}
|
||||
|
||||
public String getExcerptFr() {
|
||||
return excerptFr;
|
||||
}
|
||||
|
||||
public void setExcerptFr(String excerptFr) {
|
||||
this.excerptFr = excerptFr;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
@@ -59,6 +135,38 @@ public class AdminUpsertShopProductRequest {
|
||||
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() {
|
||||
return seoTitle;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import jakarta.persistence.Table;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@@ -23,6 +24,8 @@ import java.util.UUID;
|
||||
@Index(name = "ix_shop_product_featured_sort", columnList = "is_featured, is_active, sort_order")
|
||||
})
|
||||
public class ShopProduct {
|
||||
public static final List<String> SUPPORTED_LANGUAGES = List.of("it", "en", "de", "fr");
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
@Column(name = "shop_product_id", nullable = false)
|
||||
@@ -38,12 +41,48 @@ public class ShopProduct {
|
||||
@Column(name = "name", nullable = false, length = Integer.MAX_VALUE)
|
||||
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 = "excerpt", length = Integer.MAX_VALUE)
|
||||
private String excerpt;
|
||||
|
||||
@Column(name = "excerpt_it", length = Integer.MAX_VALUE)
|
||||
private String excerptIt;
|
||||
|
||||
@Column(name = "excerpt_en", length = Integer.MAX_VALUE)
|
||||
private String excerptEn;
|
||||
|
||||
@Column(name = "excerpt_de", length = Integer.MAX_VALUE)
|
||||
private String excerptDe;
|
||||
|
||||
@Column(name = "excerpt_fr", length = Integer.MAX_VALUE)
|
||||
private String excerptFr;
|
||||
|
||||
@Column(name = "description", length = Integer.MAX_VALUE)
|
||||
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)
|
||||
private String seoTitle;
|
||||
|
||||
@@ -152,6 +191,38 @@ public class ShopProduct {
|
||||
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 getExcerpt() {
|
||||
return excerpt;
|
||||
}
|
||||
@@ -160,6 +231,38 @@ public class ShopProduct {
|
||||
this.excerpt = excerpt;
|
||||
}
|
||||
|
||||
public String getExcerptIt() {
|
||||
return excerptIt;
|
||||
}
|
||||
|
||||
public void setExcerptIt(String excerptIt) {
|
||||
this.excerptIt = excerptIt;
|
||||
}
|
||||
|
||||
public String getExcerptEn() {
|
||||
return excerptEn;
|
||||
}
|
||||
|
||||
public void setExcerptEn(String excerptEn) {
|
||||
this.excerptEn = excerptEn;
|
||||
}
|
||||
|
||||
public String getExcerptDe() {
|
||||
return excerptDe;
|
||||
}
|
||||
|
||||
public void setExcerptDe(String excerptDe) {
|
||||
this.excerptDe = excerptDe;
|
||||
}
|
||||
|
||||
public String getExcerptFr() {
|
||||
return excerptFr;
|
||||
}
|
||||
|
||||
public void setExcerptFr(String excerptFr) {
|
||||
this.excerptFr = excerptFr;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
@@ -168,6 +271,38 @@ public class ShopProduct {
|
||||
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() {
|
||||
return seoTitle;
|
||||
}
|
||||
@@ -247,4 +382,94 @@ public class ShopProduct {
|
||||
public void setUpdatedAt(OffsetDateTime 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 getExcerptForLanguage(String language) {
|
||||
return resolveLocalizedValue(language, excerpt, excerptIt, excerptEn, excerptDe, excerptFr);
|
||||
}
|
||||
|
||||
public void setExcerptForLanguage(String language, String value) {
|
||||
switch (normalizeLanguage(language)) {
|
||||
case "it" -> excerptIt = value;
|
||||
case "en" -> excerptEn = value;
|
||||
case "de" -> excerptDe = value;
|
||||
case "fr" -> excerptFr = 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 -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.printcalculator.service.SlicerService;
|
||||
import com.printcalculator.service.media.PublicMediaQueryService;
|
||||
import com.printcalculator.service.shop.ShopStorageService;
|
||||
import com.printcalculator.service.storage.ClamAVService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -75,6 +76,7 @@ public class AdminShopProductControllerService {
|
||||
private final ShopStorageService shopStorageService;
|
||||
private final SlicerService slicerService;
|
||||
private final ClamAVService clamAVService;
|
||||
private final long maxModelFileSizeBytes;
|
||||
|
||||
public AdminShopProductControllerService(ShopProductRepository shopProductRepository,
|
||||
ShopCategoryRepository shopCategoryRepository,
|
||||
@@ -86,7 +88,8 @@ public class AdminShopProductControllerService {
|
||||
AdminMediaControllerService adminMediaControllerService,
|
||||
ShopStorageService shopStorageService,
|
||||
SlicerService slicerService,
|
||||
ClamAVService clamAVService) {
|
||||
ClamAVService clamAVService,
|
||||
@Value("${shop.model.max-file-size-bytes:104857600}") long maxModelFileSizeBytes) {
|
||||
this.shopProductRepository = shopProductRepository;
|
||||
this.shopCategoryRepository = shopCategoryRepository;
|
||||
this.shopProductVariantRepository = shopProductVariantRepository;
|
||||
@@ -98,6 +101,7 @@ public class AdminShopProductControllerService {
|
||||
this.shopStorageService = shopStorageService;
|
||||
this.slicerService = slicerService;
|
||||
this.clamAVService = clamAVService;
|
||||
this.maxModelFileSizeBytes = maxModelFileSizeBytes;
|
||||
}
|
||||
|
||||
public List<AdminShopProductDto> getProducts() {
|
||||
@@ -113,13 +117,13 @@ public class AdminShopProductControllerService {
|
||||
@Transactional
|
||||
public AdminShopProductDto createProduct(AdminUpsertShopProductRequest payload) {
|
||||
ensurePayload(payload);
|
||||
String normalizedName = normalizeRequired(payload.getName(), "Product name is required");
|
||||
String normalizedSlug = normalizeAndValidateSlug(payload.getSlug(), normalizedName);
|
||||
LocalizedProductContent localizedContent = normalizeLocalizedProductContent(payload);
|
||||
String normalizedSlug = normalizeAndValidateSlug(payload.getSlug(), localizedContent.defaultName());
|
||||
ensureSlugAvailable(normalizedSlug, null);
|
||||
|
||||
ShopProduct product = new ShopProduct();
|
||||
product.setCreatedAt(OffsetDateTime.now());
|
||||
applyProductPayload(product, payload, normalizedName, normalizedSlug, resolveCategory(payload.getCategoryId()));
|
||||
applyProductPayload(product, payload, localizedContent, normalizedSlug, resolveCategory(payload.getCategoryId()));
|
||||
ShopProduct saved = shopProductRepository.save(product);
|
||||
syncVariants(saved, payload.getVariants());
|
||||
return getProduct(saved.getId());
|
||||
@@ -131,11 +135,11 @@ public class AdminShopProductControllerService {
|
||||
ShopProduct product = shopProductRepository.findById(productId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Shop product not found"));
|
||||
|
||||
String normalizedName = normalizeRequired(payload.getName(), "Product name is required");
|
||||
String normalizedSlug = normalizeAndValidateSlug(payload.getSlug(), normalizedName);
|
||||
LocalizedProductContent localizedContent = normalizeLocalizedProductContent(payload);
|
||||
String normalizedSlug = normalizeAndValidateSlug(payload.getSlug(), localizedContent.defaultName());
|
||||
ensureSlugAvailable(normalizedSlug, productId);
|
||||
|
||||
applyProductPayload(product, payload, normalizedName, normalizedSlug, resolveCategory(payload.getCategoryId()));
|
||||
applyProductPayload(product, payload, localizedContent, normalizedSlug, resolveCategory(payload.getCategoryId()));
|
||||
ShopProduct saved = shopProductRepository.save(product);
|
||||
syncVariants(saved, payload.getVariants());
|
||||
return getProduct(saved.getId());
|
||||
@@ -291,14 +295,26 @@ public class AdminShopProductControllerService {
|
||||
|
||||
private void applyProductPayload(ShopProduct product,
|
||||
AdminUpsertShopProductRequest payload,
|
||||
String normalizedName,
|
||||
LocalizedProductContent localizedContent,
|
||||
String normalizedSlug,
|
||||
ShopCategory category) {
|
||||
product.setCategory(category);
|
||||
product.setSlug(normalizedSlug);
|
||||
product.setName(normalizedName);
|
||||
product.setExcerpt(normalizeOptional(payload.getExcerpt()));
|
||||
product.setDescription(normalizeOptional(payload.getDescription()));
|
||||
product.setName(localizedContent.defaultName());
|
||||
product.setNameIt(localizedContent.names().get("it"));
|
||||
product.setNameEn(localizedContent.names().get("en"));
|
||||
product.setNameDe(localizedContent.names().get("de"));
|
||||
product.setNameFr(localizedContent.names().get("fr"));
|
||||
product.setExcerpt(localizedContent.defaultExcerpt());
|
||||
product.setExcerptIt(localizedContent.excerpts().get("it"));
|
||||
product.setExcerptEn(localizedContent.excerpts().get("en"));
|
||||
product.setExcerptDe(localizedContent.excerpts().get("de"));
|
||||
product.setExcerptFr(localizedContent.excerpts().get("fr"));
|
||||
product.setDescription(localizedContent.defaultDescription());
|
||||
product.setDescriptionIt(localizedContent.descriptions().get("it"));
|
||||
product.setDescriptionEn(localizedContent.descriptions().get("en"));
|
||||
product.setDescriptionDe(localizedContent.descriptions().get("de"));
|
||||
product.setDescriptionFr(localizedContent.descriptions().get("fr"));
|
||||
product.setSeoTitle(normalizeOptional(payload.getSeoTitle()));
|
||||
product.setSeoDescription(normalizeOptional(payload.getSeoDescription()));
|
||||
product.setOgTitle(normalizeOptional(payload.getOgTitle()));
|
||||
@@ -436,8 +452,20 @@ public class AdminShopProductControllerService {
|
||||
dto.setCategorySlug(product.getCategory() != null ? product.getCategory().getSlug() : null);
|
||||
dto.setSlug(product.getSlug());
|
||||
dto.setName(product.getName());
|
||||
dto.setNameIt(product.getNameIt());
|
||||
dto.setNameEn(product.getNameEn());
|
||||
dto.setNameDe(product.getNameDe());
|
||||
dto.setNameFr(product.getNameFr());
|
||||
dto.setExcerpt(product.getExcerpt());
|
||||
dto.setExcerptIt(product.getExcerptIt());
|
||||
dto.setExcerptEn(product.getExcerptEn());
|
||||
dto.setExcerptDe(product.getExcerptDe());
|
||||
dto.setExcerptFr(product.getExcerptFr());
|
||||
dto.setDescription(product.getDescription());
|
||||
dto.setDescriptionIt(product.getDescriptionIt());
|
||||
dto.setDescriptionEn(product.getDescriptionEn());
|
||||
dto.setDescriptionDe(product.getDescriptionDe());
|
||||
dto.setDescriptionFr(product.getDescriptionFr());
|
||||
dto.setSeoTitle(product.getSeoTitle());
|
||||
dto.setSeoDescription(product.getSeoDescription());
|
||||
dto.setOgTitle(product.getOgTitle());
|
||||
@@ -523,6 +551,61 @@ public class AdminShopProductControllerService {
|
||||
}
|
||||
}
|
||||
|
||||
private LocalizedProductContent normalizeLocalizedProductContent(AdminUpsertShopProductRequest 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(HttpStatus.BAD_REQUEST, "Product name is required");
|
||||
}
|
||||
|
||||
Map<String, String> names = new LinkedHashMap<>();
|
||||
names.put("it", normalizeRequired(firstNonBlank(normalizeOptional(payload.getNameIt()), fallbackName), "Italian product name is required"));
|
||||
names.put("en", normalizeRequired(firstNonBlank(normalizeOptional(payload.getNameEn()), fallbackName), "English product name is required"));
|
||||
names.put("de", normalizeRequired(firstNonBlank(normalizeOptional(payload.getNameDe()), fallbackName), "German product name is required"));
|
||||
names.put("fr", normalizeRequired(firstNonBlank(normalizeOptional(payload.getNameFr()), fallbackName), "French product name is required"));
|
||||
|
||||
String fallbackExcerpt = firstNonBlank(
|
||||
normalizeOptional(payload.getExcerpt()),
|
||||
normalizeOptional(payload.getExcerptIt()),
|
||||
normalizeOptional(payload.getExcerptEn()),
|
||||
normalizeOptional(payload.getExcerptDe()),
|
||||
normalizeOptional(payload.getExcerptFr())
|
||||
);
|
||||
Map<String, String> excerpts = new LinkedHashMap<>();
|
||||
excerpts.put("it", firstNonBlank(normalizeOptional(payload.getExcerptIt()), fallbackExcerpt));
|
||||
excerpts.put("en", firstNonBlank(normalizeOptional(payload.getExcerptEn()), fallbackExcerpt));
|
||||
excerpts.put("de", firstNonBlank(normalizeOptional(payload.getExcerptDe()), fallbackExcerpt));
|
||||
excerpts.put("fr", firstNonBlank(normalizeOptional(payload.getExcerptFr()), fallbackExcerpt));
|
||||
|
||||
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));
|
||||
|
||||
return new LocalizedProductContent(
|
||||
names.get("it"),
|
||||
firstNonBlank(excerpts.get("it"), fallbackExcerpt),
|
||||
firstNonBlank(descriptions.get("it"), fallbackDescription),
|
||||
names,
|
||||
excerpts,
|
||||
descriptions
|
||||
);
|
||||
}
|
||||
|
||||
private void ensureSlugAvailable(String slug, UUID currentProductId) {
|
||||
shopProductRepository.findBySlugIgnoreCase(slug).ifPresent(existing -> {
|
||||
if (currentProductId == null || !existing.getId().equals(currentProductId)) {
|
||||
@@ -547,6 +630,18 @@ public class AdminShopProductControllerService {
|
||||
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 String normalizeAndValidateSlug(String slug, String fallbackName) {
|
||||
String source = normalizeOptional(slug);
|
||||
if (source == null) {
|
||||
@@ -579,6 +674,9 @@ public class AdminShopProductControllerService {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "3D model file is required");
|
||||
}
|
||||
if (maxModelFileSizeBytes > 0 && file.getSize() > maxModelFileSizeBytes) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "3D model file exceeds size limit");
|
||||
}
|
||||
String extension = resolveExtension(sanitizeOriginalFilename(file.getOriginalFilename()));
|
||||
if (!SUPPORTED_MODEL_EXTENSIONS.contains(extension)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unsupported 3D model type. Allowed: stl, 3mf");
|
||||
@@ -683,4 +781,14 @@ public class AdminShopProductControllerService {
|
||||
|
||||
public record ProductModelDownload(Path path, String filename, String mimeType) {
|
||||
}
|
||||
|
||||
private record LocalizedProductContent(
|
||||
String defaultName,
|
||||
String defaultExcerpt,
|
||||
String defaultDescription,
|
||||
Map<String, String> names,
|
||||
Map<String, String> excerpts,
|
||||
Map<String, String> descriptions
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ public class PublicShopCatalogService {
|
||||
List<ShopProductSummaryDto> products = productContext.entries().stream()
|
||||
.filter(entry -> allowedCategoryIds.contains(entry.product().getCategory().getId()))
|
||||
.filter(entry -> !Boolean.TRUE.equals(featuredOnly) || Boolean.TRUE.equals(entry.product().getIsFeatured()))
|
||||
.map(entry -> toProductSummaryDto(entry, productContext.productMediaBySlug()))
|
||||
.map(entry -> toProductSummaryDto(entry, productContext.productMediaBySlug(), language))
|
||||
.toList();
|
||||
|
||||
ShopCategoryDetailDto selectedCategoryDetail = selectedCategory != null
|
||||
@@ -128,7 +128,7 @@ public class PublicShopCatalogService {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Product not found");
|
||||
}
|
||||
|
||||
return toProductDetailDto(entry, productContext.productMediaBySlug());
|
||||
return toProductDetailDto(entry, productContext.productMediaBySlug(), language);
|
||||
}
|
||||
|
||||
public ProductModelDownload getProductModelDownload(String slug) {
|
||||
@@ -348,13 +348,14 @@ public class PublicShopCatalogService {
|
||||
}
|
||||
|
||||
private ShopProductSummaryDto toProductSummaryDto(ProductEntry entry,
|
||||
Map<String, List<PublicMediaUsageDto>> productMediaBySlug) {
|
||||
Map<String, List<PublicMediaUsageDto>> productMediaBySlug,
|
||||
String language) {
|
||||
List<PublicMediaUsageDto> images = productMediaBySlug.getOrDefault(productMediaUsageKey(entry.product()), List.of());
|
||||
return new ShopProductSummaryDto(
|
||||
entry.product().getId(),
|
||||
entry.product().getSlug(),
|
||||
entry.product().getName(),
|
||||
entry.product().getExcerpt(),
|
||||
entry.product().getNameForLanguage(language),
|
||||
entry.product().getExcerptForLanguage(language),
|
||||
entry.product().getIsFeatured(),
|
||||
entry.product().getSortOrder(),
|
||||
new ShopCategoryRefDto(
|
||||
@@ -371,14 +372,15 @@ public class PublicShopCatalogService {
|
||||
}
|
||||
|
||||
private ShopProductDetailDto toProductDetailDto(ProductEntry entry,
|
||||
Map<String, List<PublicMediaUsageDto>> productMediaBySlug) {
|
||||
Map<String, List<PublicMediaUsageDto>> productMediaBySlug,
|
||||
String language) {
|
||||
List<PublicMediaUsageDto> images = productMediaBySlug.getOrDefault(productMediaUsageKey(entry.product()), List.of());
|
||||
return new ShopProductDetailDto(
|
||||
entry.product().getId(),
|
||||
entry.product().getSlug(),
|
||||
entry.product().getName(),
|
||||
entry.product().getExcerpt(),
|
||||
entry.product().getDescription(),
|
||||
entry.product().getNameForLanguage(language),
|
||||
entry.product().getExcerptForLanguage(language),
|
||||
entry.product().getDescriptionForLanguage(language),
|
||||
entry.product().getSeoTitle(),
|
||||
entry.product().getSeoDescription(),
|
||||
entry.product().getOgTitle(),
|
||||
|
||||
@@ -31,6 +31,7 @@ media.storage.root=${MEDIA_STORAGE_ROOT:storage_media}
|
||||
media.public.base-url=${MEDIA_PUBLIC_BASE_URL:http://localhost:8080/media}
|
||||
media.ffmpeg.path=${MEDIA_FFMPEG_PATH:ffmpeg}
|
||||
media.upload.max-file-size-bytes=${MEDIA_UPLOAD_MAX_FILE_SIZE_BYTES:26214400}
|
||||
shop.model.max-file-size-bytes=${SHOP_MODEL_MAX_FILE_SIZE_BYTES:104857600}
|
||||
shop.storage.root=${SHOP_STORAGE_ROOT:storage_shop}
|
||||
shop.cart.cookie.ttl-days=${SHOP_CART_COOKIE_TTL_DAYS:30}
|
||||
shop.cart.cookie.secure=${SHOP_CART_COOKIE_SECURE:false}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.printcalculator.entity;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class ShopProductTest {
|
||||
|
||||
@Test
|
||||
void localizedAccessorsShouldReturnLanguageSpecificValues() {
|
||||
ShopProduct product = new ShopProduct();
|
||||
product.setName("Desk Cable Clip");
|
||||
product.setNameIt("Fermacavo da scrivania");
|
||||
product.setNameEn("Desk Cable Clip");
|
||||
product.setNameDe("Schreibtisch-Kabelclip");
|
||||
product.setNameFr("Clip de cable de bureau");
|
||||
product.setExcerpt("Legacy excerpt");
|
||||
product.setExcerptIt("Clip compatta per i cavi sulla scrivania.");
|
||||
product.setExcerptEn("Compact clip to keep desk cables in place.");
|
||||
product.setExcerptDe("Kompakter Clip fur ordentliche Kabel auf dem Schreibtisch.");
|
||||
product.setExcerptFr("Clip compact pour garder les cables du bureau en ordre.");
|
||||
product.setDescription("Legacy description");
|
||||
product.setDescriptionIt("Supporto con base stabile e passaggio cavi frontale.");
|
||||
product.setDescriptionEn("Stable desk clip with front cable routing.");
|
||||
product.setDescriptionDe("Stabiler Tischclip mit frontaler Kabelfuhrung.");
|
||||
product.setDescriptionFr("Clip de bureau stable avec passage frontal des cables.");
|
||||
|
||||
assertEquals("Fermacavo da scrivania", product.getNameForLanguage("it"));
|
||||
assertEquals("Desk Cable Clip", product.getNameForLanguage("en"));
|
||||
assertEquals("Schreibtisch-Kabelclip", product.getNameForLanguage("de"));
|
||||
assertEquals("Clip de cable de bureau", product.getNameForLanguage("fr"));
|
||||
assertEquals("Compact clip to keep desk cables in place.", product.getExcerptForLanguage("en"));
|
||||
assertEquals("Clip compact pour garder les cables du bureau en ordre.", product.getExcerptForLanguage("fr"));
|
||||
assertEquals("Stabiler Tischclip mit frontaler Kabelfuhrung.", product.getDescriptionForLanguage("de"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void localizedAccessorsShouldFallbackToLegacyValues() {
|
||||
ShopProduct product = new ShopProduct();
|
||||
product.setName("Desk Cable Clip");
|
||||
product.setExcerpt("Compact desk cable clip.");
|
||||
product.setDescription("Stable clip with front cable channel.");
|
||||
|
||||
assertEquals("Desk Cable Clip", product.getNameForLanguage("it"));
|
||||
assertEquals("Compact desk cable clip.", product.getExcerptForLanguage("de"));
|
||||
assertEquals("Stable clip with front cable channel.", product.getDescriptionForLanguage("fr-CH"));
|
||||
}
|
||||
}
|
||||
@@ -114,15 +114,15 @@ class OrderServiceTest {
|
||||
|
||||
Path sourceDir = Path.of("storage_quotes").toAbsolutePath().normalize().resolve(sessionId.toString());
|
||||
Files.createDirectories(sourceDir);
|
||||
Path sourceFile = sourceDir.resolve("shop-demo.stl");
|
||||
Files.writeString(sourceFile, "solid demo\nendsolid demo\n", StandardCharsets.UTF_8);
|
||||
Path sourceFile = sourceDir.resolve("shop-product.stl");
|
||||
Files.writeString(sourceFile, "solid product\nendsolid product\n", StandardCharsets.UTF_8);
|
||||
|
||||
QuoteLineItem qItem = new QuoteLineItem();
|
||||
qItem.setId(UUID.randomUUID());
|
||||
qItem.setQuoteSession(session);
|
||||
qItem.setStatus("READY");
|
||||
qItem.setLineItemType("SHOP_PRODUCT");
|
||||
qItem.setOriginalFilename("shop-demo.stl");
|
||||
qItem.setOriginalFilename("shop-product.stl");
|
||||
qItem.setDisplayName("Desk Cable Clip");
|
||||
qItem.setQuantity(2);
|
||||
qItem.setColorCode("Coral Red");
|
||||
|
||||
@@ -45,7 +45,7 @@ class InvoicePdfRenderingServiceTest {
|
||||
OrderItem shopItem = new OrderItem();
|
||||
shopItem.setItemType("SHOP_PRODUCT");
|
||||
shopItem.setDisplayName("Desk Cable Clip");
|
||||
shopItem.setOriginalFilename("desk-cable-clip-demo.stl");
|
||||
shopItem.setOriginalFilename("desk-cable-clip.stl");
|
||||
shopItem.setShopProductName("Desk Cable Clip");
|
||||
shopItem.setShopVariantLabel("Coral Red");
|
||||
shopItem.setQuantity(2);
|
||||
|
||||
Reference in New Issue
Block a user