Merge branch 'main' into dev
All checks were successful
Build, Test, Deploy and Analysis / build-and-push (push) Successful in 18s
Build, Test, Deploy and Analysis / test-backend (push) Successful in 38s
Build, Test, Deploy and Analysis / deploy (push) Successful in 10s
Build, Test, Deploy and Analysis / qodana (push) Has been skipped

# Conflicts:
#	.gitea/workflows/cicd.yaml
This commit is contained in:
2026-03-03 12:35:11 +01:00
9 changed files with 70 additions and 23 deletions

View File

@@ -3,6 +3,8 @@ name: Build, Test, Deploy and Analysis
on: on:
push: push:
branches: [main, int, dev] branches: [main, int, dev]
pull_request:
branches: [main, int, dev]
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
@@ -12,6 +14,7 @@ concurrency:
jobs: jobs:
# --- JOB DI ANALISI (In parallelo) --- # --- JOB DI ANALISI (In parallelo) ---
qodana: qodana:
if: ${{ gitea.event_name == 'pull_request' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -19,20 +22,19 @@ jobs:
with: with:
fetch-depth: 0 # Fondamentale per Qodana per analizzare la storia fetch-depth: 0 # Fondamentale per Qodana per analizzare la storia
- name: Prepare Qodana dirs - name: Prepare Qodana directories
shell: bash shell: bash
run: | run: |
mkdir -p /tmp/qodana/caches /tmp/qodana/results mkdir -p .qodana/caches .qodana/results
mkdir -p .qodana/cache .qodana/results
- name: 'Qodana Scan' - name: 'Qodana Scan'
uses: JetBrains/qodana-action@v2025.3 uses: JetBrains/qodana-action@v2025.3
env: env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
with: with:
project-dir: backend cache-dir: .qodana/caches
cache-dir: .qodana/cache
results-dir: .qodana/results results-dir: .qodana/results
args: -i,backend
# In Gitea, pr-mode funziona se il runner ha accesso ai dati del clone # In Gitea, pr-mode funziona se il runner ha accesso ai dati del clone
pr-mode: ${{ gitea.event_name == 'pull_request' }} pr-mode: ${{ gitea.event_name == 'pull_request' }}
use-caches: false use-caches: false
@@ -59,7 +61,6 @@ jobs:
./gradlew test ./gradlew test
build-and-push: build-and-push:
needs: test-backend
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout

View File

@@ -5,8 +5,6 @@ echo "DB_URL: $DB_URL"
echo "DB_USERNAME: $DB_USERNAME" echo "DB_USERNAME: $DB_USERNAME"
echo "SPRING_DATASOURCE_URL: $SPRING_DATASOURCE_URL" echo "SPRING_DATASOURCE_URL: $SPRING_DATASOURCE_URL"
echo "SLICER_PATH: $SLICER_PATH" echo "SLICER_PATH: $SLICER_PATH"
echo "--- ALL ENV VARS ---"
env
echo "----------------------------------------------------------------" echo "----------------------------------------------------------------"
# Determine which environment variables to use for database connection # Determine which environment variables to use for database connection

View File

@@ -16,6 +16,7 @@ import com.printcalculator.repository.NozzleOptionRepository;
import com.printcalculator.repository.PrinterMachineRepository; import com.printcalculator.repository.PrinterMachineRepository;
import com.printcalculator.service.OrcaProfileResolver; import com.printcalculator.service.OrcaProfileResolver;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -54,13 +55,13 @@ public class OptionsController {
} }
@GetMapping("/api/calculator/options") @GetMapping("/api/calculator/options")
@Transactional(readOnly = true)
public ResponseEntity<OptionsResponse> getOptions( public ResponseEntity<OptionsResponse> getOptions(
@RequestParam(value = "printerMachineId", required = false) Long printerMachineId, @RequestParam(value = "printerMachineId", required = false) Long printerMachineId,
@RequestParam(value = "nozzleDiameter", required = false) Double nozzleDiameter @RequestParam(value = "nozzleDiameter", required = false) Double nozzleDiameter
) { ) {
List<FilamentMaterialType> types = materialRepo.findAll(); List<FilamentMaterialType> types = materialRepo.findAll();
List<FilamentVariant> allVariants = variantRepo.findAll().stream() List<FilamentVariant> allVariants = variantRepo.findByIsActiveTrue().stream()
.filter(v -> Boolean.TRUE.equals(v.getIsActive()))
.sorted(Comparator .sorted(Comparator
.comparing((FilamentVariant v) -> safeMaterialCode(v.getFilamentMaterialType()), String.CASE_INSENSITIVE_ORDER) .comparing((FilamentVariant v) -> safeMaterialCode(v.getFilamentMaterialType()), String.CASE_INSENSITIVE_ORDER)
.thenComparing(v -> safeString(v.getVariantDisplayName()), String.CASE_INSENSITIVE_ORDER)) .thenComparing(v -> safeString(v.getVariantDisplayName()), String.CASE_INSENSITIVE_ORDER))

View File

@@ -2,11 +2,16 @@ package com.printcalculator.repository;
import com.printcalculator.entity.FilamentVariant; import com.printcalculator.entity.FilamentVariant;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.EntityGraph;
import com.printcalculator.entity.FilamentMaterialType; import com.printcalculator.entity.FilamentMaterialType;
import java.util.List;
import java.util.Optional; import java.util.Optional;
public interface FilamentVariantRepository extends JpaRepository<FilamentVariant, Long> { public interface FilamentVariantRepository extends JpaRepository<FilamentVariant, Long> {
@EntityGraph(attributePaths = {"filamentMaterialType"})
List<FilamentVariant> findByIsActiveTrue();
// We try to match by color name if possible, or get first active // We try to match by color name if possible, or get first active
Optional<FilamentVariant> findByFilamentMaterialTypeAndColorName(FilamentMaterialType type, String colorName); Optional<FilamentVariant> findByFilamentMaterialTypeAndColorName(FilamentMaterialType type, String colorName);
Optional<FilamentVariant> findByFilamentMaterialTypeAndVariantDisplayName(FilamentMaterialType type, String variantDisplayName); Optional<FilamentVariant> findByFilamentMaterialTypeAndVariantDisplayName(FilamentMaterialType type, String variantDisplayName);

View File

@@ -3,6 +3,7 @@ package com.printcalculator.repository;
import com.printcalculator.entity.FilamentMaterialType; import com.printcalculator.entity.FilamentMaterialType;
import com.printcalculator.entity.MaterialOrcaProfileMap; import com.printcalculator.entity.MaterialOrcaProfileMap;
import com.printcalculator.entity.PrinterMachineProfile; import com.printcalculator.entity.PrinterMachineProfile;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List; import java.util.List;
@@ -14,5 +15,6 @@ public interface MaterialOrcaProfileMapRepository extends JpaRepository<Material
FilamentMaterialType filamentMaterialType FilamentMaterialType filamentMaterialType
); );
@EntityGraph(attributePaths = {"filamentMaterialType"})
List<MaterialOrcaProfileMap> findByPrinterMachineProfileAndIsActiveTrue(PrinterMachineProfile printerMachineProfile); List<MaterialOrcaProfileMap> findByPrinterMachineProfileAndIsActiveTrue(PrinterMachineProfile printerMachineProfile);
} }

View File

@@ -226,9 +226,10 @@ export class CalculatorPageComponent implements OnInit {
this.step.set('quote'); this.step.set('quote');
} }
onItemChange(event: {id?: string, fileName: string, quantity: number}) { onItemChange(event: {id?: string, index: number, fileName: string, quantity: number}) {
// 1. Update local form for consistency (UI feedback) // 1. Update local form for consistency (UI feedback)
if (this.uploadForm) { if (this.uploadForm) {
this.uploadForm.updateItemQuantityByIndex(event.index, event.quantity);
this.uploadForm.updateItemQuantityByName(event.fileName, event.quantity); this.uploadForm.updateItemQuantityByName(event.fileName, event.quantity);
} }

View File

@@ -22,7 +22,7 @@ export class QuoteResultComponent implements OnDestroy {
result = input.required<QuoteResult>(); result = input.required<QuoteResult>();
consult = output<void>(); consult = output<void>();
proceed = output<void>(); proceed = output<void>();
itemChange = output<{id?: string, fileName: string, quantity: number}>(); itemChange = output<{id?: string, index: number, fileName: string, quantity: number}>();
// Local mutable state for items to handle quantity changes // Local mutable state for items to handle quantity changes
items = signal<QuoteItem[]>([]); items = signal<QuoteItem[]>([]);
@@ -83,6 +83,7 @@ export class QuoteResultComponent implements OnDestroy {
this.itemChange.emit({ this.itemChange.emit({
id: item.id, id: item.id,
index,
fileName: item.fileName, fileName: item.fileName,
quantity: normalizedQty quantity: normalizedQty
}); });

View File

@@ -199,11 +199,28 @@ export class UploadFormComponent implements OnInit {
} }
} }
updateItemQuantityByName(fileName: string, quantity: number) { updateItemQuantityByIndex(index: number, quantity: number) {
if (!Number.isInteger(index) || index < 0) return;
const normalizedQty = this.normalizeQuantity(quantity);
this.items.update(current => { this.items.update(current => {
if (index >= current.length) return current;
const updated = [...current];
updated[index] = { ...updated[index], quantity: normalizedQty };
return updated;
});
}
updateItemQuantityByName(fileName: string, quantity: number) {
const targetName = this.normalizeFileName(fileName);
const normalizedQty = this.normalizeQuantity(quantity);
this.items.update(current => {
let matched = false;
return current.map(item => { return current.map(item => {
if (item.file.name === fileName) { if (!matched && this.normalizeFileName(item.file.name) === targetName) {
return { ...item, quantity }; matched = true;
return { ...item, quantity: normalizedQty };
} }
return item; return item;
}); });
@@ -239,14 +256,9 @@ export class UploadFormComponent implements OnInit {
updateItemQuantity(index: number, event: Event) { updateItemQuantity(index: number, event: Event) {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
let val = parseInt(input.value, 10); const parsed = parseInt(input.value, 10);
if (isNaN(val) || val < 1) val = 1; const quantity = Number.isFinite(parsed) ? parsed : 1;
this.updateItemQuantityByIndex(index, quantity);
this.items.update(current => {
const updated = [...current];
updated[index] = { ...updated[index], quantity: val };
return updated;
});
} }
updateItemColor(index: number, newSelection: string | { colorName: string; filamentVariantId?: number }) { updateItemColor(index: number, newSelection: string | { colorName: string; filamentVariantId?: number }) {
@@ -387,4 +399,19 @@ export class UploadFormComponent implements OnInit {
this.form.get('itemsTouched')?.setValue(true); this.form.get('itemsTouched')?.setValue(true);
} }
} }
private normalizeQuantity(quantity: number): number {
if (!Number.isFinite(quantity) || quantity < 1) {
return 1;
}
return Math.floor(quantity);
}
private normalizeFileName(fileName: string): string {
return (fileName || '')
.split(/[\\/]/)
.pop()
?.trim()
.toLowerCase() ?? '';
}
} }

View File

@@ -47,6 +47,10 @@
box-shadow: 0 4px 12px rgba(0,0,0,0.15); box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000; z-index: 1000;
width: 230px; /* Increased size */ width: 230px; /* Increased size */
max-height: min(62vh, 360px);
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
// Little triangle arrow // Little triangle arrow
&::before { &::before {
@@ -67,6 +71,7 @@
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 280px; /* Provide enough width for touch targets */ width: 280px; /* Provide enough width for touch targets */
max-width: 90vw; /* Safety constraint */ max-width: 90vw; /* Safety constraint */
max-height: min(72vh, 420px);
box-shadow: 0 10px 25px rgba(0,0,0,0.2); /* Stronger shadow for modal feel */ box-shadow: 0 10px 25px rgba(0,0,0,0.2); /* Stronger shadow for modal feel */
/* Hide arrow on mobile since it's detached from trigger */ /* Hide arrow on mobile since it's detached from trigger */
@@ -76,6 +81,12 @@
} }
} }
@media (max-height: 720px) {
.color-popup {
max-height: min(56vh, 300px);
}
}
.category { .category {
margin-bottom: 12px; margin-bottom: 12px;