Enhance product management and coupon features

- Added styling for input group add-ons and additional fields in SCSS.
- Updated shop coupon view to include a new column for usage count.
- Display coupon code and amount in order details if applicable.
- Improved product edit template to handle custom fields with required validation.
- Modified product save logic to include custom field requirements.
- Enhanced decimal normalization function for better input handling.
- Implemented checkbox normalization for form submissions.
- Updated custom fields in product templates to reflect required status.
- Fixed URL for fetching changelog updates.
This commit is contained in:
2025-08-19 20:35:24 +02:00
parent 325aabc0e8
commit 4897ef132a
16 changed files with 172 additions and 57 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2228,4 +2228,20 @@ textarea.form-control {
width: 50px;
text-align: center;
}
}
.input-group-addon {
width: auto;
label {
display: flex;
align-items: center;
gap: 5px;
}
}
.additional_fields {
input[type="text"] {
height: 40px;
}
}

View File

@@ -12,11 +12,11 @@ $grid -> search = [
[ 'name' => 'Wysłany', 'db' => 'send', 'type' => 'select', 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ] ]
];
$grid -> columns_view = [
[
[
'name' => 'Lp.',
'th' => [ 'class' => 'g-lp' ],
'td' => [ 'class' => 'g-center' ],
'autoincrement' => true
'autoincrement' => true
], [
'name' => 'Aktywny',
'db' => 'status',
@@ -24,6 +24,11 @@ $grid -> columns_view = [
'td' => [ 'class' => 'g-center' ],
'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ],
'sort' => true
], [
'name' => 'Użyto X razy',
'db' => 'used_count',
'td' => [ 'class' => 'g-center' ],
'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ],
], [
'name' => 'Nazwa',
'db' => 'name',
@@ -60,22 +65,22 @@ $grid -> columns_view = [
'db' => 'date_used',
'td' => [ 'class' => 'g-center' ],
'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ]
], [
'name' => 'Edytuj',
'action' => [ 'type' => 'edit', 'url' => '/admin/shop_coupon/coupon_edit/id=[id]' ],
], [
'name' => 'Edytuj',
'action' => [ 'type' => 'edit', 'url' => '/admin/shop_coupon/coupon_edit/id=[id]' ],
'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ],
'td' => [ 'class' => 'g-center' ]
], [
'name' => 'Usuń',
'action' => [ 'type' => 'delete', 'url' => '/admin/shop_coupon/coupon_delete/id=[id]' ],
], [
'name' => 'Usuń',
'action' => [ 'type' => 'delete', 'url' => '/admin/shop_coupon/coupon_delete/id=[id]' ],
'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ],
'td' => [ 'class' => 'g-center' ]
]
];
$grid -> buttons = [
[
'label' => 'Dodaj kupon',
'url' => '/admin/shop_coupon/coupon_edit/',
[
'label' => 'Dodaj kupon',
'url' => '/admin/shop_coupon/coupon_edit/',
'icon' => 'fa-plus-circle',
'class' => 'btn-success'
]

View File

@@ -44,6 +44,9 @@ ob_start();
<div class="panel">
<div class="panel-body">
<div>Kwota zamówienia <b><?= $this -> order[ 'summary' ];?> zł</b></div>
<? if ( $this -> coupon ):?>
<div>Kod rabatowy: <span style="color: #cc0000;"><?= $this -> coupon -> name;?> - <?= $this -> coupon -> amount;?> <?= $this -> coupon -> type == 1 ? '%' : 'zł';?></span></div>
<? endif;?>
<br>
<div><?= strip_tags( $this -> order[ 'transport' ] );?>: <b><?= $this -> order[ 'transport_cost' ];?> zł</b></div>
<? if ( $this -> order['transport_id'] == 2 and $this -> order[ 'inpost_paczkomat' ] ):?>

View File

@@ -663,12 +663,19 @@ ob_start();
<div>
<a href="#" class="btn btn-success" id="add_custom_field"><i class="fa fa-plus"></i> dodaj niestandardowe pole</a>
<div class="additional_fields">
<? if ( is_array( $this -> product['custom_fields'] ) ) : foreach ( $this -> product['custom_fields'] as $field ):?>
<div class="form-group row">
<? if ( is_array( $this->product['custom_fields'] ) ) : foreach ( $this->product['custom_fields'] as $field ):?>
<? $isRequired = !empty($field['is_required']); ?>
<div class="form-group row custom-field-row">
<label class="col-lg-4 control-label">Nazwa pola:</label>
<div class="col-lg-8">
<div class="input-group">
<input type="text" class="form-control" name="custom_field_name[]" value="<?= $field['name']; ?>">
<input type="text" class="form-control" name="custom_field_name[]" value="<?= htmlspecialchars($field['name']); ?>">
<span class="input-group-addon">
<label style="margin:0; font-weight:normal;">
<input type="checkbox" class="custom-field-required" <?= $isRequired ? 'checked' : '' ?> name="custom_field_required[]" />
wymagane
</label>
</span>
<span class="input-group-addon btn btn-info" onclick="remove_custom_filed( $( this ) );">usuń</span>
</div>
</div>
@@ -740,17 +747,23 @@ echo $grid->draw();
$(function() {
$( 'body' ).on( 'click', '#add_custom_field', function() {
$('body').on('click', '#add_custom_field', function() {
var html = '';
html += '<div class="form-group row">';
html += '<div class="form-group row custom-field-row">';
html += '<label class="col-lg-4 control-label">Nazwa pola:</label>';
html += '<div class="col-lg-8">';
html +='<div class="input-group">';
html += '<div class="input-group">';
html += '<input type="text" class="form-control" name="custom_field_name[]" value="">';
html += '<span class="input-group-addon">';
html += '<label style="margin:0; font-weight:normal;">';
html += '<input type="checkbox" class="custom-field-required" name="custom_field_required[]"> wymagane';
html += '</label>';
html += '</span>';
html += '<span class="input-group-addon btn btn-info" onclick="remove_custom_filed( $( this ) );">usuń</span>';
html += '</div>';
html += '</div>';
html += '</div>';
$( '.additional_fields' ).append( html );
$('.additional_fields').append(html);
});
$('body').on('click', '#product-preview', function() {

View File

@@ -64,7 +64,7 @@ echo $grid -> draw();
?>
<?
ob_start();
echo $versions = file_get_contents( 'https://shoppro.project-dc.pl/updates/changelog.php' );
echo $versions = file_get_contents( 'http://www.shoppro.project-dc.pl/updates/changelog.php' );
$out = ob_get_clean();
$grid = new \gridEdit;

View File

@@ -186,7 +186,7 @@ class ShopProduct
$values['id'], $values['name'], $values['short_description'], $values['description'], $values['status'], $values['meta_description'], $values['meta_keywords'], $values['seo_link'],
$values['copy_from'], $values['categories'], $values['price_netto'], $values['price_brutto'], $values['vat'], $values['promoted'], $values['warehouse_message_zero'], $values['warehouse_message_nonzero'], $values['tab_name_1'],
$values['tab_description_1'], $values['tab_name_2'], $values['tab_description_2'], $values['layout_id'], $values['products_related'], (int) $values['set'], $values['price_netto_promo'], $values['price_brutto_promo'],
$values['new_to_date'], $values['stock_0_buy'], $values['wp'], $values['custom_label_0'], $values['custom_label_1'], $values['custom_label_2'], $values['custom_label_3'], $values['custom_label_4'], $values['additional_message'], (int)$values['quantity'], $values['additional_message_text'], $values['additional_message_required'] == 'on' ? 1 : 0, $values['canonical'], $values['meta_title'], $values['producer_id'], $values['sku'], $values['ean'], $values['product_unit'], $values['weight'], $values['xml_name'], $values['custom_field_name']
$values['new_to_date'], $values['stock_0_buy'], $values['wp'], $values['custom_label_0'], $values['custom_label_1'], $values['custom_label_2'], $values['custom_label_3'], $values['custom_label_4'], $values['additional_message'], (int)$values['quantity'], $values['additional_message_text'], $values['additional_message_required'] == 'on' ? 1 : 0, $values['canonical'], $values['meta_title'], $values['producer_id'], $values['sku'], $values['ean'], $values['product_unit'], $values['weight'], $values['xml_name'], $values['custom_field_name'], $values['custom_field_required']
) ) {
$response = [ 'status' => 'ok', 'msg' => 'Produkt został zapisany.', 'id' => $id ];
}

View File

@@ -761,7 +761,7 @@ class ShopProduct
}
public static function save(
$product_id, $name, $short_description, $description, $status, $meta_description, $meta_keywords, $seo_link, $copy_from, $categories, $price_netto, $price_brutto, $vat, $promoted, $warehouse_message_zero, $warehouse_message_nonzero, $tab_name_1, $tab_description_1, $tab_name_2, $tab_description_2, $layout_id, $products_related, int $set_id, $price_netto_promo, $price_brutto_promo, $new_to_date, $stock_0_buy, $wp, $custom_label_0, $custom_label_1, $custom_label_2, $custom_label_3, $custom_label_4, $additional_message, int $quantity, $additional_message_text, int $additional_message_required, $canonical, $meta_title, $producer_id, $sku, $ean, $product_unit, $weight, $xml_name, $custom_field_name
$product_id, $name, $short_description, $description, $status, $meta_description, $meta_keywords, $seo_link, $copy_from, $categories, $price_netto, $price_brutto, $vat, $promoted, $warehouse_message_zero, $warehouse_message_nonzero, $tab_name_1, $tab_description_1, $tab_name_2, $tab_description_2, $layout_id, $products_related, int $set_id, $price_netto_promo, $price_brutto_promo, $new_to_date, $stock_0_buy, $wp, $custom_label_0, $custom_label_1, $custom_label_2, $custom_label_3, $custom_label_4, $additional_message, int $quantity, $additional_message_text, int $additional_message_required, $canonical, $meta_title, $producer_id, $sku, $ean, $product_unit, $weight, $xml_name, $custom_field_name, $custom_field_required
)
{
global $mdb, $user;
@@ -941,13 +941,17 @@ class ShopProduct
}
// dodatkowe pola
foreach ( $custom_field_name as $custom_field )
for ( $i = 0; $i < count( $custom_field_name ); ++$i )
{
if ( !empty( $custom_field ) )
if ( !empty( $custom_field_name[$i] ) )
{
$custom_field = $custom_field_name[$i];
$custom_field_required = isset( $custom_field_required[$i] ) ? 1 : 0;
$mdb -> insert( 'pp_shop_products_custom_fields', [
'id_product' => (int) $id,
'name' => $custom_field,
'is_required' => $custom_field_required,
] );
}
}
@@ -1268,13 +1272,20 @@ class ShopProduct
$mdb -> delete( 'pp_shop_products_custom_fields', [ 'AND' => [ 'id_product' => $product_id, 'id_additional_field[!]' => $exits_custom_ids ] ] );
foreach ( $custom_field_name as $custom_field )
// $custom_field_name i $custom_field_required
foreach ( $custom_field_name as $i => $custom_field )
{
if ( !empty( $custom_field ) )
{
$is_required = !empty( $custom_field_required[$i] ) ? 1 : 0;
if ( !$mdb -> count( 'pp_shop_products_custom_fields', [ 'AND' => [ 'id_product' => $product_id, 'name' => $custom_field ] ] ) )
{
$mdb -> insert( 'pp_shop_products_custom_fields', [ 'id_product' => $product_id, 'name' => $custom_field ] );
$mdb -> insert( 'pp_shop_products_custom_fields', [ 'id_product' => $product_id, 'name' => $custom_field, 'is_required' => $is_required ] );
}
else
{
$mdb -> update( 'pp_shop_products_custom_fields', [ 'is_required' => $is_required ], [ 'AND' => [ 'id_product' => $product_id, 'name' => $custom_field ] ] );
}
}
}

View File

@@ -322,17 +322,42 @@ class S
return $result;
}
public static function normalize_decimal( $val, $precision = 2 )
static public function normalize_decimal($val, int $precision = 2)
{
$input = str_replace( ' ', '', $val );
$number = str_replace( ',', '.', $input );
if ( strpos( $number, '.' ) )
{
$groups = explode( '.', str_replace( ',', '.', $number ) );
$lastGroup = array_pop( $groups );
$number = implode( '', $groups ) . '.' . $lastGroup;
if ($val === null || $val === '') {
return number_format(0, $precision, '.', '');
}
return bcadd( round( $number, $precision ), 0, $precision );
// 1) wstępne czyszczenie
$s = (string)$val;
$s = str_replace(["\xC2\xA0", ' ', '', "'"], '', $s); // spacje (w tym NBSP) i apostrofy
$s = preg_replace('/[^0-9.,\-]/', '', $s); // zostaw tylko cyfry, . , i -
// 2) ustalenie separatora dziesiętnego
$lastDot = strrpos($s, '.');
$lastComma = strrpos($s, ',');
if ($lastDot !== false && $lastComma !== false) {
// oba występują prawy znak traktujemy jako dziesiętny
if ($lastDot > $lastComma) {
$s = str_replace(',', '', $s); // , = tysiące
} else {
$s = str_replace('.', '', $s); // . = tysiące
$s = str_replace(',', '.', $s); // , = dziesiętny
}
} elseif ($lastComma !== false) {
// tylko przecinek traktuj jako dziesiętny
$s = str_replace(',', '.', $s);
} elseif (substr_count($s, '.') > 1) {
// wiele kropek ostatnia dziesiętna, pozostałe tysiące
$pos = strrpos($s, '.');
$s = str_replace('.', '', substr($s, 0, $pos)) . '.' . substr($s, $pos + 1);
}
// na tym etapie mamy opcjonalny '-' i co najwyżej jedną kropkę dziesiętną
// 3) zaokrąglenie i normalizacja
$rounded = round((float)$s, $precision);
return number_format($rounded, $precision, '.', '');
}
public static function decimal( $val, $precision = 2, $dec_point = ',', $thousands_sep = ' ' )

View File

@@ -485,6 +485,8 @@ jQuery( 'body' ).on( 'click', '#g-cancel', function() {
});
});
jQuery( 'body' ).on( 'click', '#g-save, #g-edit-save', function()
{
var back_url = jQuery( this ).attr( 'back_url' );
@@ -534,6 +536,44 @@ jQuery( 'body' ).on( 'click', '#g-save, #g-edit-save', function()
}
});
/* === UNIWERSALNA NORMALIZACJA CHECKBOXÓW TABLICOWYCH ===
Dla wszystkich input[type=checkbox] z name="coś[]":
- jeśli wartości są typu boolean ('', '1', 'on', 'true', 'yes') → zbuduj pełną tablicę 1/0 w kolejności DOM,
tak aby indeksy były ciągłe [0..N-1] nawet gdy część jest odznaczona.
- jeśli to lista wyboru (np. categories[] z różnymi ID) → pozostaw jak z serializeArray (tylko zaznaczone).
*/
(function normalizeCheckboxArrays() {
var $form = jQuery('#fg-' + gtable);
if (!$form.length) return;
// zgrupuj checkboxy po pełnej nazwie (z [] na końcu)
var groups = {};
$form.find('input[type="checkbox"][name$="[]"]').each(function () {
var n = this.name; // np. "required[]", "visible[]", "categories[]"
(groups[n] = groups[n] || []).push(this);
});
Object.keys(groups).forEach(function (nameWithBrackets) {
var inputs = groups[nameWithBrackets];
if (!inputs.length) return;
// sprawdź, czy wszystkie wartości wyglądają „booleanowo”
var uniqVals = Array.from(new Set(inputs.map(function (el) {
return (el.getAttribute('value') || '').toLowerCase();
})));
var boolSet = new Set(['', '1', 'on', 'true', 'yes']);
var isBooleanLike = uniqVals.every(function (v) { return boolSet.has(v); });
if (isBooleanLike) {
var baseKey = nameWithBrackets.replace(/\[\]$/, ''); // usuń [] → "required", "visible"
// pełna tablica 1/0 w kolejności w formularzu
formattedValues[baseKey] = inputs.map(function (el) { return el.checked ? '1' : '0'; });
}
// else: zostawiamy formattedValues tak jak już zbudowane z serializeArray()
});
})();
var url = jQuery( this ).attr( 'url' );
if ( url !== '' )

BIN
templates/.DS_Store vendored

Binary file not shown.

View File

@@ -4,7 +4,7 @@
<?= $custom_field['name'];?>:
</div>
<div class="_input">
<input type="text" class="form-control" name="custom_field[<?= $custom_field['id_additional_field'];?>]" field_name="<?= $custom_field['name'];?>" value="">
<input type="text" class="form-control" name="custom_field[<?= $custom_field['id_additional_field'];?>]" field_name="<?= $custom_field['name'];?>" value="" <? if ( !empty( $custom_field['is_required'] ) ): ?>required<? endif; ?>>
</div>
</div>
<? endforeach; endif;?>

View File

@@ -255,22 +255,24 @@
<script class="footer" type="text/javascript" src="/libraries/fancybox3/js/wheel.js"></script>
<script class="footer" type="text/javascript" src="/plugins/OwlCarousel/owl.carousel.js"></script>
<script type="text/javascript">
<? if ( $this -> settings['google_tag_manager_id'] ):?>
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: "view_item",
ecommerce: {
items: [
{
item_id: "<?= $this -> product -> id;?>",
item_name: "<?= str_replace( '"', '', $this -> product -> language['name'] );?>",
price: '<? if ( $this -> product -> price_brutto_promo ): echo \S::normalize_decimal( $this -> product -> price_brutto_promo ); else: echo \S::normalize_decimal( $this -> product -> price_brutto ); endif;?>',
quantity: 1
}
]
}
});
<? endif;?>
$(function(){
<? if ( $this -> settings['google_tag_manager_id'] ):?>
dataLayer.push({ ecommerce: null });
dataLayer.push({
event: "view_item",
ecommerce: {
items: [
{
item_id: "<?= $this -> product -> id;?>",
item_name: "<?= str_replace( '"', '', $this -> product -> language['name'] );?>",
price: '<? if ( $this -> product -> price_brutto_promo ): echo \S::normalize_decimal( $this -> product -> price_brutto_promo ); else: echo \S::normalize_decimal( $this -> product -> price_brutto ); endif;?>',
quantity: 1
}
]
}
});
<? endif;?>
});
</script>
<script class="footer" type="text/javascript">
$( function ()
@@ -524,7 +526,7 @@
}
// dodatkowe pola muszą być uzupełnione
$( '.custom-field input' ).each( function( index, element )
$( '.custom-field input[required]' ).each( function( index, element )
{
if ( $.trim( $( element ).val() ) == '' )
{

View File

@@ -1,11 +1,11 @@
<? if ( is_array( $this -> custom_fields ) ): foreach ( $this -> custom_fields as $custom_field ):?>
<div class="custom-field">
<div class="_name">
<?= $custom_field['name'];?>:
<?= $custom_field['name'];?><? if ( !empty( $custom_field['is_required'] ) ): ?>*<? endif; ?>:
</div>
<div class="_input">
<div class="grow-wrap">
<textarea name="custom_field[<?= $custom_field['id_additional_field'];?>]" field_name="<?= $custom_field['name'];?>" onInput="this.parentNode.dataset.replicatedValue = this.value"></textarea>
<textarea name="custom_field[<?= $custom_field['id_additional_field'];?>]" field_name="<?= $custom_field['name'];?>" onInput="this.parentNode.dataset.replicatedValue = this.value" <? if ( !empty( $custom_field['is_required'] ) ): ?>required<? endif; ?>></textarea>
</div>
</div>
</div>

View File

@@ -537,7 +537,7 @@
}
// dodatkowe pola muszą być uzupełnione
$( '.custom-field textarea' ).each( function( index, element )
$( '.custom-field textarea[required]' ).each( function( index, element )
{
if ( $.trim( $( element ).val() ) == '' )
{