feat(back-end): db connections implemented and created users
This commit is contained in:
@@ -92,7 +92,7 @@ jobs:
|
|||||||
echo "ENV=dev" >> "$GITHUB_ENV"
|
echo "ENV=dev" >> "$GITHUB_ENV"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Trigger deploy on Unraid (forced command key)
|
- name: Setup SSH key
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -120,9 +120,39 @@ jobs:
|
|||||||
# 5) Validazione: se fallisce qui, la chiave NON è valida/corrotta
|
# 5) Validazione: se fallisce qui, la chiave NON è valida/corrotta
|
||||||
ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null
|
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
|
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 <<EOF
|
||||||
|
DB_URL=${DB_URL}
|
||||||
|
DB_USERNAME=${DB_USER}
|
||||||
|
DB_PASSWORD=${DB_PASS}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
ssh -i ~/.ssh/id_ed25519 -o BatchMode=yes "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" \
|
||||||
|
"setenv ${{ env.ENV }}" < /tmp/pc.env
|
||||||
|
|
||||||
|
- name: Trigger deploy on Unraid (forced command key)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
# Aggiungiamo le opzioni di verbosità se dovesse fallire ancora,
|
# Aggiungiamo le opzioni di verbosità se dovesse fallire ancora,
|
||||||
# e assicuriamoci che l'input sia pulito
|
# e assicuriamoci che l'input sia pulito
|
||||||
ssh -i ~/.ssh/id_ed25519 -o BatchMode=yes "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "${{ env.ENV }}"
|
ssh -i ~/.ssh/id_ed25519 -o BatchMode=yes "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}" "deploy ${{ env.ENV }}"
|
||||||
|
|||||||
10
Makefile
10
Makefile
@@ -1,10 +0,0 @@
|
|||||||
.PHONY: install s
|
|
||||||
install:
|
|
||||||
@echo "Installing Backend dependencies..."
|
|
||||||
cd backend && pip install -r requirements.txt || pip install fastapi uvicorn trimesh python-multipart numpy
|
|
||||||
@echo "Installing Frontend dependencies..."
|
|
||||||
cd frontend && npm install
|
|
||||||
|
|
||||||
start:
|
|
||||||
@echo "Starting development environment..."
|
|
||||||
./start.sh
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.printcalculator.model;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
public record CostBreakdown(
|
|
||||||
BigDecimal materialCost,
|
|
||||||
BigDecimal machineCost,
|
|
||||||
BigDecimal energyCost,
|
|
||||||
BigDecimal subtotal,
|
|
||||||
BigDecimal markup
|
|
||||||
) {}
|
|
||||||
@@ -1,27 +1,15 @@
|
|||||||
package com.printcalculator.model;
|
package com.printcalculator.model;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class QuoteResult {
|
public class QuoteResult {
|
||||||
private double totalPrice;
|
private double totalPrice;
|
||||||
private String currency;
|
private String currency;
|
||||||
private PrintStats stats;
|
private PrintStats stats;
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private CostBreakdown breakdown;
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
private List<String> notes;
|
|
||||||
|
|
||||||
private double setupCost;
|
private double setupCost;
|
||||||
|
|
||||||
public QuoteResult(double totalPrice, String currency, PrintStats stats, CostBreakdown breakdown, List<String> notes, double setupCost) {
|
public QuoteResult(double totalPrice, String currency, PrintStats stats, double setupCost) {
|
||||||
this.totalPrice = totalPrice;
|
this.totalPrice = totalPrice;
|
||||||
this.currency = currency;
|
this.currency = currency;
|
||||||
this.stats = stats;
|
this.stats = stats;
|
||||||
this.breakdown = breakdown;
|
|
||||||
this.notes = notes;
|
|
||||||
this.setupCost = setupCost;
|
this.setupCost = setupCost;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,14 +25,6 @@ public class QuoteResult {
|
|||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CostBreakdown getBreakdown() {
|
|
||||||
return breakdown;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getNotes() {
|
|
||||||
return notes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getSetupCost() {
|
public double getSetupCost() {
|
||||||
return setupCost;
|
return setupCost;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,15 @@ public class GCodeParser {
|
|||||||
// ; estimated printing time = 1h 2m 3s
|
// ; estimated printing time = 1h 2m 3s
|
||||||
// ; filament used [g] = 12.34
|
// ; filament used [g] = 12.34
|
||||||
// ; filament used [mm] = 1234.56
|
// ; 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_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*(.*)");
|
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 + "'");
|
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);
|
Matcher timeMatcher = TIME_PATTERN.matcher(line);
|
||||||
if (timeMatcher.find()) {
|
if (timeMatcher.find()) {
|
||||||
timeFormatted = timeMatcher.group(1).trim();
|
timeFormatted = timeMatcher.group(1).trim();
|
||||||
@@ -72,21 +96,60 @@ public class GCodeParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private long parseTimeString(String timeStr) {
|
private long parseTimeString(String timeStr) {
|
||||||
// Formats: "1d 2h 3m 4s" or "1h 20m 10s"
|
// Formats: "1d 2h 3m 4s", "1h 20m 10s", "01:23:45", "12:34"
|
||||||
long totalSeconds = 0;
|
String lower = timeStr.toLowerCase();
|
||||||
|
double totalSeconds = 0;
|
||||||
|
boolean matched = false;
|
||||||
|
|
||||||
Matcher d = Pattern.compile("(\\d+)d").matcher(timeStr);
|
Matcher d = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*d").matcher(lower);
|
||||||
if (d.find()) totalSeconds += Long.parseLong(d.group(1)) * 86400;
|
if (d.find()) {
|
||||||
|
totalSeconds += Double.parseDouble(d.group(1)) * 86400;
|
||||||
|
matched = true;
|
||||||
|
}
|
||||||
|
|
||||||
Matcher h = Pattern.compile("(\\d+)h").matcher(timeStr);
|
Matcher h = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*h").matcher(lower);
|
||||||
if (h.find()) totalSeconds += Long.parseLong(h.group(1)) * 3600;
|
if (h.find()) {
|
||||||
|
totalSeconds += Double.parseDouble(h.group(1)) * 3600;
|
||||||
|
matched = true;
|
||||||
|
}
|
||||||
|
|
||||||
Matcher m = Pattern.compile("(\\d+)m").matcher(timeStr);
|
Matcher m = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*m").matcher(lower);
|
||||||
if (m.find()) totalSeconds += Long.parseLong(m.group(1)) * 60;
|
if (m.find()) {
|
||||||
|
totalSeconds += Double.parseDouble(m.group(1)) * 60;
|
||||||
|
matched = true;
|
||||||
|
}
|
||||||
|
|
||||||
Matcher s = Pattern.compile("(\\d+)s").matcher(timeStr);
|
Matcher s = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*s").matcher(lower);
|
||||||
if (s.find()) totalSeconds += Long.parseLong(s.group(1));
|
if (s.find()) {
|
||||||
|
totalSeconds += Double.parseDouble(s.group(1));
|
||||||
|
matched = true;
|
||||||
|
}
|
||||||
|
|
||||||
return totalSeconds;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import com.printcalculator.entity.FilamentVariant;
|
|||||||
import com.printcalculator.entity.PricingPolicy;
|
import com.printcalculator.entity.PricingPolicy;
|
||||||
import com.printcalculator.entity.PricingPolicyMachineHourTier;
|
import com.printcalculator.entity.PricingPolicyMachineHourTier;
|
||||||
import com.printcalculator.entity.PrinterMachine;
|
import com.printcalculator.entity.PrinterMachine;
|
||||||
import com.printcalculator.model.CostBreakdown;
|
|
||||||
import com.printcalculator.model.PrintStats;
|
import com.printcalculator.model.PrintStats;
|
||||||
import com.printcalculator.model.QuoteResult;
|
import com.printcalculator.model.QuoteResult;
|
||||||
import com.printcalculator.repository.FilamentMaterialTypeRepository;
|
import com.printcalculator.repository.FilamentMaterialTypeRepository;
|
||||||
@@ -18,10 +17,7 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class QuoteCalculator {
|
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 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 totalPrice = subtotal.multiply(markupFactor).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
BigDecimal markupAmount = totalPrice.subtract(subtotal);
|
return new QuoteResult(totalPrice.doubleValue(), "CHF", stats, fixedFee.doubleValue());
|
||||||
|
|
||||||
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<String> 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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private BigDecimal calculateMachineCost(PricingPolicy policy, BigDecimal hours) {
|
private BigDecimal calculateMachineCost(PricingPolicy policy, BigDecimal hours) {
|
||||||
|
|||||||
@@ -74,4 +74,40 @@ class GCodeParserTest {
|
|||||||
|
|
||||||
tempFile.delete();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ services:
|
|||||||
- ENERGY_COST_PER_KWH=${ENERGY_COST_PER_KWH}
|
- ENERGY_COST_PER_KWH=${ENERGY_COST_PER_KWH}
|
||||||
- PRINTER_POWER_WATTS=${PRINTER_POWER_WATTS}
|
- PRINTER_POWER_WATTS=${PRINTER_POWER_WATTS}
|
||||||
- MARKUP_PERCENT=${MARKUP_PERCENT}
|
- MARKUP_PERCENT=${MARKUP_PERCENT}
|
||||||
|
- DB_URL=${DB_URL}
|
||||||
|
- DB_USERNAME=${DB_USERNAME}
|
||||||
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
- TEMP_DIR=/app/temp
|
- TEMP_DIR=/app/temp
|
||||||
- PROFILES_DIR=/app/profiles
|
- PROFILES_DIR=/app/profiles
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
@@ -26,11 +26,11 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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) */
|
/* Mode Selector (Segmented Control style) */
|
||||||
|
|||||||
@@ -191,6 +191,11 @@ export class UploadFormComponent implements OnInit {
|
|||||||
|
|
||||||
const item = this.items().find(i => i.file === file);
|
const item = this.items().find(i => i.file === file);
|
||||||
if (item) {
|
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 getColorHex(item.color);
|
||||||
}
|
}
|
||||||
return '#facf0a';
|
return '#facf0a';
|
||||||
|
|||||||
@@ -49,6 +49,18 @@ interface BackendResponse {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BackendQuoteResult {
|
||||||
|
totalPrice: number;
|
||||||
|
currency: string;
|
||||||
|
setupCost: number;
|
||||||
|
stats: {
|
||||||
|
printTimeSeconds: number;
|
||||||
|
printTimeFormatted: string;
|
||||||
|
filamentWeightGrams: number;
|
||||||
|
filamentLengthMm: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Options Interfaces
|
// Options Interfaces
|
||||||
export interface MaterialOption {
|
export interface MaterialOption {
|
||||||
code: string;
|
code: string;
|
||||||
@@ -159,7 +171,7 @@ export class QuoteEstimatorService {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth);
|
if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth);
|
||||||
|
|
||||||
return this.http.post<BackendResponse>(`${environment.apiUrl}/api/quote`, formData, {
|
return this.http.post<BackendResponse | BackendQuoteResult>(`${environment.apiUrl}/api/quote`, formData, {
|
||||||
headers,
|
headers,
|
||||||
reportProgress: true,
|
reportProgress: true,
|
||||||
observe: 'events'
|
observe: 'events'
|
||||||
@@ -206,6 +218,8 @@ export class QuoteEstimatorService {
|
|||||||
|
|
||||||
// Calculate Results
|
// Calculate Results
|
||||||
let setupCost = 10;
|
let setupCost = 10;
|
||||||
|
let setupCostFromBackend: number | null = null;
|
||||||
|
let currencyFromBackend: string | null = null;
|
||||||
|
|
||||||
if (request.nozzleDiameter && request.nozzleDiameter !== 0.4) {
|
if (request.nozzleDiameter && request.nozzleDiameter !== 0.4) {
|
||||||
setupCost += 2;
|
setupCost += 2;
|
||||||
@@ -214,18 +228,27 @@ export class QuoteEstimatorService {
|
|||||||
const items: QuoteItem[] = [];
|
const items: QuoteItem[] = [];
|
||||||
|
|
||||||
finalResponses.forEach((res, idx) => {
|
finalResponses.forEach((res, idx) => {
|
||||||
if (res && res.success) {
|
if (!res) return;
|
||||||
const originalItem = request.items[idx];
|
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({
|
items.push({
|
||||||
fileName: res.fileName,
|
fileName: res.fileName,
|
||||||
unitPrice: res.data.cost.total,
|
unitPrice: normalized.unitPrice,
|
||||||
unitTime: res.data.print_time_seconds,
|
unitTime: normalized.unitTime,
|
||||||
unitWeight: res.data.material_grams,
|
unitWeight: normalized.unitWeight,
|
||||||
quantity: res.originalQty, // Use the requested quantity
|
quantity: res.originalQty, // Use the requested quantity
|
||||||
material: request.material,
|
material: request.material,
|
||||||
color: originalItem.color || 'Default'
|
color: originalItem.color || 'Default'
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
@@ -234,7 +257,8 @@ export class QuoteEstimatorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initial Aggregation
|
// Initial Aggregation
|
||||||
let grandTotal = setupCost;
|
const useBackendSetup = setupCostFromBackend != null;
|
||||||
|
let grandTotal = useBackendSetup ? 0 : setupCost;
|
||||||
let totalTime = 0;
|
let totalTime = 0;
|
||||||
let totalWeight = 0;
|
let totalWeight = 0;
|
||||||
|
|
||||||
@@ -249,8 +273,8 @@ export class QuoteEstimatorService {
|
|||||||
|
|
||||||
const result: QuoteResult = {
|
const result: QuoteResult = {
|
||||||
items,
|
items,
|
||||||
setupCost,
|
setupCost: useBackendSetup ? setupCostFromBackend! : setupCost,
|
||||||
currency: 'CHF',
|
currency: currencyFromBackend || 'CHF',
|
||||||
totalPrice: Math.round(grandTotal * 100) / 100,
|
totalPrice: Math.round(grandTotal * 100) / 100,
|
||||||
totalTimeHours: totalHours,
|
totalTimeHours: totalHours,
|
||||||
totalTimeMinutes: totalMinutes,
|
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 {
|
private mapMaterial(mat: string): string {
|
||||||
const m = mat.toUpperCase();
|
const m = mat.toUpperCase();
|
||||||
if (m.includes('PLA')) return 'pla_basic';
|
if (m.includes('PLA')) return 'pla_basic';
|
||||||
|
|||||||
Reference in New Issue
Block a user