From 8fac8ac8924da2d5993cb3b8d8d195b84fe855fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Wed, 11 Feb 2026 14:53:46 +0100 Subject: [PATCH] feat(back-end): db connections implemented and created users --- .gitea/workflows/cicd.yaml | 36 +++++++- Makefile | 10 --- .../printcalculator/model/CostBreakdown.java | 11 --- .../printcalculator/model/QuoteResult.java | 22 +---- .../printcalculator/service/GCodeParser.java | 89 ++++++++++++++++--- .../service/QuoteCalculator.java | 21 +---- .../service/GCodeParserTest.java | 36 ++++++++ docker-compose.deploy.yml | 5 +- .../calculator/calculator-page.component.scss | 8 +- .../upload-form/upload-form.component.ts | 5 ++ .../services/quote-estimator.service.ts | 81 +++++++++++++---- 11 files changed, 225 insertions(+), 99 deletions(-) delete mode 100644 Makefile delete mode 100644 backend/src/main/java/com/printcalculator/model/CostBreakdown.java diff --git a/.gitea/workflows/cicd.yaml b/.gitea/workflows/cicd.yaml index 0c975d8..4ef1bdb 100644 --- a/.gitea/workflows/cicd.yaml +++ b/.gitea/workflows/cicd.yaml @@ -92,7 +92,7 @@ jobs: echo "ENV=dev" >> "$GITHUB_ENV" fi - - name: Trigger deploy on Unraid (forced command key) + - name: Setup SSH key shell: bash run: | set -euo pipefail @@ -120,9 +120,39 @@ jobs: # 5) Validazione: se fallisce qui, la chiave NON è valida/corrotta ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null - # ... (resto del codice uguale) ssh-keyscan -H "${{ secrets.SERVER_HOST }}" >> ~/.ssh/known_hosts 2>/dev/null + - name: Write DB env on server + shell: bash + run: | + if [[ "${{ env.ENV }}" == "prod" ]]; then + DB_URL="${{ secrets.DB_URL_PROD }}" + DB_USER="${{ secrets.DB_USERNAME_PROD }}" + DB_PASS="${{ secrets.DB_PASSWORD_PROD }}" + elif [[ "${{ env.ENV }}" == "int" ]]; then + DB_URL="${{ secrets.DB_URL_INT }}" + DB_USER="${{ secrets.DB_USERNAME_INT }}" + DB_PASS="${{ secrets.DB_PASSWORD_INT }}" + else + DB_URL="${{ secrets.DB_URL_DEV }}" + DB_USER="${{ secrets.DB_USERNAME_DEV }}" + DB_PASS="${{ secrets.DB_PASSWORD_DEV }}" + fi + + cat > /tmp/pc.env < notes; - private double setupCost; - public QuoteResult(double totalPrice, String currency, PrintStats stats, CostBreakdown breakdown, List notes, double setupCost) { + public QuoteResult(double totalPrice, String currency, PrintStats stats, double setupCost) { this.totalPrice = totalPrice; this.currency = currency; this.stats = stats; - this.breakdown = breakdown; - this.notes = notes; this.setupCost = setupCost; } @@ -36,14 +24,6 @@ public class QuoteResult { public PrintStats getStats() { return stats; } - - public CostBreakdown getBreakdown() { - return breakdown; - } - - public List getNotes() { - return notes; - } public double getSetupCost() { return setupCost; diff --git a/backend/src/main/java/com/printcalculator/service/GCodeParser.java b/backend/src/main/java/com/printcalculator/service/GCodeParser.java index 1257a38..cbb912b 100644 --- a/backend/src/main/java/com/printcalculator/service/GCodeParser.java +++ b/backend/src/main/java/com/printcalculator/service/GCodeParser.java @@ -17,7 +17,15 @@ public class GCodeParser { // ; estimated printing time = 1h 2m 3s // ; filament used [g] = 12.34 // ; filament used [mm] = 1234.56 - private static final Pattern TIME_PATTERN = Pattern.compile(";\\s*estimated printing time.*=\\s*(.*)", Pattern.CASE_INSENSITIVE); + private static final Pattern TOTAL_ESTIMATED_TIME_PATTERN = Pattern.compile( + ";\\s*.*total\\s+estimated\\s+time\\s*[:=]\\s*([^;]+)", + Pattern.CASE_INSENSITIVE); + private static final Pattern MODEL_PRINTING_TIME_PATTERN = Pattern.compile( + ";\\s*.*model\\s+printing\\s+time\\s*[:=]\\s*([^;]+)", + Pattern.CASE_INSENSITIVE); + private static final Pattern TIME_PATTERN = Pattern.compile( + ";\\s*(?:estimated\\s+printing\\s+time|estimated\\s+print\\s+time|print\\s+time).*[:=]\\s*(.*)", + Pattern.CASE_INSENSITIVE); private static final Pattern FILAMENT_G_PATTERN = Pattern.compile(";\\s*filament used \\[g\\]\\s*=\\s*(.*)"); private static final Pattern FILAMENT_MM_PATTERN = Pattern.compile(";\\s*filament used \\[mm\\]\\s*=\\s*(.*)"); @@ -43,6 +51,22 @@ public class GCodeParser { System.out.println("DEBUG: Found potential time line: '" + line + "'"); } + Matcher totalTimeMatcher = TOTAL_ESTIMATED_TIME_PATTERN.matcher(line); + if (totalTimeMatcher.find()) { + timeFormatted = totalTimeMatcher.group(1).trim(); + seconds = parseTimeString(timeFormatted); + System.out.println("GCodeParser: Found total estimated time: " + timeFormatted + " (" + seconds + "s)"); + continue; + } + + Matcher modelTimeMatcher = MODEL_PRINTING_TIME_PATTERN.matcher(line); + if (modelTimeMatcher.find()) { + timeFormatted = modelTimeMatcher.group(1).trim(); + seconds = parseTimeString(timeFormatted); + System.out.println("GCodeParser: Found model printing time: " + timeFormatted + " (" + seconds + "s)"); + continue; + } + Matcher timeMatcher = TIME_PATTERN.matcher(line); if (timeMatcher.find()) { timeFormatted = timeMatcher.group(1).trim(); @@ -72,21 +96,60 @@ public class GCodeParser { } private long parseTimeString(String timeStr) { - // Formats: "1d 2h 3m 4s" or "1h 20m 10s" - long totalSeconds = 0; - - Matcher d = Pattern.compile("(\\d+)d").matcher(timeStr); - if (d.find()) totalSeconds += Long.parseLong(d.group(1)) * 86400; + // Formats: "1d 2h 3m 4s", "1h 20m 10s", "01:23:45", "12:34" + String lower = timeStr.toLowerCase(); + double totalSeconds = 0; + boolean matched = false; - Matcher h = Pattern.compile("(\\d+)h").matcher(timeStr); - if (h.find()) totalSeconds += Long.parseLong(h.group(1)) * 3600; + Matcher d = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*d").matcher(lower); + if (d.find()) { + totalSeconds += Double.parseDouble(d.group(1)) * 86400; + matched = true; + } - Matcher m = Pattern.compile("(\\d+)m").matcher(timeStr); - if (m.find()) totalSeconds += Long.parseLong(m.group(1)) * 60; + Matcher h = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*h").matcher(lower); + if (h.find()) { + totalSeconds += Double.parseDouble(h.group(1)) * 3600; + matched = true; + } - Matcher s = Pattern.compile("(\\d+)s").matcher(timeStr); - if (s.find()) totalSeconds += Long.parseLong(s.group(1)); + Matcher m = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*m").matcher(lower); + if (m.find()) { + totalSeconds += Double.parseDouble(m.group(1)) * 60; + matched = true; + } - return totalSeconds; + Matcher s = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*s").matcher(lower); + if (s.find()) { + totalSeconds += Double.parseDouble(s.group(1)); + matched = true; + } + + if (matched) { + return Math.round(totalSeconds); + } + + long daySeconds = 0; + Matcher dayPrefix = Pattern.compile("(\\d+)\\s*d").matcher(lower); + if (dayPrefix.find()) { + daySeconds = Long.parseLong(dayPrefix.group(1)) * 86400; + } + + Matcher hms = Pattern.compile("(\\d{1,2}):(\\d{2}):(\\d{2})").matcher(lower); + if (hms.find()) { + long hours = Long.parseLong(hms.group(1)); + long minutes = Long.parseLong(hms.group(2)); + long seconds = Long.parseLong(hms.group(3)); + return daySeconds + hours * 3600 + minutes * 60 + seconds; + } + + Matcher ms = Pattern.compile("(\\d{1,2}):(\\d{2})").matcher(lower); + if (ms.find()) { + long minutes = Long.parseLong(ms.group(1)); + long seconds = Long.parseLong(ms.group(2)); + return daySeconds + minutes * 60 + seconds; + } + + return 0; } } diff --git a/backend/src/main/java/com/printcalculator/service/QuoteCalculator.java b/backend/src/main/java/com/printcalculator/service/QuoteCalculator.java index 206ff33..432c62f 100644 --- a/backend/src/main/java/com/printcalculator/service/QuoteCalculator.java +++ b/backend/src/main/java/com/printcalculator/service/QuoteCalculator.java @@ -6,7 +6,6 @@ import com.printcalculator.entity.FilamentVariant; import com.printcalculator.entity.PricingPolicy; import com.printcalculator.entity.PricingPolicyMachineHourTier; import com.printcalculator.entity.PrinterMachine; -import com.printcalculator.model.CostBreakdown; import com.printcalculator.model.PrintStats; import com.printcalculator.model.QuoteResult; import com.printcalculator.repository.FilamentMaterialTypeRepository; @@ -18,10 +17,7 @@ import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.ArrayList; -import java.util.Comparator; import java.util.List; -import java.util.Optional; @Service public class QuoteCalculator { @@ -101,22 +97,7 @@ public class QuoteCalculator { BigDecimal markupFactor = BigDecimal.ONE.add(policy.getMarkupPercent().divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP)); BigDecimal totalPrice = subtotal.multiply(markupFactor).setScale(2, RoundingMode.HALF_UP); - BigDecimal markupAmount = totalPrice.subtract(subtotal); - - CostBreakdown breakdown = new CostBreakdown( - materialCost.setScale(2, RoundingMode.HALF_UP), - machineCost.setScale(2, RoundingMode.HALF_UP), - energyCost.setScale(2, RoundingMode.HALF_UP), - subtotal.setScale(2, RoundingMode.HALF_UP), - markupAmount.setScale(2, RoundingMode.HALF_UP) - ); - - List notes = new ArrayList<>(); - notes.add("Policy: " + policy.getPolicyName()); - notes.add("Machine: " + machine.getPrinterDisplayName()); - notes.add("Material: " + variant.getVariantDisplayName()); - - return new QuoteResult(totalPrice.doubleValue(), "CHF", stats, breakdown, notes, fixedFee.doubleValue()); + return new QuoteResult(totalPrice.doubleValue(), "CHF", stats, fixedFee.doubleValue()); } private BigDecimal calculateMachineCost(PricingPolicy policy, BigDecimal hours) { diff --git a/backend/src/test/java/com/printcalculator/service/GCodeParserTest.java b/backend/src/test/java/com/printcalculator/service/GCodeParserTest.java index abbc95b..04055b5 100644 --- a/backend/src/test/java/com/printcalculator/service/GCodeParserTest.java +++ b/backend/src/test/java/com/printcalculator/service/GCodeParserTest.java @@ -74,4 +74,40 @@ class GCodeParserTest { tempFile.delete(); } + + @Test + void parse_colonFormattedTime_returnsCorrectStats() throws IOException { + File tempFile = File.createTempFile("test_colon", ".gcode"); + try (FileWriter writer = new FileWriter(tempFile)) { + writer.write("; generated by OrcaSlicer\n"); + writer.write("; print time: 01:02:03\n"); + writer.write("; filament used [g] = 7.5\n"); + } + + GCodeParser parser = new GCodeParser(); + PrintStats stats = parser.parse(tempFile); + + assertEquals(3723L, stats.printTimeSeconds()); + assertEquals("01:02:03", stats.printTimeFormatted()); + + tempFile.delete(); + } + + @Test + void parse_totalEstimatedTimeInline_returnsCorrectStats() throws IOException { + File tempFile = File.createTempFile("test_total", ".gcode"); + try (FileWriter writer = new FileWriter(tempFile)) { + writer.write("; generated by OrcaSlicer\n"); + writer.write("; model printing time: 5m 17s; total estimated time: 5m 21s\n"); + writer.write("; filament used [g] = 2.0\n"); + } + + GCodeParser parser = new GCodeParser(); + PrintStats stats = parser.parse(tempFile); + + assertEquals(321L, stats.printTimeSeconds()); + assertEquals("5m 21s", stats.printTimeFormatted()); + + tempFile.delete(); + } } diff --git a/docker-compose.deploy.yml b/docker-compose.deploy.yml index 94bdfb2..9f0837a 100644 --- a/docker-compose.deploy.yml +++ b/docker-compose.deploy.yml @@ -13,6 +13,9 @@ services: - ENERGY_COST_PER_KWH=${ENERGY_COST_PER_KWH} - PRINTER_POWER_WATTS=${PRINTER_POWER_WATTS} - MARKUP_PERCENT=${MARKUP_PERCENT} + - DB_URL=${DB_URL} + - DB_USERNAME=${DB_USERNAME} + - DB_PASSWORD=${DB_PASSWORD} - TEMP_DIR=/app/temp - PROFILES_DIR=/app/profiles restart: always @@ -31,4 +34,4 @@ services: volumes: backend_profiles_prod: backend_profiles_int: - backend_profiles_dev: \ No newline at end of file + backend_profiles_dev: diff --git a/frontend/src/app/features/calculator/calculator-page.component.scss b/frontend/src/app/features/calculator/calculator-page.component.scss index 02cdec0..f118857 100644 --- a/frontend/src/app/features/calculator/calculator-page.component.scss +++ b/frontend/src/app/features/calculator/calculator-page.component.scss @@ -26,11 +26,11 @@ min-width: 0; display: flex; flex-direction: column; +} - /* Make children (specifically app-card) stretch */ - > * { - flex: 1; - } +/* Stretch only the loading card so the spinner stays centered */ +.col-result > .loading-state { + flex: 1; } /* Mode Selector (Segmented Control style) */ diff --git a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts index 3d843aa..726a4a5 100644 --- a/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts +++ b/frontend/src/app/features/calculator/components/upload-form/upload-form.component.ts @@ -191,6 +191,11 @@ export class UploadFormComponent implements OnInit { const item = this.items().find(i => i.file === file); if (item) { + const vars = this.currentMaterialVariants(); + if (vars && vars.length > 0) { + const found = vars.find(v => v.colorName === item.color); + if (found) return found.hexColor; + } return getColorHex(item.color); } return '#facf0a'; 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 8c216a7..c251b45 100644 --- a/frontend/src/app/features/calculator/services/quote-estimator.service.ts +++ b/frontend/src/app/features/calculator/services/quote-estimator.service.ts @@ -49,6 +49,18 @@ interface BackendResponse { error?: string; } +interface BackendQuoteResult { + totalPrice: number; + currency: string; + setupCost: number; + stats: { + printTimeSeconds: number; + printTimeFormatted: string; + filamentWeightGrams: number; + filamentLengthMm: number; + }; +} + // Options Interfaces export interface MaterialOption { code: string; @@ -159,7 +171,7 @@ export class QuoteEstimatorService { // @ts-ignore if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth); - return this.http.post(`${environment.apiUrl}/api/quote`, formData, { + return this.http.post(`${environment.apiUrl}/api/quote`, formData, { headers, reportProgress: true, observe: 'events' @@ -206,7 +218,9 @@ export class QuoteEstimatorService { // Calculate Results let setupCost = 10; - + let setupCostFromBackend: number | null = null; + let currencyFromBackend: string | null = null; + if (request.nozzleDiameter && request.nozzleDiameter !== 0.4) { setupCost += 2; } @@ -214,18 +228,27 @@ export class QuoteEstimatorService { const items: QuoteItem[] = []; finalResponses.forEach((res, idx) => { - if (res && res.success) { - const originalItem = request.items[idx]; - items.push({ - fileName: res.fileName, - unitPrice: res.data.cost.total, - unitTime: res.data.print_time_seconds, - unitWeight: res.data.material_grams, - quantity: res.originalQty, // Use the requested quantity - material: request.material, - color: originalItem.color || 'Default' - }); + if (!res) return; + const originalItem = request.items[idx]; + const normalized = this.normalizeResponse(res); + if (!normalized.success) return; + + if (normalized.currency && currencyFromBackend == null) { + currencyFromBackend = normalized.currency; } + if (normalized.setupCost != null && setupCostFromBackend == null) { + setupCostFromBackend = normalized.setupCost; + } + + items.push({ + fileName: res.fileName, + unitPrice: normalized.unitPrice, + unitTime: normalized.unitTime, + unitWeight: normalized.unitWeight, + quantity: res.originalQty, // Use the requested quantity + material: request.material, + color: originalItem.color || 'Default' + }); }); if (items.length === 0) { @@ -234,7 +257,8 @@ export class QuoteEstimatorService { } // Initial Aggregation - let grandTotal = setupCost; + const useBackendSetup = setupCostFromBackend != null; + let grandTotal = useBackendSetup ? 0 : setupCost; let totalTime = 0; let totalWeight = 0; @@ -249,8 +273,8 @@ export class QuoteEstimatorService { const result: QuoteResult = { items, - setupCost, - currency: 'CHF', + setupCost: useBackendSetup ? setupCostFromBackend! : setupCost, + currency: currencyFromBackend || 'CHF', totalPrice: Math.round(grandTotal * 100) / 100, totalTimeHours: totalHours, totalTimeMinutes: totalMinutes, @@ -274,6 +298,31 @@ export class QuoteEstimatorService { }); } + private normalizeResponse(res: any): { success: boolean; unitPrice: number; unitTime: number; unitWeight: number; setupCost?: number; currency?: string } { + if (res && typeof res.totalPrice === 'number' && res.stats && typeof res.stats.printTimeSeconds === 'number') { + return { + success: true, + unitPrice: res.totalPrice, + unitTime: res.stats.printTimeSeconds, + unitWeight: res.stats.filamentWeightGrams, + setupCost: res.setupCost, + currency: res.currency + }; + } + + if (res && res.success && res.data) { + return { + success: true, + unitPrice: res.data.cost.total, + unitTime: res.data.print_time_seconds, + unitWeight: res.data.material_grams, + currency: 'CHF' + }; + } + + return { success: false, unitPrice: 0, unitTime: 0, unitWeight: 0 }; + } + private mapMaterial(mat: string): string { const m = mat.toUpperCase(); if (m.includes('PLA')) return 'pla_basic';