|
@@ -304,14 +304,20 @@
|
|
|
<!-- Estimate -->
|
|
<!-- Estimate -->
|
|
|
<Transition enter-active-class="transition duration-300" enter-from-class="opacity-0 translate-y-2" enter-to-class="opacity-100 translate-y-0">
|
|
<Transition enter-active-class="transition duration-300" enter-from-class="opacity-0 translate-y-2" enter-to-class="opacity-100 translate-y-0">
|
|
|
<div v-if="estimatedPrice !== null"
|
|
<div v-if="estimatedPrice !== null"
|
|
|
- class="p-6 bg-gradient-to-br from-primary/20 via-primary/5 to-background border border-primary/30 rounded-2xl flex items-center justify-between">
|
|
|
|
|
- <div>
|
|
|
|
|
|
|
+ class="p-6 bg-gradient-to-br from-primary/20 via-primary/5 to-background border border-primary/30 rounded-2xl flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
|
|
|
+ <div class="space-y-1">
|
|
|
<p class="text-[10px] font-bold uppercase tracking-widest text-primary/80 mb-1">{{ t("upload.estimatedTotal") }}</p>
|
|
<p class="text-[10px] font-bold uppercase tracking-widest text-primary/80 mb-1">{{ t("upload.estimatedTotal") }}</p>
|
|
|
|
|
+ <div v-if="estimateMetadata?.speed_coeff && estimateMetadata.speed_coeff > 1" class="flex items-center gap-1.5 px-2 py-0.5 bg-primary/10 text-primary rounded-md w-fit mb-2">
|
|
|
|
|
+ <AlertCircle class="w-3 h-3" />
|
|
|
|
|
+ <span class="text-[10px] font-bold uppercase">{{ t("admin.fields.speedCoeff") }}: x{{ estimateMetadata.speed_coeff }}</span>
|
|
|
|
|
+ </div>
|
|
|
<p class="text-xs text-muted-foreground">{{ t("upload.priceDisclaimer") }}</p>
|
|
<p class="text-xs text-muted-foreground">{{ t("upload.priceDisclaimer") }}</p>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="text-right">
|
|
<div class="text-right">
|
|
|
- <span class="text-3xl font-display font-bold text-primary">{{ estimatedPrice }}</span>
|
|
|
|
|
- <span class="text-xs font-bold text-primary ml-1 uppercase">EUR</span>
|
|
|
|
|
|
|
+ <div class="flex items-baseline justify-end gap-1">
|
|
|
|
|
+ <span class="text-3xl font-display font-bold text-primary">{{ estimatedPrice }}</span>
|
|
|
|
|
+ <span class="text-xs font-bold text-primary uppercase">EUR</span>
|
|
|
|
|
+ </div>
|
|
|
<p class="text-[9px] text-muted-foreground mt-1 opacity-60">incl. 21% PDV</p>
|
|
<p class="text-[9px] text-muted-foreground mt-1 opacity-60">incl. 21% PDV</p>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -341,7 +347,7 @@ const emit = defineEmits<{
|
|
|
import { ref, computed, watch, onMounted } from "vue";
|
|
import { ref, computed, watch, onMounted } from "vue";
|
|
|
import { useI18n } from "vue-i18n";
|
|
import { useI18n } from "vue-i18n";
|
|
|
import { toast } from "vue-sonner";
|
|
import { toast } from "vue-sonner";
|
|
|
-import { Upload, FileBox, X, Check, Link as LinkIcon, MapPin, User, Phone, Mail, Loader2, ShieldCheck, Hash, FileText } from "lucide-vue-next";
|
|
|
|
|
|
|
+import { Upload, FileBox, X, Check, Link as LinkIcon, MapPin, User, Phone, Mail, Loader2, ShieldCheck, Hash, FileText, AlertCircle } from "lucide-vue-next";
|
|
|
import Button from "./ui/button.vue";
|
|
import Button from "./ui/button.vue";
|
|
|
import { defineAsyncComponent } from "vue";
|
|
import { defineAsyncComponent } from "vue";
|
|
|
const StlViewer = defineAsyncComponent(() => import("@/components/StlViewer.vue"));
|
|
const StlViewer = defineAsyncComponent(() => import("@/components/StlViewer.vue"));
|
|
@@ -373,6 +379,7 @@ const agreeToNuances = ref(false);
|
|
|
const materials = ref<any[]>([]);
|
|
const materials = ref<any[]>([]);
|
|
|
const selectedMaterial = ref("1");
|
|
const selectedMaterial = ref("1");
|
|
|
const selectedColor = ref("");
|
|
const selectedColor = ref("");
|
|
|
|
|
+const estimateMetadata = ref<any>(null);
|
|
|
const selectedMaterialColors = computed(() => {
|
|
const selectedMaterialColors = computed(() => {
|
|
|
const mat = materials.value.find(m => String(m.id) === selectedMaterial.value);
|
|
const mat = materials.value.find(m => String(m.id) === selectedMaterial.value);
|
|
|
return mat?.available_colors || [];
|
|
return mat?.available_colors || [];
|
|
@@ -425,13 +432,14 @@ watch([() => files.value.length, selectedMaterial, modelLink], async () => {
|
|
|
file_quantities: files.value.map(f => 1), // Only get base unit cost
|
|
file_quantities: files.value.map(f => 1), // Only get base unit cost
|
|
|
});
|
|
});
|
|
|
// Assign individual prices to files for local quantity multiplication
|
|
// Assign individual prices to files for local quantity multiplication
|
|
|
- if (res.file_prices && Array.isArray(res.file_prices)) {
|
|
|
|
|
- files.value.forEach((f, i) => {
|
|
|
|
|
- f.basePrice = res.file_prices[i];
|
|
|
|
|
- });
|
|
|
|
|
- // Force reactivity update
|
|
|
|
|
- files.value = [...files.value];
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (res.file_prices && Array.isArray(res.file_prices)) {
|
|
|
|
|
+ files.value.forEach((f, i) => {
|
|
|
|
|
+ f.basePrice = res.file_prices[i];
|
|
|
|
|
+ });
|
|
|
|
|
+ estimateMetadata.value = res.metadata;
|
|
|
|
|
+ // Force reactivity update
|
|
|
|
|
+ files.value = [...files.value];
|
|
|
|
|
+ }
|
|
|
} catch { /* silent */ }
|
|
} catch { /* silent */ }
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|