summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoger Rehnelt <noreply@maennchen1.de>2026-10-04 18:01:05 +0200
committerRoger Rehnelt <noreply@maennchen1.de>2026-10-04 18:01:05 +0200
commiteed31a0036c57329c27f93616405b020bd9023de (patch)
tree02b3cb1f91dea3ac1aa29197c8c57ad1152862c9
parent696b02cdc035b2c794e04c974bb27b82442fe489 (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.phtml166
-rw-r--r--views/mods/mod_reviews/page.phtml4
-rw-r--r--views/mods/mod_reviews/wpsg_reviews_admin.css37
-rw-r--r--views/mods/mod_reviews/wpsg_reviews_admin.js64
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();
+
+ } );
+
+} )();