diff --git a/backend/build.gradle b/backend/build.gradle index 72fed37..b15d8ef 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -25,13 +25,21 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'xyz.capybara:clamav-client:2.1.2' runtimeOnly 'org.postgresql:postgresql' developmentOnly 'org.springframework.boot:spring-boot-devtools' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'com.h2database:h2' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' compileOnly 'org.projectlombok:lombok' - implementation 'xyz.capybara:clamav-client:2.1.2' annotationProcessor 'org.projectlombok:lombok' + implementation 'io.github.openhtmltopdf:openhtmltopdf-pdfbox:1.1.37' + implementation 'io.github.openhtmltopdf:openhtmltopdf-svg-support:1.1.37' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'net.codecrete.qrbill:qrbill-generator:3.4.0' + + + } tasks.named('test') { diff --git a/backend/src/main/java/com/printcalculator/controller/OrderController.java b/backend/src/main/java/com/printcalculator/controller/OrderController.java index 2d4e637..3c70513 100644 --- a/backend/src/main/java/com/printcalculator/controller/OrderController.java +++ b/backend/src/main/java/com/printcalculator/controller/OrderController.java @@ -1,13 +1,17 @@ package com.printcalculator.controller; +import com.printcalculator.dto.*; import com.printcalculator.entity.*; import com.printcalculator.repository.*; +import com.printcalculator.service.InvoicePdfRenderingService; +import com.printcalculator.service.OrderService; +import com.printcalculator.service.QrBillService; +import com.printcalculator.service.StorageService; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import com.fasterxml.jackson.annotation.JsonProperty; import java.io.IOException; import java.math.BigDecimal; @@ -15,209 +19,61 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; import java.util.UUID; -import java.util.Optional; +import java.util.Map; +import java.util.HashMap; +import java.util.stream.Collectors; @RestController @RequestMapping("/api/orders") public class OrderController { + private final OrderService orderService; private final OrderRepository orderRepo; private final OrderItemRepository orderItemRepo; private final QuoteSessionRepository quoteSessionRepo; private final QuoteLineItemRepository quoteLineItemRepo; private final CustomerRepository customerRepo; - private final com.printcalculator.service.ClamAVService clamAVService; + private final StorageService storageService; + private final InvoicePdfRenderingService invoiceService; + private final QrBillService qrBillService; - // TODO: Inject Storage Service or use a base path property - private static final String STORAGE_ROOT = "storage_orders"; - public OrderController(OrderRepository orderRepo, + public OrderController(OrderService orderService, + OrderRepository orderRepo, OrderItemRepository orderItemRepo, QuoteSessionRepository quoteSessionRepo, QuoteLineItemRepository quoteLineItemRepo, CustomerRepository customerRepo, - com.printcalculator.service.ClamAVService clamAVService) { + StorageService storageService, + InvoicePdfRenderingService invoiceService, + QrBillService qrBillService) { + this.orderService = orderService; this.orderRepo = orderRepo; this.orderItemRepo = orderItemRepo; this.quoteSessionRepo = quoteSessionRepo; this.quoteLineItemRepo = quoteLineItemRepo; this.customerRepo = customerRepo; - this.clamAVService = clamAVService; + this.storageService = storageService; + this.invoiceService = invoiceService; + this.qrBillService = qrBillService; } // 1. Create Order from Quote @PostMapping("/from-quote/{quoteSessionId}") @Transactional - public ResponseEntity createOrderFromQuote( + public ResponseEntity createOrderFromQuote( @PathVariable UUID quoteSessionId, @RequestBody com.printcalculator.dto.CreateOrderRequest request ) { - // 1. Fetch Quote Session - QuoteSession session = quoteSessionRepo.findById(quoteSessionId) - .orElseThrow(() -> new RuntimeException("Quote Session not found")); - - if (!"ACTIVE".equals(session.getStatus())) { - // Allow converting only active sessions? Or check if not already converted? - // checking convertedOrderId might be better - } - if (session.getConvertedOrderId() != null) { - return ResponseEntity.badRequest().body(null); // Already converted - } - - // 2. Handle Customer (Find or Create) - Customer customer = customerRepo.findByEmail(request.getCustomer().getEmail()) - .orElseGet(() -> { - Customer newC = new Customer(); - newC.setEmail(request.getCustomer().getEmail()); - newC.setCreatedAt(OffsetDateTime.now()); - return customerRepo.save(newC); - }); - // Update customer details? - customer.setPhone(request.getCustomer().getPhone()); - customer.setCustomerType(request.getCustomer().getCustomerType()); - customer.setUpdatedAt(OffsetDateTime.now()); - customerRepo.save(customer); - - // 3. Create Order - Order order = new Order(); - order.setSourceQuoteSession(session); - order.setCustomer(customer); - order.setCustomerEmail(request.getCustomer().getEmail()); - order.setCustomerPhone(request.getCustomer().getPhone()); - order.setStatus("PENDING_PAYMENT"); - order.setCreatedAt(OffsetDateTime.now()); - order.setUpdatedAt(OffsetDateTime.now()); - order.setCurrency("CHF"); - // Initialize all NOT NULL monetary fields before first persist. - order.setSetupCostChf(session.getSetupCostChf() != null ? session.getSetupCostChf() : BigDecimal.ZERO); - order.setShippingCostChf(BigDecimal.ZERO); - order.setDiscountChf(BigDecimal.ZERO); - order.setSubtotalChf(BigDecimal.ZERO); - order.setTotalChf(BigDecimal.ZERO); - - // Billing - order.setBillingCustomerType(request.getCustomer().getCustomerType()); - if (request.getBillingAddress() != null) { - order.setBillingFirstName(request.getBillingAddress().getFirstName()); - order.setBillingLastName(request.getBillingAddress().getLastName()); - order.setBillingCompanyName(request.getBillingAddress().getCompanyName()); - order.setBillingContactPerson(request.getBillingAddress().getContactPerson()); - order.setBillingAddressLine1(request.getBillingAddress().getAddressLine1()); - order.setBillingAddressLine2(request.getBillingAddress().getAddressLine2()); - order.setBillingZip(request.getBillingAddress().getZip()); - order.setBillingCity(request.getBillingAddress().getCity()); - order.setBillingCountryCode(request.getBillingAddress().getCountryCode() != null ? request.getBillingAddress().getCountryCode() : "CH"); - } - - // Shipping - order.setShippingSameAsBilling(request.isShippingSameAsBilling()); - if (!request.isShippingSameAsBilling() && request.getShippingAddress() != null) { - order.setShippingFirstName(request.getShippingAddress().getFirstName()); - order.setShippingLastName(request.getShippingAddress().getLastName()); - order.setShippingCompanyName(request.getShippingAddress().getCompanyName()); - order.setShippingContactPerson(request.getShippingAddress().getContactPerson()); - order.setShippingAddressLine1(request.getShippingAddress().getAddressLine1()); - order.setShippingAddressLine2(request.getShippingAddress().getAddressLine2()); - order.setShippingZip(request.getShippingAddress().getZip()); - order.setShippingCity(request.getShippingAddress().getCity()); - order.setShippingCountryCode(request.getShippingAddress().getCountryCode() != null ? request.getShippingAddress().getCountryCode() : "CH"); - } else { - // Copy billing to shipping? Or leave empty and rely on flag? - // Usually explicit copy is safer for queries - order.setShippingFirstName(order.getBillingFirstName()); - order.setShippingLastName(order.getBillingLastName()); - order.setShippingCompanyName(order.getBillingCompanyName()); - order.setShippingContactPerson(order.getBillingContactPerson()); - order.setShippingAddressLine1(order.getBillingAddressLine1()); - order.setShippingAddressLine2(order.getBillingAddressLine2()); - order.setShippingZip(order.getBillingZip()); - order.setShippingCity(order.getBillingCity()); - order.setShippingCountryCode(order.getBillingCountryCode()); - } - - // Financials from Session (Assuming mocked/calculated in session) - // We re-calculate totals from line items to be safe - List quoteItems = quoteLineItemRepo.findByQuoteSessionId(quoteSessionId); - - BigDecimal subtotal = BigDecimal.ZERO; - - // Save Order first to get ID - order = orderRepo.save(order); - - // 4. Create Order Items - for (QuoteLineItem qItem : quoteItems) { - OrderItem oItem = new OrderItem(); - oItem.setOrder(order); - oItem.setOriginalFilename(qItem.getOriginalFilename()); - oItem.setQuantity(qItem.getQuantity()); - oItem.setColorCode(qItem.getColorCode()); - oItem.setMaterialCode(session.getMaterialCode()); // Or per item if supported - - // Pricing - oItem.setUnitPriceChf(qItem.getUnitPriceChf()); - oItem.setLineTotalChf(qItem.getUnitPriceChf().multiply(BigDecimal.valueOf(qItem.getQuantity()))); - oItem.setPrintTimeSeconds(qItem.getPrintTimeSeconds()); - oItem.setMaterialGrams(qItem.getMaterialGrams()); - - // File Handling Check - // "orders/{orderId}/3d-files/{orderItemId}/{uuid}.{ext}" - UUID fileUuid = UUID.randomUUID(); - String ext = getExtension(qItem.getOriginalFilename()); - String storedFilename = fileUuid.toString() + "." + ext; - - oItem.setStoredFilename(storedFilename); - oItem.setStoredRelativePath("PENDING"); // Placeholder - oItem.setMimeType("application/octet-stream"); // specific type if known - - oItem = orderItemRepo.save(oItem); - - // Update Path now that we have ID - String relativePath = "orders/" + order.getId() + "/3d-files/" + oItem.getId() + "/" + storedFilename; - oItem.setStoredRelativePath(relativePath); - - // COPY FILE from Quote to Order - if (qItem.getStoredPath() != null) { - try { - Path sourcePath = Paths.get(qItem.getStoredPath()); - if (Files.exists(sourcePath)) { - Path targetPath = Paths.get(STORAGE_ROOT, relativePath); - Files.createDirectories(targetPath.getParent()); - Files.copy(sourcePath, targetPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - - oItem.setFileSizeBytes(Files.size(targetPath)); - } - } catch (IOException e) { - e.printStackTrace(); // Log error but allow order creation? Or fail? - // Ideally fail or mark as error - } - } - - orderItemRepo.save(oItem); - - subtotal = subtotal.add(oItem.getLineTotalChf()); - } - - // Update Order Totals - order.setSubtotalChf(subtotal); - order.setSetupCostChf(session.getSetupCostChf() != null ? session.getSetupCostChf() : BigDecimal.ZERO); - order.setShippingCostChf(BigDecimal.valueOf(9.00)); // Default shipping? or 0? - // TODO: Calc implementation for shipping - - BigDecimal total = subtotal.add(order.getSetupCostChf()).add(order.getShippingCostChf()).subtract(order.getDiscountChf() != null ? order.getDiscountChf() : BigDecimal.ZERO); - order.setTotalChf(total); - - // Link session - session.setConvertedOrderId(order.getId()); - session.setStatus("CONVERTED"); // or CLOSED - quoteSessionRepo.save(session); - - return ResponseEntity.ok(orderRepo.save(order)); + Order order = orderService.createOrderFromQuote(quoteSessionId, request); + List items = orderItemRepo.findByOrder_Id(order.getId()); + return ResponseEntity.ok(convertToDto(order, items)); } - // 2. Upload file for Order Item @PostMapping(value = "/{orderId}/items/{orderItemId}/file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Transactional public ResponseEntity uploadOrderItemFile( @@ -232,42 +88,103 @@ public class OrderController { if (!item.getOrder().getId().equals(orderId)) { return ResponseEntity.badRequest().build(); } - - // Scan for virus - clamAVService.scan(file.getInputStream()); - // Ensure path logic String relativePath = item.getStoredRelativePath(); if (relativePath == null || relativePath.equals("PENDING")) { - // Should verify consistency - // If we used the logic above, it should have a path. - // If it's "PENDING", regen it. String ext = getExtension(file.getOriginalFilename()); String storedFilename = UUID.randomUUID().toString() + "." + ext; relativePath = "orders/" + orderId + "/3d-files/" + orderItemId + "/" + storedFilename; item.setStoredRelativePath(relativePath); item.setStoredFilename(storedFilename); - // Update item } - // Save file to disk - Path absolutePath = Paths.get(STORAGE_ROOT, relativePath); - Files.createDirectories(absolutePath.getParent()); - - if (Files.exists(absolutePath)) { - Files.delete(absolutePath); // Overwrite? - } - - Files.copy(file.getInputStream(), absolutePath); - + storageService.store(file, Paths.get(relativePath)); item.setFileSizeBytes(file.getSize()); item.setMimeType(file.getContentType()); - // Calculate SHA256? (Optional) - orderItemRepo.save(item); return ResponseEntity.ok().build(); } + + @GetMapping("/{orderId}") + public ResponseEntity getOrder(@PathVariable UUID orderId) { + return orderRepo.findById(orderId) + .map(o -> { + List items = orderItemRepo.findByOrder_Id(o.getId()); + return ResponseEntity.ok(convertToDto(o, items)); + }) + .orElse(ResponseEntity.notFound().build()); + } + + @GetMapping("/{orderId}/invoice") + public ResponseEntity getInvoice(@PathVariable UUID orderId) { + Order order = orderRepo.findById(orderId) + .orElseThrow(() -> new RuntimeException("Order not found")); + + List items = orderItemRepo.findByOrder_Id(orderId); + + Map vars = new HashMap<>(); + vars.put("sellerDisplayName", "3D Fab Switzerland"); + vars.put("sellerAddressLine1", "Sede Ticino, Svizzera"); + vars.put("sellerAddressLine2", "Sede Bienne, Svizzera"); + vars.put("sellerEmail", "info@3dfab.ch"); + + vars.put("invoiceNumber", "INV-" + order.getId().toString().substring(0, 8).toUpperCase()); + vars.put("invoiceDate", order.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE)); + vars.put("dueDate", order.getCreatedAt().plusDays(7).format(DateTimeFormatter.ISO_LOCAL_DATE)); + + String buyerName = order.getBillingCustomerType().equals("BUSINESS") + ? order.getBillingCompanyName() + : order.getBillingFirstName() + " " + order.getBillingLastName(); + vars.put("buyerDisplayName", buyerName); + vars.put("buyerAddressLine1", order.getBillingAddressLine1()); + vars.put("buyerAddressLine2", order.getBillingZip() + " " + order.getBillingCity() + ", " + order.getBillingCountryCode()); + + List> invoiceLineItems = items.stream().map(i -> { + Map line = new HashMap<>(); + line.put("description", "Stampa 3D: " + i.getOriginalFilename()); + line.put("quantity", i.getQuantity()); + line.put("unitPriceFormatted", String.format("CHF %.2f", i.getUnitPriceChf())); + line.put("lineTotalFormatted", String.format("CHF %.2f", i.getLineTotalChf())); + return line; + }).collect(Collectors.toList()); + + Map setupLine = new HashMap<>(); + setupLine.put("description", "Costo Setup"); + setupLine.put("quantity", 1); + setupLine.put("unitPriceFormatted", String.format("CHF %.2f", order.getSetupCostChf())); + setupLine.put("lineTotalFormatted", String.format("CHF %.2f", order.getSetupCostChf())); + invoiceLineItems.add(setupLine); + + Map shippingLine = new HashMap<>(); + shippingLine.put("description", "Spedizione"); + shippingLine.put("quantity", 1); + shippingLine.put("unitPriceFormatted", String.format("CHF %.2f", order.getShippingCostChf())); + shippingLine.put("lineTotalFormatted", String.format("CHF %.2f", order.getShippingCostChf())); + invoiceLineItems.add(shippingLine); + + vars.put("invoiceLineItems", invoiceLineItems); + vars.put("subtotalFormatted", String.format("CHF %.2f", order.getSubtotalChf())); + vars.put("grandTotalFormatted", String.format("CHF %.2f", order.getTotalChf())); + vars.put("paymentTermsText", "Pagamento entro 7 giorni via Bonifico o TWINT. Grazie."); + + String qrBillSvg = new String(qrBillService.generateQrBillSvg(order), java.nio.charset.StandardCharsets.UTF_8); + + // Strip XML declaration and DOCTYPE if present, as they validity break the embedding HTML page + if (qrBillSvg.contains(" items) { + OrderDto dto = new OrderDto(); + dto.setId(order.getId()); + dto.setStatus(order.getStatus()); + dto.setCustomerEmail(order.getCustomerEmail()); + dto.setCustomerPhone(order.getCustomerPhone()); + dto.setBillingCustomerType(order.getBillingCustomerType()); + dto.setCurrency(order.getCurrency()); + dto.setSetupCostChf(order.getSetupCostChf()); + dto.setShippingCostChf(order.getShippingCostChf()); + dto.setDiscountChf(order.getDiscountChf()); + dto.setSubtotalChf(order.getSubtotalChf()); + dto.setTotalChf(order.getTotalChf()); + dto.setCreatedAt(order.getCreatedAt()); + dto.setShippingSameAsBilling(order.getShippingSameAsBilling()); + + AddressDto billing = new AddressDto(); + billing.setFirstName(order.getBillingFirstName()); + billing.setLastName(order.getBillingLastName()); + billing.setCompanyName(order.getBillingCompanyName()); + billing.setContactPerson(order.getBillingContactPerson()); + billing.setAddressLine1(order.getBillingAddressLine1()); + billing.setAddressLine2(order.getBillingAddressLine2()); + billing.setZip(order.getBillingZip()); + billing.setCity(order.getBillingCity()); + billing.setCountryCode(order.getBillingCountryCode()); + dto.setBillingAddress(billing); + + if (!order.getShippingSameAsBilling()) { + AddressDto shipping = new AddressDto(); + shipping.setFirstName(order.getShippingFirstName()); + shipping.setLastName(order.getShippingLastName()); + shipping.setCompanyName(order.getShippingCompanyName()); + shipping.setContactPerson(order.getShippingContactPerson()); + shipping.setAddressLine1(order.getShippingAddressLine1()); + shipping.setAddressLine2(order.getShippingAddressLine2()); + shipping.setZip(order.getShippingZip()); + shipping.setCity(order.getShippingCity()); + shipping.setCountryCode(order.getShippingCountryCode()); + dto.setShippingAddress(shipping); + } + + List itemDtos = items.stream().map(i -> { + OrderItemDto idto = new OrderItemDto(); + idto.setId(i.getId()); + idto.setOriginalFilename(i.getOriginalFilename()); + idto.setMaterialCode(i.getMaterialCode()); + idto.setColorCode(i.getColorCode()); + idto.setQuantity(i.getQuantity()); + idto.setPrintTimeSeconds(i.getPrintTimeSeconds()); + idto.setMaterialGrams(i.getMaterialGrams()); + idto.setUnitPriceChf(i.getUnitPriceChf()); + idto.setLineTotalChf(i.getLineTotalChf()); + return idto; + }).collect(Collectors.toList()); + dto.setItems(itemDtos); + + return dto; + } + } diff --git a/backend/src/main/java/com/printcalculator/dto/OrderDto.java b/backend/src/main/java/com/printcalculator/dto/OrderDto.java new file mode 100644 index 0000000..982d9c3 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/dto/OrderDto.java @@ -0,0 +1,74 @@ +package com.printcalculator.dto; + +import java.math.BigDecimal; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +public class OrderDto { + private UUID id; + private String status; + private String customerEmail; + private String customerPhone; + private String billingCustomerType; + private AddressDto billingAddress; + private AddressDto shippingAddress; + private Boolean shippingSameAsBilling; + private String currency; + private BigDecimal setupCostChf; + private BigDecimal shippingCostChf; + private BigDecimal discountChf; + private BigDecimal subtotalChf; + private BigDecimal totalChf; + private OffsetDateTime createdAt; + private List items; + + // Getters and Setters + public UUID getId() { return id; } + public void setId(UUID id) { this.id = id; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public String getCustomerEmail() { return customerEmail; } + public void setCustomerEmail(String customerEmail) { this.customerEmail = customerEmail; } + + public String getCustomerPhone() { return customerPhone; } + public void setCustomerPhone(String customerPhone) { this.customerPhone = customerPhone; } + + public String getBillingCustomerType() { return billingCustomerType; } + public void setBillingCustomerType(String billingCustomerType) { this.billingCustomerType = billingCustomerType; } + + public AddressDto getBillingAddress() { return billingAddress; } + public void setBillingAddress(AddressDto billingAddress) { this.billingAddress = billingAddress; } + + public AddressDto getShippingAddress() { return shippingAddress; } + public void setShippingAddress(AddressDto shippingAddress) { this.shippingAddress = shippingAddress; } + + public Boolean getShippingSameAsBilling() { return shippingSameAsBilling; } + public void setShippingSameAsBilling(Boolean shippingSameAsBilling) { this.shippingSameAsBilling = shippingSameAsBilling; } + + public String getCurrency() { return currency; } + public void setCurrency(String currency) { this.currency = currency; } + + public BigDecimal getSetupCostChf() { return setupCostChf; } + public void setSetupCostChf(BigDecimal setupCostChf) { this.setupCostChf = setupCostChf; } + + public BigDecimal getShippingCostChf() { return shippingCostChf; } + public void setShippingCostChf(BigDecimal shippingCostChf) { this.shippingCostChf = shippingCostChf; } + + public BigDecimal getDiscountChf() { return discountChf; } + public void setDiscountChf(BigDecimal discountChf) { this.discountChf = discountChf; } + + public BigDecimal getSubtotalChf() { return subtotalChf; } + public void setSubtotalChf(BigDecimal subtotalChf) { this.subtotalChf = subtotalChf; } + + public BigDecimal getTotalChf() { return totalChf; } + public void setTotalChf(BigDecimal totalChf) { this.totalChf = totalChf; } + + public OffsetDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; } + + public List getItems() { return items; } + public void setItems(List items) { this.items = items; } +} diff --git a/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java b/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java new file mode 100644 index 0000000..d31d208 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java @@ -0,0 +1,44 @@ +package com.printcalculator.dto; + +import java.math.BigDecimal; +import java.util.UUID; + +public class OrderItemDto { + private UUID id; + private String originalFilename; + private String materialCode; + private String colorCode; + private Integer quantity; + private Integer printTimeSeconds; + private BigDecimal materialGrams; + private BigDecimal unitPriceChf; + private BigDecimal lineTotalChf; + + // Getters and Setters + public UUID getId() { return id; } + public void setId(UUID id) { this.id = id; } + + public String getOriginalFilename() { return originalFilename; } + public void setOriginalFilename(String originalFilename) { this.originalFilename = originalFilename; } + + public String getMaterialCode() { return materialCode; } + public void setMaterialCode(String materialCode) { this.materialCode = materialCode; } + + public String getColorCode() { return colorCode; } + public void setColorCode(String colorCode) { this.colorCode = colorCode; } + + public Integer getQuantity() { return quantity; } + public void setQuantity(Integer quantity) { this.quantity = quantity; } + + public Integer getPrintTimeSeconds() { return printTimeSeconds; } + public void setPrintTimeSeconds(Integer printTimeSeconds) { this.printTimeSeconds = printTimeSeconds; } + + public BigDecimal getMaterialGrams() { return materialGrams; } + public void setMaterialGrams(BigDecimal materialGrams) { this.materialGrams = materialGrams; } + + public BigDecimal getUnitPriceChf() { return unitPriceChf; } + public void setUnitPriceChf(BigDecimal unitPriceChf) { this.unitPriceChf = unitPriceChf; } + + public BigDecimal getLineTotalChf() { return lineTotalChf; } + public void setLineTotalChf(BigDecimal lineTotalChf) { this.lineTotalChf = lineTotalChf; } +} diff --git a/backend/src/main/java/com/printcalculator/exception/StorageException.java b/backend/src/main/java/com/printcalculator/exception/StorageException.java new file mode 100644 index 0000000..0a0da37 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/exception/StorageException.java @@ -0,0 +1,12 @@ +package com.printcalculator.exception; + +public class StorageException extends RuntimeException { + + public StorageException(String message) { + super(message); + } + + public StorageException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/backend/src/main/java/com/printcalculator/repository/OrderItemRepository.java b/backend/src/main/java/com/printcalculator/repository/OrderItemRepository.java index b43a305..0068809 100644 --- a/backend/src/main/java/com/printcalculator/repository/OrderItemRepository.java +++ b/backend/src/main/java/com/printcalculator/repository/OrderItemRepository.java @@ -3,7 +3,9 @@ package com.printcalculator.repository; import com.printcalculator.entity.OrderItem; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.UUID; public interface OrderItemRepository extends JpaRepository { + List findByOrder_Id(UUID orderId); } \ No newline at end of file diff --git a/backend/src/main/java/com/printcalculator/service/FileSystemStorageService.java b/backend/src/main/java/com/printcalculator/service/FileSystemStorageService.java new file mode 100644 index 0000000..7db4aa8 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/service/FileSystemStorageService.java @@ -0,0 +1,94 @@ +package com.printcalculator.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import com.printcalculator.exception.StorageException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +@Service +public class FileSystemStorageService implements StorageService { + + private final Path rootLocation; + private final ClamAVService clamAVService; + + public FileSystemStorageService(@Value("${storage.location:storage_orders}") String storageLocation, ClamAVService clamAVService) { + this.rootLocation = Paths.get(storageLocation); + this.clamAVService = clamAVService; + } + + @Override + public void init() { + try { + Files.createDirectories(rootLocation); + } catch (IOException e) { + throw new StorageException("Could not initialize storage", e); + } + } + + @Override + public void store(MultipartFile file, Path destinationRelativePath) throws IOException { + Path destinationFile = this.rootLocation.resolve(destinationRelativePath).normalize().toAbsolutePath(); + if (!destinationFile.getParent().startsWith(this.rootLocation.toAbsolutePath())) { + throw new StorageException("Cannot store file outside current directory."); + } + + // 1. Salva prima il file su disco per evitare problemi di stream con file grandi + Files.createDirectories(destinationFile.getParent()); + file.transferTo(destinationFile.toFile()); + + // 2. Scansiona il file appena salvato aprendo un nuovo stream + try (InputStream inputStream = new FileInputStream(destinationFile.toFile())) { + if (!clamAVService.scan(inputStream)) { + // Se infetto, cancella il file e solleva eccezione + Files.deleteIfExists(destinationFile); + throw new StorageException("File rejected by antivirus scanner."); + } + } catch (Exception e) { + if (e instanceof StorageException) throw e; + // Se l'antivirus fallisce per motivi tecnici, lasciamo il file (fail-open come concordato) + } + } + + @Override + public void store(Path source, Path destinationRelativePath) throws IOException { + Path destinationFile = this.rootLocation.resolve(destinationRelativePath).normalize().toAbsolutePath(); + if (!destinationFile.getParent().startsWith(this.rootLocation.toAbsolutePath())) { + throw new StorageException("Cannot store file outside current directory."); + } + Files.createDirectories(destinationFile.getParent()); + Files.copy(source, destinationFile, StandardCopyOption.REPLACE_EXISTING); + } + + @Override + public void delete(Path path) throws IOException { + Path file = rootLocation.resolve(path); + Files.deleteIfExists(file); + } + + @Override + public Resource loadAsResource(Path path) throws IOException { + try { + Path file = rootLocation.resolve(path); + Resource resource = new UrlResource(file.toUri()); + if (resource.exists() || resource.isReadable()) { + return resource; + } else { + throw new RuntimeException("Could not read file: " + path); + } + } catch (MalformedURLException e) { + throw new RuntimeException("Could not read file: " + path, e); + } + } +} diff --git a/backend/src/main/java/com/printcalculator/service/InvoicePdfRenderingService.java b/backend/src/main/java/com/printcalculator/service/InvoicePdfRenderingService.java new file mode 100644 index 0000000..a21e59f --- /dev/null +++ b/backend/src/main/java/com/printcalculator/service/InvoicePdfRenderingService.java @@ -0,0 +1,48 @@ +package com.printcalculator.service; + +import com.openhtmltopdf.pdfboxout.PdfRendererBuilder; +import com.openhtmltopdf.svgsupport.BatikSVGDrawer; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Locale; +import java.util.Map; + +@Service +public class InvoicePdfRenderingService { + + private final TemplateEngine thymeleafTemplateEngine; + + public InvoicePdfRenderingService(TemplateEngine thymeleafTemplateEngine) { + this.thymeleafTemplateEngine = thymeleafTemplateEngine; + } + + public byte[] generateInvoicePdfBytesFromTemplate(Map invoiceTemplateVariables, String qrBillSvg) { + try { + Context thymeleafContextWithInvoiceData = new Context(Locale.ITALY); + thymeleafContextWithInvoiceData.setVariables(invoiceTemplateVariables); + thymeleafContextWithInvoiceData.setVariable("qrBillSvg", qrBillSvg); + + String renderedInvoiceHtml = thymeleafTemplateEngine.process("invoice", thymeleafContextWithInvoiceData); + + String classpathBaseUrlForHtmlResources = new ClassPathResource("templates/").getURL().toExternalForm(); + + ByteArrayOutputStream generatedPdfByteArrayOutputStream = new ByteArrayOutputStream(); + + PdfRendererBuilder openHtmlToPdfRendererBuilder = new PdfRendererBuilder(); + openHtmlToPdfRendererBuilder.useFastMode(); + openHtmlToPdfRendererBuilder.useSVGDrawer(new BatikSVGDrawer()); + openHtmlToPdfRendererBuilder.withHtmlContent(renderedInvoiceHtml, classpathBaseUrlForHtmlResources); + openHtmlToPdfRendererBuilder.toStream(generatedPdfByteArrayOutputStream); + openHtmlToPdfRendererBuilder.run(); + + return generatedPdfByteArrayOutputStream.toByteArray(); + } catch (Exception pdfGenerationException) { + throw new IllegalStateException("PDF invoice generation failed", pdfGenerationException); + } + } +} diff --git a/backend/src/main/java/com/printcalculator/service/OrderService.java b/backend/src/main/java/com/printcalculator/service/OrderService.java new file mode 100644 index 0000000..02d3289 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/service/OrderService.java @@ -0,0 +1,300 @@ +package com.printcalculator.service; + +import com.printcalculator.dto.AddressDto; +import com.printcalculator.dto.CreateOrderRequest; +import com.printcalculator.entity.*; +import com.printcalculator.repository.CustomerRepository; +import com.printcalculator.repository.OrderItemRepository; +import com.printcalculator.repository.OrderRepository; +import com.printcalculator.repository.QuoteLineItemRepository; +import com.printcalculator.repository.QuoteSessionRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class OrderService { + + private final OrderRepository orderRepo; + private final OrderItemRepository orderItemRepo; + private final QuoteSessionRepository quoteSessionRepo; + private final QuoteLineItemRepository quoteLineItemRepo; + private final CustomerRepository customerRepo; + private final StorageService storageService; + private final InvoicePdfRenderingService invoiceService; + private final QrBillService qrBillService; + + public OrderService(OrderRepository orderRepo, + OrderItemRepository orderItemRepo, + QuoteSessionRepository quoteSessionRepo, + QuoteLineItemRepository quoteLineItemRepo, + CustomerRepository customerRepo, + StorageService storageService, + InvoicePdfRenderingService invoiceService, + QrBillService qrBillService) { + this.orderRepo = orderRepo; + this.orderItemRepo = orderItemRepo; + this.quoteSessionRepo = quoteSessionRepo; + this.quoteLineItemRepo = quoteLineItemRepo; + this.customerRepo = customerRepo; + this.storageService = storageService; + this.invoiceService = invoiceService; + this.qrBillService = qrBillService; + } + + @Transactional + public Order createOrderFromQuote(UUID quoteSessionId, CreateOrderRequest request) { + QuoteSession session = quoteSessionRepo.findById(quoteSessionId) + .orElseThrow(() -> new RuntimeException("Quote Session not found")); + + if (session.getConvertedOrderId() != null) { + throw new IllegalStateException("Quote session already converted to order"); + } + + Customer customer = customerRepo.findByEmail(request.getCustomer().getEmail()) + .orElseGet(() -> { + Customer newC = new Customer(); + newC.setEmail(request.getCustomer().getEmail()); + newC.setCustomerType(request.getCustomer().getCustomerType()); + newC.setCreatedAt(OffsetDateTime.now()); + newC.setUpdatedAt(OffsetDateTime.now()); + return customerRepo.save(newC); + }); + + customer.setPhone(request.getCustomer().getPhone()); + customer.setCustomerType(request.getCustomer().getCustomerType()); + customer.setUpdatedAt(OffsetDateTime.now()); + customerRepo.save(customer); + + Order order = new Order(); + order.setSourceQuoteSession(session); + order.setCustomer(customer); + order.setCustomerEmail(request.getCustomer().getEmail()); + order.setCustomerPhone(request.getCustomer().getPhone()); + order.setStatus("PENDING_PAYMENT"); + order.setCreatedAt(OffsetDateTime.now()); + order.setUpdatedAt(OffsetDateTime.now()); + order.setCurrency("CHF"); + + order.setBillingCustomerType(request.getCustomer().getCustomerType()); + if (request.getBillingAddress() != null) { + order.setBillingFirstName(request.getBillingAddress().getFirstName()); + order.setBillingLastName(request.getBillingAddress().getLastName()); + order.setBillingCompanyName(request.getBillingAddress().getCompanyName()); + order.setBillingContactPerson(request.getBillingAddress().getContactPerson()); + order.setBillingAddressLine1(request.getBillingAddress().getAddressLine1()); + order.setBillingAddressLine2(request.getBillingAddress().getAddressLine2()); + order.setBillingZip(request.getBillingAddress().getZip()); + order.setBillingCity(request.getBillingAddress().getCity()); + order.setBillingCountryCode(request.getBillingAddress().getCountryCode() != null ? request.getBillingAddress().getCountryCode() : "CH"); + } + + order.setShippingSameAsBilling(request.isShippingSameAsBilling()); + if (!request.isShippingSameAsBilling() && request.getShippingAddress() != null) { + order.setShippingFirstName(request.getShippingAddress().getFirstName()); + order.setShippingLastName(request.getShippingAddress().getLastName()); + order.setShippingCompanyName(request.getShippingAddress().getCompanyName()); + order.setShippingContactPerson(request.getShippingAddress().getContactPerson()); + order.setShippingAddressLine1(request.getShippingAddress().getAddressLine1()); + order.setShippingAddressLine2(request.getShippingAddress().getAddressLine2()); + order.setShippingZip(request.getShippingAddress().getZip()); + order.setShippingCity(request.getShippingAddress().getCity()); + order.setShippingCountryCode(request.getShippingAddress().getCountryCode() != null ? request.getShippingAddress().getCountryCode() : "CH"); + } else { + order.setShippingFirstName(order.getBillingFirstName()); + order.setShippingLastName(order.getBillingLastName()); + order.setShippingCompanyName(order.getBillingCompanyName()); + order.setShippingContactPerson(order.getBillingContactPerson()); + order.setShippingAddressLine1(order.getBillingAddressLine1()); + order.setShippingAddressLine2(order.getBillingAddressLine2()); + order.setShippingZip(order.getBillingZip()); + order.setShippingCity(order.getBillingCity()); + order.setShippingCountryCode(order.getBillingCountryCode()); + } + + List quoteItems = quoteLineItemRepo.findByQuoteSessionId(quoteSessionId); + + BigDecimal subtotal = BigDecimal.ZERO; + order.setSubtotalChf(BigDecimal.ZERO); + order.setTotalChf(BigDecimal.ZERO); + order.setDiscountChf(BigDecimal.ZERO); + order.setSetupCostChf(session.getSetupCostChf() != null ? session.getSetupCostChf() : BigDecimal.ZERO); + order.setShippingCostChf(BigDecimal.valueOf(9.00)); + + order = orderRepo.save(order); + + List savedItems = new ArrayList<>(); + + for (QuoteLineItem qItem : quoteItems) { + OrderItem oItem = new OrderItem(); + oItem.setOrder(order); + oItem.setOriginalFilename(qItem.getOriginalFilename()); + oItem.setQuantity(qItem.getQuantity()); + oItem.setColorCode(qItem.getColorCode()); + oItem.setMaterialCode(session.getMaterialCode()); + + oItem.setUnitPriceChf(qItem.getUnitPriceChf()); + oItem.setLineTotalChf(qItem.getUnitPriceChf().multiply(BigDecimal.valueOf(qItem.getQuantity()))); + oItem.setPrintTimeSeconds(qItem.getPrintTimeSeconds()); + oItem.setMaterialGrams(qItem.getMaterialGrams()); + + UUID fileUuid = UUID.randomUUID(); + String ext = getExtension(qItem.getOriginalFilename()); + String storedFilename = fileUuid.toString() + "." + ext; + + oItem.setStoredFilename(storedFilename); + oItem.setStoredRelativePath("PENDING"); + oItem.setMimeType("application/octet-stream"); + oItem.setCreatedAt(OffsetDateTime.now()); + + oItem = orderItemRepo.save(oItem); + + String relativePath = "orders/" + order.getId() + "/3d-files/" + oItem.getId() + "/" + storedFilename; + oItem.setStoredRelativePath(relativePath); + + if (qItem.getStoredPath() != null) { + try { + Path sourcePath = Paths.get(qItem.getStoredPath()); + if (Files.exists(sourcePath)) { + storageService.store(sourcePath, Paths.get(relativePath)); + oItem.setFileSizeBytes(Files.size(sourcePath)); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + oItem = orderItemRepo.save(oItem); + savedItems.add(oItem); + subtotal = subtotal.add(oItem.getLineTotalChf()); + } + + order.setSubtotalChf(subtotal); + if (order.getShippingCostChf() == null) { + order.setShippingCostChf(BigDecimal.valueOf(9.00)); + } + + BigDecimal total = subtotal.add(order.getSetupCostChf()).add(order.getShippingCostChf()).subtract(order.getDiscountChf() != null ? order.getDiscountChf() : BigDecimal.ZERO); + order.setTotalChf(total); + + session.setConvertedOrderId(order.getId()); + session.setStatus("CONVERTED"); + quoteSessionRepo.save(session); + + // Generate Invoice and QR Bill + generateAndSaveDocuments(order, savedItems); + + return orderRepo.save(order); + } + + private void generateAndSaveDocuments(Order order, List items) { + try { + // 1. Generate QR Bill + byte[] qrBillSvgBytes = qrBillService.generateQrBillSvg(order); + String qrBillSvg = new String(qrBillSvgBytes, StandardCharsets.UTF_8); + + // Strip XML declaration and DOCTYPE if present, as they validity break the embedding HTML page + if (qrBillSvg.contains(" vars = new HashMap<>(); + vars.put("sellerDisplayName", "3D Fab Switzerland"); + vars.put("sellerAddressLine1", "Sede Ticino, Svizzera"); + vars.put("sellerAddressLine2", "Sede Bienne, Svizzera"); + vars.put("sellerEmail", "info@3dfab.ch"); + + vars.put("invoiceNumber", "INV-" + order.getId().toString().substring(0, 8).toUpperCase()); + vars.put("invoiceDate", order.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE)); + vars.put("dueDate", order.getCreatedAt().plusDays(7).format(DateTimeFormatter.ISO_LOCAL_DATE)); + + String buyerName = "BUSINESS".equals(order.getBillingCustomerType()) + ? order.getBillingCompanyName() + : order.getBillingFirstName() + " " + order.getBillingLastName(); + vars.put("buyerDisplayName", buyerName); + vars.put("buyerAddressLine1", order.getBillingAddressLine1()); + vars.put("buyerAddressLine2", order.getBillingZip() + " " + order.getBillingCity() + ", " + order.getBillingCountryCode()); + + List> invoiceLineItems = items.stream().map(i -> { + Map line = new HashMap<>(); + line.put("description", "Stampa 3D: " + i.getOriginalFilename()); + line.put("quantity", i.getQuantity()); + line.put("unitPriceFormatted", String.format("CHF %.2f", i.getUnitPriceChf())); + line.put("lineTotalFormatted", String.format("CHF %.2f", i.getLineTotalChf())); + return line; + }).collect(Collectors.toList()); + + Map setupLine = new HashMap<>(); + setupLine.put("description", "Costo Setup"); + setupLine.put("quantity", 1); + setupLine.put("unitPriceFormatted", String.format("CHF %.2f", order.getSetupCostChf())); + setupLine.put("lineTotalFormatted", String.format("CHF %.2f", order.getSetupCostChf())); + invoiceLineItems.add(setupLine); + + Map shippingLine = new HashMap<>(); + shippingLine.put("description", "Spedizione"); + shippingLine.put("quantity", 1); + shippingLine.put("unitPriceFormatted", String.format("CHF %.2f", order.getShippingCostChf())); + shippingLine.put("lineTotalFormatted", String.format("CHF %.2f", order.getShippingCostChf())); + invoiceLineItems.add(shippingLine); + + vars.put("invoiceLineItems", invoiceLineItems); + vars.put("subtotalFormatted", String.format("CHF %.2f", order.getSubtotalChf())); + vars.put("grandTotalFormatted", String.format("CHF %.2f", order.getTotalChf())); + vars.put("paymentTermsText", "Appena riceviamo il pagamento l'ordine entrerà nella coda di stampa. Grazie per la fiducia"); + + // 3. Generate PDF + byte[] pdfBytes = invoiceService.generateInvoicePdfBytesFromTemplate(vars, qrBillSvg); + + // Save PDF + String pdfRelativePath = "orders/" + order.getId() + "/documents/invoice-" + order.getId() + ".pdf"; + saveFileBytes(pdfBytes, pdfRelativePath); + + } catch (Exception e) { + e.printStackTrace(); + // Don't fail the order if document generation fails, but log it + // TODO: Better error handling + } + } + + private void saveFileBytes(byte[] content, String relativePath) { + // Since StorageService takes paths, we might need to write to temp first or check if it supports bytes/streams + // Simulating via temp file for now as StorageService.store takes a Path + try { + Path tempFile = Files.createTempFile("print-calc-upload", ".tmp"); + Files.write(tempFile, content); + storageService.store(tempFile, Paths.get(relativePath)); + Files.delete(tempFile); + } catch (IOException e) { + throw new RuntimeException("Failed to save file " + relativePath, e); + } + } + + private String getExtension(String filename) { + if (filename == null) return "stl"; + int i = filename.lastIndexOf('.'); + if (i > 0) { + return filename.substring(i + 1); + } + return "stl"; + } +} diff --git a/backend/src/main/java/com/printcalculator/service/QrBillService.java b/backend/src/main/java/com/printcalculator/service/QrBillService.java new file mode 100644 index 0000000..093739f --- /dev/null +++ b/backend/src/main/java/com/printcalculator/service/QrBillService.java @@ -0,0 +1,68 @@ +package com.printcalculator.service; + +import com.printcalculator.entity.Order; +import net.codecrete.qrbill.generator.Bill; +import net.codecrete.qrbill.generator.GraphicsFormat; +import net.codecrete.qrbill.generator.QRBill; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +public class QrBillService { + + public byte[] generateQrBillSvg(Order order) { + Bill bill = createBillFromOrder(order); + return QRBill.generate(bill); + } + + public Bill createBillFromOrder(Order order) { + Bill bill = new Bill(); + + // Creditor (Merchant) + bill.setAccount("CH7409000000154821581"); // TODO: Configurable IBAN + bill.setCreditor(createAddress( + "Küng, Joe", + "Via G. Pioda 29a", + "6710", + "Biasca", + "CH" + )); + + // Debtor (Customer) + String debtorName; + if ("BUSINESS".equals(order.getBillingCustomerType())) { + debtorName = order.getBillingCompanyName(); + } else { + debtorName = order.getBillingFirstName() + " " + order.getBillingLastName(); + } + + bill.setDebtor(createAddress( + debtorName, + order.getBillingAddressLine1(), // Assuming simple address for now. Splitting might be needed if street/house number are separate + order.getBillingZip(), + order.getBillingCity(), + order.getBillingCountryCode() + )); + + // Amount + bill.setAmount(order.getTotalChf()); + bill.setCurrency("CHF"); + + // Reference + // bill.setReference(QRBill.createCreditorReference("...")); // If using QRR + bill.setUnstructuredMessage("Order " + order.getId()); + + return bill; + } + + private net.codecrete.qrbill.generator.Address createAddress(String name, String street, String zip, String city, String country) { + net.codecrete.qrbill.generator.Address address = new net.codecrete.qrbill.generator.Address(); + address.setName(name); + address.setStreet(street); + address.setPostalCode(zip); + address.setTown(city); + address.setCountryCode(country); + return address; + } +} diff --git a/backend/src/main/java/com/printcalculator/service/StorageService.java b/backend/src/main/java/com/printcalculator/service/StorageService.java new file mode 100644 index 0000000..5fe2321 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/service/StorageService.java @@ -0,0 +1,14 @@ +package com.printcalculator.service; + +import org.springframework.core.io.Resource; +import org.springframework.web.multipart.MultipartFile; +import java.nio.file.Path; +import java.io.IOException; + +public interface StorageService { + void init(); + void store(MultipartFile file, Path destination) throws IOException; + void store(Path source, Path destination) throws IOException; + void delete(Path path) throws IOException; + Resource loadAsResource(Path path) throws IOException; +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index fc32567..3d70ca6 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -18,3 +18,8 @@ profiles.root=${PROFILES_DIR:profiles} # File Upload Limits spring.servlet.multipart.max-file-size=200MB spring.servlet.multipart.max-request-size=200MB + +# ClamAV Configuration +clamav.host=${CLAMAV_HOST:clamav} +clamav.port=${CLAMAV_PORT:3310} +clamav.enabled=${CLAMAV_ENABLED:false} diff --git a/backend/src/main/resources/templates/invoice.html b/backend/src/main/resources/templates/invoice.html new file mode 100644 index 0000000..657d5d7 --- /dev/null +++ b/backend/src/main/resources/templates/invoice.html @@ -0,0 +1,249 @@ + + + + + + + +
+ + + + + + +
+
Nome Cognome
+
Via Esempio 12
+
6500 Bellinzona, CH
+
email@example.com
+
+
Fattura
+
Numero: 2026-000123
+
Data: 2026-02-13
+
Scadenza: 2026-02-20
+
+ +
Fatturare a
+
+
+
Cliente SA
+
Via Cliente 7
+
8000 Zürich, CH
+
+
+ + + + + + + + + + + + + + + + + + +
DescrizioneQtàPrezzoTotale
Stampa 3D pezzo X1CHF 10.00CHF 10.00
+ + + + + + + + + + +
SubtotaleCHF 10.00
TotaleCHF 10.00
+ +
+ Pagamento entro 7 giorni. Grazie. +
+ +
+ +
+
+
+
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e3aaedd..1a67380 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -57,13 +57,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1902.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.19.tgz", - "integrity": "sha512-iexYDIYpGAeAU7T60bGcfrGwtq1bxpZixYxWuHYiaD1b5baQgNSfd1isGEOh37GgDNsf4In9i2LOLPm0wBdtgQ==", + "version": "0.1902.20", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1902.20.tgz", + "integrity": "sha512-tEM8PX9RTIvgEPJH/9nDaGlhbjZf9BBFS2FXKuOwKB+NFvfZuuDpPH7CzJKyyvkQLPtoNh2Y9C92m2f+RXsBmQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", + "@angular-devkit/core": "19.2.20", "rxjs": "7.8.1" }, "engines": { @@ -83,17 +83,17 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.2.19.tgz", - "integrity": "sha512-uIxi6Vzss6+ycljVhkyPUPWa20w8qxJL9lEn0h6+sX/fhM8Djt0FHIuTQjoX58EoMaQ/1jrXaRaGimkbaFcG9A==", + "version": "19.2.20", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.2.20.tgz", + "integrity": "sha512-m7J+k0lJEFvr6STGUQROx6TyoGn0WQsQiooO8WTkM8QUWKxSUmq4WImlPSq6y+thc+Jzx1EBw3yn73+phNIZag==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1902.19", - "@angular-devkit/build-webpack": "0.1902.19", - "@angular-devkit/core": "19.2.19", - "@angular/build": "19.2.19", + "@angular-devkit/architect": "0.1902.20", + "@angular-devkit/build-webpack": "0.1902.20", + "@angular-devkit/core": "19.2.20", + "@angular/build": "19.2.20", "@babel/core": "7.26.10", "@babel/generator": "7.26.10", "@babel/helper-annotate-as-pure": "7.25.9", @@ -104,7 +104,7 @@ "@babel/preset-env": "7.26.9", "@babel/runtime": "7.26.10", "@discoveryjs/json-ext": "0.6.3", - "@ngtools/webpack": "19.2.19", + "@ngtools/webpack": "19.2.20", "@vitejs/plugin-basic-ssl": "1.2.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -139,7 +139,7 @@ "terser": "5.39.0", "tree-kill": "1.2.2", "tslib": "2.8.1", - "webpack": "5.98.0", + "webpack": "5.105.0", "webpack-dev-middleware": "7.4.2", "webpack-dev-server": "5.2.2", "webpack-merge": "6.0.1", @@ -158,7 +158,7 @@ "@angular/localize": "^19.0.0 || ^19.2.0-next.0", "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", - "@angular/ssr": "^19.2.19", + "@angular/ssr": "^19.2.20", "@web/test-runner": "^0.20.0", "browser-sync": "^3.0.2", "jest": "^29.5.0", @@ -219,13 +219,13 @@ } }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1902.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1902.19.tgz", - "integrity": "sha512-x2tlGg5CsUveFzuRuqeHknSbGirSAoRynEh+KqPRGK0G3WpMViW/M8SuVurecasegfIrDWtYZ4FnVxKqNbKwXQ==", + "version": "0.1902.20", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1902.20.tgz", + "integrity": "sha512-T8RLKZOR0+l3FBMBTUQk83I/Dr5RpNPCOE6tWqGjAMRPKoL1m5BbqhkQ7ygnyd8/ZRz/x1RUVM08l0AeuzWUmA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1902.19", + "@angular-devkit/architect": "0.1902.20", "rxjs": "7.8.1" }, "engines": { @@ -249,9 +249,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz", - "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==", + "version": "19.2.20", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.20.tgz", + "integrity": "sha512-4AAmHlv+H1/2Nmsp6QsX8YQxjC/v5QAzc+76He7K/x3iIuLCntQE2BYxonSZMiQ3M8gc/yxTfyZoPYjSDDvWMA==", "dev": true, "license": "MIT", "dependencies": { @@ -287,13 +287,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.19.tgz", - "integrity": "sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==", + "version": "19.2.20", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.20.tgz", + "integrity": "sha512-o2eexF1fLZU93V3utiQLNgyNaGvFhDqpITNQcI1qzv2ZkvFHg9WZjFtZKtm805JAE/DND8oAJ1p+BoxU++Qg8g==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", + "@angular-devkit/core": "19.2.20", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", @@ -316,14 +316,14 @@ } }, "node_modules/@angular/build": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.19.tgz", - "integrity": "sha512-SFzQ1bRkNFiOVu+aaz+9INmts7tDUrsHLEr9HmARXr9qk5UmR8prlw39p2u+Bvi6/lCiJ18TZMQQl9mGyr63lg==", + "version": "19.2.20", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.2.20.tgz", + "integrity": "sha512-8bQ1afN8AJ6N9lJJgxYF08M0gp4R/4SIedSJfSLohscgHumYJ1mITEygoB1JK5O9CEKlr4YyLYfgay8xr92wbQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1902.19", + "@angular-devkit/architect": "0.1902.20", "@babel/core": "7.26.10", "@babel/helper-annotate-as-pure": "7.25.9", "@babel/helper-split-export-declaration": "7.24.7", @@ -363,7 +363,7 @@ "@angular/localize": "^19.0.0 || ^19.2.0-next.0", "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0", - "@angular/ssr": "^19.2.19", + "@angular/ssr": "^19.2.20", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^19.0.0 || ^19.2.0-next.0", @@ -417,18 +417,18 @@ } }, "node_modules/@angular/cli": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.19.tgz", - "integrity": "sha512-e9tAzFNOL4mMWfMnpC9Up83OCTOp2siIj8W41FCp8jfoEnw79AXDDLh3d70kOayiObchksTJVShslTogLUyhMw==", + "version": "19.2.20", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.20.tgz", + "integrity": "sha512-3vw49xDGqOi63FES/6D+Lw0Sl42FSZKowUxBMY0CnXD8L93Qwvcf4ASFmUoNJRSTOJuuife1+55vY62cpOWBdg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1902.19", - "@angular-devkit/core": "19.2.19", - "@angular-devkit/schematics": "19.2.19", + "@angular-devkit/architect": "0.1902.20", + "@angular-devkit/core": "19.2.20", + "@angular-devkit/schematics": "19.2.20", "@inquirer/prompts": "7.3.2", "@listr2/prompt-adapter-inquirer": "2.0.18", - "@schematics/angular": "19.2.19", + "@schematics/angular": "19.2.20", "@yarnpkg/lockfile": "1.1.0", "ini": "5.0.0", "jsonc-parser": "3.3.1", @@ -3341,9 +3341,9 @@ } }, "node_modules/@jsonjoy.com/buffers": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.65.0.tgz", - "integrity": "sha512-eBrIXd0/Ld3p9lpDDlMaMn6IEfWqtHMD+z61u0JrIiPzsV1r7m6xDZFRxJyvIFTEO+SWdYF9EiQbXZGd8BzPfA==", + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3549,9 +3549,9 @@ } }, "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.65.0.tgz", - "integrity": "sha512-Xrh7Fm/M0QAYpekSgmskdZYnFdSGnsxJ/tHaolA4bNwWdG9i65S8m83Meh7FOxyJyQAdo4d4J97NOomBLEfkDQ==", + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3566,9 +3566,9 @@ } }, "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.65.0.tgz", - "integrity": "sha512-7MXcRYe7n3BG+fo3jicvjB0+6ypl2Y/bQp79Sp7KeSiiCgLqw4Oled6chVv07/xLVTdo3qa1CD0VCCnPaw+RGA==", + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3583,17 +3583,17 @@ } }, "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.65.0.tgz", - "integrity": "sha512-e0SG/6qUCnVhHa0rjDJHgnXnbsacooHVqQHxspjvlYQSkHm+66wkHw6Gql+3u/WxI/b1VsOdUi0M+fOtkgKGdQ==", + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/base64": "17.65.0", - "@jsonjoy.com/buffers": "17.65.0", - "@jsonjoy.com/codegen": "17.65.0", - "@jsonjoy.com/json-pointer": "17.65.0", - "@jsonjoy.com/util": "17.65.0", + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", "hyperdyperid": "^1.2.0", "thingies": "^2.5.0", "tree-dump": "^1.1.0" @@ -3610,13 +3610,13 @@ } }, "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.65.0.tgz", - "integrity": "sha512-uhTe+XhlIZpWOxgPcnO+iSCDgKKBpwkDVTyYiXX9VayGV8HSFVJM67M6pUE71zdnXF1W0Da21AvnhlmdwYPpow==", + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/util": "17.65.0" + "@jsonjoy.com/util": "17.67.0" }, "engines": { "node": ">=10.0" @@ -3630,14 +3630,14 @@ } }, "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.65.0.tgz", - "integrity": "sha512-cWiEHZccQORf96q2y6zU3wDeIVPeidmGqd9cNKJRYoVHTV0S1eHPy5JTbHpMnGfDvtvujQwQozOqgO9ABu6h0w==", + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@jsonjoy.com/buffers": "17.65.0", - "@jsonjoy.com/codegen": "17.65.0" + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" }, "engines": { "node": ">=10.0" @@ -4291,9 +4291,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.19.tgz", - "integrity": "sha512-R9aeTrOBiRVl8I698JWPniUAAEpSvzc8SUGWSM5UXWMcHnWqd92cOnJJ1aXDGJZKXrbhMhCBx9Dglmcks5IDpg==", + "version": "19.2.20", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.2.20.tgz", + "integrity": "sha512-nuCjcxLmFrn0s53G67V5R19mUpYjewZBLz6Wrg7BtJkjq08xfO0QgaJg3e6wzEmj1AclH7eMKRnuQhm5otyutg==", "dev": true, "license": "MIT", "engines": { @@ -4489,9 +4489,9 @@ } }, "node_modules/@npmcli/package-json": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.1.1.tgz", - "integrity": "sha512-d5qimadRAUCO4A/Txw71VM7UrRZzV+NPclxz/dc+M6B2oYwjWTjqh8HA/sGQgs9VZuJ6I/P7XIAlJvgrl27ZOw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.2.0.tgz", + "integrity": "sha512-rCNLSB/JzNvot0SEyXqWZ7tX2B5dD2a1br2Dp0vSYVo5jh8Z0EZ7lS9TsZ1UtziddB1UfNUaMCc538/HztnJGA==", "dev": true, "license": "ISC", "dependencies": { @@ -5131,9 +5131,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", - "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", "cpu": [ "loong64" ], @@ -5145,9 +5145,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", - "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", "cpu": [ "loong64" ], @@ -5187,9 +5187,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", - "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", "cpu": [ "ppc64" ], @@ -5201,9 +5201,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", - "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", "cpu": [ "ppc64" ], @@ -5229,9 +5229,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", - "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", "cpu": [ "riscv64" ], @@ -5285,9 +5285,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", - "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", "cpu": [ "x64" ], @@ -5299,9 +5299,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", - "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", "cpu": [ "arm64" ], @@ -5341,9 +5341,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", - "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", "cpu": [ "x64" ], @@ -5369,14 +5369,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.19.tgz", - "integrity": "sha512-6/0pvbPCY4UHeB4lnM/5r250QX5gcLgOYbR5FdhFu+22mOPHfWpRc5tNuY9kCephDHzAHjo6fTW1vefOOmA4jw==", + "version": "19.2.20", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.2.20.tgz", + "integrity": "sha512-xDrYxZvk9dGA2eVqufqLYmVSMSXxVtv30pBHGGU/2xr4QzHzdmMHflk4It8eh4WMNLhn7kqnzMREwtNI3eW/Gw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", - "@angular-devkit/schematics": "19.2.19", + "@angular-devkit/core": "19.2.20", + "@angular-devkit/schematics": "19.2.20", "jsonc-parser": "3.3.1" }, "engines": { @@ -6053,9 +6053,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -6065,6 +6065,19 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, "node_modules/adjust-sourcemap-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", @@ -6407,6 +6420,19 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -6554,9 +6580,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -6574,10 +6600,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -6764,9 +6791,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", "dev": true, "funding": [ { @@ -7469,9 +7496,9 @@ } }, "node_modules/default-browser": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz", - "integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", "dev": true, "license": "MIT", "dependencies": { @@ -7685,9 +7712,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.155", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", - "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", "dev": true, "license": "ISC" }, @@ -7793,14 +7820,14 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -7910,9 +7937,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, @@ -9414,9 +9441,9 @@ "license": "MIT" }, "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", "dev": true, "license": "MIT", "dependencies": { @@ -10046,9 +10073,9 @@ } }, "node_modules/launch-editor": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", - "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.0.tgz", + "integrity": "sha512-u+9asUHMJ99lA15VRMXw5XKfySFR9dGXwgsgS14YTbUq3GITP58mIM32At90P5fZ+MUId5Yw+IwI/yKub7jnCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10270,13 +10297,17 @@ } }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loader-utils": { @@ -11070,9 +11101,9 @@ } }, "node_modules/node-gyp": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.2.0.tgz", - "integrity": "sha512-T0S1zqskVUSxcsSTkAsLc7xCycrRYmtDHadDinzocrThjyQCn5kMlEBSj6H4qDbgsIOSLmmlRIeb0lZXj+UArA==", + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11137,9 +11168,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "dev": true, "license": "MIT" }, @@ -12090,9 +12121,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -12647,9 +12678,9 @@ "optional": true }, "node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", "dependencies": { @@ -13602,13 +13633,17 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar": { @@ -13658,9 +13693,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", + "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -13806,15 +13841,15 @@ "license": "0BSD" }, "node_modules/tuf-js": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.0.1.tgz", - "integrity": "sha512-+68OP1ZzSF84rTckf3FA95vJ1Zlx/uaXyiiKyPd1pA4rZNkpEvDAKmsu1xUSmbF/chCRYgZ6UZkDwC7PmzmAyA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.1.0.tgz", + "integrity": "sha512-3T3T04WzowbwV2FDiGXBbr81t64g1MUGGJRgT4x5o97N+8ArdhVCAF9IxFrxuSJmM3E5Asn7nKHkao0ibcZXAg==", "dev": true, "license": "MIT", "dependencies": { "@tufjs/models": "3.0.1", - "debug": "^4.3.6", - "make-fetch-happen": "^14.0.1" + "debug": "^4.4.1", + "make-fetch-happen": "^14.0.3" }, "engines": { "node": "^18.17.0 || >=20.5.0" @@ -14006,9 +14041,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -14170,9 +14205,9 @@ } }, "node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", - "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", "cpu": [ "arm" ], @@ -14184,9 +14219,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", - "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", "cpu": [ "arm64" ], @@ -14198,9 +14233,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", - "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", "cpu": [ "arm64" ], @@ -14212,9 +14247,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", - "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", "cpu": [ "x64" ], @@ -14226,9 +14261,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", - "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", "cpu": [ "arm64" ], @@ -14240,9 +14275,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", - "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", "cpu": [ "x64" ], @@ -14254,9 +14289,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", - "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", "cpu": [ "arm" ], @@ -14268,9 +14303,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", - "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", "cpu": [ "arm" ], @@ -14282,9 +14317,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", - "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", "cpu": [ "arm64" ], @@ -14296,9 +14331,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", - "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", "cpu": [ "arm64" ], @@ -14310,9 +14345,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", - "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", "cpu": [ "riscv64" ], @@ -14324,9 +14359,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", - "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", "cpu": [ "s390x" ], @@ -14338,9 +14373,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", - "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", "cpu": [ "x64" ], @@ -14352,9 +14387,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", - "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", "cpu": [ "x64" ], @@ -14366,9 +14401,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", - "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", "cpu": [ "arm64" ], @@ -14380,9 +14415,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", - "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", "cpu": [ "ia32" ], @@ -14394,9 +14429,9 @@ ] }, "node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", - "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", "cpu": [ "x64" ], @@ -14444,9 +14479,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "4.57.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", - "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", "dependencies": { @@ -14460,31 +14495,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.0", - "@rollup/rollup-android-arm64": "4.57.0", - "@rollup/rollup-darwin-arm64": "4.57.0", - "@rollup/rollup-darwin-x64": "4.57.0", - "@rollup/rollup-freebsd-arm64": "4.57.0", - "@rollup/rollup-freebsd-x64": "4.57.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", - "@rollup/rollup-linux-arm-musleabihf": "4.57.0", - "@rollup/rollup-linux-arm64-gnu": "4.57.0", - "@rollup/rollup-linux-arm64-musl": "4.57.0", - "@rollup/rollup-linux-loong64-gnu": "4.57.0", - "@rollup/rollup-linux-loong64-musl": "4.57.0", - "@rollup/rollup-linux-ppc64-gnu": "4.57.0", - "@rollup/rollup-linux-ppc64-musl": "4.57.0", - "@rollup/rollup-linux-riscv64-gnu": "4.57.0", - "@rollup/rollup-linux-riscv64-musl": "4.57.0", - "@rollup/rollup-linux-s390x-gnu": "4.57.0", - "@rollup/rollup-linux-x64-gnu": "4.57.0", - "@rollup/rollup-linux-x64-musl": "4.57.0", - "@rollup/rollup-openbsd-x64": "4.57.0", - "@rollup/rollup-openharmony-arm64": "4.57.0", - "@rollup/rollup-win32-arm64-msvc": "4.57.0", - "@rollup/rollup-win32-ia32-msvc": "4.57.0", - "@rollup/rollup-win32-x64-gnu": "4.57.0", - "@rollup/rollup-win32-x64-msvc": "4.57.0", + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" } }, @@ -14541,35 +14576,37 @@ "optional": true }, "node_modules/webpack": { - "version": "5.98.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", - "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" @@ -14802,9 +14839,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "dev": true, "license": "MIT", "engines": { @@ -14833,6 +14870,13 @@ } } }, + "node_modules/webpack/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, "node_modules/webpack/node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -14840,6 +14884,20 @@ "dev": true, "license": "MIT" }, + "node_modules/webpack/node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", diff --git a/frontend/src/app/features/calculator/services/quote-estimator.service.ts b/frontend/src/app/features/calculator/services/quote-estimator.service.ts index 9d8668d..55fa6ae 100644 --- a/frontend/src/app/features/calculator/services/quote-estimator.service.ts +++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts @@ -144,6 +144,23 @@ export class QuoteEstimatorService { if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth); return this.http.post(`${environment.apiUrl}/api/orders/from-quote/${sessionId}`, orderDetails, { headers }); } + + getOrder(orderId: string): Observable { + const headers: any = {}; + // @ts-ignore + if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth); + return this.http.get(`${environment.apiUrl}/api/orders/${orderId}`, { headers }); + } + + getOrderInvoice(orderId: string): Observable { + const headers: any = {}; + // @ts-ignore + if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth); + return this.http.get(`${environment.apiUrl}/api/orders/${orderId}/invoice`, { + headers, + responseType: 'blob' + }); + } calculate(request: QuoteRequest): Observable { console.log('QuoteEstimatorService: Calculating quote...', request); diff --git a/frontend/src/app/features/checkout/checkout.component.html b/frontend/src/app/features/checkout/checkout.component.html index bc21c61..d3972a3 100644 --- a/frontend/src/app/features/checkout/checkout.component.html +++ b/frontend/src/app/features/checkout/checkout.component.html @@ -1,120 +1,122 @@
-

{{ 'CHECKOUT.TITLE' | translate }}

+
+

{{ 'CHECKOUT.TITLE' | translate }}

+
-
- - -
- -
- {{ error }} -
+
+
+ + +
+ +
+ {{ error }} +
-
- - -
-
-

{{ 'CHECKOUT.CONTACT_INFO' | translate }}

-
-
+ + + + +
+

{{ 'CHECKOUT.CONTACT_INFO' | translate }}

+
-
-
+ - -
-
-

{{ 'CHECKOUT.BILLING_ADDR' | translate }}

-
-
-
- - + + +
+

{{ 'CHECKOUT.BILLING_ADDR' | translate }}

- - - - -
- - - -
- - -
-
- {{ 'CONTACT.TYPE_PRIVATE' | translate }} +
+ + +
+ +
-
- {{ 'CONTACT.TYPE_COMPANY' | translate }} + + +
+ + +
+ + +
+
+ {{ 'CONTACT.TYPE_PRIVATE' | translate }} +
+
+ {{ 'CONTACT.TYPE_COMPANY' | translate }} +
+
+ + + + +
+ + +
+ - -
- - -
+ +
+
-
- -
- -
+ + +
+

{{ 'CHECKOUT.SHIPPING_ADDR' | translate }}

+
+
+
+ + +
+ +
+ + +
- -
-
-

{{ 'CHECKOUT.SHIPPING_ADDR' | translate }}

+ + +
+ + + +
+
+ + +
+ + {{ (isSubmitting() ? 'CHECKOUT.PROCESSING' : 'CHECKOUT.PLACE_ORDER') | translate }} +
-
-
- - -
- -
- - -
- - -
- - - -
+ +
+ + +
+ +
+

{{ 'CHECKOUT.SUMMARY_TITLE' | translate }}

-
- -
- - {{ isSubmitting() ? ('CHECKOUT.PROCESSING' | translate) : ('CHECKOUT.PLACE_ORDER' | translate) }} - -
- - -
- - -
-
-
-

{{ 'CHECKOUT.SUMMARY_TITLE' | translate }}

-
- -
+
@@ -142,15 +144,18 @@ {{ 'CHECKOUT.SETUP_FEE' | translate }} {{ session.session.setupCostChf | currency:'CHF' }}
-
-
+
+ {{ 'CHECKOUT.SHIPPING' | translate }} + {{ 9.00 | currency:'CHF' }} +
+
{{ 'CHECKOUT.TOTAL' | translate }} - {{ session.grandTotalChf | currency:'CHF' }} + {{ (session.grandTotalChf + 9.00) | currency:'CHF' }}
-
+
-
+
diff --git a/frontend/src/app/features/checkout/checkout.component.scss b/frontend/src/app/features/checkout/checkout.component.scss index f2a6741..edd1bfd 100644 --- a/frontend/src/app/features/checkout/checkout.component.scss +++ b/frontend/src/app/features/checkout/checkout.component.scss @@ -1,84 +1,76 @@ -.checkout-page { - padding: 3rem 1rem; - max-width: 1200px; - margin: 0 auto; +.hero { + padding: var(--space-8) 0; + text-align: center; + + .section-title { + font-size: 2.5rem; + margin-bottom: var(--space-2); + } } .checkout-layout { display: grid; - grid-template-columns: 1fr 380px; + grid-template-columns: 1fr 420px; gap: var(--space-8); align-items: start; + margin-bottom: var(--space-12); - @media (max-width: 900px) { + @media (max-width: 1024px) { grid-template-columns: 1fr; - gap: var(--space-6); + gap: var(--space-8); } } -.section-title { - font-size: 2rem; - font-weight: 700; +.card-header-simple { margin-bottom: var(--space-6); - color: var(--color-heading); -} - -.form-card { - margin-bottom: var(--space-6); - background: var(--color-bg-surface); - border: 1px solid var(--color-border); - border-radius: var(--radius-lg); - overflow: hidden; - - .card-header { - padding: var(--space-4) var(--space-6); - border-bottom: 1px solid var(--color-border); - background: var(--color-bg-subtle); - - h3 { - font-size: 1.1rem; - font-weight: 600; - color: var(--color-heading); - margin: 0; - } - } - - .card-content { - padding: var(--space-6); + padding-bottom: var(--space-4); + border-bottom: 1px solid var(--color-border); + + h3 { + font-size: 1.25rem; + font-weight: 600; + color: var(--color-text); + margin: 0; } } .form-row { display: flex; + flex-direction: column; gap: var(--space-4); margin-bottom: var(--space-4); + @media(min-width: 768px) { + flex-direction: row; + & > * { flex: 1; } + } + + &.no-margin { + margin-bottom: 0; + } + &.three-cols { display: grid; - grid-template-columns: 1fr 2fr 1fr; + grid-template-columns: 1.5fr 2fr 1fr; gap: var(--space-4); - } - - app-input { - flex: 1; - width: 100%; - } - - @media (max-width: 600px) { - flex-direction: column; - &.three-cols { + + @media (max-width: 768px) { grid-template-columns: 1fr; } } + + app-input { + width: 100%; + } } -/* User Type Selector Styles - Matched with Contact Form */ +/* User Type Selector - Matching Contact Form Style */ .user-type-selector { display: flex; background-color: var(--color-neutral-100); border-radius: var(--radius-md); padding: 4px; - margin-bottom: var(--space-4); + margin: var(--space-6) 0; gap: 4px; width: 100%; max-width: 400px; @@ -112,12 +104,14 @@ gap: var(--space-4); padding-left: var(--space-4); border-left: 2px solid var(--color-border); - margin-top: var(--space-4); margin-bottom: var(--space-4); } .shipping-option { margin: var(--space-6) 0; + padding: var(--space-4); + background: var(--color-neutral-100); + border-radius: var(--radius-md); } /* Custom Checkbox */ @@ -125,9 +119,10 @@ display: flex; align-items: center; position: relative; - padding-left: 30px; + padding-left: 36px; cursor: pointer; font-size: 1rem; + font-weight: 500; user-select: none; color: var(--color-text); @@ -153,10 +148,10 @@ top: 50%; left: 0; transform: translateY(-50%); - height: 20px; - width: 20px; - background-color: var(--color-bg-surface); - border: 1px solid var(--color-border); + height: 24px; + width: 24px; + background-color: var(--color-bg-card); + border: 2px solid var(--color-border); border-radius: var(--radius-sm); transition: all 0.2s; @@ -164,12 +159,12 @@ content: ""; position: absolute; display: none; - left: 6px; - top: 2px; + left: 7px; + top: 3px; width: 6px; height: 12px; - border: solid white; - border-width: 0 2px 2px 0; + border: solid #000; + border-width: 0 2.5px 2.5px 0; transform: rotate(45deg); } } @@ -179,40 +174,48 @@ } } - .checkout-summary-section { position: relative; } .sticky-card { position: sticky; - top: 0; - /* Inherits styles from .form-card */ + top: var(--space-6); } .summary-items { margin-bottom: var(--space-6); - max-height: 400px; + max-height: 450px; overflow-y: auto; + padding-right: var(--space-2); + padding-top: var(--space-2); + + &::-webkit-scrollbar { + width: 4px; + } + &::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 4px; + } } .summary-item { display: flex; justify-content: space-between; align-items: flex-start; - padding: var(--space-3) 0; + padding: var(--space-4) 0; border-bottom: 1px solid var(--color-border); - &:last-child { - border-bottom: none; - } + &:first-child { padding-top: 0; } + &:last-child { border-bottom: none; } .item-details { flex: 1; .item-name { display: block; - font-weight: 500; + font-weight: 600; + font-size: 0.95rem; margin-bottom: var(--space-1); word-break: break-all; color: var(--color-text); @@ -226,8 +229,8 @@ color: var(--color-text-muted); .color-dot { - width: 12px; - height: 12px; + width: 14px; + height: 14px; border-radius: 50%; display: inline-block; border: 1px solid var(--color-border); @@ -245,55 +248,52 @@ font-weight: 600; margin-left: var(--space-3); white-space: nowrap; - color: var(--color-heading); + color: var(--color-text); } } .summary-totals { - padding-top: var(--space-4); - border-top: 1px solid var(--color-border); - margin-top: var(--space-4); + background: var(--color-neutral-100); + padding: var(--space-4); + border-radius: var(--radius-md); + margin-top: var(--space-6); .total-row { display: flex; justify-content: space-between; margin-bottom: var(--space-2); - color: var(--color-text); font-size: 0.95rem; + color: var(--color-text); + } - &.grand-total { - color: var(--color-heading); - font-weight: 700; - font-size: 1.25rem; - margin-top: var(--space-4); - padding-top: var(--space-4); - border-top: 1px solid var(--color-border); - margin-bottom: 0; - } + .grand-total { + display: flex; + justify-content: space-between; + color: var(--color-text); + font-weight: 700; + font-size: 1.5rem; + margin-top: var(--space-4); + padding-top: var(--space-4); + border-top: 2px solid var(--color-border); } } .actions { - margin-top: var(--space-6); - display: flex; - justify-content: flex-end; + margin-top: var(--space-8); app-button { width: 100%; - - @media (min-width: 900px) { - width: auto; - min-width: 200px; - } } } .error-message { - color: var(--color-danger); - background: var(--color-danger-subtle); + color: var(--color-error); + background: #fef2f2; padding: var(--space-4); border-radius: var(--radius-md); margin-bottom: var(--space-6); - border: 1px solid var(--color-danger); + border: 1px solid #fee2e2; + font-weight: 500; } +.mb-6 { margin-bottom: var(--space-6); } diff --git a/frontend/src/app/features/checkout/checkout.component.ts b/frontend/src/app/features/checkout/checkout.component.ts index 84a5ccf..78a2aa2 100644 --- a/frontend/src/app/features/checkout/checkout.component.ts +++ b/frontend/src/app/features/checkout/checkout.component.ts @@ -6,6 +6,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { QuoteEstimatorService } from '../calculator/services/quote-estimator.service'; import { AppInputComponent } from '../../shared/components/app-input/app-input.component'; import { AppButtonComponent } from '../../shared/components/app-button/app-button.component'; +import { AppCardComponent } from '../../shared/components/app-card/app-card.component'; @Component({ selector: 'app-checkout', @@ -15,7 +16,8 @@ import { AppButtonComponent } from '../../shared/components/app-button/app-butto ReactiveFormsModule, TranslateModule, AppInputComponent, - AppButtonComponent + AppButtonComponent, + AppCardComponent ], templateUrl: './checkout.component.html', styleUrls: ['./checkout.component.scss'] @@ -75,20 +77,27 @@ export class CheckoutComponent implements OnInit { const type = isCompany ? 'BUSINESS' : 'PRIVATE'; this.checkoutForm.patchValue({ customerType: type }); - // Update validators based on type const billingGroup = this.checkoutForm.get('billingAddress') as FormGroup; const companyControl = billingGroup.get('companyName'); const referenceControl = billingGroup.get('referencePerson'); + const firstNameControl = billingGroup.get('firstName'); + const lastNameControl = billingGroup.get('lastName'); if (isCompany) { companyControl?.setValidators([Validators.required]); referenceControl?.setValidators([Validators.required]); + firstNameControl?.clearValidators(); + lastNameControl?.clearValidators(); } else { companyControl?.clearValidators(); referenceControl?.clearValidators(); + firstNameControl?.setValidators([Validators.required]); + lastNameControl?.setValidators([Validators.required]); } companyControl?.updateValueAndValidity(); referenceControl?.updateValueAndValidity(); + firstNameControl?.updateValueAndValidity(); + lastNameControl?.updateValueAndValidity(); } ngOnInit(): void { diff --git a/frontend/src/app/features/contact/components/contact-form/contact-form.component.html b/frontend/src/app/features/contact/components/contact-form/contact-form.component.html index b0f7bed..c7e33a1 100644 --- a/frontend/src/app/features/contact/components/contact-form/contact-form.component.html +++ b/frontend/src/app/features/contact/components/contact-form/contact-form.component.html @@ -37,7 +37,7 @@
- +
@@ -47,10 +47,10 @@

{{ 'CONTACT.UPLOAD_HINT' | translate }}

- -
-

{{ 'CONTACT.DROP_FILES' | translate }}

diff --git a/frontend/src/app/features/payment/payment.component.html b/frontend/src/app/features/payment/payment.component.html index 733e839..a86265b 100644 --- a/frontend/src/app/features/payment/payment.component.html +++ b/frontend/src/app/features/payment/payment.component.html @@ -1,21 +1,109 @@ -
- - - payment - Payment Integration - Order #{{ orderId }} - - -
-

Coming Soon

-

The online payment system is currently under development.

-

Your order has been saved. Please contact us to arrange payment.

-
-
- - - -
+
+

{{ 'PAYMENT.TITLE' | translate }}

+

{{ 'CHECKOUT.SUBTITLE' | translate }}

+
+ +
+
+
+ +
+

{{ 'PAYMENT.METHOD' | translate }}

+
+ +
+
+
+ TWINT +
+
+ QR Bill / Bank Transfer +
+
+
+ +
+
+

{{ 'PAYMENT.TWINT_TITLE' | translate }}

+
+
+
+ QR CODE +
+

{{ 'PAYMENT.TWINT_DESC' | translate }}

+

{{ 'PAYMENT.TOTAL' | translate }}: {{ o.totalChf | currency:'CHF' }}

+
+
+ +
+
+

{{ 'PAYMENT.BANK_TITLE' | translate }}

+
+
+

{{ 'PAYMENT.BANK_OWNER' | translate }}: 3D Fab Switzerland

+

{{ 'PAYMENT.BANK_IBAN' | translate }}: CH98 0000 0000 0000 0000 0

+

{{ 'PAYMENT.BANK_REF' | translate }}: {{ o.id }}

+ +
+ + {{ 'PAYMENT.DOWNLOAD_QR' | translate }} + +
+
+
+ +
+ + {{ 'PAYMENT.CONFIRM' | translate }} + +
+
+
+ +
+ +
+

{{ 'PAYMENT.SUMMARY_TITLE' | translate }}

+

#{{ o.id.substring(0, 8) }}

+
+ +
+
+ {{ 'PAYMENT.SUBTOTAL' | translate }} + {{ o.subtotalChf | currency:'CHF' }} +
+
+ {{ 'PAYMENT.SHIPPING' | translate }} + {{ o.shippingCostChf | currency:'CHF' }} +
+
+ {{ 'PAYMENT.SETUP_FEE' | translate }} + {{ o.setupCostChf | currency:'CHF' }} +
+
+ {{ 'PAYMENT.TOTAL' | translate }} + {{ o.totalChf | currency:'CHF' }} +
+
+
+
+
+ +
+ +

{{ 'PAYMENT.LOADING' | translate }}

+
+
+ +
+ +

{{ error() }}

+
+
diff --git a/frontend/src/app/features/payment/payment.component.scss b/frontend/src/app/features/payment/payment.component.scss index d4475db..4d3ba7f 100644 --- a/frontend/src/app/features/payment/payment.component.scss +++ b/frontend/src/app/features/payment/payment.component.scss @@ -1,35 +1,202 @@ -.payment-container { - display: flex; - justify-content: center; - align-items: center; - min-height: 80vh; - padding: 2rem; - background-color: #f5f5f5; -} - -.payment-card { - max-width: 500px; - width: 100%; -} - -.coming-soon { +.hero { + padding: var(--space-12) 0 var(--space-8); text-align: center; - padding: 2rem 0; - - h3 { - margin-bottom: 1rem; - color: #555; - } - - p { - color: #777; - margin-bottom: 0.5rem; + + h1 { + font-size: 2.5rem; + margin-bottom: var(--space-2); } } -mat-icon { - font-size: 40px; - width: 40px; - height: 40px; - color: #3f51b5; +.subtitle { + font-size: 1.125rem; + color: var(--color-text-muted); + max-width: 600px; + margin: 0 auto; +} + +.payment-layout { + display: grid; + grid-template-columns: 1fr 400px; + gap: var(--space-8); + align-items: start; + margin-bottom: var(--space-12); + + @media (max-width: 1024px) { + grid-template-columns: 1fr; + gap: var(--space-8); + } +} + +.card-header-simple { + margin-bottom: var(--space-6); + padding-bottom: var(--space-4); + border-bottom: 1px solid var(--color-border); + + h3 { + font-size: 1.25rem; + font-weight: 600; + color: var(--color-text); + margin: 0; + } + + .order-id { + font-size: 0.875rem; + color: var(--color-text-muted); + margin-top: 2px; + } +} + +.payment-selection { + margin-bottom: var(--space-6); +} + +.methods-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-4); + + @media (max-width: 600px) { + grid-template-columns: 1fr; + } +} + +.type-option { + border: 2px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-6); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + background: var(--color-bg-card); + text-align: center; + font-weight: 600; + color: var(--color-text-muted); + + &:hover { + border-color: var(--color-brand); + color: var(--color-text); + } + + &.selected { + border-color: var(--color-brand); + background-color: var(--color-neutral-100); + color: #000; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + } +} + +.payment-details { + background: var(--color-neutral-100); + border-radius: var(--radius-md); + padding: var(--space-6); + margin-bottom: var(--space-6); + border: 1px solid var(--color-border); + + .details-header { + margin-bottom: var(--space-4); + h4 { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + } + } +} + +.qr-placeholder { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + + .qr-box { + width: 180px; + height: 180px; + background-color: white; + border: 2px solid var(--color-neutral-900); + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + margin-bottom: var(--space-4); + border-radius: var(--radius-md); + } + + .amount { + font-size: 1.25rem; + font-weight: 700; + margin-top: var(--space-2); + color: var(--color-text); + } +} + +.bank-details { + p { + margin-bottom: var(--space-2); + font-size: 1rem; + color: var(--color-text); + } +} + +.qr-bill-actions { + margin-top: var(--space-4); +} + +.sticky-card { + position: sticky; + top: var(--space-6); +} + +.summary-totals { + background: var(--color-neutral-100); + padding: var(--space-6); + border-radius: var(--radius-md); + + .total-row { + display: flex; + justify-content: space-between; + margin-bottom: var(--space-2); + font-size: 0.95rem; + color: var(--color-text-muted); + } + + .grand-total-row { + display: flex; + justify-content: space-between; + color: var(--color-text); + font-weight: 700; + font-size: 1.5rem; + margin-top: var(--space-4); + padding-top: var(--space-4); + border-top: 2px solid var(--color-border); + } +} + +.actions { + margin-top: var(--space-8); +} + +.fade-in { + animation: fadeIn 0.4s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-5px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.mb-6 { margin-bottom: var(--space-6); } + +.error-message, +.loading-state { + margin-top: var(--space-12); + text-align: center; } diff --git a/frontend/src/app/features/payment/payment.component.ts b/frontend/src/app/features/payment/payment.component.ts index 671a36e..520e486 100644 --- a/frontend/src/app/features/payment/payment.component.ts +++ b/frontend/src/app/features/payment/payment.component.ts @@ -1,34 +1,75 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, inject, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatIconModule } from '@angular/material/icon'; +import { AppButtonComponent } from '../../shared/components/app-button/app-button.component'; +import { AppCardComponent } from '../../shared/components/app-card/app-card.component'; +import { QuoteEstimatorService } from '../calculator/services/quote-estimator.service'; +import { TranslateModule } from '@ngx-translate/core'; @Component({ selector: 'app-payment', standalone: true, - imports: [CommonModule, MatButtonModule, MatCardModule, MatIconModule], + imports: [CommonModule, AppButtonComponent, AppCardComponent, TranslateModule], templateUrl: './payment.component.html', styleUrl: './payment.component.scss' }) export class PaymentComponent implements OnInit { - orderId: string | null = null; + private route = inject(ActivatedRoute); + private router = inject(Router); + private quoteService = inject(QuoteEstimatorService); - constructor( - private route: ActivatedRoute, - private router: Router - ) {} + orderId: string | null = null; + selectedPaymentMethod: 'twint' | 'bill' | null = null; + order = signal(null); + loading = signal(true); + error = signal(null); ngOnInit(): void { this.orderId = this.route.snapshot.paramMap.get('orderId'); + if (this.orderId) { + this.loadOrder(); + } else { + this.error.set('Order ID not found.'); + this.loading.set(false); + } + } + + loadOrder() { + if (!this.orderId) return; + this.quoteService.getOrder(this.orderId).subscribe({ + next: (order) => { + this.order.set(order); + this.loading.set(false); + }, + error: (err) => { + console.error('Failed to load order', err); + this.error.set('Failed to load order details.'); + this.loading.set(false); + } + }); + } + + selectPayment(method: 'twint' | 'bill'): void { + this.selectedPaymentMethod = method; + } + + downloadInvoice() { + if (!this.orderId) return; + this.quoteService.getOrderInvoice(this.orderId).subscribe({ + next: (blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `invoice-${this.orderId}.pdf`; + a.click(); + window.URL.revokeObjectURL(url); + }, + error: (err) => console.error('Failed to download invoice', err) + }); } completeOrder(): void { - // Simulate payment completion alert('Payment Simulated! Order marked as PAID.'); - // Here you would call the backend to mark as paid if we had that endpoint ready - // For now, redirect home or show success this.router.navigate(['/']); } } diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 0c8f67f..23ce97f 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -151,6 +151,7 @@ }, "CHECKOUT": { "TITLE": "Checkout", + "SUBTITLE": "Complete your order by entering the shipping and payment details.", "CONTACT_INFO": "Contact Information", "BILLING_ADDR": "Billing Address", "SHIPPING_ADDR": "Shipping Address", @@ -171,6 +172,25 @@ "SUBTOTAL": "Subtotal", "SETUP_FEE": "Setup Fee", "TOTAL": "Total", - "QTY": "Qty" + "QTY": "Qty", + "SHIPPING": "Shipping" + }, + "PAYMENT": { + "TITLE": "Payment", + "METHOD": "Payment Method", + "TWINT_TITLE": "Pay with TWINT", + "TWINT_DESC": "Scan the code with your TWINT app", + "BANK_TITLE": "Bank Transfer", + "BANK_OWNER": "Owner", + "BANK_IBAN": "IBAN", + "BANK_REF": "Reference", + "DOWNLOAD_QR": "Download QR-Invoice (PDF)", + "CONFIRM": "Confirm Order", + "SUMMARY_TITLE": "Order Summary", + "SUBTOTAL": "Subtotal", + "SHIPPING": "Shipping", + "SETUP_FEE": "Setup Fee", + "TOTAL": "Total", + "LOADING": "Loading order details..." } } diff --git a/frontend/src/assets/i18n/it.json b/frontend/src/assets/i18n/it.json index 245b62c..70ce4f9 100644 --- a/frontend/src/assets/i18n/it.json +++ b/frontend/src/assets/i18n/it.json @@ -130,6 +130,7 @@ }, "CHECKOUT": { "TITLE": "Checkout", + "SUBTITLE": "Completa il tuo ordine inserendo i dettagli per la spedizione e il pagamento.", "CONTACT_INFO": "Informazioni di Contatto", "BILLING_ADDR": "Indirizzo di Fatturazione", "SHIPPING_ADDR": "Indirizzo di Spedizione", @@ -150,6 +151,25 @@ "SUBTOTAL": "Subtotale", "SETUP_FEE": "Costo di Avvio", "TOTAL": "Totale", - "QTY": "Qtà" + "QTY": "Qtà", + "SHIPPING": "Spedizione" + }, + "PAYMENT": { + "TITLE": "Pagamento", + "METHOD": "Metodo di Pagamento", + "TWINT_TITLE": "Paga con TWINT", + "TWINT_DESC": "Inquadra il codice con l'app TWINT", + "BANK_TITLE": "Bonifico Bancario", + "BANK_OWNER": "Titolare", + "BANK_IBAN": "IBAN", + "BANK_REF": "Riferimento", + "DOWNLOAD_QR": "Scarica QR-Fattura (PDF)", + "CONFIRM": "Conferma Ordine", + "SUMMARY_TITLE": "Riepilogo Ordine", + "SUBTOTAL": "Subtotale", + "SHIPPING": "Spedizione", + "SETUP_FEE": "Costo Setup", + "TOTAL": "Totale", + "LOADING": "Caricamento dettagli ordine..." } }