Probo Configurator WooCommerce Integration

User
<script>
" rel="text/javascript" >
</script>

we have this
thenthis proxi api
public function handle_probo_product_configurator(WP_REST_Request $request) {
$baseUrl = 'https://api.proboprints.com';
$token = get_option( 'probo_api_key' );

$method = $request->get_method();
$curl = curl_init();

curl_setopt($curl, CURLOPT_HTTPHEADER, [
'Accept: application/json',
'Content-Type: application/json',
'Authorization: Basic ' . $token
]);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

if ($method === 'POST') {
$body = $request->get_json_params();

if (!isset($body['url']) || !isset($body['data'])) {
return new WP_REST_Response(['error' => 'Invalid request. Missing `url` or `data`.'], 400);
}

curl_setopt($curl, CURLOPT_URL, $baseUrl . $body['url']);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($body['data']));
} else {
$url = $request->get_param('url');

if (!$url) {
return new WP_REST_Response(['error' => 'Missing `url` parameter.'], 400);
}

curl_setopt($curl, CURLOPT_URL, $baseUrl . $url);
}

$response = curl_exec($curl);
$httpcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);

return new WP_REST_Response(json_decode($response, true), $httpcode);
}
How to save each option as cart meta data adn also get price from probo api and then option to add to cart

Assistant
To **save selected options from the Probo Configurator as WooCommerce cart item meta** and **get the price from the Probo API**, you need to follow these key steps:

### ✅ STEP 1: Handle "Add to Cart" With Configurator Data
Instead of the default add-to-cart button, you must **intercept** the add-to-cart action and send the configurator options and pricing to the server, then store them in cart item data.

### ✅ STEP 2: Update JS to Trigger Add-to-Cart

Update your “ logic to expose the finalized configuration and then programmatically submit to cart via AJAX:

document.addEventListener("DOMContentLoaded", () => {
	const button = document.getElementById('launchConfigurator');

	button?.addEventListener('click', async () => {
		if (!window.connectConfigurator || typeof window.connectConfigurator.init !== 'function') {
			console.error('connectConfigurator not loaded');
			return;
		}

		try {
			const configurator = await window.connectConfigurator.init({
				proxy: '/wp-json/probo-api/v1/configurator/api',
				language: 'en',
			});

			await configurator.setProduct("<?php echo esc_js($api_code); ?>");

			configurator.on('submit', async (configData) => {
				console.log("Configurator Submitted:", configData);

				// Get final price (optional if included)
				const price = configData.price || 0;

				// Send to WooCommerce cart via AJAX
				const response = await fetch('/wp-admin/admin-ajax.php?action=add_probo_to_cart', {
					method: 'POST',
					headers: {
						'Content-Type': 'application/json',
					},
					body: JSON.stringify({
						product_id: <?php echo intval($product->get_id()); ?>,
						config_data: configData,
						price: price,
					}),
				});

				const result = await response.json();
				if (result.success) {
					window.location.href = result.redirect || '/cart';
				} else {
					alert('Failed to add product to cart: ' + result.message);
				}
			});
		} catch (e) {
			console.error('Error initializing configurator:', e);
		}
	});
});

### ✅ STEP 3: Add AJAX Handler in WordPress

Add this in your plugin or theme `functions.php`:

add_action('wp_ajax_add_probo_to_cart', 'handle_add_probo_to_cart');
add_action('wp_ajax_nopriv_add_probo_to_cart', 'handle_add_probo_to_cart');

function handle_add_probo_to_cart() {
	$data = json_decode(file_get_contents('php://input'), true);

	if (empty($data['product_id']) || empty($data['config_data'])) {
		wp_send_json_error(['message' => 'Missing product ID or config data']);
	}

	$product_id = absint($data['product_id']);
	$price      = floatval($data['price']);
	$config     = $data['config_data'];

	// Optional: Update product price dynamically (not ideal unless it's a custom product)
	// Otherwise, use cart item data to reflect pricing

	// Add to cart
	$cart_item_key = WC()->cart->add_to_cart($product_id, 1, 0, [], [
		'probo_config' => $config,
		'probo_price'  => $price,
	]);

	if ($cart_item_key) {
		wp_send_json_success(['redirect' => wc_get_cart_url()]);
	} else {
		wp_send_json_error(['message' => 'Failed to add to cart']);
	}
}

### ✅ STEP 4: Display Probo Config Data in Cart/Checkout

add_filter('woocommerce_get_item_data', function($item_data, $cart_item) {
	if (!empty($cart_item['probo_config'])) {
		$config = $cart_item['probo_config'];

		foreach ($config as $key => $value) {
			$item_data[] = [
				'name'  => ucfirst(str_replace('_', ' ', $key)),
				'value' => is_array($value) ? implode(', ', $value) : $value,
			];
		}
	}
	return $item_data;
}, 10, 2);

### ✅ STEP 5: Save Config Data to Order Meta

add_action('woocommerce_add_order_item_meta', function($item_id, $values, $cart_item_key) {
	if (!empty($values['probo_config'])) {
		wc_add_order_item_meta($item_id, '_probo_config', $values['probo_config']);
	}
	if (!empty($values['probo_price'])) {
		wc_add_order_item_meta($item_id, '_probo_price', $values['probo_price']);
	}
}, 10, 3);

### ✅ STEP 6: Override Cart Item Price (if dynamic)

To reflect the Probo-calculated price in the cart:

add_action('woocommerce_before_calculate_totals', function($cart) {
	if (is_admin() && !defined('DOING_AJAX')) return;

	foreach ($cart->get_cart() as $cart_item) {
		if (!empty($cart_item['probo_price'])) {
			$cart_item['data']->set_price($cart_item['probo_price']);
		}
	}
});

### ✅ Optional: Format for Order Emails

add_filter('woocommerce_order_item_name', function($item_name, $item) {
	$config = wc_get_order_item_meta($item->get_id(), '_probo_config', true);
	if (!empty($config)) {
		$item_name .= '<br><small>Probo Options:</small><ul>';
		foreach ($config as $key => $value) {
			$item_name .= '<li><strong>' . ucfirst($key) . '</strong>: ' . esc_html(is_array($value) ? implode(', ', $value) : $value) . '</li>';
		}
		$item_name .= '</ul>';
	}
	return $item_name;
}, 10, 2);

Let me know if:
– The price is not coming from the configurator response?
– You want to restrict quantity or variations?
– You want to update product thumbnails or images too?

User
for this
const configurator = await window.connectConfigurator.init({
proxy: '/wp-json/probo-api/v1/configurator/api',
language: 'en',
});

await configurator.setProduct("");
we are using

Assistant
Thanks for the clarification — since you're using the Web Component “ directly in your HTML (rather than initializing it manually via `window.connectConfigurator.init()`), the approach changes slightly.

### ✅ Here’s what you need to do:

Instead of trying to manually call `setProduct()` or `init()`, **you should interact with the existing component directly via JavaScript**, like this:

### ✅ STEP 1: Access the Web Component and listen for configuration submit

Update your JS like this:

<script type="module">
document.addEventListener("DOMContentLoaded", () => {
	const configuratorEl = document.querySelector('connect-product-configurator');
	const launchBtn = document.getElementById('launchConfigurator');

	// Optionally trigger a visual action, if needed
	launchBtn?.addEventListener('click', () => {
		// If your component has an open/launch method, you could call it here
		// configuratorEl.open(); // Only if supported
	});

	// Wait for the component to be ready and listen to 'submit' or 'completed' event
	configuratorEl?.addEventListener('submit', async (event) => {
		const configData = event.detail;

		console.log('Probo configuration submitted:', configData);

		const productId = <?php echo intval($product->get_id()); ?>;
		const response = await fetch('/wp-admin/admin-ajax.php?action=add_probo_to_cart', {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json',
			},
			body: JSON.stringify({
				product_id: productId,
				config_data: configData,
				price: configData.price || 0,
			}),
		});

		const result = await response.json();
		if (result.success) {
			window.location.href = result.redirect || '/cart';
		} else {
			alert('Failed to add to cart: ' + result.message);
		}
	});
});
</script>

### ✅ Key Differences

| Manual init (your first example) | Web Component (your current setup) |
|——————————————————|———————————————–|
| You call `connectConfigurator.init()` manually | The browser initializes “ itself |
| You call `.setProduct(code)` | You pass `product-code="…"` as an attribute |
| You control the flow and UI | The Web Component provides its own UI |
| You listen to `.on('submit')` | You listen to `.addEventListener('submit')` |

### 🧠 Tip

Ensure your “ component dispatches the `submit` event with `event.detail` containing all the config info (including `price`). You can verify it via `console.log(event.detail)`.

Let me know if you also want to:
– Disable the native "Add to cart" button.
– Pre-fill some fields or load saved configurations.
– Handle language switching dynamically.

User
<script>
" rel="text/javascript" >
</script>
<script>
document.addEventListener("DOMContentLoaded", () => {
	window.connectConfigurator.init({
				proxy: '/wp-json/probo-api/v1/configurator/api',
				language: 'nl',
				productCode: "<?php echo esc_js($api_code); ?>",
			});
	});
</script>

whe we use js method to load configuration then its not load

Assistant
Thanks for the detailed clarification.

You're **using the custom element “ in the HTML**, **and then trying to initialize it using** `window.connectConfigurator.init()` **via JavaScript.** The key issue here is:

> When you include the “ tag **manually in HTML**, you don’t need (and shouldn’t) call `connectConfigurator.init()` separately — **that’s meant for dynamic rendering via JS**, not static component tags.

### ✅ Solution Options

You must choose **either** of the following approaches — not both:

## ✅ Option 1: Static Web Component (Recommended)

Use the HTML tag **with attributes**, and it will auto-initialize:

<connect-product-configurator 
  proxy="/wp-json/probo-api/v1/configurator/api" 
  language="nl" 
  product-code="<?php echo esc_attr( $api_code ); ?>">
</connect-product-configurator>

👉 Remove this JS block entirely:

window.connectConfigurator.init({ ... });

The component itself handles loading config, language, and proxy internally based on attributes.

## ✅ Option 2: JavaScript-based Initialization (for dynamic injection)

If you **don’t** place the tag in your HTML and instead **want to inject and initialize it with JS**, you can do this:

### HTML:

<!-- Do NOT place the tag manually -->
<div id="probo-dynamic-placeholder"></div>

### JS (JavaScript module):

document.addEventListener("DOMContentLoaded", async () => {
	const configurator = await window.connectConfigurator.init({
		proxy: '/wp-json/probo-api/v1/configurator/api',
		language: 'nl',
		productCode: "<?php echo esc_js($api_code); ?>",
		target: '#probo-dynamic-placeholder', // The container where it will inject
	});
});

### ❌ Problem With Your Current Setup

You're doing this:

<connect-product-configurator></connect-product-configurator>
<script>
  window.connectConfigurator.init({ ... });
</script>

That causes a **conflict**:
– The component already exists and tries to self-initialize.
– Then `connectConfigurator.init()` is trying to mount again → **double init fails silently**.

### ✅ Best Practice for You

Since you’re already outputting the HTML tag, **use this and only this**:

<connect-product-configurator 
  proxy="/wp-json/probo-api/v1/configurator/api" 
  language="nl" 
  product-code="<?php echo esc_attr( $api_code ); ?>">
</connect-product-configurator>

And **remove this part**:

window.connectConfigurator.init({ ... });

Let me know if:
– You want to listen for the configurator’s `submit` event to add to cart.
– You want to dynamically insert the configurator instead of using the HTML tag.
– You're still seeing blank / unresponsive component after fixing the init.

User
banner/:395 Uncaught TypeError: Cannot read properties of undefined (reading 'init')
Assistant
The error:

Uncaught TypeError: Cannot read properties of undefined (reading 'init')

means that `window.connectConfigurator` is `undefined`, so calling `window.connectConfigurator.init()` fails.

### ✅ Why This Is Happening

You're loading the component via this script:

<script type="module" src="<?php echo PC_PLUGIN_URL . 'assets/src/connect-product-configurator.js' ?>" rel="text/javascript"></script>

But then calling:

window.connectConfigurator.init(...)

Right after the DOM is ready — **before the JS module has fully loaded or exposed the `window.connectConfigurator` object.**

### ✅ How to Fix It

If you're using the **“ tag**, **you do NOT need to use** `connectConfigurator.init()` at all.

But if for some reason you want to access it, you must **wait for the JS module to load completely**.

### ✅ Fix Option A – Only use “ HTML tag (Recommended)

Just do:

<connect-product-configurator 
  proxy="/wp-json/probo-api/v1/configurator/api" 
  language="nl" 
  product-code="<?php echo esc_attr( $api_code ); ?>">
</connect-product-configurator>

➡️ Remove any JS that calls `connectConfigurator.init()`.
It will auto-initialize using the attributes.

### ✅ Fix Option B – Wait for the module to be defined

If you're going full JS and want to manually init it, **wait for the script to load**, like:

window.addEventListener('load', async () => {
  if (!window.connectConfigurator || !window.connectConfigurator.init) {
    console.error('connectConfigurator not ready');
    return;
  }

  const configurator = await window.connectConfigurator.init({
    proxy: '/wp-json/probo-api/v1/configurator/api',
    language: 'nl',
    productCode: "<?php echo esc_js($api_code); ?>",
    target: '#probo-placeholder'
  });
});

Or better yet, you can poll until it's defined:

const waitForConfigurator = async () => {
  while (!window.connectConfigurator || !window.connectConfigurator.init) {
    await new Promise(resolve => setTimeout(resolve, 100));
  }

  // Safe to init now
  window.connectConfigurator.init({
    proxy: '/wp-json/probo-api/v1/configurator/api',
    language: 'nl',
    productCode: "<?php echo esc_js($api_code); ?>",
    target: '#probo-placeholder'
  });
};

waitForConfigurator();

### 🧠 Summary

| If you're using… | Do this |
|——————–|———|
| “ in HTML | Remove `window.connectConfigurator.init()` completely |
| No tag in HTML (pure JS) | Use `connectConfigurator.init()` **only after the module is fully loaded** |

Let me know which approach you're going with and I can give the exact working code.

User
when exclide connect-product-configurator then this said connectConfigurator not ready
Assistant
Thanks — that confirms you're mixing both approaches. Here's a **clear breakdown** and fix based on your case.

## 🔥 Problem Recap:

You're:
1. **Placing “ in the HTML**, AND
2. **Trying to call `window.connectConfigurator.init()`**, which is **meant for dynamically rendering** (when the HTML tag is not used).

As a result:
– The browser loads the HTML tag and tries to initialize it.
– Your JS tries to call `.init()` on something that doesn't exist yet → `connectConfigurator not ready`.

## ✅ Your Correct Setup (Use This and Remove JS `init()`)

If you are using the HTML tag, **do this**:

### ✅ 1. Use the tag **with attributes**:

<connect-product-configurator
  proxy="/wp-json/probo-api/v1/configurator/api"
  language="nl"
  product-code="<?php echo esc_attr( $api_code ); ?>">
</connect-product-configurator>

✅ This **automatically initializes**.

### ✅ 2. Listen for the configurator's `submit` event (add to cart)

document.addEventListener("DOMContentLoaded", () => {
  const configurator = document.querySelector("connect-product-configurator");

  if (!configurator) {
    console.error("Configurator component not found.");
    return;
  }

  configurator.addEventListener("submit", async (event) => {
    const configData = event.detail;
    console.log("Config data submitted:", configData);

    const productId = <?php echo intval($product->get_id()); ?>;
    const response = await fetch("/wp-admin/admin-ajax.php?action=add_probo_to_cart", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        product_id: productId,
        config_data: configData,
        price: configData.price || 0,
      }),
    });

    const result = await response.json();
    if (result.success) {
      window.location.href = result.redirect || "/cart";
    } else {
      alert("Failed to add to cart: " + result.message);
    }
  });
});

### ❌ DO NOT USE THIS ANYMORE:

window.connectConfigurator.init({...});

That’s only for dynamic usage if you did:

<div id="dynamic-placeholder"></div>

and then initialized like:

connectConfigurator.init({ productCode: '...', target: '#dynamic-placeholder' });

## ✅ Final Notes

– **If “ is in HTML**, remove all `window.connectConfigurator.init()` JS.
– Let the Web Component auto-initialize via attributes (`product-code`, `language`, `proxy`).
– Just listen for `submit` events and handle them.

Let me know if:
– The Web Component is still not displaying
– You want dynamic rendering instead of static tag
– You want custom loading behavior or integration into a popup/modal

User
at the end of configurator we return calculation_id
public function handle_probo_product_configurator(WP_REST_Request $request) {
$baseUrl = 'https://api.proboprints.com';
$token = get_option( 'probo_api_key' );

$method = $request->get_method();
$curl = curl_init();

curl_setopt($curl, CURLOPT_HTTPHEADER, [
'Accept: application/json',
'Content-Type: application/json',
'Authorization: Basic ' . $token
]);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

if ($method === 'POST') {
$body = $request->get_json_params();

if (!isset($body['url']) || !isset($body['data'])) {
return new WP_REST_Response(['error' => 'Invalid request. Missing `url` or `data`.'], 400);
}

curl_setopt($curl, CURLOPT_URL, $baseUrl . $body['url']);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($body['data']));
} else {
$url = $request->get_param('url');

if (!$url) {
return new WP_REST_Response(['error' => 'Missing `url` parameter.'], 400);
}

curl_setopt($curl, CURLOPT_URL, $baseUrl . $url);
}

$response = curl_exec($curl);
$httpcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);

return new WP_REST_Response(json_decode($response, true), $httpcode);
}
this function so when we recieve calculation id then activate add to cart button and when click save each attribute
{
"status": "ok",
"code": 200,
"message": "Options calculated",
"products": [
{
"id": 1,
"code": "banner",
"can_order": true,
"amount": 2,
"width": 25,
"height": 15,
"length": 0,
"available_options": [],
"selected_options": [
{
"type_code": "width",
"name": "Breedte",
"label": null,
"description": null,
"value": "25",
"code": "width",
"default_value": null,
"min_value": 0,
"max_value": 99999,
"step_size": null,
"scale": 1,
"reversible": true,
"parent_code": "size",
"parent_name": "Afmeting",
"unit_code": "cm",
"images": []
},
{
"type_code": "height",
"name": "Hoogte",
"label": null,
"description": null,
"value": "15",
"code": "height",
"default_value": null,
"min_value": 0,
"max_value": 99999,
"step_size": null,
"scale": 1,
"reversible": true,
"parent_code": "size",
"parent_name": "Afmeting",
"unit_code": "cm",
"images": []
},
{
"type_code": "amount",
"name": "Hoeveelheid",
"label": null,
"description": null,
"value": "2",
"code": "amount",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "amount",
"parent_name": "Hoeveelheid",
"unit_code": "pc",
"images": []
},
{
"type_code": "radio",
"name": "Enkelzijdig",
"label": null,
"description": null,
"value": null,
"code": "single-sided",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "print-side",
"parent_name": "Print",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/122/1157641fbfa8a2d9.svg"
}
]
},
{
"type_code": "radio",
"name": "Soundmesh",
"label": null,
"description": "Lucht- en geluiddoorlatend PVC doek met een gewicht van 180 gr./m2.nMinimale afname van 10 m2",
"value": null,
"code": "soundmesh",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "material",
"parent_name": "Materiaal",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/202408/8805/8f3d55f833365265.jpg"
}
]
},
{
"type_code": "radio",
"name": "Voor alle zijden gelijk",
"label": null,
"description": "Alle zijden van het doek krijgen dezelfde afwerking.",
"value": null,
"code": "finishing-all-sides",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "finishing-per-side-all-sides",
"parent_name": "Afwerking voor alle zijden / per zijde",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/72/99e39932389f5591.svg"
}
]
},
{
"type_code": "radio",
"name": "Zomen en ringen",
"label": "Meest gekozen",
"description": "Voor de bevestiging krijgt het doek rondom een zoom van 4 cm en ringen van nikkel. De zoom wordt met hitte gelast.",
"value": null,
"code": "hem-and-rings",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "finishing",
"parent_name": "Gewenste afwerking",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/84/af0c678a9f4c4248.jpg"
}
]
},
{
"type_code": "radio",
"name": "In elke hoek",
"label": null,
"description": null,
"value": null,
"code": "every-corner",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "position-rings",
"parent_name": "Positie ringen",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/76/e437bd1efbb3afdd.svg"
}
]
},
{
"type_code": "cross_sell_pc",
"name": "Tiewrap – Wit – set van 20 stuks",
"label": null,
"description": null,
"value": "1",
"code": "tiewrap-white-20",
"default_value": "ceil(IA*(({AF-UV-RING:CA}/IA)/20))",
"min_value": null,
"max_value": null,
"step_size": 1,
"scale": null,
"reversible": null,
"parent_code": "accessories-cross-sell",
"parent_name": "Accessoires",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/202503/10350/da91c98b63e775f6.png"
}
]
}
],
"upload": true,
"uploaders": [
{
"type": "single",
"amount": 2,
"width": 25,
"height": 15,
"length": 0,
"minimal_dpi": 72,
"mirror_enabled": true,
"fill_enabled": true,
"rotation_enabled": true,
"file_limit": 500,
"require_white_spot": false,
"required_cut_names": [],
"optional_cut_names": [],
"tiling_enabled": false,
"tiling_mandatory": false,
"tiling_direction": null,
"max_width_excl_overlap": 490,
"max_height_excl_overlap": 2396,
"max_width_incl_overlap": 486,
"max_height_incl_overlap": 2392,
"bleed_top": 1,
"bleed_right": 1,
"bleed_bottom": 1,
"bleed_left": 1
}
],
"linked_products": [
{
"title": "Soundmesh",
"description": "Soundmesh",
"code": "soundmesh",
"article_group_code": "MA",
"unit_code": "m2",
"shipping_weight": 0.2
}
],
"prices_total": {
"24": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 40,
"purchase_price": 186.2,
"purchase_price_incl_vat": 225.3,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"48": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 30,
"purchase_price": 176.2,
"purchase_price_incl_vat": 213.2,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"72": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 15,
"purchase_price": 161.2,
"purchase_price_incl_vat": 195.05,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"96": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"120": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"144": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"168": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"192": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"216": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"240": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"264": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"288": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"312": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"336": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"360": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"384": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"408": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"432": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"456": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"480": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
}
},
"prices_per_product": {
"24": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 20,
"purchase_price": 93.1,
"purchase_price_incl_vat": 112.651,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"48": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 15,
"purchase_price": 88.1,
"purchase_price_incl_vat": 106.601,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"72": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 7.5,
"purchase_price": 80.6,
"purchase_price_incl_vat": 97.526,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"96": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"120": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"144": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"168": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"192": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"216": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"240": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"264": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"288": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"312": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"336": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"360": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"384": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"408": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"432": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"456": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"480": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
}
}
}
],
"calculation_id": 80386353
}
under selected_options to cart item meta data

Assistant
To store the `calculation_id` and all `selected_options` from the Probo configurator as **WooCommerce cart item meta**, and also activate the **“Add to cart” button only when `calculation_id` is received**, follow this **complete step-by-step** solution:

## ✅ Step 1: Modify the Web Component `submit` Listener

### ✅ Updated JS

document.addEventListener("DOMContentLoaded", () => {
	const configurator = document.querySelector("connect-product-configurator");
	const addToCartBtn = document.getElementById("launchConfigurator");
	addToCartBtn.disabled = true; // Disable initially

	configurator?.addEventListener("submit", async (event) => {
		const configData = event.detail;

		// Ensure calculation_id is present
		if (!configData?.calculation_id || !configData?.products?.[0]) {
			alert("Invalid configurator response.");
			return;
		}

		const selectedOptions = configData.products[0].selected_options;
		const calculationId = configData.calculation_id;

		console.log("Configurator Submitted:");
		console.log("Calculation ID:", calculationId);
		console.log("Selected Options:", selectedOptions);

		// Enable Add to Cart button now
		addToCartBtn.disabled = false;

		// Store in window to use on click
		window.proboCartData = {
			product_id: <?php echo intval($product->get_id()); ?>,
			calculation_id: calculationId,
			selected_options: selectedOptions,
			price: configData.products[0].prices_total['96']?.purchase_price_incl_vat || 0 // pick default delivery time
		};
	});

	addToCartBtn?.addEventListener("click", async () => {
		if (!window.proboCartData) {
			alert("Please finish customization first.");
			return;
		}

		const res = await fetch("/wp-admin/admin-ajax.php?action=add_probo_to_cart", {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify(window.proboCartData),
		});

		const result = await res.json();
		if (result.success) {
			window.location.href = result.redirect || "/cart";
		} else {
			alert("Failed to add to cart: " + result.message);
		}
	});
});

## ✅ Step 2: Handle Add to Cart in PHP (AJAX)

Add this to your plugin or theme `functions.php`:

add_action('wp_ajax_add_probo_to_cart', 'handle_add_probo_to_cart');
add_action('wp_ajax_nopriv_add_probo_to_cart', 'handle_add_probo_to_cart');

function handle_add_probo_to_cart() {
	$data = json_decode(file_get_contents('php://input'), true);

	if (empty($data['product_id']) || empty($data['selected_options']) || empty($data['calculation_id'])) {
		wp_send_json_error(['message' => 'Missing required configuration.']);
	}

	$product_id = absint($data['product_id']);
	$selected_options = $data['selected_options'];
	$calculation_id = sanitize_text_field($data['calculation_id']);
	$price = floatval($data['price']);

	$cart_item_data = [
		'probo_selected_options' => $selected_options,
		'probo_calculation_id' => $calculation_id,
		'probo_price' => $price
	];

	$cart_item_key = WC()->cart->add_to_cart($product_id, 1, 0, [], $cart_item_data);

	if ($cart_item_key) {
		wp_send_json_success(['redirect' => wc_get_cart_url()]);
	} else {
		wp_send_json_error(['message' => 'Failed to add item to cart.']);
	}
}

## ✅ Step 3: Show Selected Options in Cart/Checkout

add_filter('woocommerce_get_item_data', function($item_data, $cart_item) {
	if (!empty($cart_item['probo_calculation_id'])) {
		$item_data[] = [
			'name' => 'Calculation ID',
			'value' => esc_html($cart_item['probo_calculation_id']),
		];
	}

	if (!empty($cart_item['probo_selected_options'])) {
		foreach ($cart_item['probo_selected_options'] as $option) {
			$name = $option['name'] ?? $option['code'];
			$value = $option['value'] ?? '';
			if ($name && $value) {
				$item_data[] = [
					'name' => esc_html($name),
					'value' => esc_html($value),
				];
			}
		}
	}

	return $item_data;
}, 10, 2);

## ✅ Step 4: Save to Order Metadata

add_action('woocommerce_add_order_item_meta', function($item_id, $values) {
	if (!empty($values['probo_selected_options'])) {
		wc_add_order_item_meta($item_id, '_probo_selected_options', $values['probo_selected_options']);
	}
	if (!empty($values['probo_calculation_id'])) {
		wc_add_order_item_meta($item_id, '_probo_calculation_id', $values['probo_calculation_id']);
	}
}, 10, 2);

## ✅ Step 5: Set Custom Price in Cart

add_action('woocommerce_before_calculate_totals', function($cart) {
	if (is_admin() && !defined('DOING_AJAX')) return;

	foreach ($cart->get_cart() as $item) {
		if (!empty($item['probo_price'])) {
			$item['data']->set_price($item['probo_price']);
		}
	}
});

## ✅ Bonus: Show in Order Email

add_filter('woocommerce_order_item_name', function($name, $item) {
	$options = wc_get_order_item_meta($item->get_id(), '_probo_selected_options', true);
	$calculation_id = wc_get_order_item_meta($item->get_id(), '_probo_calculation_id', true);

	if ($calculation_id) {
		$name .= '<br><strong>Calculation ID:</strong> ' . esc_html($calculation_id);
	}

	if ($options && is_array($options)) {
		$name .= '<ul>';
		foreach ($options as $opt) {
			if (!empty($opt['name']) && isset($opt['value'])) {
				$name .= '<li>' . esc_html($opt['name']) . ': ' . esc_html($opt['value']) . '</li>';
			}
		}
		$name .= '</ul>';
	}

	return $name;
}, 10, 2);

Let me know if you'd also like to:
– Display option images in the cart.
– Support multiple quantities with dynamic recalculation.
– Include uploaded files via Probo's uploader in order metadata.

User
<script>
" rel="text/javascript" >
</script>

we are using like above configurator

Assistant
Perfect — based on your updated code, you're correctly using the “ Web Component in static HTML with the necessary `proxy`, `language`, and `product-code` attributes.

Now your goal is:

1. ✅ **Listen for the configurator submission.**
2. ✅ **Extract `calculation_id` and `selected_options`.**
3. ✅ **Enable the "Customize product" button only after valid submission.**
4. ✅ **On button click, send everything to WooCommerce cart via AJAX.**

## ✅ Final Working Solution (Based on Your HTML)

### ✅ Add this script just below the component:

<script type="module">
document.addEventListener("DOMContentLoaded", () => {
	const configurator = document.querySelector("connect-product-configurator");
	const addToCartBtn = document.getElementById("launchConfigurator");

	addToCartBtn.disabled = true; // Disable by default

	let proboCartData = null;

	configurator?.addEventListener("submit", async (event) => {
		const detail = event.detail;

		// Check for calculation ID and product data
		if (!detail?.calculation_id || !detail?.products?.[0]) {
			console.error("Invalid configurator response");
			alert("Configuration is incomplete or invalid.");
			return;
		}

		const productId = <?php echo intval($product->get_id()); ?>;
		const selectedOptions = detail.products[0].selected_options || [];
		const price = detail.products[0].prices_total?.["96"]?.purchase_price_incl_vat || 0;

		// Store cart data for later use
		proboCartData = {
			product_id: productId,
			calculation_id: detail.calculation_id,
			selected_options: selectedOptions,
			price: price
		};

		addToCartBtn.disabled = false; // Enable the button
	});

	addToCartBtn?.addEventListener("click", async () => {
		if (!proboCartData) {
			alert("Please finish customizing the product before adding to cart.");
			return;
		}

		const res = await fetch("/wp-admin/admin-ajax.php?action=add_probo_to_cart", {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify(proboCartData)
		});

		const result = await res.json();
		if (result.success) {
			window.location.href = result.redirect || "/cart";
		} else {
			alert("Error: " + result.message);
		}
	});
});
</script>

## ✅ Add this in `functions.php` (or plugin)

### AJAX handler for `add_probo_to_cart`:

add_action('wp_ajax_add_probo_to_cart', 'handle_add_probo_to_cart');
add_action('wp_ajax_nopriv_add_probo_to_cart', 'handle_add_probo_to_cart');

function handle_add_probo_to_cart() {
	$data = json_decode(file_get_contents('php://input'), true);

	if (empty($data['product_id']) || empty($data['selected_options']) || empty($data['calculation_id'])) {
		wp_send_json_error(['message' => 'Missing data for cart.']);
	}

	$product_id = absint($data['product_id']);
	$selected_options = $data['selected_options'];
	$calculation_id = sanitize_text_field($data['calculation_id']);
	$price = floatval($data['price']);

	$cart_item_data = [
		'probo_selected_options' => $selected_options,
		'probo_calculation_id' => $calculation_id,
		'probo_price' => $price
	];

	$cart_key = WC()->cart->add_to_cart($product_id, 1, 0, [], $cart_item_data);

	if ($cart_key) {
		wp_send_json_success(['redirect' => wc_get_cart_url()]);
	} else {
		wp_send_json_error(['message' => 'Could not add to cart.']);
	}
}

## ✅ Show Probo Config in Cart + Order

### Cart/checkout display:

add_filter('woocommerce_get_item_data', function($item_data, $cart_item) {
	if (!empty($cart_item['probo_calculation_id'])) {
		$item_data[] = ['name' => 'Calculation ID', 'value' => $cart_item['probo_calculation_id']];
	}
	if (!empty($cart_item['probo_selected_options'])) {
		foreach ($cart_item['probo_selected_options'] as $opt) {
			if (!empty($opt['name']) && isset($opt['value'])) {
				$item_data[] = [
					'name' => esc_html($opt['name']),
					'value' => esc_html($opt['value']),
				];
			}
		}
	}
	return $item_data;
}, 10, 2);

### Save to order:

add_action('woocommerce_add_order_item_meta', function($item_id, $values) {
	if (!empty($values['probo_calculation_id'])) {
		wc_add_order_item_meta($item_id, '_probo_calculation_id', $values['probo_calculation_id']);
	}
	if (!empty($values['probo_selected_options'])) {
		wc_add_order_item_meta($item_id, '_probo_selected_options', $values['probo_selected_options']);
	}
}, 10, 2);

### Optional: Price override (if you're using dynamic pricing)

add_action('woocommerce_before_calculate_totals', function($cart) {
	if (is_admin() && !defined('DOING_AJAX')) return;
	foreach ($cart->get_cart() as $item) {
		if (!empty($item['probo_price'])) {
			$item['data']->set_price($item['probo_price']);
		}
	}
});

Let me know if you'd like:
– The selected option images shown in cart/checkout
– A delivery time selector linked to the correct price
– To disable native WooCommerce "Add to cart" button on product page

This setup ensures your `connect-product-configurator` behaves like a true product customizer.

User
but this rsst api that cofnirue in element already return
data so why sibmit
Assistant
Great observation!

You're absolutely right: the “ Web Component **already fetches the configuration and pricing via the proxy API**, and **you receive the full configuration data (including `calculation_id`, `selected_options`, and pricing)** without needing to hit the REST API again.

But here's the **important clarification**:

## 🧩 Why We Still Use the `submit` Event

Even though the Web Component internally calls the Probo REST API and gets the data:

### ✅ The `submit` event is the **only reliable moment** when:
– The user **completes the configuration**.
– The component **emits the full config result** including:
– `calculation_id`
– `products`
– `selected_options`
– `price`
– `uploaders` (if enabled)

> So the `submit` event is your trigger to collect the finished data and send it to your cart.

## 🧠 Think of It Like This

| Component Part | Responsibility |
|—————————-|———————————————-|
| `proxy`, `language`, `product-code` | Tells component where to fetch config data |
| Internal REST call | Gets options, renders configurator UI |
| `submit` event | Emits finalized configuration (user-confirmed) ✅ |

## ✅ Summary

– You don’t call the REST API again manually.
– The `submit` event gives you **finalized user choices**, not partial or intermediate values.
– That’s why you **wait for `submit`** → then send that data to WooCommerce via AJAX.

## 🚀 Your Flow Is Already Correct

So keep this:

configurator.addEventListener("submit", async (event) => {
	const configData = event.detail;
	// use configData.calculation_id, selected_options, price etc.
});

You are **not duplicating REST requests** — you’re just **listening for the result** of the component’s internal logic.

Let me know if you want to:
– Show a mini summary (width, height, material…) on the page after config.
– Capture uploaded file names from `configData.uploaders`.
– Automatically add to cart without button click (on submit).

User
{
"status": "ok",
"code": 200,
"message": "Options calculated",
"products": [
{
"id": 1,
"code": "banner",
"can_order": true,
"amount": 2,
"width": 25,
"height": 15,
"length": 0,
"available_options": [],
"selected_options": [
{
"type_code": "width",
"name": "Breedte",
"label": null,
"description": null,
"value": "25",
"code": "width",
"default_value": null,
"min_value": 0,
"max_value": 99999,
"step_size": null,
"scale": 1,
"reversible": true,
"parent_code": "size",
"parent_name": "Afmeting",
"unit_code": "cm",
"images": []
},
{
"type_code": "height",
"name": "Hoogte",
"label": null,
"description": null,
"value": "15",
"code": "height",
"default_value": null,
"min_value": 0,
"max_value": 99999,
"step_size": null,
"scale": 1,
"reversible": true,
"parent_code": "size",
"parent_name": "Afmeting",
"unit_code": "cm",
"images": []
},
{
"type_code": "amount",
"name": "Hoeveelheid",
"label": null,
"description": null,
"value": "2",
"code": "amount",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "amount",
"parent_name": "Hoeveelheid",
"unit_code": "pc",
"images": []
},
{
"type_code": "radio",
"name": "Enkelzijdig",
"label": null,
"description": null,
"value": null,
"code": "single-sided",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "print-side",
"parent_name": "Print",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/122/1157641fbfa8a2d9.svg"
}
]
},
{
"type_code": "radio",
"name": "Soundmesh",
"label": null,
"description": "Lucht- en geluiddoorlatend PVC doek met een gewicht van 180 gr./m2.nMinimale afname van 10 m2",
"value": null,
"code": "soundmesh",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "material",
"parent_name": "Materiaal",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/202408/8805/8f3d55f833365265.jpg"
}
]
},
{
"type_code": "radio",
"name": "Voor alle zijden gelijk",
"label": null,
"description": "Alle zijden van het doek krijgen dezelfde afwerking.",
"value": null,
"code": "finishing-all-sides",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "finishing-per-side-all-sides",
"parent_name": "Afwerking voor alle zijden / per zijde",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/72/99e39932389f5591.svg"
}
]
},
{
"type_code": "radio",
"name": "Zomen en ringen",
"label": "Meest gekozen",
"description": "Voor de bevestiging krijgt het doek rondom een zoom van 4 cm en ringen van nikkel. De zoom wordt met hitte gelast.",
"value": null,
"code": "hem-and-rings",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "finishing",
"parent_name": "Gewenste afwerking",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/84/af0c678a9f4c4248.jpg"
}
]
},
{
"type_code": "radio",
"name": "In elke hoek",
"label": null,
"description": null,
"value": null,
"code": "every-corner",
"default_value": null,
"min_value": null,
"max_value": null,
"step_size": null,
"scale": null,
"reversible": null,
"parent_code": "position-rings",
"parent_name": "Positie ringen",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/201803/76/e437bd1efbb3afdd.svg"
}
]
},
{
"type_code": "cross_sell_pc",
"name": "Tiewrap – Wit – set van 20 stuks",
"label": null,
"description": null,
"value": "1",
"code": "tiewrap-white-20",
"default_value": "ceil(IA*(({AF-UV-RING:CA}/IA)/20))",
"min_value": null,
"max_value": null,
"step_size": 1,
"scale": null,
"reversible": null,
"parent_code": "accessories-cross-sell",
"parent_name": "Accessoires",
"unit_code": null,
"images": [
{
"language": "all",
"url": "https://cdn.print-uploader.com/202503/10350/da91c98b63e775f6.png"
}
]
}
],
"upload": true,
"uploaders": [
{
"type": "single",
"amount": 2,
"width": 25,
"height": 15,
"length": 0,
"minimal_dpi": 72,
"mirror_enabled": true,
"fill_enabled": true,
"rotation_enabled": true,
"file_limit": 500,
"require_white_spot": false,
"required_cut_names": [],
"optional_cut_names": [],
"tiling_enabled": false,
"tiling_mandatory": false,
"tiling_direction": null,
"max_width_excl_overlap": 490,
"max_height_excl_overlap": 2396,
"max_width_incl_overlap": 486,
"max_height_incl_overlap": 2392,
"bleed_top": 1,
"bleed_right": 1,
"bleed_bottom": 1,
"bleed_left": 1
}
],
"linked_products": [
{
"title": "Soundmesh",
"description": "Soundmesh",
"code": "soundmesh",
"article_group_code": "MA",
"unit_code": "m2",
"shipping_weight": 0.2
}
],
"prices_total": {
"24": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 40,
"purchase_price": 186.2,
"purchase_price_incl_vat": 225.3,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"48": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 30,
"purchase_price": 176.2,
"purchase_price_incl_vat": 213.2,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"72": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 15,
"purchase_price": 161.2,
"purchase_price_incl_vat": 195.05,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"96": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"120": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"144": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"168": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"192": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"216": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"240": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"264": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"288": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"312": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"336": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"360": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"384": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"408": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"432": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"456": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"480": {
"purchase_base_price": 146.2,
"purchase_rush_surcharge": 0,
"purchase_price": 146.2,
"purchase_price_incl_vat": 176.9,
"sales_price": 0,
"sales_price_incl_vat": 0
}
},
"prices_per_product": {
"24": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 20,
"purchase_price": 93.1,
"purchase_price_incl_vat": 112.651,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"48": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 15,
"purchase_price": 88.1,
"purchase_price_incl_vat": 106.601,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"72": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 7.5,
"purchase_price": 80.6,
"purchase_price_incl_vat": 97.526,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"96": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"120": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"144": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"168": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"192": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"216": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"240": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"264": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"288": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"312": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"336": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"360": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"384": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"408": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"432": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"456": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
},
"480": {
"purchase_base_price": 73.1,
"purchase_rush_surcharge": 0,
"purchase_price": 73.1,
"purchase_price_incl_vat": 88.451,
"sales_price": 0,
"sales_price_incl_vat": 0
}
}
}
],
"calculation_id": 80386353
}
what is price_per_product
Assistant
Great question!

### 🔍 `prices_per_product` in the Probo API response refers to:
> ✅ The **unit price** (price per single product) based on the selected **delivery time in hours**.

### 🧠 Key Difference:

| Field | What it means |
|————————–|——————————————-|
| `prices_total` | Total price for the **entire quantity** |
| `prices_per_product` | Price **per unit** of the product |

### 🔧 Example:

From your data (delivery time `96` hours):

"prices_total": {
  "96": {
    "purchase_base_price": 146.2,
    "purchase_price_incl_vat": 176.9
  }
},
"prices_per_product": {
  "96": {
    "purchase_base_price": 73.1,
    "purchase_price_incl_vat": 88.451
  }
}

#### Assuming:
– Quantity = 2
– Unit price = `88.451`
– Total = `88.451 x 2 = 176.902` → matches `prices_total['96'].purchase_price_incl_vat`

### ✅ When to Use Which?

| Use Case | Use `prices_total` | Use `prices_per_product` |
|—————————-|————————-|—————————|
| Add total line item price | ✅ Yes | ❌ No |
| Display unit price | ❌ Optional (for info) | ✅ Yes |
| Store for WooCommerce cart | ✅ Use total price | Optional, but you can show unit price too |

### 🔧 Final Suggestion for WooCommerce Integration

You should use:

const price = configData.products[0].prices_total["96"]?.purchase_price_incl_vat;

And optionally store/display:

const unitPrice = configData.products[0].prices_per_product["96"]?.purchase_price_incl_vat;
const quantity = configData.products[0].amount;

Then in WooCommerce:
– Save both as cart meta (optional)
– Use `total` for cart pricing
– Use `unit` only for display (optional)

Let me know if you want help calculating price dynamically based on user-chosen delivery time or showing both prices in the cart.

User
what is linked_products
Assistant
Great question!

### 🔍 `linked_products` in the Probo API response represents:

> ✅ **Extra items** or **associated products** that are related to the main product configuration.
These are often **accessories** or **materials** that need to be ordered along with the main item.

### 💡 Example from your data:

"linked_products": [
  {
    "title": "Soundmesh",
    "description": "Soundmesh",
    "code": "soundmesh",
    "article_group_code": "MA",
    "unit_code": "m2",
    "shipping_weight": 0.2
  }
]

### 🧠 This means:
– A **material** called **Soundmesh** is required (likely selected or implied by the configuration).
– It’s a **related product** with:
– `code`: "soundmesh" — used in backend systems
– `unit_code`: "m2" — likely priced per square meter
– `shipping_weight`: 0.2 — per unit (e.g., per m²)

### ✅ Common Use Cases for `linked_products`:

| Use Case | Example |
|———-|———|
| Display info in cart/order summary | “Includes material: Soundmesh (0.2kg per m²)” |
| Pass to fulfillment system | So the API knows what material/code to ship |
| Charge separately (advanced case) | If each linked product has its own SKU and cost |

### ✅ How to Use in WooCommerce (Recommended):

You **can store it in the cart item meta** like this (PHP):

if (!empty($config['linked_products'])) {
	wc_add_order_item_meta($item_id, '_probo_linked_products', $config['linked_products']);
}

And **display it** in the cart or order emails like:

$linked = wc_get_order_item_meta($item->get_id(), '_probo_linked_products', true);
if ($linked && is_array($linked)) {
	echo '<ul><strong>Included Materials:</strong>';
	foreach ($linked as $lp) {
		echo '<li>' . esc_html($lp['title']) . ' (' . esc_html($lp['unit_code']) . ')</li>';
	}
	echo '</ul>';
}

### 🛑 Note:
– `linked_products` are not user-selected separately — they are **attached automatically** based on the main configuration (like a dependency).
– They’re **informational**, unless you have a use case to bill or display them prominently.

Let me know if you want:
– These linked products shown on the cart page or order summary
– A way to use their weights for shipping calculation
– To show them as a line item (separately) in WooCommerce

User
<script>
document.addEventListener("DOMContentLoaded", () => {
	const configurator = document.querySelector("connect-product-configurator");
	const addToCartBtn = document.getElementById("launchConfigurator");

	addToCartBtn.disabled = true; // Disable by default

	let proboCartData = null;

	configurator?.addEventListener("submit", async (event) => {
		const detail = event.detail;

		// Check for calculation ID and product data
		if (!detail?.calculation_id || !detail?.products?.[0]) {
			console.error("Invalid configurator response");
			alert("Configuration is incomplete or invalid.");
			return;
		}

		const productId = <?php echo intval($product->get_id()); ?>;
		const selectedOptions = detail.products[0].selected_options || [];
		const price = detail.products[0].prices_total?.["96"]?.purchase_price_incl_vat || 0;

		// Store cart data for later use
		proboCartData = {
			product_id: productId,
			calculation_id: detail.calculation_id,
			selected_options: selectedOptions,
			price: price
		};

		addToCartBtn.disabled = false; // Enable the button
	});

	addToCartBtn?.addEventListener("click", async () => {
		if (!proboCartData) {
			alert("Please finish customizing the product before adding to cart.");
			return;
		}

		const res = await fetch("/wp-admin/admin-ajax.php?action=add_probo_to_cart", {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify(proboCartData)
		});

		const result = await res.json();
		if (result.success) {
			window.location.href = result.redirect || "/cart";
		} else {
			alert("Error: " + result.message);
		}
	});
});
</script>

not working

Assistant
Thanks for sharing your current JS block. Let's troubleshoot and resolve this properly.

## ✅ 1. Double-Check: Is the `submit` event from “ really firing?

The most likely reasons it's **not working** are:

### 🔴 Either:
– The `submit` event is not fired by the component.
– Or the event name is different (e.g., `complete`, `configuratorSubmit`, etc.).
– Or it fires but `event.detail` is missing or malformed.
– Or the JS block runs **before the component is fully defined**.

## ✅ 2. Confirm If Event Fires (Add Logging Early)

Update your code to **log ALL events** emitted from the component:

document.addEventListener("DOMContentLoaded", () => {
	const configurator = document.querySelector("connect-product-configurator");
	const addToCartBtn = document.getElementById("launchConfigurator");

	if (!configurator) {
		console.error("Configurator element not found!");
		return;
	}

	// Log all custom events
	configurator.addEventListener("submit", (event) => {
		console.log("EVENT: submit", event.detail);
	});
	configurator.addEventListener("change", (event) => {
		console.log("EVENT: change", event.detail);
	});
	configurator.addEventListener("calculated", (event) => {
		console.log("EVENT: calculated", event.detail);
	});
	configurator.addEventListener("completed", (event) => {
		console.log("EVENT: completed", event.detail);
	});

	// Disable button by default
	addToCartBtn.disabled = true;

	let proboCartData = null;

	// Handle submit or completed
	configurator.addEventListener("submit", (event) => {
		const detail = event.detail;
		console.log("Configurator Submitted:", detail);

		if (!detail?.calculation_id || !detail?.products?.[0]) {
			alert("Invalid configurator data.");
			return;
		}

		const productId = <?php echo intval($product->get_id()); ?>;
		const selectedOptions = detail.products[0].selected_options || [];
		const price = detail.products[0].prices_total?.["96"]?.purchase_price_incl_vat || 0;

		proboCartData = {
			product_id: productId,
			calculation_id: detail.calculation_id,
			selected_options: selectedOptions,
			price: price
		};

		addToCartBtn.disabled = false;
	});

	// Handle Add to Cart click
	addToCartBtn.addEventListener("click", async () => {
		if (!proboCartData) {
			alert("Please complete configuration first.");
			return;
		}

		const res = await fetch("/wp-admin/admin-ajax.php?action=add_probo_to_cart", {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify(proboCartData)
		});

		const result = await res.json();
		if (result.success) {
			window.location.href = result.redirect || "/cart";
		} else {
			alert("Error: " + result.message);
		}
	});
});

## ✅ 3. Check in Browser Console

1. Open your page in Chrome.
2. Go to **Developer Tools → Console tab**.
3. Customize a product → See if any of the events (`submit`, `completed`, etc.) appear.
4. If **none appear**, then the event name is different or the component isn't emitting anything.

## ✅ 4. Next Steps Based on Result

| What you see | What to do |
|————–|————|
| `submit` logged | Your script works. You may have misconfigured pricing or button |
| No event logged | Contact Probo or check their Web Component documentation — the event might be called `completed`, `configuratorFinished`, or require a callback |
| Error in JS console | The component might not be initialized (e.g., wrong script or JS file missing) |

## ✅ Also Double-Check

– The component script is loaded (no 404 errors for `connect-product-configurator.js`).
– You're not calling `window.connectConfigurator.init()` if you're using the tag directly.
– No JS syntax errors above the script tag.

If you'd like, I can debug live with the HTML/JS code in place — or you can paste any console log/error here.

Leave a Comment

Your email address will not be published. Required fields are marked *