Plugin Directory

Changeset 3718469


Ignore:
Timestamp:
09/29/2026 07:37:37 AM (5 weeks ago)
Author:
webskipper1
Message:

Version 1.2.0: product variations for vendors, immediate price/SKU/stock changes, vendor wp-admin lockout fix, search filter and UI improvements

Location:
vendboard-vendor-dashboard-for-woocommerce/trunk
Files:
1 added
10 edited

Legend:

Unmodified
Added
Removed
  • vendboard-vendor-dashboard-for-woocommerce/trunk/admin/assets/css/vendor-dashboard.css

    r3664998 r3718469  
    16891689  color: #334155;
    16901690}
     1691
     1692/* ------------------------------------------------------
     1693 * VARIATIONS
     1694 * ------------------------------------------------------ */
     1695.vendboard-variation-row {
     1696  border: 1px solid #e2e8f0;
     1697  border-radius: 6px;
     1698  padding: 14px 16px;
     1699  margin-bottom: 12px;
     1700  background: #fff;
     1701}
     1702
     1703.vendboard-variation-head {
     1704  display: flex;
     1705  flex-wrap: wrap;
     1706  align-items: center;
     1707  gap: 8px;
     1708  margin-bottom: 12px;
     1709  font-size: 15px;
     1710}
     1711
     1712.vendboard-variation-fields,
     1713.vendboard-variation-add-fields {
     1714  display: grid;
     1715  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
     1716  gap: 12px;
     1717  align-items: end;
     1718}
     1719
     1720.vendboard-variation-field {
     1721  display: flex;
     1722  flex-direction: column;
     1723  gap: 5px;
     1724  font-size: 13px;
     1725  font-weight: 600;
     1726  color: #475569;
     1727  text-transform: none;
     1728  letter-spacing: 0;
     1729}
     1730
     1731.vendboard-variation-field .vendboard-text-input {
     1732  padding: 9px 12px;
     1733  font-size: 14px;
     1734}
     1735
     1736.vendboard-variation-toggle {
     1737  justify-content: flex-end;
     1738}
     1739
     1740.vendboard-variation-toggle input[type="checkbox"] {
     1741  width: 20px;
     1742  height: 20px;
     1743  margin: 6px 0;
     1744}
     1745
     1746.vendboard-variation-add {
     1747  margin-top: 18px;
     1748  padding: 16px;
     1749  border: 2px dashed #cbd5e1;
     1750  border-radius: 6px;
     1751  background: #f8fafc;
     1752}
     1753
     1754.vendboard-variation-add h4 {
     1755  margin: 0 0 6px;
     1756  font-size: 16px;
     1757  color: #334155;
     1758}
     1759
     1760.vendboard-variation-add-note {
     1761  margin-top: 10px;
     1762  color: #64748b;
     1763  font-style: italic;
     1764}
     1765
     1766.vendboard-variation-review {
     1767  width: 100%;
     1768  border-collapse: collapse;
     1769  font-size: 14px;
     1770}
     1771
     1772.vendboard-variation-review td {
     1773  padding: 8px 10px;
     1774  border-bottom: 1px solid #e2e8f0;
     1775  vertical-align: top;
     1776}
     1777
     1778.vendboard-variation-review-label {
     1779  font-weight: 600;
     1780  white-space: nowrap;
     1781}
     1782
     1783@media (max-width: 600px) {
     1784  .vendboard-variation-fields,
     1785  .vendboard-variation-add-fields {
     1786    grid-template-columns: 1fr;
     1787  }
     1788}
     1789
     1790/* ------------------------------------------------------
     1791 * STOCK FILTER
     1792 * ------------------------------------------------------ */
     1793.vendboard-stock-filter {
     1794  flex: 0 0 auto;
     1795  padding: 12px 14px;
     1796  border: 2px solid #e2e8f0;
     1797  border-radius: 5px;
     1798  font-size: 14px;
     1799  background: #f8fafc;
     1800  cursor: pointer;
     1801}
     1802
     1803.vendboard-stock-filter:focus {
     1804  outline: none;
     1805  border-color: var(--vendboard-primary, #0000c8);
     1806  background: #fff;
     1807}
     1808
     1809/* ------------------------------------------------------
     1810 * PRODUCT TYPE + OPTION PICKER
     1811 * ------------------------------------------------------ */
     1812.vendboard-simple-only[hidden],
     1813.vendboard-variable-only[hidden] {
     1814  display: none !important;
     1815}
     1816
     1817.vendboard-attribute-set {
     1818  margin: 0 0 14px;
     1819  padding: 12px 16px;
     1820  border: 1px solid #e2e8f0;
     1821  border-radius: 6px;
     1822  background: #fff;
     1823}
     1824
     1825.vendboard-attribute-set legend {
     1826  padding: 0 6px;
     1827  font-weight: 700;
     1828  color: #334155;
     1829}
     1830
     1831.vendboard-attribute-terms {
     1832  display: flex;
     1833  flex-wrap: wrap;
     1834  gap: 8px 18px;
     1835}
     1836
     1837/* Beat the generic ".vendboard-form-group label" rule (big, uppercase, block) */
     1838.vendboard-form-group .vendboard-attribute-terms label {
     1839  display: inline-flex;
     1840  align-items: center;
     1841  gap: 6px;
     1842  margin: 0;
     1843  font-size: 14px;
     1844  font-weight: 400;
     1845  text-transform: none;
     1846  letter-spacing: 0;
     1847  cursor: pointer;
     1848}
     1849
     1850.vendboard-form-group .vendboard-variation-field {
     1851  display: flex;
     1852  margin: 0;
     1853  font-size: 13px;
     1854  font-weight: 600;
     1855  text-transform: none;
     1856  letter-spacing: 0;
     1857}
     1858
     1859/* ------------------------------------------------------
     1860 * TEXT DIRECTION
     1861 * Follows the dashboard language setting, not the site locale. Hebrew
     1862 * themes often force RTL on body or on form fields, so the dashboard
     1863 * re-asserts its own direction for everything inside it. Table cells keep
     1864 * their own (centred) alignment.
     1865 * ------------------------------------------------------ */
     1866.main-vendboard-container.vendboard-dir-ltr,
     1867.main-vendboard-container.vendboard-dir-ltr * {
     1868  direction: ltr;
     1869}
     1870
     1871.main-vendboard-container.vendboard-dir-rtl,
     1872.main-vendboard-container.vendboard-dir-rtl * {
     1873  direction: rtl;
     1874}
     1875
     1876.main-vendboard-container.vendboard-dir-ltr {
     1877  text-align: left;
     1878}
     1879
     1880.main-vendboard-container.vendboard-dir-rtl {
     1881  text-align: right;
     1882}
     1883
     1884.main-vendboard-container.vendboard-dir-ltr input,
     1885.main-vendboard-container.vendboard-dir-ltr select,
     1886.main-vendboard-container.vendboard-dir-ltr textarea,
     1887.main-vendboard-container.vendboard-dir-ltr legend,
     1888.main-vendboard-container.vendboard-dir-ltr .vendboard-form-group,
     1889.main-vendboard-container.vendboard-dir-ltr .vendboard-search-summary {
     1890  text-align: left;
     1891}
     1892
     1893.main-vendboard-container.vendboard-dir-rtl input,
     1894.main-vendboard-container.vendboard-dir-rtl select,
     1895.main-vendboard-container.vendboard-dir-rtl textarea,
     1896.main-vendboard-container.vendboard-dir-rtl legend,
     1897.main-vendboard-container.vendboard-dir-rtl .vendboard-form-group,
     1898.main-vendboard-container.vendboard-dir-rtl .vendboard-search-summary {
     1899  text-align: right;
     1900}
     1901
     1902.main-vendboard-container .vendboard-search-form {
     1903  justify-content: flex-start;
     1904}
     1905
     1906/* ------------------------------------------------------
     1907 * SELECT SIZING
     1908 * Themes commonly give selects a fixed height, which clips the text once
     1909 * our padding is added. Size them by content instead and leave room for
     1910 * the dropdown arrow on whichever side it sits.
     1911 * ------------------------------------------------------ */
     1912.main-vendboard-container select {
     1913  box-sizing: border-box;
     1914  height: auto;
     1915  min-height: 44px;
     1916  max-width: 100%;
     1917  line-height: 1.4;
     1918  padding-top: 8px;
     1919  padding-bottom: 8px;
     1920  padding-inline-start: 12px;
     1921  padding-inline-end: 36px;
     1922}
     1923
     1924.main-vendboard-container .vendboard-search-form .vendboard-search-input,
     1925.main-vendboard-container .vendboard-search-form .vendboard-stock-filter {
     1926  box-sizing: border-box;
     1927  height: auto;
     1928  min-height: 48px;
     1929  margin: 0;
     1930}
     1931
     1932.main-vendboard-container .vendboard-search-form .vendboard-stock-filter {
     1933  width: auto;
     1934  min-width: 200px;
     1935  padding-top: 10px;
     1936  padding-bottom: 10px;
     1937  padding-inline-start: 14px;
     1938  padding-inline-end: 40px;
     1939}
     1940
     1941.main-vendboard-container .vendboard-variation-field select.vendboard-text-input {
     1942  padding-inline-end: 36px;
     1943}
     1944
     1945@media (max-width: 600px) {
     1946  .main-vendboard-container .vendboard-search-form .vendboard-stock-filter {
     1947    flex: 1 1 100%;
     1948    min-width: 0;
     1949    width: 100%;
     1950  }
     1951}
     1952
     1953/* ------------------------------------------------------
     1954 * NEW VARIATIONS TABLE
     1955 * ------------------------------------------------------ */
     1956.vendboard-combo-fill-bar {
     1957  display: flex;
     1958  flex-wrap: wrap;
     1959  align-items: flex-end;
     1960  gap: 12px;
     1961  margin: 12px 0;
     1962}
     1963
     1964.vendboard-combo-fill-bar .vendboard-variation-field {
     1965  flex: 0 1 180px;
     1966}
     1967
     1968.vendboard-combo-scroll {
     1969  overflow-x: auto;
     1970}
     1971
     1972.vendboard-combo-table {
     1973  width: 100%;
     1974  min-width: 560px;
     1975  border-collapse: collapse;
     1976  background: #fff;
     1977  font-size: 14px;
     1978}
     1979
     1980.vendboard-combo-table th,
     1981.vendboard-combo-table td {
     1982  padding: 8px 10px;
     1983  border-bottom: 1px solid #e2e8f0;
     1984  vertical-align: middle;
     1985  text-align: start;
     1986}
     1987
     1988.vendboard-combo-table th {
     1989  font-weight: 700;
     1990  color: #334155;
     1991  white-space: nowrap;
     1992}
     1993
     1994.vendboard-combo-table .vendboard-text-input {
     1995  width: 100%;
     1996  min-width: 80px;
     1997  padding: 8px 10px;
     1998  font-size: 14px;
     1999}
     2000
     2001.vendboard-combo-label {
     2002  font-weight: 600;
     2003}
     2004
     2005.vendboard-combo-table input[type="checkbox"] {
     2006  width: 18px;
     2007  height: 18px;
     2008}
     2009
     2010/* Beat the generic ".vendboard-form-group label" rule */
     2011.vendboard-form-group .vendboard-combo-all-label {
     2012  display: inline-flex;
     2013  align-items: center;
     2014  gap: 6px;
     2015  margin: 0;
     2016  font-size: 13px;
     2017  font-weight: 700;
     2018  text-transform: none;
     2019  letter-spacing: 0;
     2020  white-space: nowrap;
     2021  cursor: pointer;
     2022}
     2023
     2024/* ------------------------------------------------------
     2025 * COLLAPSIBLE SECTIONS
     2026 * ------------------------------------------------------ */
     2027.vendboard-attribute-set,
     2028.vendboard-variations-list,
     2029.vendboard-variation-add {
     2030  margin: 0 0 14px;
     2031  padding: 0;
     2032  border: 1px solid #e2e8f0;
     2033  border-radius: 6px;
     2034  background: #fff;
     2035}
     2036
     2037.vendboard-attribute-set > summary,
     2038.vendboard-variations-list > summary,
     2039.vendboard-variation-add > summary {
     2040  display: flex;
     2041  align-items: center;
     2042  gap: 10px;
     2043  padding: 12px 16px;
     2044  background: #f8fafc;
     2045  border-radius: 6px;
     2046  cursor: pointer;
     2047  list-style: none;
     2048  font-size: 15px;
     2049  font-weight: 700;
     2050  color: #334155;
     2051}
     2052
     2053.vendboard-attribute-set[open] > summary,
     2054.vendboard-variations-list[open] > summary,
     2055.vendboard-variation-add[open] > summary {
     2056  border-bottom: 1px solid #e2e8f0;
     2057  border-radius: 6px 6px 0 0;
     2058}
     2059
     2060.vendboard-attribute-set > summary::-webkit-details-marker,
     2061.vendboard-variations-list > summary::-webkit-details-marker,
     2062.vendboard-variation-add > summary::-webkit-details-marker {
     2063  display: none;
     2064}
     2065
     2066.vendboard-attribute-set > summary::after,
     2067.vendboard-variations-list > summary::after,
     2068.vendboard-variation-add > summary::after {
     2069  content: "\25BE";
     2070  margin-inline-start: auto;
     2071  font-size: 14px;
     2072  color: #64748b;
     2073}
     2074
     2075.vendboard-attribute-set[open] > summary::after,
     2076.vendboard-variations-list[open] > summary::after,
     2077.vendboard-variation-add[open] > summary::after {
     2078  content: "\25B4";
     2079}
     2080
     2081.vendboard-summary-count {
     2082  font-size: 13px;
     2083  font-weight: 400;
     2084  color: #64748b;
     2085}
     2086
     2087.vendboard-attribute-set > .vendboard-attribute-terms {
     2088  padding: 14px 16px;
     2089}
     2090
     2091.vendboard-variations-list > .vendboard-variation-row {
     2092  margin: 14px 16px;
     2093}
     2094
     2095.vendboard-variation-add > p,
     2096.vendboard-variation-add > .vendboard-combo-fill-bar,
     2097.vendboard-variation-add > .vendboard-combo-scroll {
     2098  margin-inline: 16px;
     2099}
     2100
     2101.vendboard-variation-add > .vendboard-combo-scroll {
     2102  padding-bottom: 16px;
     2103}
  • vendboard-vendor-dashboard-for-woocommerce/trunk/admin/assets/js/vendboard-dashboard.js

    r3664998 r3718469  
    11jQuery(document).ready(function ($) {
     2    $('.vendboard-stock-filter').on('change', function () {
     3        this.form.submit();
     4    });
     5
    26    $('#vendboard-onboarding-close').on('click', function () {
    37        $('.vendboard-onboarding-popup').fadeOut(300);
  • vendboard-vendor-dashboard-for-woocommerce/trunk/admin/assets/js/vendboard-product-edit.js

    r3663532 r3718469  
    109109    }
    110110
     111    /* ------------------------------------------------------
     112     * PRODUCT TYPE
     113     * A product with options is priced and stocked per variation, so the
     114     * single-product fields are hidden and disabled (disabled fields are not
     115     * submitted), and the option picker is shown instead.
     116     * ------------------------------------------------------ */
     117    var $productType = $('select[name="vendboard_product_type"]');
     118
     119    function vendboardApplyProductType() {
     120
     121        if (!$productType.length) {
     122            return;
     123        }
     124
     125        var isVariable = ($productType.val() === 'variable');
     126
     127        $('.vendboard-simple-only').each(function () {
     128            this.hidden = isVariable;
     129            $(this).find(':input').prop('disabled', isVariable);
     130        });
     131
     132        $('.vendboard-variable-only').each(function () {
     133            this.hidden = !isVariable;
     134            $(this).find(':input').prop('disabled', !isVariable);
     135        });
     136
     137        // Re-applies the quantity lock on the status select after re-enabling
     138        if (!isVariable && $stockQty.length && $stockStatus.length) {
     139            vendboardSyncStockStatus();
     140        }
     141    }
     142
     143    $productType.on('change', vendboardApplyProductType);
     144    vendboardApplyProductType();
     145
     146    /* ------------------------------------------------------
     147     * NEW VARIATIONS (combination table)
     148     * ------------------------------------------------------ */
     149    $(document).on('change', '.vendboard-combo-all', function () {
     150        $('.vendboard-combo-pick').prop('checked', this.checked);
     151    });
     152
     153    // Typing into a row selects it, so filled-in rows are not lost
     154    $(document).on('input', '.vendboard-combo-price, .vendboard-combo-stock, .vendboard-combo-sku', function () {
     155        if (String(this.value).trim() !== '') {
     156            $(this).closest('tr').find('.vendboard-combo-pick').prop('checked', true);
     157        }
     158    });
     159
     160    $(document).on('click', '.vendboard-combo-fill', function () {
     161        var price = String($('.vendboard-combo-fill-price').val()).trim();
     162        var stock = String($('.vendboard-combo-fill-stock').val()).trim();
     163
     164        $('.vendboard-combo-pick:checked').each(function () {
     165            var $row = $(this).closest('tr');
     166            if (price !== '') {
     167                $row.find('.vendboard-combo-price').val(price);
     168            }
     169            if (stock !== '') {
     170                $row.find('.vendboard-combo-stock').val(stock);
     171            }
     172        });
     173    });
     174
    111175
    112176    $('#vendboard-edit-form').on('submit', function (e) {
    … …  
    130194
    131195        // Price (only for new product)
    132         if ($('input[name="product_price"]').length) {
    133             let priceField = $('input[name="product_price"]');
     196        if ($('input[name="product_price"]:not(:disabled)').length) {
     197            let priceField = $('input[name="product_price"]:not(:disabled)');
    134198            let price = priceField.val().trim();
    135199
    … …  
    145209
    146210        // Stock quantity (optional, but must be a positive whole number)
    147         if ($stockQty.length) {
     211        if ($stockQty.length && !$stockQty.prop('disabled')) {
    148212            let stock = String($stockQty.val()).trim();
    149213
    … …  
    169233        }
    170234
     235        // Removing variations is permanent, so confirm it once
     236        var removing = $('.vendboard-variation-remove:checked').length;
     237
     238        if (removing && !window.confirm(String(vendboardProductData.i18n.confirm_remove).replace('%d', removing))) {
     239            return false;
     240        }
     241
    171242        var formData = new FormData(this);
    172243
    173244        // Disabled fields are skipped by FormData, so send the status explicitly.
    174         if ($stockStatus.length && $stockStatus.prop('disabled')) {
     245        if ($stockStatus.length && $stockStatus.prop('disabled') &&
     246            !$stockStatus.closest('.vendboard-simple-only').prop('hidden')) {
    175247            formData.set('product_stock_status', $stockStatus.val());
    176248        }
    … …  
    206278                        msgBox.fadeOut(300, function () {
    207279                            $(this).remove();
    208                             window.location.href = vendboardProductData.redirect_url;
     280                            window.location.href = (res.data && res.data.redirect)
     281                                ? res.data.redirect
     282                                : vendboardProductData.redirect_url;
    209283                        });
    210284                    }, 1500);
  • vendboard-vendor-dashboard-for-woocommerce/trunk/admin/class-vendboard-branding-settings.php

    r3664998 r3718469  
    4747                'sanitize_callback' => 'sanitize_text_field',
    4848                'default' => 'blue',
     49            ]
     50        );
     51
     52        // Attributes vendors may use (empty = all of them)
     53        register_setting(
     54            'vendboard_branding_settings',
     55            'vendboard_vendor_attributes',
     56            [
     57                'type' => 'array',
     58                'sanitize_callback' => [__CLASS__, 'sanitize_vendor_attributes'],
     59                'default' => [],
    4960            ]
    5061        );
    … …  
    6778
    6879    /**
     80     * Keep only real global attribute keys. An empty list means "all of them".
     81     */
     82    public static function sanitize_vendor_attributes($value)
     83    {
     84        if (!is_array($value)) {
     85            return [];
     86        }
     87
     88        $valid = function_exists('vendboard_get_global_attribute_choices')
     89            ? vendboard_get_global_attribute_choices()
     90            : [];
     91
     92        $out = [];
     93
     94        foreach ($value as $key) {
     95            $key = (string) $key;
     96
     97            if (isset($valid[$key])) {
     98                $out[] = $key;
     99            }
     100        }
     101
     102        return $out;
     103    }
     104
     105    /**
    69106     * Predefined themes map
    70107     */
    … …  
    240277                                    <?php esc_html_e('Select one of the predefined color themes', 'vendboard-vendor-dashboard-for-woocommerce'); ?>
    241278                                </p>
     279                            </div>
     280
     281                            <!-- ===============================
     282                             WHICH OPTIONS VENDORS MAY USE
     283                        =============================== -->
     284                            <div class="vendboard-color-control">
     285                                <label><?php esc_html_e('Options vendors can use', 'vendboard-vendor-dashboard-for-woocommerce'); ?></label>
     286
     287                                <?php
     288                                $attribute_choices = function_exists('vendboard_get_global_attribute_choices')
     289                                    ? vendboard_get_global_attribute_choices()
     290                                    : [];
     291                                $allowed_attributes = (array) get_option('vendboard_vendor_attributes', []);
     292                                $allowed_attributes = array_map('strval', $allowed_attributes);
     293                                ?>
     294
     295                                <?php if (empty($attribute_choices)): ?>
     296                                    <p class="vendboard-color-desc">
     297                                        <?php esc_html_e('No product attributes have been set up yet. Add them under Products > Attributes.', 'vendboard-vendor-dashboard-for-woocommerce'); ?>
     298                                    </p>
     299                                <?php else: ?>
     300                                    <!-- Always submitted, so unticking everything still saves -->
     301                                    <input type="hidden" name="vendboard_vendor_attributes[]" value="">
     302
     303                                    <?php foreach ($attribute_choices as $attribute_key => $attribute_choice): ?>
     304                                        <label style="display:block;margin:6px 0;font-weight:400;">
     305                                            <input type="checkbox" name="vendboard_vendor_attributes[]"
     306                                                value="<?php echo esc_attr($attribute_key); ?>"
     307                                                <?php checked(empty($allowed_attributes) || in_array((string) $attribute_key, $allowed_attributes, true)); ?>>
     308                                            <?php echo esc_html($attribute_choice['label']); ?>
     309                                        </label>
     310                                    <?php endforeach; ?>
     311
     312                                    <p class="vendboard-color-desc">
     313                                        <?php esc_html_e('Vendors can only choose from the ticked options when setting up a product with options. Ticking all of them is the same as ticking none. Attributes already on a product are never removed.', 'vendboard-vendor-dashboard-for-woocommerce'); ?>
     314                                    </p>
     315                                <?php endif; ?>
    242316                            </div>
    243317
  • vendboard-vendor-dashboard-for-woocommerce/trunk/admin/class-vendboard-pending-review.php

    r3666364 r3718469  
    578578            </div>
    579579
     580            <?php
     581            $variation_rows = class_exists('VendBoard_Variations')
     582                ? VendBoard_Variations::get_pending_rows($pid)
     583                : [];
     584            ?>
     585            <?php if (!empty($variation_rows)): ?>
     586                <!-- VARIATIONS -->
     587                <div class="vendboard-comparison-item vendboard-comparison-variations">
     588                    <h4 class="vendboard-comparison-label">
     589                        <span class="dashicons dashicons-screenoptions"></span>
     590                        <?php esc_html_e('Variations', 'vendboard-vendor-dashboard-for-woocommerce'); ?>
     591                    </h4>
     592
     593                    <table class="vendboard-variation-review">
     594                        <tbody>
     595                            <?php foreach ($variation_rows as $row): ?>
     596                                <tr>
     597                                    <td class="vendboard-variation-review-label">
     598                                        <?php echo esc_html($row['label']); ?>
     599                                        <?php if (!empty($row['is_new'])): ?>
     600                                            <span class="vendboard-new-product-label">
     601                                                <?php esc_html_e('New', 'vendboard-vendor-dashboard-for-woocommerce'); ?>
     602                                            </span>
     603                                        <?php endif; ?>
     604                                    </td>
     605                                    <td><?php echo esc_html($row['change']); ?></td>
     606                                </tr>
     607                            <?php endforeach; ?>
     608                        </tbody>
     609                    </table>
     610                </div>
     611            <?php endif; ?>
     612
    580613            <!-- SKU -->
    581614            <div class="vendboard-comparison-item">
    … …  
    851884        if ($pending_barcode !== '') {
    852885            vendboard_set_barcode($pid, $pending_barcode);
     886        }
     887
     888        /* ------------------------------
     889         * VARIATIONS
     890         *
     891         * Enables variations the vendor added and applies staged per-variation
     892         * price and SKU values.
     893         * ------------------------------ */
     894        if (class_exists('VendBoard_Variations')) {
     895            VendBoard_Variations::approve($pid);
    853896        }
    854897
    … …  
    9721015        }
    9731016
     1017        // Staged variation changes are discarded too. Variations the vendor
     1018        // added are left in place but stay disabled, so nothing is destroyed.
     1019        if (class_exists('VendBoard_Variations')) {
     1020            VendBoard_Variations::reject($pid);
     1021        }
     1022
    9741023        // Save status BEFORE sending response
    9751024        update_post_meta($pid, '_vendboard_last_status', 'rejected');
  • vendboard-vendor-dashboard-for-woocommerce/trunk/admin/class-vendboard-vendor-dashboard.php

    r3666364 r3718469  
    1717        add_action('admin_init', [__CLASS__, 'redirect_vendor_to_dashboard']);
    1818        add_action('wp', [__CLASS__, 'restrict_vendor_to_dashboard']);
     19
     20        // Send vendors straight to their dashboard when they log in
     21        add_filter('login_redirect', [__CLASS__, 'login_redirect'], 20, 3);
     22        add_filter('woocommerce_login_redirect', [__CLASS__, 'woocommerce_login_redirect'], 20, 2);
    1923
    2024        // Dashboard template loader
    … …  
    9195                    'saved_successfully' => __('Saved successfully.', 'vendboard-vendor-dashboard-for-woocommerce'),
    9296                    'error_occurred' => __('An error occurred. Please try again.', 'vendboard-vendor-dashboard-for-woocommerce'),
     97                    /* translators: %d: number of variations */
     98                    'confirm_remove' => __('Remove %d variation(s)? This cannot be undone.', 'vendboard-vendor-dashboard-for-woocommerce'),
    9399                ]
    94100            ]);
    … …  
    120126    public static function redirect_vendor_to_dashboard()
    121127    {
     128        if (!is_admin() || wp_doing_ajax()) {
     129            return;
     130        }
     131
    122132        $user = wp_get_current_user();
    123         if (in_array('vendor', (array) $user->roles, true) && is_admin() && !defined('DOING_AJAX')) {
    124 
    125             // Emergency escape hatch
    126             if (isset($_GET['vendboard_no_redirect']) && $_GET['vendboard_no_redirect'] === 'emergency') {
    127                 return;
    128             }
    129 
    130             $screen = function_exists('get_current_screen') ? get_current_screen() : null;
    131 
    132             // Allow plugin admin pages only
    133             if ($screen && strpos($screen->id, 'vendor') !== false)
    134                 return;
    135 
    136             // Check if dashboard page exists
    137             if (!self::vendor_dashboard_page_exists()) {
    138                 return;
    139             }
    140 
    141             wp_safe_redirect(site_url('/vendor-dashboard/'));
    142             exit;
    143         }
     133
     134        // Someone who is also an administrator keeps wp-admin
     135        if (!in_array('vendor', (array) $user->roles, true) || user_can($user, 'manage_options')) {
     136            return;
     137        }
     138
     139        /*
     140         * Fail closed. Vendors never fall through to wp-admin, where they
     141         * could edit products outside the approval workflow. If there is no
     142         * published dashboard page, they go to the home page instead.
     143         *
     144         * There is deliberately no bypass parameter: one would only ever
     145         * apply to vendors, since administrators are never redirected.
     146         */
     147        wp_safe_redirect(self::vendor_dashboard_url());
     148        exit;
     149    }
     150
     151    /**
     152     * Where a vendor should land: their dashboard, or the home page if it is
     153     * unavailable.
     154     */
     155    private static function vendor_dashboard_url()
     156    {
     157        return self::vendor_dashboard_page_exists() ? site_url('/vendor-dashboard/') : home_url('/');
     158    }
     159
     160    /**
     161     * Send vendors to their dashboard after logging in through wp-login.php.
     162     *
     163     * @param string           $redirect_to Default destination.
     164     * @param string           $requested   Destination the form asked for.
     165     * @param WP_User|WP_Error $user        The user logging in.
     166     */
     167    public static function login_redirect($redirect_to, $requested = '', $user = null)
     168    {
     169        if (
     170            $user instanceof WP_User &&
     171            in_array('vendor', (array) $user->roles, true) &&
     172            !user_can($user, 'manage_options')
     173        ) {
     174            return self::vendor_dashboard_url();
     175        }
     176
     177        return $redirect_to;
     178    }
     179
     180    /**
     181     * Same, for the WooCommerce My Account login form.
     182     */
     183    public static function woocommerce_login_redirect($redirect, $user = null)
     184    {
     185        return self::login_redirect($redirect, '', $user);
    144186    }
    145187
    … …  
    180222    private static function vendor_dashboard_page_exists()
    181223    {
    182         $dashboard_page = get_page_by_path('vendor-dashboard');
    183         return ($dashboard_page && $dashboard_page->post_status === 'publish');
     224        // Recreates the page if it has gone missing
     225        return vendboard_ensure_dashboard_page() > 0;
    184226    }
    185227
    … …  
    240282
    241283        return in_array((int) $assigned_brand, array_map('intval', $product_brands), true);
     284    }
     285
     286    /**
     287     * Text direction of the vendor dashboard.
     288     *
     289     * Follows the dashboard language setting, not the site locale: a Hebrew
     290     * site can run an English dashboard, and vice versa.
     291     */
     292    private static function text_direction()
     293    {
     294        return (get_option('vendboard_dashboard_lang', 'en') === 'he') ? 'rtl' : 'ltr';
    242295    }
    243296
    … …  
    418471        $search = isset($_GET['vb_s']) ? trim(sanitize_text_field(wp_unslash($_GET['vb_s']))) : '';
    419472        $paged = isset($_GET['vb_page']) ? max(1, absint($_GET['vb_page'])) : 1;
     473
     474        // Stock status filter; anything not a known status is ignored
     475        $stock_filter = isset($_GET['vb_stock']) ? sanitize_key(wp_unslash($_GET['vb_stock'])) : '';
     476        $stock_filter = array_key_exists($stock_filter, vendboard_get_stock_statuses()) ? $stock_filter : '';
    420477
    421478        /**
    … …  
    481538        ];
    482539
     540        if ($stock_filter !== '') {
     541            $main_args['meta_query'] = [
     542                'relation' => 'AND',
     543                $main_args['meta_query'],
     544                [
     545                    'key' => '_stock_status',
     546                    'value' => $stock_filter,
     547                    'compare' => '=',
     548                ],
     549            ];
     550        }
     551
    483552        $no_search_matches = false;
    484553
    … …  
    588657        // Selected theme logic moved to enqueue_frontend_assets
    589658        ?>
    590         <div class="main-vendboard-container">
     659        <div class="main-vendboard-container vendboard-dir-<?php echo esc_attr(self::text_direction()); ?>"
     660            dir="<?php echo esc_attr(self::text_direction()); ?>">
    591661            <div class="vendboard-container" dir="<?php echo esc_attr($rtl); ?>">
    592662
    … …  
    635705                            value="<?php echo esc_attr($search); ?>"
    636706                            placeholder="<?php esc_attr_e('Search by name, SKU or barcode', 'vendboard-vendor-dashboard-for-woocommerce'); ?>">
     707                        <select name="vb_stock" class="vendboard-stock-filter"
     708                            aria-label="<?php esc_attr_e('Filter by stock status', 'vendboard-vendor-dashboard-for-woocommerce'); ?>">
     709                            <option value=""><?php esc_html_e('All stock statuses', 'vendboard-vendor-dashboard-for-woocommerce'); ?></option>
     710                            <?php foreach (vendboard_get_stock_statuses() as $status_key => $status_label): ?>
     711                                <option value="<?php echo esc_attr($status_key); ?>" <?php selected($stock_filter, $status_key); ?>>
     712                                    <?php echo esc_html($status_label); ?>
     713                                </option>
     714                            <?php endforeach; ?>
     715                        </select>
    637716                        <button type="submit"
    638717                            class="vendboard-submit-btn"><?php esc_html_e('Search', 'vendboard-vendor-dashboard-for-woocommerce'); ?></button>
    639                         <?php if ($search !== ''): ?>
     718                        <?php if ($search !== '' || $stock_filter !== ''): ?>
    640719                            <a class="vendboard-cancel-btn"
    641720                                href="<?php echo esc_url(site_url('/vendor-dashboard/')); ?>"><?php esc_html_e('Clear', 'vendboard-vendor-dashboard-for-woocommerce'); ?></a>
    … …  
    655734                            ?>
    656735                        </p>
     736                    <?php elseif ($stock_filter !== ''): ?>
     737                        <p class="vendboard-search-summary">
     738                            <?php
     739                            $found = $query ? (int) $query->found_posts : 0;
     740                            $stock_labels = vendboard_get_stock_statuses();
     741                            printf(
     742                                /* translators: 1: number of products, 2: stock status such as "Out of stock" */
     743                                esc_html(_n('Showing %1$s product: %2$s', 'Showing %1$s products: %2$s', $found, 'vendboard-vendor-dashboard-for-woocommerce')),
     744                                esc_html(number_format_i18n($found)),
     745                                esc_html($stock_labels[$stock_filter])
     746                            );
     747                            ?>
     748                        </p>
    657749                    <?php endif; ?>
    658750
    659751                    <?php echo wp_kses_post(self::render_product_table($products, false)); ?>
    660                     <?php echo wp_kses_post(self::render_pagination($query, $paged, $search)); ?>
     752                    <?php echo wp_kses_post(self::render_pagination($query, $paged, $search, $stock_filter)); ?>
    661753                </div>
    662754
    … …  
    699791     * Pagination links for the product list, preserving the search term.
    700792     */
    701     private static function render_pagination($query, $paged, $search)
     793    private static function render_pagination($query, $paged, $search, $stock_filter = '')
    702794    {
    703795        if (!$query || (int) $query->max_num_pages < 2) {
    … …  
    709801        if ($search !== '') {
    710802            $base_args['vb_s'] = $search;
     803        }
     804
     805        if ($stock_filter !== '') {
     806            $base_args['vb_stock'] = $stock_filter;
    711807        }
    712808
    … …  
    10191115        // Selected theme logic moved to enqueue_frontend_assets
    10201116        ?>
    1021         <div class="main-vendboard-container">
     1117        <div class="main-vendboard-container vendboard-dir-<?php echo esc_attr(self::text_direction()); ?>"
     1118            dir="<?php echo esc_attr(self::text_direction()); ?>">
    10221119            <div class="vendboard-edit">
    10231120                <div class="vendboard-header-section">
    … …  
    10421139                        <input type="hidden" name="product_id" value="<?php echo esc_attr($pid); ?>">
    10431140                        <?php wp_nonce_field('vendboard_save_product', 'vendboard_nonce'); ?>
     1141
     1142                        <?php
     1143                        // Product type + options, only while the product is unapproved
     1144                        VendBoard_Variations::render_structure_controls($pid);
     1145                        ?>
    10441146
    10451147
    … …  
    11041206                            <div class="vendboard-form-group">
    11051207                                <label><?php esc_html_e('Product Price', 'vendboard-vendor-dashboard-for-woocommerce'); ?></label>
    1106                                 <input type="number" name="product_price" class="vendboard-text-input" step="0.01" min="0"
    1107                                     value="<?php echo esc_attr($display_price); ?>" required>
    1108                                 <p class="vendboard-error vendboard-price-error" style="display:none;color:red;margin-top:5px;">
    1109                                     <?php esc_html_e('Price is required.', 'vendboard-vendor-dashboard-for-woocommerce'); ?>
    1110                                 </p>
     1208                                <?php if (VendBoard_Variations::is_variable($pid)): ?>
     1209                                    <p class="vendboard-na">
     1210                                        <?php esc_html_e('This product is priced per variation. Set the prices in the Variations section below.', 'vendboard-vendor-dashboard-for-woocommerce'); ?>
     1211                                    </p>
     1212                                <?php else: ?>
     1213                                    <div class="vendboard-simple-only">
     1214                                        <input type="number" name="product_price" class="vendboard-text-input" step="0.01" min="0"
     1215                                            value="<?php echo esc_attr($display_price); ?>" required>
     1216                                        <p class="vendboard-error vendboard-price-error" style="display:none;color:red;margin-top:5px;">
     1217                                            <?php esc_html_e('Price is required.', 'vendboard-vendor-dashboard-for-woocommerce'); ?>
     1218                                        </p>
     1219                                        <p class="vendboard-stock-live-note">
     1220                                            <?php esc_html_e('Applies straight away, without admin approval.', 'vendboard-vendor-dashboard-for-woocommerce'); ?>
     1221                                        </p>
     1222                                    </div>
     1223                                    <p class="vendboard-na vendboard-variable-only" hidden>
     1224                                        <?php esc_html_e('Priced per variation. You can set prices after saving, once you have added its variations.', 'vendboard-vendor-dashboard-for-woocommerce'); ?>
     1225                                    </p>
     1226                                <?php endif; ?>
    11111227                            </div>
    11121228
    … …  
    12071323                        </div>
    12081324
     1325                        <p class="vendboard-stock-live-note">
     1326                            <?php esc_html_e('SKU and barcode changes apply straight away, without admin approval.', 'vendboard-vendor-dashboard-for-woocommerce'); ?>
     1327                        </p>
     1328
    12091329                        <?php
    12101330                        /* ------------------------------------------------------
    … …  
    12171337                        ?>
    12181338
     1339                        <?php if (VendBoard_Variations::is_variable($pid)): ?>
     1340
     1341                            <div class="vendboard-form-group">
     1342                                <label><?php esc_html_e('Stock', 'vendboard-vendor-dashboard-for-woocommerce'); ?></label>
     1343                                <p class="vendboard-na">
     1344                                    <?php esc_html_e('This product keeps stock per variation. Set the quantities in the Variations section below.', 'vendboard-vendor-dashboard-for-woocommerce'); ?>
     1345                                </p>
     1346                            </div>
     1347
     1348                        <?php else: ?>
     1349
     1350                        <div class="vendboard-simple-only">
    12191351                        <div class="vendboard-form-row">
    12201352
    … …  
    12531385
    12541386                        </div>
     1387                        </div>
     1388
     1389                        <?php endif; ?>
    12551390
    12561391                        <?php
    … …  
    13061441                        </div>
    13071442
     1443                        <?php
     1444                        // Variations panel, only for products that have variations.
     1445                        // Prints its own escaped markup; see render_panel().
     1446                        VendBoard_Variations::render_panel($pid);
     1447                        ?>
     1448
    13081449
    13091450                        <!-- Short Description -->
    13101451                        <div class="vendboard-form-group">
    1311                             <label><?php esc_html_e('YouTube link / Short Description', 'vendboard-vendor-dashboard-for-woocommerce'); ?></label>
     1452                            <label><?php esc_html_e('Short Description', 'vendboard-vendor-dashboard-for-woocommerce'); ?></label>
    13121453
    13131454                            <?php
    … …  
    14751616        }
    14761617
     1618        // New products are created without WooCommerce's default category;
     1619        // some themes cannot render a product page without one.
     1620        vendboard_ensure_product_category($pid);
     1621
    14771622        /**
    14781623         * Tracks whether anything that needs admin approval actually changed.
    … …  
    14831628
    14841629        /* ------------------------------------------------------
    1485          * PRICE  always save pending value
     1630         * PRICE, SKU & BARCODE
     1631         *
     1632         * Applied straight away, like stock. Written through the WooCommerce
     1633         * product object rather than raw meta, so an active sale price keeps
     1634         * working and SKU uniqueness is enforced. Nothing is written until
     1635         * every submitted value has passed validation.
    14861636         * ------------------------------------------------------ */
    1487         if (isset($_POST['product_price'])) {
    1488 
    1489             $price = floatval($_POST['product_price'] ?? 0);
    1490             if ($price < 0) {
     1637        $fields_applied = false;
     1638        $live_product = function_exists('wc_get_product') ? wc_get_product($pid) : null;
     1639
     1640        if (isset($_POST['product_price']) && $live_product) {
     1641
     1642            $raw_price = trim(sanitize_text_field(wp_unslash($_POST['product_price'])));
     1643
     1644            if ($raw_price === '') {
     1645                restore_previous_locale();
     1646                wp_send_json_error(['msg' => __('Price required', 'vendboard-vendor-dashboard-for-woocommerce')]);
     1647            }
     1648
     1649            if (!is_numeric($raw_price)) {
     1650                restore_previous_locale();
     1651                wp_send_json_error(['msg' => __('Price must be a number', 'vendboard-vendor-dashboard-for-woocommerce')]);
     1652            }
     1653
     1654            if ((float) $raw_price < 0) {
     1655                restore_previous_locale();
    14911656                wp_send_json_error(['msg' => __('Price cannot be negative', 'vendboard-vendor-dashboard-for-woocommerce')]);
    14921657            }
    14931658
    1494             if ($price === '') {
    1495                 wp_send_json_error(['msg' => __('Price required', 'vendboard-vendor-dashboard-for-woocommerce')]);
    1496             }
    1497 
    1498             $live_price = get_post_meta($pid, '_regular_price', true);
    1499             if (abs($price - (float) $live_price) > 0.00001) {
    1500                 $content_changed = true;
    1501             }
    1502 
    1503             update_post_meta($pid, '_vendboard_pending_price', $price);
    1504 
    1505             if ($is_new) {
    1506                 update_post_meta($pid, '_regular_price', $price);
    1507                 update_post_meta($pid, '_price', $price);
    1508             }
    1509         }
    1510 
    1511         /* ------------------------------------------------------
    1512          * SKU & BARCODE (pending only)
    1513          * ------------------------------------------------------ */
    1514 
    1515         /* ------------------------------------------------------
    1516          * SKU & BARCODE
    1517          * ------------------------------------------------------ */
    1518 
    1519         if (isset($_POST['product_sku'])) {
    1520 
    1521             $sku = sanitize_text_field(wp_unslash($_POST['product_sku']));
    1522 
    1523             if ($sku !== (string) get_post_meta($pid, '_sku', true)) {
    1524                 $content_changed = true;
    1525             }
    1526 
    1527             // Always store pending
    1528             update_post_meta($pid, '_vendboard_pending_sku', $sku);
    1529 
    1530             // NEW PRODUCT → update live immediately
    1531             if ($is_new) {
    1532                 update_post_meta($pid, '_sku', $sku);
    1533             }
     1659            $live_price = (string) $live_product->get_regular_price('edit');
     1660
     1661            if ($live_price === '' || abs((float) $raw_price - (float) $live_price) > 0.00001) {
     1662                $live_product->set_regular_price(wc_format_decimal($raw_price));
     1663                $fields_applied = true;
     1664            }
     1665        }
     1666
     1667        if (isset($_POST['product_sku']) && $live_product) {
     1668
     1669            $sku = trim(sanitize_text_field(wp_unslash($_POST['product_sku'])));
     1670
     1671            if ($sku !== (string) $live_product->get_sku('edit')) {
     1672                try {
     1673                    $live_product->set_sku($sku);
     1674                    $fields_applied = true;
     1675                } catch (Exception $e) {
     1676                    restore_previous_locale();
     1677                    wp_send_json_error(['msg' => __('That SKU is already in use. Please choose another.', 'vendboard-vendor-dashboard-for-woocommerce')]);
     1678                }
     1679            }
     1680        }
     1681
     1682        if ($fields_applied) {
     1683            $live_product->save();
    15341684        }
    15351685
    15361686        if (isset($_POST['product_barcode'])) {
    15371687
    1538             $barcode = sanitize_text_field(wp_unslash($_POST['product_barcode']));
     1688            $barcode = trim(sanitize_text_field(wp_unslash($_POST['product_barcode'])));
    15391689
    15401690            if ($barcode !== vendboard_get_barcode($pid)) {
    1541                 $content_changed = true;
    1542             }
    1543 
    1544             // Always store pending
    1545             update_post_meta($pid, '_vendboard_pending_barcode', $barcode);
    1546 
    1547             // NEW PRODUCT → update live immediately
    1548             if ($is_new) {
    15491691                vendboard_set_barcode($pid, $barcode);
     1692                $fields_applied = true;
     1693            }
     1694        }
     1695
     1696        // Values queued for approval by an earlier version are superseded by
     1697        // what the vendor has just submitted.
     1698        $superseded = [
     1699            'product_price' => '_vendboard_pending_price',
     1700            'product_sku' => '_vendboard_pending_sku',
     1701            'product_barcode' => '_vendboard_pending_barcode',
     1702        ];
     1703
     1704        foreach ($superseded as $field => $meta_key) {
     1705            if (isset($_POST[$field])) {
     1706                delete_post_meta($pid, $meta_key);
    15501707            }
    15511708        }
    … …  
    16701827        }
    16711828
     1829        /* ------------------------------------------------------
     1830         * VARIATIONS
     1831         *
     1832         * Ownership of $pid is already established above; the class re-checks
     1833         * every variation id against it, because those ids arrive over POST.
     1834         *
     1835         * Runs before the options step, so a vendor can remove variations and
     1836         * untick the values they used in the same save. The rows on screen were
     1837         * also built from the options as they were before this save.
     1838         * ------------------------------------------------------ */
     1839        $variation_result = VendBoard_Variations::save($pid);
     1840
     1841        if (!empty($variation_result['error'])) {
     1842            restore_previous_locale();
     1843            wp_send_json_error(['msg' => $variation_result['error']]);
     1844        }
     1845
     1846        if (!empty($variation_result['content_changed'])) {
     1847            $content_changed = true;
     1848        }
     1849
     1850        if (!empty($variation_result['applied'])) {
     1851            $publishing_applied = true;
     1852        }
     1853
     1854        /* ------------------------------------------------------
     1855         * PRODUCT TYPE + OPTIONS
     1856         *
     1857         * The type changes only before the product is first approved; options
     1858         * can be edited on any variable product. Neither needs approval: a new
     1859         * product is reviewed as a whole anyway.
     1860         * ------------------------------------------------------ */
     1861        $structure_result = VendBoard_Variations::save_structure($pid);
     1862
     1863        if (!empty($structure_result['error'])) {
     1864            restore_previous_locale();
     1865            wp_send_json_error(['msg' => $structure_result['error']]);
     1866        }
     1867
     1868        if (!empty($structure_result['changed'])) {
     1869            $publishing_applied = true;
     1870        }
     1871
    16721872
    16731873
    … …  
    18972097            $msg = __('Changes saved and pending admin review', 'vendboard-vendor-dashboard-for-woocommerce');
    18982098
    1899             // Stock is live already, so say so rather than implying it is queued
     2099            // These are live already, so say so rather than implying they are queued
     2100            if ($fields_applied) {
     2101                $msg = rtrim($msg, '. ') . '. ' . __('Price, SKU and barcode changes were applied straight away.', 'vendboard-vendor-dashboard-for-woocommerce');
     2102            }
     2103
    19002104            if ($stock_applied) {
    1901                 $msg .= ' ' . __('The stock level was updated straight away.', 'vendboard-vendor-dashboard-for-woocommerce');
    1902             }
    1903         } elseif ($stock_applied || $publishing_applied) {
     2105                $msg = rtrim($msg, '. ') . '. ' . __('The stock level was updated straight away.', 'vendboard-vendor-dashboard-for-woocommerce');
     2106            }
     2107        } elseif ($stock_applied || $publishing_applied || $fields_applied) {
    19042108
    19052109            $msg = __('Updated. No admin approval needed.', 'vendboard-vendor-dashboard-for-woocommerce');
    … …  
    19102114
    19112115
     2116        $response = ['msg' => $msg];
     2117
     2118        if (!empty($variation_result['summary'])) {
     2119            $response['msg'] = rtrim($response['msg'], '. ') . '. ' . $variation_result['summary'];
     2120        }
     2121
     2122        // Keep the vendor on the edit screen when there is more to do there:
     2123        // adding variations, or setting a price after switching back.
     2124        if (!empty($structure_result['changed']) || VendBoard_Variations::is_variable($pid)) {
     2125
     2126            $response['redirect'] = add_query_arg('id', $pid, site_url('/vendor-dashboard/'));
     2127
     2128            if (!empty($structure_result['changed']) && VendBoard_Variations::is_variable($pid)) {
     2129                $response['msg'] = rtrim($response['msg'], '. ') . '. ' . __('You can now add its variations.', 'vendboard-vendor-dashboard-for-woocommerce');
     2130            }
     2131        }
     2132
    19122133        restore_previous_locale();
    19132134
    1914         wp_send_json_success(['msg' => $msg]);
     2135        wp_send_json_success($response);
    19152136    }
    19162137
  • vendboard-vendor-dashboard-for-woocommerce/trunk/languages/vendboard-vendor-dashboard-for-woocommerce-he_IL.po

    r3664998 r3718469  
    659659msgid "Updated. No admin approval needed."
    660660msgstr "עודכן. לא נדרש אישור מנהל."
     661
     662msgid "Variations"
     663msgstr "וריאציות"
     664
     665msgid "Variation"
     666msgstr "וריאציה"
     667
     668msgid "Stock and availability apply straight away. Price and SKU changes wait for admin approval."
     669msgstr "מלאי וזמינות נכנסים לתוקף מיד. שינויי מחיר ומק\"ט ממתינים לאישור המנהל."
     670
     671msgid "This product has no variations yet."
     672msgstr "למוצר זה אין עדיין וריאציות."
     673
     674msgid "Awaiting approval"
     675msgstr "ממתין לאישור"
     676
     677msgid "Not for sale"
     678msgstr "לא למכירה"
     679
     680msgid "Change pending"
     681msgstr "שינוי ממתין"
     682
     683msgid "Price"
     684msgstr "מחיר"
     685
     686msgid "For sale"
     687msgstr "למכירה"
     688
     689msgid "Once approved"
     690msgstr "לאחר אישור"
     691
     692msgid "Add a variation"
     693msgstr "הוספת וריאציה"
     694
     695msgid "New variations are not for sale until the store owner approves them."
     696msgstr "וריאציות חדשות אינן למכירה עד שבעל החנות מאשר אותן."
     697
     698msgid "Leave the options blank if you do not want to add anything."
     699msgstr "השאירו את האפשרויות ריקות אם אינכם רוצים להוסיף דבר."
     700
     701msgid "No variation options have been set up for this product yet. Ask the store owner to add them."
     702msgstr "לא הוגדרו עדיין אפשרויות וריאציה למוצר זה. בקשו מבעל החנות להוסיף אותן."
     703
     704msgid "This product is priced per variation. Set the prices in the Variations section below."
     705msgstr "מוצר זה מתומחר לפי וריאציה. הגדירו את המחירים בקטע הווריאציות שלמטה."
     706
     707msgid "Variation stock must be a number"
     708msgstr "מלאי הווריאציה חייב להיות מספר"
     709
     710msgid "Variation stock cannot be negative"
     711msgstr "מלאי הווריאציה אינו יכול להיות שלילי"
     712
     713msgid "Variation price must be a number"
     714msgstr "מחיר הווריאציה חייב להיות מספר"
     715
     716msgid "Variation price cannot be negative"
     717msgstr "מחיר הווריאציה אינו יכול להיות שלילי"
     718
     719msgid "That variation option is not available for this product."
     720msgstr "אפשרות וריאציה זו אינה זמינה עבור מוצר זה."
     721
     722msgid "Please choose every option before adding a variation."
     723msgstr "יש לבחור את כל האפשרויות לפני הוספת וריאציה."
     724
     725msgid "That combination already exists on this product."
     726msgstr "שילוב זה כבר קיים במוצר זה."
     727
     728msgid "The variation could not be created."
     729msgstr "לא ניתן היה ליצור את הווריאציה."
     730
     731msgid "New variation, not for sale until approved"
     732msgstr "וריאציה חדשה, לא למכירה עד לאישור"
     733
     734msgid "Filter by stock status"
     735msgstr "סינון לפי סטטוס מלאי"
     736
     737msgid "All stock statuses"
     738msgstr "כל סטטוסי המלאי"
     739
     740msgid "Product Type"
     741msgstr "סוג מוצר"
     742
     743msgid "Single product"
     744msgstr "מוצר בודד"
     745
     746msgid "Product with options (sizes, colours and so on)"
     747msgstr "מוצר עם אפשרויות (מידות, צבעים וכדומה)"
     748
     749msgid "This product already has variations, so its type can no longer be changed."
     750msgstr "למוצר זה כבר יש וריאציות, ולכן לא ניתן עוד לשנות את סוגו."
     751
     752msgid "You can change this until the store owner approves the product."
     753msgstr "ניתן לשנות זאת עד שבעל החנות יאשר את המוצר."
     754
     755msgid "Options"
     756msgstr "אפשרויות"
     757
     758msgid "The store owner has not set up any options, such as Size or Colour, yet."
     759msgstr "בעל החנות עדיין לא הגדיר אפשרויות, כמו מידה או צבע."
     760
     761msgid "Tick the options this product comes in and save. You can then add its variations below."
     762msgstr "סמנו את האפשרויות שבהן המוצר מגיע ושמרו. לאחר מכן תוכלו להוסיף את הווריאציות שלו למטה."
     763
     764msgid "This product already has variations, so it cannot be switched back."
     765msgstr "למוצר זה כבר יש וריאציות, ולכן לא ניתן להחזיר אותו למוצר בודד."
     766
     767msgid "The product type could not be changed."
     768msgstr "לא ניתן היה לשנות את סוג המוצר."
     769
     770msgid "\"%s\" is used by an existing variation, so it cannot be removed."
     771msgstr "\"%s\" נמצא בשימוש בווריאציה קיימת, ולכן לא ניתן להסיר אותו."
     772
     773msgid "You can now add its variations."
     774msgstr "כעת ניתן להוסיף את הווריאציות שלו."
     775
     776msgid "Priced per variation. You can set prices after saving, once you have added its variations."
     777msgstr "התמחור הוא לפי וריאציה. תוכלו להגדיר מחירים לאחר השמירה, כשתוסיפו את הווריאציות."
     778
     779msgid "Tick the options this product comes in above and save, then you can add variations here."
     780msgstr "סמנו למעלה את האפשרויות שבהן המוצר מגיע ושמרו, ואז תוכלו להוסיף כאן וריאציות."
     781
     782msgid "Variations are saved with the product and go live when the store owner approves it."
     783msgstr "הווריאציות נשמרות יחד עם המוצר ונכנסות לתוקף כשבעל החנות מאשר אותו."
     784
     785msgid "Stock, availability, and adding or removing variations apply straight away. Price and SKU changes to existing variations wait for admin approval."
     786msgstr "מלאי, זמינות והוספה או הסרה של וריאציות נכנסים לתוקף מיד. שינויי מחיר ומק\"ט בווריאציות קיימות ממתינים לאישור המנהל."
     787
     788msgid "Remove"
     789msgstr "הסרה"
     790
     791msgid "Add variations"
     792msgstr "הוספת וריאציות"
     793
     794msgid "Tick the combinations you sell, fill in their details and save."
     795msgstr "סמנו את השילובים שאתם מוכרים, מלאו את הפרטים ושמרו."
     796
     797msgid "Showing the first %1$s of %2$s combinations."
     798msgstr "מוצגים %1$s הראשונים מתוך %2$s שילובים."
     799
     800msgid "Price for selected"
     801msgstr "מחיר לנבחרים"
     802
     803msgid "Stock for selected"
     804msgstr "מלאי לנבחרים"
     805
     806msgid "Fill selected rows"
     807msgstr "מילוי השורות שנבחרו"
     808
     809msgid "Select all"
     810msgstr "בחירת הכל"
     811
     812msgid "Every combination of the chosen options already exists."
     813msgstr "כל השילובים של האפשרויות שנבחרו כבר קיימים."
     814
     815msgid "There are too many combinations to list (%s). Untick some option values to narrow them down."
     816msgstr "יש יותר מדי שילובים להצגה (%s). בטלו את הסימון של חלק מהאפשרויות כדי לצמצם."
     817
     818msgid "This product already has variations, so a new kind of option cannot be added to it. Remove its variations first, or add values to the options it already uses."
     819msgstr "למוצר זה כבר יש וריאציות, ולכן לא ניתן להוסיף לו סוג אפשרות חדש. הסירו קודם את הווריאציות, או הוסיפו ערכים לאפשרויות הקיימות."
     820
     821msgid "Variations added: %s."
     822msgstr "וריאציות שנוספו: %s."
     823
     824msgid "Variations removed: %s."
     825msgstr "וריאציות שהוסרו: %s."
     826
     827msgid "Some SKUs were already in use and were left blank."
     828msgstr "חלק מהמק\"טים כבר היו בשימוש ונשארו ריקים."
     829
     830msgid "%s selected combinations were skipped because the options changed. Please select them again."
     831msgstr "%s שילובים שנבחרו דולגו כי האפשרויות השתנו. יש לבחור אותם שוב."
     832
     833msgid "Remove %d variation(s)? This cannot be undone."
     834msgstr "להסיר %d וריאציות? לא ניתן לבטל פעולה זו."
     835
     836msgid "Price %1$s, stock %2$s"
     837msgstr "מחיר %1$s, מלאי %2$s"
     838
     839msgid "All changes to variations apply straight away, without admin approval."
     840msgstr "כל השינויים בווריאציות נכנסים לתוקף מיד, ללא אישור מנהל."
     841
     842msgid "Price required"
     843msgstr "נדרש מחיר"
     844
     845msgid "Price must be a number"
     846msgstr "המחיר חייב להיות מספר"
     847
     848msgid "Price cannot be negative"
     849msgstr "המחיר אינו יכול להיות שלילי"
     850
     851msgid "That SKU is already in use. Please choose another."
     852msgstr "המק\"ט הזה כבר בשימוש. בחרו מק\"ט אחר."
     853
     854msgid "Price, SKU and barcode changes were applied straight away."
     855msgstr "שינויי מחיר, מק\"ט וברקוד נכנסו לתוקף מיד."
     856
     857msgid "Applies straight away, without admin approval."
     858msgstr "נכנס לתוקף מיד, ללא אישור מנהל."
     859
     860msgid "SKU and barcode changes apply straight away, without admin approval."
     861msgstr "שינויי מק\"ט וברקוד נכנסים לתוקף מיד, ללא אישור מנהל."
     862
     863msgid "VendBoard: the Vendor Dashboard page (/vendor-dashboard/) is not published, so vendors are sent to the home page instead of their dashboard. Publish the page to fix this."
     864msgstr "VendBoard: עמוד לוח הבקרה לספקים (/vendor-dashboard/) אינו מפורסם, ולכן ספקים מועברים לעמוד הבית במקום ללוח הבקרה שלהם. פרסמו את העמוד כדי לתקן זאת."
     865
     866msgid "%1$s of %2$s selected"
     867msgstr "%1$s מתוך %2$s נבחרו"
     868
     869msgid "Current variations"
     870msgstr "וריאציות קיימות"
     871
     872msgid "%s not created yet"
     873msgstr "%s טרם נוצרו"
     874
     875msgid "Options vendors can use"
     876msgstr "אפשרויות שספקים יכולים להשתמש בהן"
     877
     878msgid "No product attributes have been set up yet. Add them under Products > Attributes."
     879msgstr "לא הוגדרו עדיין מאפייני מוצר. הוסיפו אותם תחת מוצרים > מאפיינים."
     880
     881msgid "Vendors can only choose from the ticked options when setting up a product with options. Ticking all of them is the same as ticking none. Attributes already on a product are never removed."
     882msgstr "ספקים יכולים לבחור רק מהאפשרויות המסומנות בעת הגדרת מוצר עם אפשרויות. סימון כולן זהה לאי-סימון אף אחת. מאפיינים שכבר קיימים במוצר לעולם אינם מוסרים."
  • vendboard-vendor-dashboard-for-woocommerce/trunk/readme.txt

    r3666364 r3718469  
    66Requires PHP: 7.4
    77Requires at least WooCommerce: 9.4
    8 Stable tag: 1.1.1
     8Stable tag: 1.2.0
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    1919* Add new products
    2020* Edit product content (title, short and full description)
    21 * Change product price, SKU, and barcode
     21* Change product price, SKU, and barcode (applied immediately)
    2222* Update stock quantity and stock status (applied immediately)
    2323* Publish or unpublish a product and set its catalog visibility (applied immediately)
     24* Add, remove and edit product variations: stock, price and SKU for each size or colour (applied immediately)
    2425* Upload product images and videos
    2526
    26 All vendor changes are saved as Pending Review, while the store admin keeps full control over what gets published. Stock levels are the one exception: they are applied immediately, because holding a stock figure in a queue while orders keep coming in would let an approval roll the level back and oversell the product.
     27New products and content changes (title, descriptions and images) are saved as Pending Review, while the store admin keeps full control over what gets published. Operational changes apply immediately instead: price, SKU, barcode, stock levels, publishing and catalog visibility, and product variations. Holding a stock figure in a queue while orders keep coming in, for example, would let an approval roll the level back and oversell the product.
    2728
    2829This plugin is ideal for multi-vendor stores, brands working with external suppliers, and marketplaces that require moderation before publishing product changes.
    … …  
    4142* Paginated product list that stays fast with large catalogues
    4243* Clear indicators showing missing or incomplete content
    43 * Changes are never published automatically
     44* New products and content changes are never published without approval
    4445
    4546Admin Moderation
    … …  
    6061Why Stores Choose This Plugin
    6162* Safe Editing: vendors never risk site integrity
    62 * Full Control: all changes require admin approval
     63* Full Control: new products and content changes require admin approval
    6364* Time Saver: reduces admin workload
    6465* Vendor-Friendly: simple and intuitive interface
    … …  
    6869
    6970= What does this plugin do? =
    70 This plugin allows vendors to safely add and edit their own WooCommerce products without access to wp-admin. All changes require admin approval before publishing.
     71This plugin allows vendors to safely add and edit their own WooCommerce products without access to wp-admin. New products and content changes require admin approval before publishing.
    7172
    7273= Do vendors get access to wp-admin? =
    … …  
    9394Yes. The dashboard has a search box that matches product name, description, SKU and barcode, and the product list is paginated. Products waiting for approval are always listed in full, above the searchable list, so they never get lost behind a filter.
    9495
     96= Can vendors manage product variations? =
     97Yes. On any product that uses variations, vendors can set the stock, price and SKU for each variation from their dashboard, and can add new variations by choosing from the options you have already set up on that product.
     98
     99All variation changes apply immediately, without admin approval: stock, price, SKU, availability, and adding or removing variations. To add variations, vendors tick the combinations they sell in one list, fill in price, stock and SKU, and save them all at once. On a new product everything is reviewed together when you approve the product.
     100
     101= Can I choose which options vendors see? =
     102Yes. Under Vendor Dashboard > Branding there is a list of your product attributes, and vendors can only use the ones you tick. Attributes already attached to a product are left alone, so hiding one never changes existing products. The values inside each attribute come from Products > Attributes, so editing or removing a value there changes what vendors can choose.
     103
     104= Can vendors create their own attributes? =
     105No, and this is deliberate. Attributes such as Size and Colour are site-wide taxonomies shared by every product and every vendor, so only administrators create them and their values. Vendors choose from the values you have attached to their product, which means one vendor can never introduce a term that shows up in another vendor's products or your shop filters.
     106
     107When creating a product, a vendor chooses whether it is a single product or one with options, and ticks which of your attribute values it comes in. They can change the type until you first approve the product; after that it is fixed, because changing it on a live product affects its pricing. Vendors can still tick or untick option values later, to add a new size for example.
     108
    95109= Do stock changes need admin approval? =
    96 No, and this is deliberate. Stock is the only field that goes live immediately. Every other field still waits for your approval. Stock is operational data that your own sales keep changing, so if a vendor's figure sat in the approval queue while orders came in, approving it later would overwrite those sales and leave the store showing more units than actually exist. Applying stock straight away keeps the number honest. Vendors still only ever reach their own brand's products.
     110No, and this is deliberate. Stock is operational data that your own sales keep changing, so if a vendor's figure sat in the approval queue while orders came in, approving it later would overwrite those sales and leave the store showing more units than actually exist. Applying stock straight away keeps the number honest. Vendors still only ever reach their own brand's products.
    97111
    98112= Are vendor changes published automatically? =
    99 No. All vendor changes and new products are saved as Pending Review until approved by an administrator.
     113New products, and changes to titles, descriptions and images, are saved as Pending Review until an administrator approves them. Price, SKU, barcode, stock, publishing, visibility and variation changes apply immediately.
    100114
    101115= How does admin moderation work? =
    … …  
    140154
    141155== Changelog ==
     156
     157= 1.2.0 =
     158* The options and variations sections on the vendor edit screen are now collapsible, with counts in each heading, so a product with many options is easier to work through.
     159* Added a setting (Vendor Dashboard > Branding) for choosing which product attributes vendors may use, so attributes that are not relevant to them can be hidden without removing them from the store.
     160* Renamed the "YouTube link / Short Description" field to "Short Description".
     161* Security: vendors are now always kept out of wp-admin. Previously, if the Vendor Dashboard page was missing or unpublished, vendors were let into wp-admin, and a URL parameter documented only in the source code let any vendor skip the redirect.
     162* The Vendor Dashboard page is recreated automatically if it has been deleted, and administrators see a notice if it has been unpublished.
     163* Vendors are taken straight to their dashboard when they log in.
     164* Fixed products created by vendors having no category. WooCommerce files every product under the default category, and without one some themes could not display the product page. Existing products without a category are filed under the default category the first time an administrator opens wp-admin after updating.
     165* A product's price, SKU and barcode now apply immediately, without admin approval. Prices are saved through WooCommerce, so an active sale price is no longer overwritten. New products and changes to titles, descriptions and images still require approval.
     166* Added a stock status filter to the vendor product list, so vendors with large catalogues can show only out of stock (or in stock, or on backorder) products. Works together with search and pagination.
     167* Vendors can now manage product variations from their dashboard: stock, price and SKU for each variation, plus switching individual variations on and off.
     168* Vendors can create many variations at once: every missing combination of the product's options is listed, and vendors tick the ones they sell and fill in price, stock and SKU. New variations go live immediately, without admin approval.
     169* Vendors can remove variations. Removal is permanent and asks for confirmation.
     170* When creating a product, vendors can make it a product with options and tick which of your global attribute values it comes in. The type can be changed until an administrator first approves the product; option values stay editable afterwards.
     171* Vendors cannot create attributes or attribute terms, and cannot change a product's type once it has been approved. Both stay with the administrator.
     172* All variation changes apply immediately, without admin approval: stock, price, SKU and availability.
     173* Fixed the product price field on variable products. It previously saved and was approved but had no effect, because WooCommerce derives a variable product's price from its variations rather than the parent. The field is now replaced with a pointer to the variations section.
    142174
    143175= 1.1.1 =
    … …  
    205237== Upgrade Notice ==
    206238
     239= 1.2.0 =
     240Security fix: vendors could reach wp-admin if the Vendor Dashboard page was missing or unpublished. Updating is recommended for every site. Please also note a deliberate change to the approval workflow: price, SKU, barcode, stock and variations now apply immediately, while new products and changes to titles, descriptions and images still require admin approval. Vendors can now create and manage product variations.
     241
    207242= 1.1.1 =
    208243Fixes barcodes being saved under a key the plugin never displayed, and products being falsely flagged for approval. Recommended for anyone using the barcode field.
  • vendboard-vendor-dashboard-for-woocommerce/trunk/vendor-dashboard.php

    r3666364 r3718469  
    33 * Plugin Name: VendBoard - Vendor Dashboard for WooCommerce
    44 * Description: Empowers store owners by creating a personal dashboard for vendors to easily manage and edit their own products, with smart brand linking and admin approval before publishing. (Requires WooCommerce 9.4+)
    5  * Version: 1.1.1
     5 * Version: 1.2.0
    66 * Author: WebSkipper
    77 * Text Domain: vendboard-vendor-dashboard-for-woocommerce
    … …  
    3737define('VENDBOARD_PLUGIN_PATH', plugin_dir_path(__FILE__));
    3838define('VENDBOARD_PLUGIN_URL', plugin_dir_url(__FILE__));
    39 define('VENDBOARD_PLUGIN_VERSION', '1.1.1');
     39define('VENDBOARD_PLUGIN_VERSION', '1.2.0');
    4040
    4141// -----------------------------
    … …  
    4646require_once VENDBOARD_PLUGIN_PATH . 'includes/class-vendboard-role.php';
    4747require_once VENDBOARD_PLUGIN_PATH . 'includes/class-vendboard-user-brand.php';
     48require_once VENDBOARD_PLUGIN_PATH . 'includes/class-vendboard-variations.php';
    4849
    4950require_once VENDBOARD_PLUGIN_PATH . 'admin/admin-menu.php';
    … …  
    447448    }
    448449}
     450
     451/**
     452 * Helper: Make sure a product is filed under at least one category.
     453 *
     454 * WooCommerce puts every product it creates into the default product
     455 * category ("Uncategorized"). Products created with wp_insert_post() skip
     456 * that step, and some themes fail to render a product page that has no
     457 * category at all, so the same default is applied here.
     458 *
     459 * @return bool True if a category was assigned.
     460 */
     461function vendboard_ensure_product_category($product_id)
     462{
     463    $default = (int) get_option('default_product_cat', 0);
     464
     465    if (!$product_id || !$default || !term_exists($default, 'product_cat')) {
     466        return false;
     467    }
     468
     469    $current = wp_get_object_terms($product_id, 'product_cat', ['fields' => 'ids']);
     470
     471    if (is_wp_error($current) || !empty($current)) {
     472        return false;
     473    }
     474
     475    wp_set_object_terms($product_id, [$default], 'product_cat');
     476
     477    return true;
     478}
     479
     480/**
     481 * One-time data repairs after an update.
     482 *
     483 * Runs when an administrator opens wp-admin, so it never slows down vendors
     484 * or shoppers, and only once per repair version.
     485 */
     486function vendboard_maybe_upgrade()
     487{
     488    if (!current_user_can('manage_options') || !taxonomy_exists('product_cat')) {
     489        return;
     490    }
     491
     492    if (version_compare((string) get_option('vendboard_db_version', '0'), '1.2.0', '>=')) {
     493        return;
     494    }
     495
     496    // 1.2.0: products created by vendors had no category (see above)
     497    $uncategorised = get_posts([
     498        'post_type' => 'product',
     499        'post_status' => ['publish', 'draft', 'pending', 'private', 'future'],
     500        'posts_per_page' => -1,
     501        'fields' => 'ids',
     502        'no_found_rows' => true,
     503        'tax_query' => [
     504            [
     505                'taxonomy' => 'product_cat',
     506                'operator' => 'NOT EXISTS',
     507            ],
     508        ],
     509    ]);
     510
     511    foreach ($uncategorised as $product_id) {
     512        vendboard_ensure_product_category($product_id);
     513    }
     514
     515    update_option('vendboard_db_version', '1.2.0', false);
     516}
     517add_action('admin_init', 'vendboard_maybe_upgrade');
     518
     519/**
     520 * Helper: The published vendor dashboard page, created if it is missing.
     521 *
     522 * Everything vendor-facing lives on the page at /vendor-dashboard/. The page
     523 * is normally created on activation, but can be missing on a site where
     524 * activation was interrupted, or after it was deleted. A missing page is
     525 * recreated. A page the store owner has unpublished is left as it is, and
     526 * they are told about it instead (see the admin notice below).
     527 *
     528 * @return int Page ID, or 0 if there is no published dashboard page.
     529 */
     530function vendboard_ensure_dashboard_page()
     531{
     532    $page = get_page_by_path('vendor-dashboard');
     533
     534    if ($page) {
     535        return ($page->post_status === 'publish') ? (int) $page->ID : 0;
     536    }
     537
     538    $admins = get_users(['role' => 'administrator', 'number' => 1, 'fields' => 'ID']);
     539
     540    $page_id = wp_insert_post([
     541        'post_title' => 'Vendor Dashboard',
     542        'post_name' => 'vendor-dashboard',
     543        'post_content' => '[vendboard_dashboard]',
     544        'post_status' => 'publish',
     545        'post_type' => 'page',
     546        'post_author' => $admins ? (int) $admins[0] : 0,
     547        'comment_status' => 'closed',
     548    ], true);
     549
     550    if (is_wp_error($page_id) || !$page_id) {
     551        return 0;
     552    }
     553
     554    update_option('vendboard_vendor_dashboard_page_id', (int) $page_id);
     555
     556    // wp_insert_post() may have made the slug unique; the plugin needs this one.
     557    return (get_post_field('post_name', $page_id) === 'vendor-dashboard') ? (int) $page_id : 0;
     558}
     559
     560/**
     561 * Tell administrators when vendors cannot reach their dashboard.
     562 */
     563function vendboard_dashboard_page_notice()
     564{
     565    if (!current_user_can('manage_options') || vendboard_ensure_dashboard_page()) {
     566        return;
     567    }
     568
     569    echo '<div class="notice notice-error"><p>' .
     570        esc_html__('VendBoard: the Vendor Dashboard page (/vendor-dashboard/) is not published, so vendors are sent to the home page instead of their dashboard. Publish the page to fix this.', 'vendboard-vendor-dashboard-for-woocommerce') .
     571        '</p></div>';
     572}
     573add_action('admin_notices', 'vendboard_dashboard_page_notice');
     574
     575/**
     576 * Helper: Global attributes, keyed the way the plugin compares them.
     577 *
     578 * @return array [ key => ['taxonomy' => 'pa_size', 'label' => 'Size'] ]
     579 */
     580function vendboard_get_global_attribute_choices()
     581{
     582    $out = [];
     583
     584    if (!function_exists('wc_get_attribute_taxonomies')) {
     585        return $out;
     586    }
     587
     588    foreach ((array) wc_get_attribute_taxonomies() as $tax_obj) {
     589
     590        $taxonomy = wc_attribute_taxonomy_name($tax_obj->attribute_name);
     591
     592        if (!taxonomy_exists($taxonomy)) {
     593            continue;
     594        }
     595
     596        $out[sanitize_title($taxonomy)] = [
     597            'taxonomy' => $taxonomy,
     598            'label' => $tax_obj->attribute_label ? $tax_obj->attribute_label : $tax_obj->attribute_name,
     599        ];
     600    }
     601
     602    return $out;
     603}
     604
     605/**
     606 * Helper: May vendors use this global attribute?
     607 *
     608 * The store owner picks the list under Vendor Dashboard > Branding. An empty
     609 * setting means every attribute is available, which is the default.
     610 */
     611function vendboard_vendor_attribute_allowed($key)
     612{
     613    $allowed = get_option('vendboard_vendor_attributes', []);
     614
     615    if (!is_array($allowed) || empty($allowed)) {
     616        return true;
     617    }
     618
     619    return in_array((string) $key, array_map('strval', $allowed), true);
     620}
Note: See TracChangeset for help on using the changeset viewer.