'DEUCES-01', 'object_type' => 'prodavaonica', 'filename_prefix' => '', 'filename_suffix' => '', ); } private function settings() { $saved = get_option(self::SETTINGS, array()); return wp_parse_args(is_array($saved) ? $saved : array(), $this->defaults()); } public function sanitize_settings($input) { $input = is_array($input) ? $input : array(); $settings = $this->defaults(); foreach ($settings as $key => $default) { if (isset($input[$key]) && is_scalar($input[$key])) { $settings[$key] = sanitize_text_field(wp_unslash($input[$key])); } } return $settings; } public function admin_menu() { add_menu_page('Deuces cjenici', 'Deuces cjenici', 'manage_woocommerce', 'deuces-cjenici', array($this, 'admin_page'), 'dashicons-media-spreadsheet', 56); add_submenu_page('deuces-cjenici', 'Proizvodi i sidrene cijene', 'Proizvodi', 'manage_woocommerce', 'deuces-cjenici', array($this, 'admin_page')); add_submenu_page('deuces-cjenici', 'Postavke cjenika', 'Postavke', 'manage_woocommerce', 'deuces-cjenici-settings', array($this, 'settings_page')); } private function can_manage() { return current_user_can('manage_woocommerce'); } public function product_fields() { echo '
'; woocommerce_wp_text_input(array( 'id' => '_deuces_anchor_price', 'label' => 'Sidrena cijena (10.09.2026.)', 'desc_tip' => true, 'description' => 'Cijena koja je vrijedila na referentni datum.', 'type' => 'text', )); woocommerce_wp_text_input(array('id' => '_deuces_barcode', 'label' => 'Barkod')); woocommerce_wp_text_input(array('id' => '_deuces_brand', 'label' => 'Marka')); woocommerce_wp_text_input(array('id' => '_deuces_unit', 'label' => 'Jedinica prodaje')); woocommerce_wp_text_input(array('id' => '_deuces_unit_price', 'label' => 'Jedinična cijena')); echo '
'; } public function variation_fields($loop, $variation_data, $variation) { $id = $variation->ID; $anchor = $this->scalar_meta($id, '_deuces_anchor_price'); $barcode = $this->scalar_meta($id, '_deuces_barcode'); echo '

'; woocommerce_wp_text_input(array('id' => '_deuces_anchor_price[' . $loop . ']', 'name' => '_deuces_anchor_price[' . $loop . ']', 'value' => $anchor, 'label' => 'Sidrena cijena', 'wrapper_class' => 'form-row form-row-first', 'type' => 'text')); woocommerce_wp_text_input(array('id' => '_deuces_barcode[' . $loop . ']', 'name' => '_deuces_barcode[' . $loop . ']', 'value' => $barcode, 'label' => 'Barkod', 'wrapper_class' => 'form-row form-row-last', 'type' => 'text')); echo '

'; } public function save_product_fields($post_id) { if (!$this->can_manage() || (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE)) { return; } $keys = array('_deuces_anchor_price', '_deuces_barcode', '_deuces_brand', '_deuces_unit', '_deuces_unit_price'); foreach ($keys as $key) { if (!isset($_POST[$key]) || is_array($_POST[$key])) { continue; } $value = wc_clean(wp_unslash($_POST[$key])); update_post_meta($post_id, $key, is_scalar($value) ? $value : ''); } $this->invalidate_map($post_id); } private function posted_variation_value($key, $variation_id, $loop) { if (!isset($_POST[$key]) || !is_array($_POST[$key])) { return null; } $values = wp_unslash($_POST[$key]); if (array_key_exists($variation_id, $values) && is_scalar($values[$variation_id])) { return $values[$variation_id]; } if (array_key_exists($loop, $values) && is_scalar($values[$loop])) { return $values[$loop]; } return null; } public function save_variation_fields($variation_id, $loop) { if (!$this->can_manage()) { return; } foreach (array('_deuces_anchor_price', '_deuces_barcode') as $key) { $value = $this->posted_variation_value($key, $variation_id, $loop); if ($value === null) { continue; } update_post_meta($variation_id, $key, wc_clean($value)); } $this->invalidate_map($variation_id); } public function scalar_meta($post_id, $key) { $value = get_post_meta((int) $post_id, $key, true); return is_scalar($value) ? (string) $value : ''; } private function product_object($product) { if (is_numeric($product)) { $product = wc_get_product((int) $product); } return $product instanceof WC_Product ? $product : false; } public function resolve_anchor($product) { $product = $this->product_object($product); if (!$product) { return ''; } $own = $this->scalar_meta($product->get_id(), '_deuces_anchor_price'); if ($own !== '') { return $own; } if (!$product->is_type('variable')) { return ''; } $values = array(); foreach ($product->get_children() as $child_id) { $value = $this->scalar_meta($child_id, '_deuces_anchor_price'); if ($value !== '' && is_numeric($value)) { $values[] = (float) $value; } } if (!$values) { return ''; } $values = array_values(array_unique($values, SORT_NUMERIC)); sort($values, SORT_NUMERIC); return count($values) === 1 ? (string) $values[0] : array('min' => $values[0], 'max' => $values[count($values) - 1]); } private function resolve_variation_meta($product, $key) { $product = $this->product_object($product); if (!$product) { return ''; } $own = $this->scalar_meta($product->get_id(), $key); if ($own !== '') { return $own; } if ($product->is_type('variable')) { foreach ($product->get_children() as $child_id) { $value = $this->scalar_meta($child_id, $key); if ($value !== '') { return $value; } } } return ''; } public function resolve_barcode($product) { return $this->resolve_variation_meta($product, '_deuces_barcode'); } public function resolve_brand($product) { return $this->resolve_variation_meta($product, '_deuces_brand'); } private function anchor_number($anchor) { if (is_array($anchor)) { return ''; } return is_scalar($anchor) && is_numeric($anchor) ? (float) $anchor : ''; } private function anchor_html($anchor) { if (is_array($anchor) && isset($anchor['min'], $anchor['max'])) { return wp_kses_post(wc_price($anchor['min']) . ' – ' . wc_price($anchor['max'])); } $number = $this->anchor_number($anchor); return $number === '' ? '' : wp_kses_post(wc_price($number)); } private function anchor_label($anchor) { $html = $this->anchor_html($anchor); return $html === '' ? '' : 'Cijena na 10.09.2026.: ' . $html . ''; } public function price_html($html, $product) { if (!$product instanceof WC_Product || is_product()) { return $html; } $line = $this->loop_anchor_fallback($product); return $line === '' ? $html : $html . $line; } public function loop_anchor_fallback($product) { $product = $this->product_object($product); if (!$product) { return ''; } $anchor = $this->resolve_anchor($product); $label = $this->anchor_label($anchor); if ($label === '') { return ''; } return '' . wp_kses_post($label) . ''; } public function single_product_anchor_line() { if (!is_product()) { return; } global $product; if (!$product instanceof WC_Product) { return; } $line = $this->anchor_label($this->resolve_anchor($product)); if ($line !== '') { echo '

' . wp_kses_post($line) . '

'; } } private function item_products() { $ids = wc_get_products(array('limit' => -1, 'status' => array('publish', 'private'), 'return' => 'ids')); $items = array(); foreach ($ids as $id) { $product = wc_get_product($id); if (!$product) { continue; } if ($product->is_type('variable') && $product->get_children()) { foreach ($product->get_children() as $child_id) { $child = wc_get_product($child_id); if ($child) { $items[] = $child; } } } elseif (!$product->is_type('variation')) { $items[] = $product; } } return $items; } private function row_name($product) { $name = $product->get_name(); if ($product->is_type('variation') && $product->get_parent_id()) { $parent = wc_get_product($product->get_parent_id()); if ($parent) { $name = $parent->get_name() . ' ' . wc_get_formatted_variation($product, true, false, true); } } return trim(wp_strip_all_tags($name)); } private function clean_line($value) { return preg_replace('/[\r\n\t]+/u', ' ', (string) $value); } public function csv_value($value) { if (is_array($value)) { if (isset($value['min'], $value['max'])) { $min = is_numeric($value['min']) ? number_format((float) $value['min'], 2, '.', '') : ''; $max = is_numeric($value['max']) ? number_format((float) $value['max'], 2, '.', '') : ''; return $min !== '' && $max !== '' ? $min . '-' . $max : ''; } return ''; } if (!is_scalar($value)) { return ''; } $value = (string) $value; if (strcasecmp(trim($value), 'array') === 0) { return ''; } if (function_exists('mb_check_encoding') && !mb_check_encoding($value, 'UTF-8')) { $value = function_exists('mb_convert_encoding') ? mb_convert_encoding($value, 'UTF-8', 'auto') : $value; } if (function_exists('iconv')) { $value = iconv('UTF-8', 'UTF-8//IGNORE', $value); } return (string) $value; } private function decimal($value) { if ($value === '' || $value === null || is_array($value) || !is_numeric(str_replace(',', '.', (string) $value))) { return ''; } $value = str_replace(',', '.', (string) $value); return str_replace(',', '.', wc_format_decimal($value, 2)); } private function decimal_or_range($value) { if (is_array($value) && isset($value['min'], $value['max'])) { return $this->decimal($value['min']) . '-' . $this->decimal($value['max']); } return $this->decimal($value); } public function rows() { $products = $this->item_products(); $rows = array(); $report = array('products' => count($products), 'rows' => 0, 'on_sale' => 0, 'on_sale_without_anchor' => 0, 'empty_anchor' => 0, 'empty_anchor_not_on_sale' => 0, 'empty_barcode' => 0, 'empty_sku' => 0, 'skipped' => 0, 'issues' => array()); foreach ($products as $product) { $name = $this->clean_line($this->row_name($product)); $sku = $this->clean_line($product->get_sku()); if ($name === '' && $sku === '') { $report['skipped']++; continue; } $anchor = $this->resolve_anchor($product); $anchor_for_csv = $this->csv_value($anchor); $barcode = $this->clean_line($this->resolve_barcode($product)); $brand = $this->clean_line($this->resolve_brand($product)); $on_sale = $product->is_on_sale(); if ($on_sale) { $report['on_sale']++; } if ($anchor_for_csv === '') { $report['empty_anchor']++; if ($on_sale) { $report['on_sale_without_anchor']++; } else { $report['empty_anchor_not_on_sale']++; } } if ($barcode === '') { $report['empty_barcode']++; } if ($sku === '') { $report['empty_sku']++; } if ($anchor_for_csv === '' || $barcode === '') { if (count($report['issues']) < 200) { $report['issues'][] = $name . ' | SKU: ' . ($sku !== '' ? $sku : '—') . ' | sidrena: ' . ($anchor_for_csv !== '' ? $anchor_for_csv : 'prazna') . ' | barkod: ' . ($barcode !== '' ? $barcode : 'prazan'); } } $unit = $this->scalar_meta($product->get_id(), '_deuces_unit'); $unit_price = $this->scalar_meta($product->get_id(), '_deuces_unit_price'); $rows[] = array( $this->csv_value($name), $this->csv_value($sku), $this->csv_value($brand), $this->decimal($product->get_price()), $on_sale ? 'DA' : 'NE', $on_sale ? 'Akcija' : '', $this->decimal_or_range($anchor), $this->csv_value($barcode), $product->is_in_stock() ? 'DA' : 'NE', ); } $report['rows'] = count($rows); $this->last_report = $report; return $rows; } public function filename($date = null) { $settings = $this->settings(); $parts = array($settings['filename_prefix'], $settings['object_type'], $settings['object_code'], $settings['filename_suffix'], $date ?: current_time('Y-m-d')); $parts = array_values(array_filter(array_map(function ($part) { return sanitize_file_name((string) $part); }, $parts), function ($part) { return $part !== ''; })); return ($parts ? implode('_', $parts) : 'deuces') . '.csv'; } private function upload_dir() { $uploads = wp_upload_dir(); $dir = trailingslashit($uploads['basedir']) . 'deuces-cjenici'; if (!is_dir($dir)) { wp_mkdir_p($dir); } return $dir; } private function report_text($report, $filename) { $lines = array( 'Izvještaj za ' . $filename, 'Broj proizvoda: ' . (int) $report['products'], 'Broj redaka: ' . (int) $report['rows'], 'Redaka na akciji: ' . (int) $report['on_sale'], 'Redaka na akciji bez sidrene cijene: ' . (int) $report['on_sale_without_anchor'], 'Redaka s praznom sidrenom cijenom: ' . (int) $report['empty_anchor'], 'Redaka koji nisu na akciji s praznom sidrenom cijenom: ' . (int) $report['empty_anchor_not_on_sale'], 'Redaka s praznim barkodom: ' . (int) $report['empty_barcode'], 'Proizvoda s praznom šifrom: ' . (int) $report['empty_sku'], 'Preskočenih redaka bez naziva i šifre: ' . (int) $report['skipped'], '', 'Redci s praznom sidrenom cijenom ili barkodom (najviše 200):', ); $lines = array_merge($lines, $report['issues']); $lines[] = ''; $lines[] = 'error_log: sidrena=' . (int) $report['empty_anchor'] . '; barkod=' . (int) $report['empty_barcode'] . '; preskočeno=' . (int) $report['skipped'] . '.'; return implode("\n", $lines) . "\n"; } private function generate_csv() { $filename = $this->filename(); $path = trailingslashit($this->upload_dir()) . $filename; $file = fopen($path, 'wb'); if (!$file) { return new WP_Error('deuces_cjenik_file', 'CSV datoteka nije se mogla otvoriti za pisanje.'); } fwrite($file, "\xEF\xBB\xBF"); fputcsv($file, array('naziv', 'sifra', 'marka', 'maloprodajna_cijena', 'posebni_oblik_prodaje', 'naziv_posebnog_oblika', 'sidrena_cijena', 'barkod', 'dostupnost'), ';', '"', ''); $rows = $this->rows(); foreach ($rows as $row) { fputcsv($file, $row, ';', '"', ''); } fclose($file); $report = $this->last_report; $report_date = preg_match('/(\d{4}-\d{2}-\d{2})/', $filename, $match) ? str_replace('-', '', $match[1]) : current_time('Ymd'); $report_path = trailingslashit($this->upload_dir()) . '_report_' . $report_date . '.txt'; file_put_contents($report_path, $this->report_text($report, $filename)); $this->cleanup_files(); return array('path' => $path, 'filename' => $filename, 'report' => $report); } public function generate_csv_action() { if (!$this->can_manage() || !check_admin_referer('deuces_cjenik_generate')) { wp_die(esc_html__('Niste ovlašteni za ovu radnju.', 'deuces-cjenik')); } $result = $this->generate_csv(); $url = add_query_arg(array('page' => 'deuces-cjenici', 'generated' => is_wp_error($result) ? 0 : 1), admin_url('admin.php')); wp_safe_redirect($url); exit; } private function cleanup_files() { $dir = $this->upload_dir(); foreach (array('*.csv', '_report_*.txt') as $pattern) { $files = glob(trailingslashit($dir) . $pattern); if (!$files) { continue; } usort($files, function ($a, $b) { return filemtime($b) <=> filemtime($a); }); foreach ($files as $index => $file) { if ($index < 5 || filemtime($file) >= time() - (30 * DAY_IN_SECONDS)) { continue; } wp_delete_file($file); } } } public function cleanup() { if (get_option(self::CLEANUP_OPTION, false)) { return; } $ids = get_posts(array('post_type' => 'product', 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids')); $keys = array('_deuces_anchor_price', '_deuces_barcode', '_deuces_brand', '_deuces_unit', '_deuces_unit_price'); foreach ($ids as $id) { foreach ($keys as $key) { if (is_array(get_post_meta($id, $key, true))) { delete_post_meta($id, $key); } } } update_option(self::CLEANUP_OPTION, current_time('mysql'), false); } public function audit() { $result = array('anchor' => 0, 'barcode' => 0, 'brand' => 0, 'on_sale_without_anchor' => 0); foreach ($this->item_products() as $product) { $anchor = $this->scalar_meta($product->get_id(), '_deuces_anchor_price'); $barcode = $this->scalar_meta($product->get_id(), '_deuces_barcode'); $brand = $this->scalar_meta($product->get_id(), '_deuces_brand'); if ($anchor === '') { $result['anchor']++; if ($product->is_on_sale()) { $result['on_sale_without_anchor']++; } } if ($barcode === '') { $result['barcode']++; } if ($brand === '') { $result['brand']++; } } return $result; } public function auto_prepare() { if (!$this->can_manage() || !check_admin_referer('deuces_cjenik_auto_prepare')) { wp_die(esc_html__('Niste ovlašteni za ovu radnju.', 'deuces-cjenik')); } $prepared = 0; $late = 0; foreach (wc_get_products(array('limit' => -1, 'status' => array('publish', 'private'))) as $product) { if ($product->is_type('variable')) { foreach ($product->get_children() as $child_id) { $child = wc_get_product($child_id); if (!$child) { continue; } $post = get_post($child_id); if ($post && strtotime($post->post_date) > strtotime('2026-09-10 23:59:59')) { $late++; continue; } if ($this->scalar_meta($child_id, '_deuces_anchor_price') !== '') { continue; } $value = $child->get_regular_price('edit'); if ($value === '') { $value = $child->get_price('edit'); } if ($value !== '') { update_post_meta($child_id, '_deuces_anchor_price', wc_format_decimal($value, 2)); $prepared++; } } continue; } $id = $product->get_id(); $post = get_post($id); if ($post && strtotime($post->post_date) > strtotime('2026-09-10 23:59:59')) { $late++; continue; } if ($this->scalar_meta($id, '_deuces_anchor_price') !== '') { continue; } $value = $product->get_regular_price('edit'); if ($value === '') { $value = $product->get_price('edit'); } if ($value !== '') { update_post_meta($id, '_deuces_anchor_price', wc_format_decimal($value, 2)); $prepared++; } } set_transient('deuces_cjenik_auto_prepare_notice', array('prepared' => $prepared, 'late' => $late), HOUR_IN_SECONDS); $this->invalidate_map(); wp_safe_redirect(add_query_arg(array('page' => 'deuces-cjenici', 'prepared' => 1), admin_url('admin.php'))); exit; } public function bulk_save() { if (!$this->can_manage() || !check_admin_referer('deuces_cjenik_bulk_save')) { wp_die(esc_html__('Niste ovlašteni za ovu radnju.', 'deuces-cjenik')); } $values = isset($_POST['anchor']) && is_array($_POST['anchor']) ? wp_unslash($_POST['anchor']) : array(); foreach ($values as $id => $value) { if (is_array($value) || !get_post((int) $id)) { continue; } $value = wc_format_decimal(wc_clean($value), 2); if ($value === '') { delete_post_meta((int) $id, '_deuces_anchor_price'); } else { update_post_meta((int) $id, '_deuces_anchor_price', $value); } } $this->invalidate_map(); wp_safe_redirect(add_query_arg(array('page' => 'deuces-cjenici', 'saved' => 1), admin_url('admin.php'))); exit; } private function latest_file($pattern) { $files = glob(trailingslashit($this->upload_dir()) . $pattern); if (!$files) { return ''; } usort($files, function ($a, $b) { return filemtime($b) <=> filemtime($a); }); return $files[0]; } public function archive_shortcode() { $csv = $this->latest_file('*.csv'); $report = $this->latest_file('_report_*.txt'); $count = '—'; if ($report && is_readable($report)) { $content = file_get_contents($report); if (preg_match('/Broj proizvoda:\s*(\d+)/u', $content, $match)) { $count = (int) $match[1]; } } $uploads = wp_upload_dir(); $csv_url = $csv ? trailingslashit($uploads['baseurl']) . 'deuces-cjenici/' . rawurlencode(basename($csv)) : ''; ob_start(); ?>

Deuces Vape Shop

Cjenici proizvoda

Najnoviji cjenik sadrži proizvoda.

Cjenik je objavljen u strojno čitljivom CSV formatu, kako zahtijeva važeći pravilnik.

Preuzmi najnoviji CSV

Prethodni cjenici ostaju dostupni najmanje 30 dana.

can_manage() || !check_admin_referer('deuces_cjenik_save_settings')) { wp_die(esc_html__('Niste ovlašteni za ovu radnju.', 'deuces-cjenik')); } update_option(self::SETTINGS, $this->sanitize_settings(isset($_POST[self::SETTINGS]) ? $_POST[self::SETTINGS] : array()), false); wp_safe_redirect(add_query_arg(array('page' => 'deuces-cjenici-settings', 'saved' => 1), admin_url('admin.php'))); exit; } public function settings_page() { if (!$this->can_manage()) { return; } $settings = $this->settings(); echo '

Postavke cjenika

'; wp_nonce_field('deuces_cjenik_save_settings'); echo '
'; submit_button('Spremi postavke'); echo '
'; } public function admin_page() { if (!$this->can_manage()) { return; } $audit = $this->audit(); $products = array_slice($this->item_products(), 0, 100); $notice = get_transient('deuces_cjenik_auto_prepare_notice'); if ($notice) { delete_transient('deuces_cjenik_auto_prepare_notice'); echo '

Pripremljeno sidrenih cijena: ' . esc_html((string) $notice['prepared']) . '. Preskočeno zbog datuma nakon 10.09.2026.: ' . esc_html((string) $notice['late']) . '.

'; } echo '

Deuces cjenici

Bez sidrene cijene: ' . esc_html((string) $audit['anchor']) . ' · Bez barkoda: ' . esc_html((string) $audit['barcode']) . ' · Na akciji bez sidrene cijene: ' . esc_html((string) $audit['on_sale_without_anchor']) . '

'; echo '

Generiraj CSV Pripremi iz redovne cijene

'; echo '
'; wp_nonce_field('deuces_cjenik_bulk_save'); echo ''; foreach ($products as $product) { $raw = get_post_meta($product->get_id(), '_deuces_anchor_price', true); $value = $this->scalar_meta($product->get_id(), '_deuces_anchor_price'); $warning = is_array($raw) ? 'podatak je bio neispravan, obriši i upiši broj' : ''; echo ''; } echo '
ProizvodSKUSidrena cijena
' . wp_kses_post($this->row_name($product)) . '' . esc_html($product->get_sku()) . '' . wp_kses_post($warning) . '
'; submit_button('Spremi sidrene cijene'); echo '
'; } public function invalidate_map($post_id = 0) { delete_transient(self::MAP_TRANSIENT); } public function product_updated_props($product, $updated_props) { if (is_array($updated_props) && array_intersect($updated_props, array('price', 'regular_price', 'sale_price', 'stock_status', 'stock_quantity'))) { $this->invalidate_map($product instanceof WC_Product ? $product->get_id() : 0); } } private function frontend_map() { $map = get_transient(self::MAP_TRANSIENT); if ($map !== false && is_array($map)) { return $map; } $map = array(); foreach (wc_get_products(array('limit' => -1, 'status' => 'publish')) as $product) { $anchor = $this->resolve_anchor($product); $unit = $this->scalar_meta($product->get_id(), '_deuces_unit_price'); if ($anchor === '' && $unit === '') { continue; } $permalink = get_permalink($product->get_id()); if (!$permalink) { continue; } $map[untrailingslashit($permalink)] = array( 'a' => $this->clean_line($product->get_name()), 'p' => $this->anchor_html($anchor), 'u' => $unit !== '' ? wp_kses_post(wc_price($unit)) : '', ); } set_transient(self::MAP_TRANSIENT, $map, 30 * MINUTE_IN_SECONDS); return $map; } public function homepage_anchor_injector() { if (is_admin() || is_product() || is_cart() || is_checkout() || is_account_page()) { return; } $map = $this->frontend_map(); if (!$map) { return; } $json = wp_json_encode($map, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?> WP_REST_Server::READABLE, 'callback' => function () { return rest_ensure_response($this->rows()); }, 'permission_callback' => '__return_true')); } } new Deuces_Cjenik_HR();