Redesign logo & storefront, light subtle admin theme, and 3D print cost calculator
This commit is contained in:
@@ -89,6 +89,15 @@ class ContentController extends Controller
|
||||
// Offline / Maintenance Mode
|
||||
'shop_offline_title',
|
||||
'shop_offline_message',
|
||||
|
||||
// 3D Print Kostprijscalculator Tarieven
|
||||
'calc_filament_price_kg',
|
||||
'calc_electricity_kwh',
|
||||
'calc_printer_wattage',
|
||||
'calc_machine_hourly_rate',
|
||||
'calc_handling_flat',
|
||||
'calc_default_markup',
|
||||
'calc_failure_rate',
|
||||
];
|
||||
|
||||
// Handle checkbox toggles that aren't present if unchecked
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Category;
|
||||
use App\Models\Product;
|
||||
use App\Services\PriceCalculatorService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -171,6 +172,25 @@ class ProductController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate 3D printing cost price and suggested retail price.
|
||||
*/
|
||||
public function calculatePrice(Request $request, PriceCalculatorService $calculator): JsonResponse
|
||||
{
|
||||
$weight = (float) $request->input('weight_grams', 80);
|
||||
$hours = (float) $request->input('print_time_hours', 4.0);
|
||||
$colors = (int) $request->input('color_count', 1);
|
||||
$material = (string) $request->input('material', 'pla');
|
||||
$markup = $request->filled('markup_percent') ? (float) $request->input('markup_percent') : null;
|
||||
|
||||
$result = $calculator->calculate($weight, $hours, $colors, $material, $markup);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and filter gallery images.
|
||||
*
|
||||
|
||||
@@ -653,19 +653,14 @@ class MakerWorldService
|
||||
$dimensions = is_array($design['dimensions']) ? implode(' x ', $design['dimensions']) : (string) $design['dimensions'];
|
||||
}
|
||||
|
||||
// Calculate a smart suggested retail price
|
||||
// Base starting price of 9.95, + ~0.08 per gram + 1.25 per print hour
|
||||
$suggestedPrice = 14.95;
|
||||
if ($weightGrams || $printTimeHours) {
|
||||
$cost = 5.0;
|
||||
if ($weightGrams) {
|
||||
$cost += ($weightGrams * 0.07);
|
||||
}
|
||||
if ($printTimeHours) {
|
||||
$cost += ($printTimeHours * 1.50);
|
||||
}
|
||||
$suggestedPrice = round(max(9.95, $cost * 1.6), 2);
|
||||
// Calculate a smart suggested retail price using the 3D Printing Cost Calculator
|
||||
$colorCount = 1;
|
||||
if (! empty($profile['filaments']) && is_array($profile['filaments'])) {
|
||||
$colorCount = max(1, min(4, count($profile['filaments'])));
|
||||
}
|
||||
$calculator = app(PriceCalculatorService::class);
|
||||
$calcResult = $calculator->calculate($weightGrams ?: 85, $printTimeHours ?: 4.0, $colorCount);
|
||||
$suggestedPrice = $calcResult['suggested_price'];
|
||||
|
||||
// Creator and MakerWorld URL
|
||||
$authorName = $design['designCreator']['name'] ?? $design['designCreator']['handle'] ?? 'MakerWorld Creator';
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Setting;
|
||||
|
||||
class PriceCalculatorService
|
||||
{
|
||||
/**
|
||||
* Purge/waste multipliers depending on the number of AMS/filament colors.
|
||||
*/
|
||||
public const COLOR_WASTE_MULTIPLIERS = [
|
||||
1 => 1.00, // 0% extra waste for single-color
|
||||
2 => 1.15, // +15% purge tower & poop waste
|
||||
3 => 1.25, // +25% purge tower & poop waste
|
||||
4 => 1.35, // +35% purge tower & poop waste
|
||||
];
|
||||
|
||||
/**
|
||||
* Material base multipliers relative to standard PLA.
|
||||
*/
|
||||
public const MATERIAL_MULTIPLIERS = [
|
||||
'pla' => 1.00,
|
||||
'petg' => 1.20,
|
||||
'silk' => 1.30,
|
||||
'tpu' => 1.40,
|
||||
];
|
||||
|
||||
/**
|
||||
* Calculate comprehensive 3D printing costs and suggested retail price.
|
||||
*
|
||||
* @param float|int|null $weightGrams Model weight in grams
|
||||
* @param float|int|null $printTimeHours Printing duration in hours
|
||||
* @param int $colorCount Number of filament colors (1 to 4)
|
||||
* @param string $material Material code ('pla', 'petg', 'silk', 'tpu')
|
||||
* @param float|null $customMarkupPercent Optional override for markup percentage (e.g. 60%)
|
||||
* @return array<string, mixed> Detailed calculation breakdown
|
||||
*/
|
||||
public function calculate(
|
||||
float|int|null $weightGrams = 80,
|
||||
float|int|null $printTimeHours = 4.0,
|
||||
int $colorCount = 1,
|
||||
string $material = 'pla',
|
||||
?float $customMarkupPercent = null
|
||||
): array {
|
||||
$weight = max(1.0, (float) ($weightGrams ?: 80));
|
||||
$hours = max(0.2, (float) ($printTimeHours ?: 4.0));
|
||||
$colors = max(1, min(4, $colorCount));
|
||||
$matCode = strtolower(trim($material)) ?: 'pla';
|
||||
|
||||
// 1. Settings / Rates configuration
|
||||
$filamentPriceKg = (float) Setting::get('calc_filament_price_kg', 20.00); // € 20 / kg
|
||||
$electricityPriceKwh = (float) Setting::get('calc_electricity_kwh', 0.35); // € 0.35 / kWh
|
||||
$printerWattage = (int) Setting::get('calc_printer_wattage', 150); // 150 W average Bambu Lab
|
||||
$machineHourlyRate = (float) Setting::get('calc_machine_hourly_rate', 0.50); // € 0.50 / uur afschrijving & nozzle/bed slijtage
|
||||
$handlingFlat = (float) Setting::get('calc_handling_flat', 1.50); // € 1.50 voorbereiding, supports & verpakking
|
||||
$failureRate = (float) Setting::get('calc_failure_rate', 5.0); // 5% faalkans opslag
|
||||
$markupPercent = $customMarkupPercent !== null ? $customMarkupPercent : (float) Setting::get('calc_default_markup', 65.0); // 65% winstmarge
|
||||
$vatPercent = 21.0; // 21% btw
|
||||
|
||||
// 2. Multipliers
|
||||
$colorWasteMult = self::COLOR_WASTE_MULTIPLIERS[$colors] ?? 1.00;
|
||||
$matMult = self::MATERIAL_MULTIPLIERS[$matCode] ?? 1.00;
|
||||
|
||||
// 3. Material cost (includes multi-color purge tower waste)
|
||||
$effectiveWeight = round($weight * $colorWasteMult, 1);
|
||||
$materialCost = ($effectiveWeight / 1000.0) * ($filamentPriceKg * $matMult);
|
||||
|
||||
// 4. Electricity cost
|
||||
$kw = $printerWattage / 1000.0;
|
||||
$kwhUsed = $hours * $kw;
|
||||
$electricityCost = $kwhUsed * $electricityPriceKwh;
|
||||
|
||||
// 5. Machine wear & depreciation
|
||||
$machineCost = $hours * $machineHourlyRate;
|
||||
|
||||
// 6. Multi-color handling fee (extra setup for loading AMS spools)
|
||||
$colorHandlingCost = max(0, $colors - 1) * 0.50;
|
||||
|
||||
// 7. Handling / labor
|
||||
$handlingCost = $handlingFlat + $colorHandlingCost;
|
||||
|
||||
// 8. Direct production cost
|
||||
$subtotalCost = $materialCost + $electricityCost + $machineCost + $handlingCost;
|
||||
|
||||
// 9. Failure rate risk buffer
|
||||
$failureCost = $subtotalCost * ($failureRate / 100.0);
|
||||
|
||||
// 10. Net cost price (direct production cost + risk)
|
||||
$totalCostPrice = $subtotalCost + $failureCost;
|
||||
|
||||
// 11. Margin & Retail price excl. VAT
|
||||
$profitAmount = $totalCostPrice * ($markupPercent / 100.0);
|
||||
$priceExclVat = $totalCostPrice + $profitAmount;
|
||||
|
||||
// 12. VAT (21%) & Consumer price
|
||||
$vatAmount = $priceExclVat * ($vatPercent / 100.0);
|
||||
$consumerPriceExact = $priceExclVat + $vatAmount;
|
||||
|
||||
// 13. Smart commercial rounding (.95 / .50)
|
||||
$suggestedRetailPrice = $this->smartRound($consumerPriceExact);
|
||||
|
||||
return [
|
||||
'inputs' => [
|
||||
'weight_grams' => $weight,
|
||||
'print_time_hours' => $hours,
|
||||
'color_count' => $colors,
|
||||
'material' => $matCode,
|
||||
'markup_percent' => $markupPercent,
|
||||
],
|
||||
'rates' => [
|
||||
'filament_price_kg' => $filamentPriceKg,
|
||||
'electricity_price_kwh' => $electricityPriceKwh,
|
||||
'machine_hourly_rate' => $machineHourlyRate,
|
||||
'handling_flat' => $handlingFlat,
|
||||
'failure_rate_percent' => $failureRate,
|
||||
],
|
||||
'breakdown' => [
|
||||
'effective_weight_grams' => $effectiveWeight,
|
||||
'purge_waste_grams' => round($effectiveWeight - $weight, 1),
|
||||
'material_cost' => round($materialCost, 2),
|
||||
'electricity_cost' => round($electricityCost, 2),
|
||||
'machine_cost' => round($machineCost, 2),
|
||||
'handling_cost' => round($handlingCost, 2),
|
||||
'failure_cost' => round($failureCost, 2),
|
||||
'total_cost_price' => round($totalCostPrice, 2),
|
||||
'profit_amount' => round($profitAmount, 2),
|
||||
'price_excl_vat' => round($priceExclVat, 2),
|
||||
'vat_amount' => round($vatAmount, 2),
|
||||
'price_incl_vat_exact' => round($consumerPriceExact, 2),
|
||||
],
|
||||
'suggested_price' => $suggestedRetailPrice,
|
||||
'suggested_price_formatted' => '€ '.number_format($suggestedRetailPrice, 2, ',', '.'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart commercial rounding to appealing .95 or .50 price points.
|
||||
*/
|
||||
protected function smartRound(float $rawPrice): float
|
||||
{
|
||||
// Enforce minimum price of 6.95
|
||||
if ($rawPrice <= 7.00) {
|
||||
return 6.95;
|
||||
}
|
||||
|
||||
$floor = floor($rawPrice);
|
||||
$fraction = $rawPrice - $floor;
|
||||
|
||||
if ($fraction <= 0.25) {
|
||||
$rounded = $floor - 0.05; // e.g. 14.10 -> 13.95 or 14.00 -> 13.95, but keep at least floor
|
||||
|
||||
return max(6.95, $floor < $rawPrice ? $floor + 0.50 : $floor - 0.05);
|
||||
} elseif ($fraction <= 0.70) {
|
||||
return $floor + 0.50; // e.g. 14.40 -> 14.50
|
||||
} else {
|
||||
return $floor + 0.95; // e.g. 14.80 -> 14.95
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user