db = $db; // Render upload cards UI at priority 35 on single product page. // Also suppress pvtfw's variant table + available-options button on FPP products. add_action('wp', array($this, 'maybe_suppress_pvtfw'), 5); // Read-only price overview rendered right above the dropzone (1.5.12+). add_action('woocommerce_single_product_summary', array($this, 'render_variant_price_table'), 34); add_action('woocommerce_single_product_summary', array($this, 'render_upload_area'), 35); // Add a body class on FPP product pages for CSS scoping add_filter('body_class', array($this, 'body_class')); // Server-side validation: block add-to-cart without photo (priority 1 = very early) add_filter('woocommerce_add_to_cart_validation', array($this, 'validate_add_to_cart'), 1, 6); // Safety net: if item was added without photo, remove it immediately add_action('woocommerce_add_to_cart', array($this, 'check_after_add_to_cart'), 10, 6); } /** * Detach pvtfw's variant-table + available-options-button hooks on FPP products. * * pvtfw assigns its singletons to `$pvtfw_print_table` / `$pvtfw_available_btn` at the * top of its class files, but the `require_once` call lives inside pvtfw's * PVTFW_TABLE::includes() method — so those assignments end up as method-locals, NOT * true globals. We therefore reach the singletons via their static ::instance() * (the same object pvtfw registered the actions with). The variant-table hook and * priority are derived from pvtfw's own pvtfw_variant_table_place option (default * woocommerce_after_single_product_summary_9) so admin-configured placements * remain supported. */ public function maybe_suppress_pvtfw() { if (!function_exists('is_product') || !is_product()) { return; } $product_id = get_queried_object_id(); if (!$product_id || !Studiou_WC_FPP_Product::is_fpp_product($product_id)) { return; } if (class_exists('PVTFW_PRINT_TABLE')) { $print_table = PVTFW_PRINT_TABLE::instance(); $place = get_option('pvtfw_variant_table_place', 'woocommerce_after_single_product_summary_9'); $tail = strrchr((string) $place, '_'); if ($tail !== false && strlen($tail) > 1) { $priority = (int) ltrim($tail, '_'); $hook = substr($place, 0, -strlen($tail)); if ($hook) { remove_action($hook, array($print_table, 'print_table'), $priority); } } } elseif (defined('WP_DEBUG') && WP_DEBUG) { error_log('STUDIOU FPP: PVTFW_PRINT_TABLE class not found; cannot detach pvtfw variant table.'); } // Note: pvtfw's class name has a typo ("AVAILABE" missing the L) — match it verbatim if (class_exists('PVTFW_AVAILABE_BTN')) { $available_btn = PVTFW_AVAILABE_BTN::instance(); remove_action('woocommerce_single_product_summary', array($available_btn, 'available_options_btn'), 11); } // Hide WC's native price-range element (e.g. "10,00 Kč – 40,00 Kč") on FPP products. // The variant price table we render at priority 34 already communicates per-variant // pricing, and the range is visually noisy above it. remove_action('woocommerce_single_product_summary', 'woocommerce_template_single_price', 10); } public function body_class($classes) { if (function_exists('is_product') && is_product()) { $product_id = get_queried_object_id(); if ($product_id && Studiou_WC_FPP_Product::is_fpp_product($product_id)) { $classes[] = 'studiou-fpp-product-page'; } } return $classes; } public function render_upload_area() { global $product; if (!$product || !Studiou_WC_FPP_Product::is_fpp_product($product->get_id())) { return; } wp_enqueue_style('studiou-wcfpp-frontend'); wp_enqueue_script('studiou-wcfpp-frontend'); $product_id = $product->get_id(); $max_file_size = Studiou_WC_FPP_Product::get_product_max_file_size($product_id); $max_uploads = Studiou_WC_FPP_Product::get_product_max_uploads($product_id); // Variant list (id, label, base_price, tiers) — injected into the files-cards JS $variants = $this->build_variant_list($product); // Existing batch files (rehydrate the card stack across reloads) $batch_files = self::resolve_batch_files($this->db, $product_id); $batch_cards = array(); $hero_preview_url = ''; foreach ($batch_files as $row) { $att_id = (int) $row->attachment_id; $thumb = wp_get_attachment_image_src($att_id, 'thumbnail'); $preview = wp_get_attachment_image_src($att_id, 'woocommerce_single'); if ($hero_preview_url === '') { $hero_preview_url = $preview ? $preview[0] : ($thumb ? $thumb[0] : ''); } $batch_cards[] = array( 'file_record_id' => (int) $row->id, 'attachment_id' => $att_id, 'file_name' => (string) $row->file_name, 'thumb_url' => $thumb ? $thumb[0] : ($preview ? $preview[0] : ''), 'preview_url' => $preview ? $preview[0] : ($thumb ? $thumb[0] : ''), 'variation_id' => (int) $row->variation_id, ); } wp_localize_script('studiou-wcfpp-frontend', 'studiouFppCardData', array( 'productId' => $product_id, 'variants' => $variants, 'maxUploads' => $max_uploads, 'batchFiles' => $batch_cards, 'heroPreviewUrl' => $hero_preview_url, )); // Keep studiouWcfppSession populated for legacy JS code paths that still read it wp_localize_script('studiou-wcfpp-frontend', 'studiouWcfppSession', array()); include STUDIOU_WCFPP_PLUGIN_DIR . 'views/single-product-upload.php'; if (is_product() && Studiou_WC_FPP_Product::is_fpp_product($product_id)) { // Render the order-overview panel below the cards (see views/single-product-order-overview.php) $cart = function_exists('WC') ? WC()->cart : null; include STUDIOU_WCFPP_PLUGIN_DIR . 'views/single-product-order-overview.php'; } } /** * Read-only variant price/discount table rendered above the dropzone (1.5.12+). * Customers see all variant prices and the tiered quantity discounts before uploading. * Purely informational — no radio/select/qty/add-to-cart. */ public function render_variant_price_table() { global $product; if (!$product || !Studiou_WC_FPP_Product::is_fpp_product($product->get_id())) { return; } $variants = $this->build_variant_list($product); if (empty($variants)) { return; } include STUDIOU_WCFPP_PLUGIN_DIR . 'views/single-product-price-table.php'; } /** * Produce the array of variation descriptors consumed by the upload-card JS. * One entry per in-stock, purchasable variation: id, label, base_price, tiers. */ private function build_variant_list($product) { if (!$product || !$product->is_type('variable')) { return array(); } $variants = array(); foreach ($product->get_children() as $variation_id) { $variation = wc_get_product($variation_id); if (!$variation || !$variation->is_purchasable() || !$variation->variation_is_visible()) { continue; } $base_price = get_post_meta($variation_id, '_price', true); $label_parts = array(); foreach ($variation->get_attributes() as $attr_name => $attr_value) { if ($attr_value === '') { continue; } $taxonomy = str_replace('attribute_', '', $attr_name); $term = taxonomy_exists($taxonomy) ? get_term_by('slug', $attr_value, $taxonomy) : null; // Term names or non-tax values may carry HTML entities ( , K, // á, …) pasted in by admins.