diff options
| author | Roger Rehnelt <noreply@maennchen1.de> | 2026-10-04 18:01:05 +0200 |
|---|---|---|
| committer | Roger Rehnelt <noreply@maennchen1.de> | 2026-10-04 18:01:05 +0200 |
| commit | eed31a0036c57329c27f93616405b020bd9023de (patch) | |
| tree | 02b3cb1f91dea3ac1aa29197c8c57ad1152862c9 | |
| parent | 696b02cdc035b2c794e04c974bb27b82442fe489 (diff) | |
Produktbewertungen: Moderation wie Kommentare, Schalter je Produkt
Freigeben, Ablehnen, Antworten und Löschen sind Zeilenaktionen, die Antwort öffnet sich unter der Zeile; dazu Mehrfachaktion und Seiten statt höchstens 50 Bewertungen. Neuer Schalter „Bewertungen erlauben“ am Produkt: aus heißt keine Einladung, keine Sterne, keine Liste, kein JSON-LD.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | views/mods/mod_reviews/admin_index.phtml | 166 | ||||
| -rw-r--r-- | views/mods/mod_reviews/page.phtml | 4 | ||||
| -rw-r--r-- | views/mods/mod_reviews/wpsg_reviews_admin.css | 37 | ||||
| -rw-r--r-- | views/mods/mod_reviews/wpsg_reviews_admin.js | 64 |
4 files changed, 227 insertions, 44 deletions
diff --git a/views/mods/mod_reviews/admin_index.phtml b/views/mods/mod_reviews/admin_index.phtml index d012e49..495a793 100644 --- a/views/mods/mod_reviews/admin_index.phtml +++ b/views/mods/mod_reviews/admin_index.phtml @@ -1,6 +1,6 @@ <?php /** - * Backend: Bewertungen prüfen, freigeben, ablehnen, beantworten + * Backend: Bewertungen prüfen, freigeben, ablehnen, beantworten; Zeilen- und Mehrfachaktionen wie unter Kommentare * @version 4.6.1 */ defined('ABSPATH') || exit; @@ -12,6 +12,60 @@ wpsg_review::STATUS_REJECTED => __('Abgelehnt', 'wpsg') ]; $base = admin_url('admin.php?page=wpsg-Reviews'); + $current_url = add_query_arg(['status' => $admin['status'], 'paged' => $admin['paged']], $base); + + // Freigeben nicht bei veröffentlichten, Ablehnen nicht bei abgelehnten + $arActions = []; + if ($admin['status'] !== wpsg_review::STATUS_APPROVED) $arActions['approve'] = __('Freigeben', 'wpsg'); + if ($admin['status'] !== wpsg_review::STATUS_REJECTED) $arActions['reject'] = __('Ablehnen', 'wpsg'); + $arActions['delete'] = __('Löschen', 'wpsg'); + + // Oben und unten wie im Kern; schmal blendet der Kern die obere Auswahl aus + $bulkactions = function(string $position) use ($arActions) { + + $id = 'wpsg-reviews-bulk-'.$position; + + ?> + <div class="alignleft actions bulkactions"> + <label for="<?php echo esc_attr($id); ?>" class="screen-reader-text"><?php echo esc_html__('Mehrfachaktion wählen', 'wpsg'); ?></label> + <select name="wpsg_bulk_action[<?php echo esc_attr($position); ?>]" id="<?php echo esc_attr($id); ?>"> + <option value=""><?php echo esc_html__('Mehrfachaktionen', 'wpsg'); ?></option> + <?php foreach ($arActions as $action => $label) { ?> + <option value="<?php echo esc_attr($action); ?>"><?php echo esc_html($label); ?></option> + <?php } ?> + </select> + <button type="submit" name="wpsg_review_bulk" value="<?php echo esc_attr($position); ?>" class="button action"><?php echo esc_html__('Übernehmen', 'wpsg'); ?></button> + </div> + <?php + + }; + + $pagination = function() use ($admin, $current_url) { + + $total = intval($admin['count'][$admin['status']]??0); + + ?> + <div class="tablenav-pages<?php echo (($admin['pages'] <= 1)?' one-page':''); ?>"> + <span class="displaying-num"><?php echo esc_html(sprintf(_n('%s Eintrag', '%s Einträge', $total, 'wpsg'), number_format_i18n($total))); ?></span> + <?php if ($admin['pages'] > 1) { ?> + <span class="pagination-links"> + <?php if ($admin['paged'] > 1) { ?> + <a class="prev-page button" href="<?php echo esc_url(add_query_arg('paged', $admin['paged'] - 1, $current_url)); ?>"><span class="screen-reader-text"><?php echo esc_html__('Vorherige Seite', 'wpsg'); ?></span><span aria-hidden="true">‹</span></a> + <?php } else { ?> + <span class="tablenav-pages-navspan button disabled" aria-hidden="true">‹</span> + <?php } ?> + <span class="paging-input"><span class="tablenav-paging-text"><?php echo esc_html(sprintf(__('Seite %1$s von %2$s', 'wpsg'), number_format_i18n($admin['paged']), number_format_i18n($admin['pages']))); ?></span></span> + <?php if ($admin['paged'] < $admin['pages']) { ?> + <a class="next-page button" href="<?php echo esc_url(add_query_arg('paged', $admin['paged'] + 1, $current_url)); ?>"><span class="screen-reader-text"><?php echo esc_html__('Nächste Seite', 'wpsg'); ?></span><span aria-hidden="true">›</span></a> + <?php } else { ?> + <span class="tablenav-pages-navspan button disabled" aria-hidden="true">›</span> + <?php } ?> + </span> + <?php } ?> + </div> + <?php + + }; ?> <div class="wrap wpsg-reviews-admin"> @@ -31,50 +85,74 @@ <?php } else { ?> - <table class="widefat striped"> - <thead> - <tr> - <th scope="col"><?php echo esc_html__('Produkt', 'wpsg'); ?></th> - <th scope="col"><?php echo esc_html__('Sterne', 'wpsg'); ?></th> - <th scope="col"><?php echo esc_html__('Bewertung', 'wpsg'); ?></th> - <th scope="col"><?php echo esc_html__('Name', 'wpsg'); ?></th> - <th scope="col"><?php echo esc_html__('Abgegeben', 'wpsg'); ?></th> - <th scope="col"><?php echo esc_html__('Aktion', 'wpsg'); ?></th> - </tr> - </thead> - <tbody> - <?php foreach ($admin['reviews'] as $review) { $review_id = intval($review['id']); ?> - <tr> - <td><?php echo esc_html($review['product']); ?><br /> - <small><?php echo ((intval($review['verified']) === 1)?esc_html__('Verifizierter Kauf', 'wpsg'):esc_html__('Ohne Kaufnachweis (übernommen)', 'wpsg')); ?></small></td> - <td><span aria-hidden="true"><?php echo str_repeat('★', intval($review['stars'])).str_repeat('☆', 5 - intval($review['stars'])); ?></span> - <span class="screen-reader-text"><?php echo esc_html(wpsg_translate(__('#1# von 5 Sternen', 'wpsg'), intval($review['stars']))); ?></span></td> - <td><?php if ((string)$review['title'] !== '') { ?><strong><?php echo esc_html($review['title']); ?></strong><br /><?php } ?> - <?php echo nl2br(esc_html((string)$review['text'])); ?> - <?php if ((string)$review['reply'] !== '') { ?><p><em><?php echo esc_html__('Ihre Antwort:', 'wpsg'); ?></em> <?php echo nl2br(esc_html((string)$review['reply'])); ?></p><?php } ?></td> - <td><?php echo esc_html($review['display_name']); ?></td> - <td><?php echo esc_html((string)wp_date(get_option('date_format').' '.get_option('time_format'), (int)strtotime((string)$review['submitted'].' UTC'))); ?></td> - <td> - <?php foreach (['approve' => __('Freigeben', 'wpsg'), 'reject' => __('Ablehnen', 'wpsg'), 'delete' => __('Löschen', 'wpsg')] as $action => $label) { ?> - <?php if (($action === 'approve' && $review['status'] === wpsg_review::STATUS_APPROVED) || ($action === 'reject' && $review['status'] === wpsg_review::STATUS_REJECTED)) continue; ?> - <form method="post" class="wpsg-reviews-admin__action"> - <?php wp_nonce_field('wpsg_reviews_moderate'); ?> - <input type="hidden" name="review_id" value="<?php echo $review_id; ?>" /> - <button type="submit" name="wpsg_review_action" value="<?php echo esc_attr($action); ?>" class="button<?php echo (($action === 'approve')?' button-primary':''); ?>"<?php if ($action === 'delete') { ?> onclick="return confirm('<?php echo esc_js(__('Bewertung endgültig löschen?', 'wpsg')); ?>');"<?php } ?>><?php echo esc_html($label); ?></button> - </form> - <?php } ?> - <form method="post" class="wpsg-reviews-admin__reply"> - <?php wp_nonce_field('wpsg_reviews_moderate'); ?> - <input type="hidden" name="review_id" value="<?php echo $review_id; ?>" /> + <form method="post" action="<?php echo esc_url(add_query_arg('noheader', '1', $current_url)); ?>" class="wpsg-reviews-admin__form" data-confirm-bulk="<?php echo esc_attr__('Die ausgewählten Bewertungen endgültig löschen?', 'wpsg'); ?>"> + + <?php wp_nonce_field('wpsg_reviews_moderate'); ?> + + <div class="tablenav top"> + <?php $bulkactions('top'); ?> + <?php $pagination(); ?> + <br class="clear" /> + </div> + + <table class="wp-list-table widefat fixed striped table-view-list wpsg-reviews-admin__table"> + <thead> + <tr> + <td class="manage-column column-cb check-column"><input id="wpsg-reviews-select-all" type="checkbox" /><label for="wpsg-reviews-select-all"><span class="screen-reader-text"><?php echo esc_html__('Alle auswählen', 'wpsg'); ?></span></label></td> + <th scope="col" class="manage-column column-review column-primary"><?php echo esc_html__('Bewertung', 'wpsg'); ?></th> + <th scope="col" class="manage-column column-product"><?php echo esc_html__('Produkt', 'wpsg'); ?></th> + <th scope="col" class="manage-column column-author"><?php echo esc_html__('Name', 'wpsg'); ?></th> + <th scope="col" class="manage-column column-date"><?php echo esc_html__('Abgegeben', 'wpsg'); ?></th> + </tr> + </thead> + <tbody> + <?php foreach ($admin['reviews'] as $review) { $review_id = intval($review['id']); $bReply = ((string)$review['reply'] !== ''); ?> + <tr id="wpsg-review-<?php echo $review_id; ?>"> + <th scope="row" class="check-column"><input type="checkbox" id="wpsg-review-cb-<?php echo $review_id; ?>" name="review_ids[]" value="<?php echo $review_id; ?>" /><label for="wpsg-review-cb-<?php echo $review_id; ?>"><span class="screen-reader-text"><?php echo esc_html(wpsg_translate(__('Bewertung von #1# auswählen', 'wpsg'), $review['display_name'])); ?></span></label></th> + <?php /* Ohne data-colname: schmal stünde die Spaltenbeschriftung über den Sternen (in 7.1 ist die Hauptspalte im Kern ein th) */ ?> + <td class="column-review column-primary has-row-actions"> + <div class="wpsg-reviews-admin__stars"><span aria-hidden="true"><?php echo str_repeat('★', intval($review['stars'])).str_repeat('☆', 5 - intval($review['stars'])); ?></span><span class="screen-reader-text"><?php echo esc_html(wpsg_translate(__('#1# von 5 Sternen', 'wpsg'), intval($review['stars']))); ?></span></div> + <?php if ((string)$review['title'] !== '') { ?><strong class="wpsg-reviews-admin__title"><?php echo esc_html($review['title']); ?></strong><?php } ?> + <div class="wpsg-reviews-admin__text"><?php echo nl2br(esc_html((string)$review['text'])); ?></div> + <div class="wpsg-reviews-admin__meta"><?php echo esc_html($review['product']); ?> · <?php echo esc_html($review['display_name']); ?></div> + <?php if ($bReply) { ?><div class="wpsg-reviews-admin__reply-text"><strong><?php echo esc_html__('Ihre Antwort:', 'wpsg'); ?></strong> <?php echo nl2br(esc_html((string)$review['reply'])); ?></div><?php } ?> + <div class="row-actions"> + <?php if (isset($arActions['approve'])) { ?><span class="wpsg-approve"><button type="submit" name="wpsg_review_action" value="approve:<?php echo $review_id; ?>" class="button-link"><?php echo esc_html($arActions['approve']); ?></button> | </span><?php } ?> + <?php if (isset($arActions['reject'])) { ?><span class="wpsg-reject"><button type="submit" name="wpsg_review_action" value="reject:<?php echo $review_id; ?>" class="button-link"><?php echo esc_html($arActions['reject']); ?></button> | </span><?php } ?> + <span class="wpsg-reply"><button type="button" class="button-link wpsg-reviews-admin__reply-toggle" aria-expanded="false" aria-controls="wpsg-review-reply-<?php echo $review_id; ?>"><?php echo esc_html(($bReply)?__('Antwort bearbeiten', 'wpsg'):__('Antworten', 'wpsg')); ?></button> | </span> + <span class="wpsg-delete"><button type="submit" name="wpsg_review_action" value="delete:<?php echo $review_id; ?>" class="button-link button-link-delete" data-confirm="<?php echo esc_attr__('Bewertung endgültig löschen?', 'wpsg'); ?>"><?php echo esc_html($arActions['delete']); ?></button></span> + </div> + <button type="button" class="toggle-row"><span class="screen-reader-text"><?php echo esc_html__('Mehr Details anzeigen', 'wpsg'); ?></span></button> + </td> + <td class="column-product" data-colname="<?php echo esc_attr__('Produkt', 'wpsg'); ?>"><?php echo esc_html($review['product']); ?><br /> + <small><?php echo ((intval($review['verified']) === 1)?esc_html__('Verifizierter Kauf', 'wpsg'):esc_html__('Ohne Kaufnachweis (übernommen)', 'wpsg')); ?></small> + <?php if (!$review['allowed']) { ?><br /><small><?php echo esc_html__('Bewertungen für dieses Produkt aus', 'wpsg'); ?></small><?php } ?></td> + <td class="column-author" data-colname="<?php echo esc_attr__('Name', 'wpsg'); ?>"><?php echo esc_html($review['display_name']); ?></td> + <td class="column-date" data-colname="<?php echo esc_attr__('Abgegeben', 'wpsg'); ?>"><?php echo esc_html((string)wp_date(get_option('date_format').' '.get_option('time_format'), (int)strtotime((string)$review['submitted'].' UTC'))); ?></td> + </tr> + <?php /* Leere Zeile hält die Streifen im Wechsel, wie die Schnellbearbeitung im Kern */ ?> + <tr class="hidden"></tr> + <tr id="wpsg-review-reply-<?php echo $review_id; ?>" class="inline-edit-row wpsg-reviews-admin__reply-row" hidden> + <td colspan="5"> <label for="wpsg_review_reply_<?php echo $review_id; ?>"><?php echo esc_html__('Öffentliche Antwort', 'wpsg'); ?></label> - <textarea id="wpsg_review_reply_<?php echo $review_id; ?>" name="reply" rows="2"><?php echo esc_textarea((string)$review['reply']); ?></textarea> - <button type="submit" name="wpsg_review_action" value="reply" class="button"><?php echo esc_html__('Antwort speichern', 'wpsg'); ?></button> - </form> - </td> - </tr> - <?php } ?> - </tbody> - </table> + <textarea id="wpsg_review_reply_<?php echo $review_id; ?>" name="reply[<?php echo $review_id; ?>]" rows="3" class="large-text"><?php echo esc_textarea((string)$review['reply']); ?></textarea> + <p class="wpsg-reviews-admin__reply-buttons"> + <button type="submit" name="wpsg_review_action" value="reply:<?php echo $review_id; ?>" class="button button-primary"><?php echo esc_html__('Antwort speichern', 'wpsg'); ?></button> + <button type="button" class="button wpsg-reviews-admin__reply-cancel"><?php echo esc_html__('Abbrechen', 'wpsg'); ?></button> + </p> + </td> + </tr> + <?php } ?> + </tbody> + </table> + + <div class="tablenav bottom"> + <?php $bulkactions('bottom'); ?> + <?php $pagination(); ?> + <br class="clear" /> + </div> + + </form> <?php } ?> diff --git a/views/mods/mod_reviews/page.phtml b/views/mods/mod_reviews/page.phtml index 3d44fdf..f246c15 100644 --- a/views/mods/mod_reviews/page.phtml +++ b/views/mods/mod_reviews/page.phtml @@ -21,6 +21,10 @@ <p class="wpsg-review-page__message"><?php echo esc_html(wpsg_translate(__('Sie haben „#1#“ bereits bewertet. Vielen Dank!', 'wpsg'), $page['product'])); ?></p> + <?php } else if ($page['state'] === 'closed') { ?> + + <p class="wpsg-review-page__message"><?php echo esc_html(wpsg_translate(__('Für „#1#“ nimmt der Shop keine Bewertungen an.', 'wpsg'), $page['product'])); ?></p> + <?php } else if ($page['state'] === 'early') { ?> <p class="wpsg-review-page__message"><?php echo esc_html(wpsg_translate(__('Sie können „#1#“ bewerten, sobald Ihre Bestellung bei Ihnen ist. Der Link bleibt gültig.', 'wpsg'), $page['product'])); ?></p> diff --git a/views/mods/mod_reviews/wpsg_reviews_admin.css b/views/mods/mod_reviews/wpsg_reviews_admin.css new file mode 100644 index 0000000..2317360 --- /dev/null +++ b/views/mods/mod_reviews/wpsg_reviews_admin.css @@ -0,0 +1,37 @@ +/* + * Modul Produktbewertungen, Backend-Liste unter wpShopGermany → Bewertungen + * Aufbau wie die Kommentare im Kern: Zeilenaktionen, Antwortfeld unter der Zeile, schmal ab 782 px aufklappbar + */ + +.wpsg-reviews-admin__table .column-product { width:22%; } +.wpsg-reviews-admin__table .column-author { width:12%; } +.wpsg-reviews-admin__table .column-date { width:14%; } + +.wpsg-reviews-admin__stars { letter-spacing:0.1em; } +.wpsg-reviews-admin__title { display:block; margin-top:0.25em; } +.wpsg-reviews-admin__text { margin-top:0.25em; } +.wpsg-reviews-admin__reply-text { margin-top:0.5em; padding:0.4em 0.75em; border-left:4px solid #c3c4c7; background:#f6f7f7; } +.wpsg-reviews-admin__meta { display:none; margin-top:0.5em; color:#646970; } + +/* Der Kern blendet .approve aus (Kommentare); eigene Klassen an den Zeilenaktionen */ +/* 13 px wie die Links im Kern; schmal setzt der Kern die Schrift der span auf 0 */ +.wpsg-reviews-admin .row-actions .button-link { font-size:13px; text-decoration:none; } +.wpsg-reviews-admin .row-actions .button-link:hover { text-decoration:underline; } + +/* 12.5rem des Kerns wären hier 125 px, die Backendseiten von wpsg setzen die Grundschrift auf 10 px */ +.wpsg-reviews-admin .tablenav select { max-width:none; } + +.wpsg-reviews-admin__reply-row td { padding:12px 10px 16px; } +.wpsg-reviews-admin__reply-row label { display:block; margin-bottom:4px; font-weight:600; } +.wpsg-reviews-admin__reply-buttons { margin:8px 0 0; } + +/* Schmal setzt der Kern .wp-list-table tr { display:flex }; das höbe hidden und .hidden auf */ +.wpsg-reviews-admin__table .wpsg-reviews-admin__reply-row[hidden], +.wpsg-reviews-admin__table tr.hidden { display:none; } + +@media screen and (max-width:782px) { + + .wpsg-reviews-admin__meta { display:block; } + .wpsg-reviews-admin__reply-row td { display:block; flex:1 1 100%; box-sizing:border-box; padding:12px 10px 16px; } + +} diff --git a/views/mods/mod_reviews/wpsg_reviews_admin.js b/views/mods/mod_reviews/wpsg_reviews_admin.js new file mode 100644 index 0000000..9406041 --- /dev/null +++ b/views/mods/mod_reviews/wpsg_reviews_admin.js @@ -0,0 +1,64 @@ +/* + * Modul Produktbewertungen, Backend-Liste: Antwortfeld unter der Zeile öffnen, Löschen bestätigen. + * Alle- und Zeilenauswahl, Zeilenaktionen bei Fokus und das Aufklappen schmaler Zeilen übernimmt der Kern (common.js). + */ +( function() { + + 'use strict'; + + function setReply( toggle, bOpen ) { + + const row = document.getElementById( toggle.getAttribute( 'aria-controls' ) ); + if ( !row ) return; + + row.hidden = !bOpen; + toggle.setAttribute( 'aria-expanded', bOpen ? 'true' : 'false' ); + + if ( bOpen ) row.querySelector( 'textarea' ).focus(); + else toggle.focus(); + + } + + document.addEventListener( 'click', function( event ) { + + const toggle = event.target.closest( '.wpsg-reviews-admin__reply-toggle' ); + + if ( toggle ) { + + setReply( toggle, toggle.getAttribute( 'aria-expanded' ) !== 'true' ); + return; + + } + + const cancel = event.target.closest( '.wpsg-reviews-admin__reply-cancel' ); + + if ( cancel ) { + + const row = cancel.closest( 'tr' ); + const opener = document.querySelector( '.wpsg-reviews-admin__reply-toggle[aria-controls="' + row.id + '"]' ); + + if ( opener ) setReply( opener, false ); + return; + + } + + const button = event.target.closest( '.wpsg-reviews-admin [data-confirm]' ); + if ( button && !window.confirm( button.getAttribute( 'data-confirm' ) ) ) event.preventDefault(); + + } ); + + // Mehrfachaktion Löschen bestätigen + document.addEventListener( 'submit', function( event ) { + + const form = event.target; + if ( !form.classList.contains( 'wpsg-reviews-admin__form' ) ) return; + + const submitter = event.submitter; + if ( !submitter || submitter.name !== 'wpsg_review_bulk' ) return; + + const select = form.elements[ 'wpsg_bulk_action[' + submitter.value + ']' ]; + if ( select && select.value === 'delete' && !window.confirm( form.getAttribute( 'data-confirm-bulk' ) ) ) event.preventDefault(); + + } ); + +} )(); |
