Files
KGV-Verein-Manager/includes/Admin/Admin.php
Ronny Grobel 7d3d543954 Release 1.16.0
Arbeitsstunden-Modul hinzugefuegt

- Pflichtstunden pro Jahr inkl. Preis je fehlender Stunde

- Arbeitsarten, Arbeitseintraege und Mehrfachzuordnung von Mitgliedern

- Mitgliederuebersicht mit Berechnung fehlender Stunden und Aufschlag

- Datenbankschema fuer Arbeitsstunden erweitert

- Stable Tag und Changelog in README/readme.txt aktualisiert
2026-04-16 21:38:59 +02:00

4481 lines
206 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Backend administration UI.
*
* @package KGV\VereinManager
*/
namespace KGV\VereinManager\Admin;
use KGV\VereinManager\Roles;
use KGV\VereinManager\Validator;
use KGV\VereinManager\Repositories\AssignmentRepository;
use KGV\VereinManager\Repositories\ChatRepository;
use KGV\VereinManager\Repositories\CostRepository;
use KGV\VereinManager\Repositories\MeterReadingRepository;
use KGV\VereinManager\Repositories\MeterRepository;
use KGV\VereinManager\Repositories\ParcelRepository;
use KGV\VereinManager\Repositories\SectionRepository;
use KGV\VereinManager\Repositories\TenantRepository;
use KGV\VereinManager\Repositories\WorkRepository;
use KGV\VereinManager\Services\ParcelService;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Admin {
/**
* Validator instance.
*
* @var Validator
*/
private $validator;
/**
* Repositories and services.
*/
private $sections;
private $parcels;
private $meters;
private $readings;
private $tenants;
private $assignments;
private $chat;
private $costs;
private $work;
private $parcel_service;
/**
* Construct admin controller.
*/
public function __construct() {
$this->validator = new Validator();
$this->sections = new SectionRepository();
$this->parcels = new ParcelRepository();
$this->meters = new MeterRepository();
$this->readings = new MeterReadingRepository();
$this->tenants = new TenantRepository();
$this->assignments = new AssignmentRepository();
$this->chat = new ChatRepository();
$this->costs = new CostRepository();
$this->work = new WorkRepository();
$this->parcel_service = new ParcelService();
}
/**
* Register WP hooks.
*
* @return void
*/
public function register_hooks() {
add_action( 'admin_menu', array( $this, 'register_menus' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
add_action( 'admin_init', array( $this, 'handle_requests' ) );
add_action( 'wp_ajax_kgvvm_fetch_chat_messages', array( $this, 'ajax_fetch_chat_messages' ) );
add_action( 'wp_ajax_kgvvm_send_chat_message', array( $this, 'ajax_send_chat_message' ) );
}
/**
* Register admin pages.
*
* @return void
*/
public function register_menus() {
$cap = 'manage_kleingarten';
if ( current_user_can( $cap ) ) {
add_menu_page(
__( 'Kleingartenverwaltung', KGVVM_TEXT_DOMAIN ),
__( 'Kleingarten', KGVVM_TEXT_DOMAIN ),
$cap,
'kgvvm-dashboard',
array( $this, 'render_dashboard' ),
'dashicons-admin-multisite',
24
);
add_submenu_page( 'kgvvm-dashboard', __( 'Dashboard', KGVVM_TEXT_DOMAIN ), __( 'Dashboard', KGVVM_TEXT_DOMAIN ), $cap, 'kgvvm-dashboard', array( $this, 'render_dashboard' ) );
add_submenu_page( 'kgvvm-dashboard', __( 'Meine Parzellen', KGVVM_TEXT_DOMAIN ), __( 'Meine Parzellen', KGVVM_TEXT_DOMAIN ), 'view_assigned_parcels', 'kgvvm-my-parcels', array( $this, 'render_my_parcels_page' ) );
add_submenu_page( 'kgvvm-dashboard', __( 'Vereinschat', KGVVM_TEXT_DOMAIN ), __( 'Vereinschat', KGVVM_TEXT_DOMAIN ), 'view_assigned_parcels', 'kgvvm-chat', array( $this, 'render_chat_page' ) );
add_submenu_page( 'kgvvm-dashboard', __( 'Sparten', KGVVM_TEXT_DOMAIN ), __( 'Sparten', KGVVM_TEXT_DOMAIN ), 'edit_sparten', 'kgvvm-sparten', array( $this, 'render_sections_page' ) );
add_submenu_page( 'kgvvm-dashboard', __( 'Parzellen', KGVVM_TEXT_DOMAIN ), __( 'Parzellen', KGVVM_TEXT_DOMAIN ), 'edit_parzellen', 'kgvvm-parzellen', array( $this, 'render_parcels_page' ) );
add_submenu_page( 'kgvvm-dashboard', __( 'Zähler', KGVVM_TEXT_DOMAIN ), __( 'Zähler', KGVVM_TEXT_DOMAIN ), 'edit_zaehler', 'kgvvm-zaehler', array( $this, 'render_meters_page' ) );
add_submenu_page( 'kgvvm-dashboard', __( 'Verbrauch', KGVVM_TEXT_DOMAIN ), __( 'Verbrauch', KGVVM_TEXT_DOMAIN ), $cap, 'kgvvm-consumption', array( $this, 'render_consumption_page' ) );
add_submenu_page( 'kgvvm-dashboard', __( 'Kosten', KGVVM_TEXT_DOMAIN ), __( 'Kosten', KGVVM_TEXT_DOMAIN ), $cap, 'kgvvm-costs', array( $this, 'render_costs_page' ) );
add_submenu_page( 'kgvvm-dashboard', __( 'Arbeitsstunden', KGVVM_TEXT_DOMAIN ), __( 'Arbeitsstunden', KGVVM_TEXT_DOMAIN ), $cap, 'kgvvm-arbeit', array( $this, 'render_work_page' ) );
add_submenu_page( 'kgvvm-dashboard', __( 'Pächter', KGVVM_TEXT_DOMAIN ), __( 'Pächter', KGVVM_TEXT_DOMAIN ), 'edit_paechter', 'kgvvm-paechter', array( $this, 'render_tenants_page' ) );
add_submenu_page( 'kgvvm-dashboard', __( 'Einstellungen', KGVVM_TEXT_DOMAIN ), __( 'Einstellungen', KGVVM_TEXT_DOMAIN ), Roles::SETTINGS_CAP, 'kgvvm-settings', array( $this, 'render_settings_page' ) );
}
if ( ! current_user_can( $cap ) && current_user_can( 'view_assigned_parcels' ) ) {
add_menu_page(
__( 'Meine Parzellen', KGVVM_TEXT_DOMAIN ),
__( 'Meine Parzellen', KGVVM_TEXT_DOMAIN ),
'view_assigned_parcels',
'kgvvm-my-parcels',
array( $this, 'render_my_parcels_page' ),
'dashicons-location-alt',
24
);
add_menu_page(
__( 'Vereinschat', KGVVM_TEXT_DOMAIN ),
__( 'Vereinschat', KGVVM_TEXT_DOMAIN ),
'view_assigned_parcels',
'kgvvm-chat',
array( $this, 'render_chat_page' ),
'dashicons-format-chat',
25
);
}
}
/**
* Load admin stylesheet on plugin pages.
*
* @param string $hook Current admin hook.
* @return void
*/
public function enqueue_assets( $hook ) {
if ( false === strpos( (string) $hook, 'kgvvm' ) ) {
return;
}
wp_enqueue_style( 'kgvvm-admin', KGVVM_PLUGIN_URL . 'assets/css/admin.css', array(), KGVVM_VERSION );
if ( false !== strpos( (string) $hook, 'kgvvm-chat' ) ) {
wp_enqueue_script( 'kgvvm-chat', KGVVM_PLUGIN_URL . 'assets/js/chat.js', array(), KGVVM_VERSION, true );
wp_localize_script(
'kgvvm-chat',
'kgvvmChatConfig',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'kgvvm_chat_nonce' ),
'refreshInterval' => 7000,
'i18n' => array(
'empty' => __( 'Noch keine Nachrichten in diesem Raum.', KGVVM_TEXT_DOMAIN ),
'loading' => __( 'Nachrichten werden geladen …', KGVVM_TEXT_DOMAIN ),
'sending' => __( 'Nachricht wird gesendet …', KGVVM_TEXT_DOMAIN ),
'fetchError' => __( 'Der Chat konnte gerade nicht aktualisiert werden.', KGVVM_TEXT_DOMAIN ),
'sendError' => __( 'Die Nachricht konnte nicht gesendet werden.', KGVVM_TEXT_DOMAIN ),
),
)
);
}
}
/**
* Handle save/delete requests.
*
* @return void
*/
public function handle_requests() {
if ( ! is_admin() || ( ! current_user_can( 'manage_kleingarten' ) && ! current_user_can( Roles::SETTINGS_CAP ) && ! current_user_can( 'submit_meter_readings' ) ) ) {
return;
}
$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
$view = isset( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : '';
$output = isset( $_GET['output'] ) ? sanitize_key( wp_unslash( $_GET['output'] ) ) : '';
if ( current_user_can( 'manage_kleingarten' ) && 'kgvvm-costs' === $page && 'statement' === $view && 'pdf' === $output ) {
$this->render_cost_statement_page();
return;
}
if ( 'POST' === strtoupper( isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '' ) && ! empty( $_POST['kgvvm_action'] ) ) {
$this->handle_post_action();
}
if ( ! empty( $_GET['kgvvm_action'] ) ) {
$this->handle_get_action();
}
}
/**
* Dispatch POST actions.
*
* @return void
*/
private function handle_post_action() {
$action = sanitize_key( wp_unslash( $_POST['kgvvm_action'] ) );
switch ( $action ) {
case 'save_section':
$this->save_section();
break;
case 'save_parcel':
$this->save_parcel();
break;
case 'save_meter':
$this->save_meter();
break;
case 'swap_meter':
$this->swap_meter();
break;
case 'save_tenant':
$this->save_tenant();
break;
case 'save_cost_year':
$this->save_cost_year();
break;
case 'save_cost_prices':
$this->save_cost_prices();
break;
case 'save_cost':
$this->save_cost();
break;
case 'save_meter_reading':
$this->save_meter_reading();
break;
case 'save_settings':
$this->save_settings();
break;
case 'save_work_year_config':
$this->save_work_year_config();
break;
case 'save_work_job':
$this->save_work_job();
break;
case 'save_work_log':
$this->save_work_log();
break;
}
}
/**
* Dispatch delete actions.
*
* @return void
*/
private function handle_get_action() {
$action = sanitize_key( wp_unslash( $_GET['kgvvm_action'] ) );
$id = absint( isset( $_GET['id'] ) ? $_GET['id'] : 0 );
$nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
switch ( $action ) {
case 'delete_section':
$this->require_cap( 'edit_sparten' );
if ( ! wp_verify_nonce( $nonce, 'kgvvm_delete_section_' . $id ) ) {
$this->redirect_with_notice( 'kgvvm-sparten', 'error', __( 'Der Löschvorgang für die Sparte wurde aus Sicherheitsgründen abgebrochen.', KGVVM_TEXT_DOMAIN ) );
}
if ( $this->sections->is_in_use( $id ) ) {
$this->redirect_with_notice( 'kgvvm-sparten', 'error', __( 'Diese Sparte ist noch mit Parzellen oder Zählern verknüpft und kann nicht gelöscht werden.', KGVVM_TEXT_DOMAIN ) );
}
$this->sections->delete( $id );
$this->redirect_with_notice( 'kgvvm-sparten', 'success', __( 'Sparte wurde gelöscht.', KGVVM_TEXT_DOMAIN ) );
break;
case 'delete_parcel':
$this->require_cap( 'edit_parzellen' );
if ( ! wp_verify_nonce( $nonce, 'kgvvm_delete_parcel_' . $id ) ) {
$this->redirect_with_notice( 'kgvvm-parzellen', 'error', __( 'Der Löschvorgang für die Parzelle wurde aus Sicherheitsgründen abgebrochen.', KGVVM_TEXT_DOMAIN ) );
}
$this->parcel_service->delete( $id );
$this->redirect_with_notice( 'kgvvm-parzellen', 'success', __( 'Parzelle wurde gelöscht.', KGVVM_TEXT_DOMAIN ) );
break;
case 'delete_meter':
$this->require_cap( 'edit_zaehler' );
if ( ! wp_verify_nonce( $nonce, 'kgvvm_delete_meter_' . $id ) ) {
$this->redirect_with_notice( 'kgvvm-zaehler', 'error', __( 'Der Löschvorgang für den Zähler wurde aus Sicherheitsgründen abgebrochen.', KGVVM_TEXT_DOMAIN ) );
}
if ( $this->meters->is_assigned( $id ) ) {
$this->redirect_with_notice( 'kgvvm-zaehler', 'error', __( 'Ein zugeordneter Zähler kann nicht gelöscht werden. Bitte zuerst die Parzellenzuordnung entfernen.', KGVVM_TEXT_DOMAIN ) );
}
$this->meters->delete( $id );
$this->redirect_with_notice( 'kgvvm-zaehler', 'success', __( 'Zähler wurde gelöscht.', KGVVM_TEXT_DOMAIN ) );
break;
case 'delete_tenant':
$this->require_cap( 'edit_paechter' );
if ( ! wp_verify_nonce( $nonce, 'kgvvm_delete_tenant_' . $id ) ) {
$this->redirect_with_notice( 'kgvvm-paechter', 'error', __( 'Der Löschvorgang für den Pächter wurde aus Sicherheitsgründen abgebrochen.', KGVVM_TEXT_DOMAIN ) );
}
$this->assignments->purge_tenant( $id );
$this->tenants->delete( $id );
$this->redirect_with_notice( 'kgvvm-paechter', 'success', __( 'Pächter wurde gelöscht.', KGVVM_TEXT_DOMAIN ) );
break;
case 'delete_cost':
$this->require_cap( 'manage_kleingarten' );
$year = absint( isset( $_GET['year'] ) ? $_GET['year'] : current_time( 'Y' ) );
if ( ! wp_verify_nonce( $nonce, 'kgvvm_delete_cost_' . $id ) ) {
$this->redirect_with_notice( 'kgvvm-costs', 'error', __( 'Der Löschvorgang für den Kostenposten wurde aus Sicherheitsgründen abgebrochen.', KGVVM_TEXT_DOMAIN ), array( 'year' => $year ) );
}
$cost = $this->costs->find( $id );
if ( ! $cost ) {
$this->redirect_with_notice( 'kgvvm-costs', 'error', __( 'Der gewünschte Kostenposten wurde nicht gefunden.', KGVVM_TEXT_DOMAIN ), array( 'year' => $year ) );
}
$year = (int) $cost->entry_year;
$this->costs->delete( $id );
$this->redirect_with_notice( 'kgvvm-costs', 'success', __( 'Kostenposten wurde gelöscht.', KGVVM_TEXT_DOMAIN ), array( 'year' => $year ) );
break;
case 'export_readings_csv':
$this->export_readings_csv();
break;
case 'delete_work_job':
$this->require_cap( 'manage_kleingarten' );
if ( ! wp_verify_nonce( $nonce, 'kgvvm_delete_work_job_' . $id ) ) {
$this->redirect_with_notice( 'kgvvm-arbeit', 'error', __( 'Der Löschvorgang wurde aus Sicherheitsgründen abgebrochen.', KGVVM_TEXT_DOMAIN ) );
}
$this->work->delete_job( $id );
$this->redirect_with_notice( 'kgvvm-arbeit', 'success', __( 'Arbeitsart wurde gelöscht.', KGVVM_TEXT_DOMAIN ), array( 'tab' => 'jobs' ) );
break;
case 'delete_work_log':
$this->require_cap( 'manage_kleingarten' );
$year = absint( isset( $_GET['year'] ) ? $_GET['year'] : current_time( 'Y' ) );
if ( ! wp_verify_nonce( $nonce, 'kgvvm_delete_work_log_' . $id ) ) {
$this->redirect_with_notice( 'kgvvm-arbeit', 'error', __( 'Der Löschvorgang wurde aus Sicherheitsgründen abgebrochen.', KGVVM_TEXT_DOMAIN ), array( 'year' => $year ) );
}
$this->work->delete_log( $id );
$this->redirect_with_notice( 'kgvvm-arbeit', 'success', __( 'Arbeitseintrag wurde gelöscht.', KGVVM_TEXT_DOMAIN ), array( 'year' => $year ) );
break;
}
}
/**
* Save section.
*
* @return void
*/
private function save_section() {
$this->require_cap( 'edit_sparten' );
check_admin_referer( 'kgvvm_save_section' );
$id = absint( isset( $_POST['id'] ) ? $_POST['id'] : 0 );
$data = $this->validator->sanitize_section( $_POST );
$errors = $this->validator->validate_section( $data );
if ( $this->sections->name_exists( $data['name'], $id ) ) {
$errors->add( 'duplicate_name', __( 'Es existiert bereits eine Sparte mit diesem Namen.', KGVVM_TEXT_DOMAIN ) );
}
if ( $errors->has_errors() ) {
$this->redirect_with_notice( 'kgvvm-sparten', 'error', $errors->get_error_message(), array( 'view' => 'form', 'id' => $id ) );
}
$section_id = $this->sections->save( $data, $id );
$main_ids = isset( $_POST['main_meter_ids'] ) ? array_map( 'absint', (array) wp_unslash( $_POST['main_meter_ids'] ) ) : array();
$this->meters->sync_main_meters_for_section( $section_id, $main_ids );
$this->redirect_with_notice( 'kgvvm-sparten', 'success', __( 'Sparte wurde gespeichert.', KGVVM_TEXT_DOMAIN ) );
}
/**
* Save parcel.
*
* @return void
*/
private function save_parcel() {
$this->require_cap( 'edit_parzellen' );
check_admin_referer( 'kgvvm_save_parcel' );
$id = absint( isset( $_POST['id'] ) ? $_POST['id'] : 0 );
$result = $this->parcel_service->save( $id, $_POST );
if ( is_wp_error( $result ) ) {
$this->redirect_with_notice( 'kgvvm-parzellen', 'error', $result->get_error_message(), array( 'view' => 'form', 'id' => $id ) );
}
$this->redirect_with_notice( 'kgvvm-parzellen', 'success', __( 'Parzelle wurde gespeichert.', KGVVM_TEXT_DOMAIN ) );
}
/**
* Save meter.
*
* @return void
*/
private function save_meter() {
$this->require_cap( 'edit_zaehler' );
check_admin_referer( 'kgvvm_save_meter' );
$id = absint( isset( $_POST['id'] ) ? $_POST['id'] : 0 );
$data = $this->validator->sanitize_meter( $_POST );
$errors = $this->validator->validate_meter( $data );
if ( $this->meters->meter_number_exists( $data['meter_number'], $data['type'], $id ) ) {
$errors->add( 'duplicate_meter_number', __( 'Diese Zählernummer existiert für den gewählten Typ bereits.', KGVVM_TEXT_DOMAIN ) );
}
if ( $errors->has_errors() ) {
$this->redirect_with_notice( 'kgvvm-zaehler', 'error', $errors->get_error_message(), array( 'view' => 'form', 'id' => $id ) );
}
$this->meters->save( $data, $id );
$this->redirect_with_notice( 'kgvvm-zaehler', 'success', __( 'Zähler wurde gespeichert.', KGVVM_TEXT_DOMAIN ) );
}
/**
* Swap a meter and archive the old one.
*
* @return void
*/
private function swap_meter() {
$this->require_cap( 'edit_zaehler' );
check_admin_referer( 'kgvvm_swap_meter' );
$id = absint( isset( $_POST['id'] ) ? $_POST['id'] : 0 );
$old_meter = $id ? $this->meters->find( $id ) : null;
if ( ! $old_meter ) {
$this->redirect_with_notice( 'kgvvm-zaehler', 'error', __( 'Der gewünschte Zähler wurde nicht gefunden.', KGVVM_TEXT_DOMAIN ) );
}
$swap_note = sanitize_textarea_field( wp_unslash( isset( $_POST['swap_note'] ) ? $_POST['swap_note'] : '' ) );
$new_data = $this->validator->sanitize_meter(
array(
'type' => $old_meter->type,
'meter_number' => isset( $_POST['new_meter_number'] ) ? $_POST['new_meter_number'] : '',
'section_id' => $old_meter->section_id,
'installed_at' => isset( $_POST['swap_date'] ) ? $_POST['swap_date'] : '',
'calibration_year' => isset( $_POST['new_calibration_year'] ) ? $_POST['new_calibration_year'] : '',
'is_main_meter' => ! empty( $old_meter->is_main_meter ) ? 1 : 0,
'is_active' => 1,
'note' => $swap_note,
)
);
$errors = $this->validator->validate_meter( $new_data );
if ( $this->meters->meter_number_exists( $new_data['meter_number'], $new_data['type'] ) ) {
$errors->add( 'duplicate_meter_number', __( 'Die neue Zählernummer existiert für diesen Typ bereits.', KGVVM_TEXT_DOMAIN ) );
}
if ( '' === $new_data['installed_at'] ) {
$errors->add( 'swap_date_required', __( 'Bitte ein Tauschdatum angeben.', KGVVM_TEXT_DOMAIN ) );
}
$parcel_id = (int) $old_meter->parcel_id;
$latest = $this->readings->get_latest_for_meter( $old_meter->id );
$old_final_raw = isset( $_POST['old_final_reading'] ) ? str_replace( ',', '.', wp_unslash( $_POST['old_final_reading'] ) ) : '';
$new_initial_raw = isset( $_POST['new_initial_reading'] ) ? str_replace( ',', '.', wp_unslash( $_POST['new_initial_reading'] ) ) : '';
$old_final_reading = '' === trim( (string) $old_final_raw ) ? '' : (float) $old_final_raw;
$new_initial_reading = '' === trim( (string) $new_initial_raw ) ? '' : (float) $new_initial_raw;
if ( '' !== trim( (string) $old_final_raw ) ) {
if ( ! is_numeric( $old_final_raw ) ) {
$errors->add( 'old_final_invalid', __( 'Bitte einen gültigen Endstand für den alten Zähler eingeben.', KGVVM_TEXT_DOMAIN ) );
} elseif ( (float) $old_final_reading < 0 ) {
$errors->add( 'old_final_negative', __( 'Der Endstand des alten Zählers darf nicht negativ sein.', KGVVM_TEXT_DOMAIN ) );
}
}
if ( $parcel_id > 0 ) {
if ( '' === trim( (string) $new_initial_raw ) ) {
$errors->add( 'new_initial_required', __( 'Bitte einen Startstand für den neuen Zähler eingeben.', KGVVM_TEXT_DOMAIN ) );
} elseif ( ! is_numeric( $new_initial_raw ) ) {
$errors->add( 'new_initial_invalid', __( 'Bitte einen gültigen Startstand für den neuen Zähler eingeben.', KGVVM_TEXT_DOMAIN ) );
} elseif ( (float) $new_initial_reading < 0 ) {
$errors->add( 'new_initial_negative', __( 'Der Startstand des neuen Zählers darf nicht negativ sein.', KGVVM_TEXT_DOMAIN ) );
}
}
if ( $latest && $new_data['installed_at'] && strtotime( $new_data['installed_at'] ) < strtotime( $latest->reading_date ) ) {
$errors->add( 'swap_date_older', __( 'Das Tauschdatum darf nicht vor der letzten gespeicherten Ablesung liegen.', KGVVM_TEXT_DOMAIN ) );
}
if ( $latest && '' !== $old_final_reading && (float) $old_final_reading < (float) $latest->reading_value ) {
$errors->add( 'swap_final_low', __( 'Der Endstand des alten Zählers darf nicht kleiner als die letzte Ablesung sein.', KGVVM_TEXT_DOMAIN ) );
}
if ( $errors->has_errors() ) {
$this->redirect_with_notice( 'kgvvm-zaehler', 'error', $errors->get_error_message(), array( 'id' => $id, 'open_swap' => 1 ) );
}
$swap_label = wp_date( 'd.m.Y', strtotime( $new_data['installed_at'] ) );
$new_data['note'] = trim(
implode(
"\n\n",
array_filter(
array(
sprintf( __( 'Ersatzzähler für %1$s seit %2$s.', KGVVM_TEXT_DOMAIN ), $old_meter->meter_number, $swap_label ),
$swap_note,
)
)
)
);
$new_meter_id = $this->meters->save( $new_data, 0 );
if ( ! $new_meter_id ) {
$this->redirect_with_notice( 'kgvvm-zaehler', 'error', __( 'Der neue Zähler konnte nicht angelegt werden.', KGVVM_TEXT_DOMAIN ), array( 'id' => $id, 'open_swap' => 1 ) );
}
if ( $parcel_id > 0 && '' !== $old_final_reading ) {
$this->readings->save(
array(
'meter_id' => $old_meter->id,
'parcel_id' => $parcel_id,
'reading_value' => $old_final_reading,
'reading_date' => $new_data['installed_at'],
'note' => __( 'Abschlussstand beim Zählerwechsel.', KGVVM_TEXT_DOMAIN ),
'submitted_by' => get_current_user_id(),
)
);
}
$this->meters->release_meter( $old_meter->id );
$this->meters->save(
array(
'type' => $old_meter->type,
'meter_number' => $old_meter->meter_number,
'section_id' => $old_meter->section_id,
'installed_at' => $old_meter->installed_at,
'calibration_year' => ! empty( $old_meter->calibration_year ) ? (int) $old_meter->calibration_year : null,
'is_active' => 0,
'note' => trim(
implode(
"\n\n",
array_filter(
array(
$old_meter->note,
sprintf( __( 'Zählerwechsel am %1$s. Neuer Zähler: %2$s.', KGVVM_TEXT_DOMAIN ), $swap_label, $new_data['meter_number'] ),
$swap_note ? sprintf( __( 'Tauschhinweis: %s', KGVVM_TEXT_DOMAIN ), $swap_note ) : '',
)
)
)
),
),
$old_meter->id
);
if ( $parcel_id > 0 ) {
$this->meters->assign_to_parcel( $new_meter_id, $parcel_id );
$this->readings->save(
array(
'meter_id' => $new_meter_id,
'parcel_id' => $parcel_id,
'reading_value' => $new_initial_reading,
'reading_date' => $new_data['installed_at'],
'note' => __( 'Startstand nach Zählerwechsel.', KGVVM_TEXT_DOMAIN ),
'submitted_by' => get_current_user_id(),
)
);
}
$this->redirect_with_notice( 'kgvvm-zaehler', 'success', __( 'Der Zähler wurde erfolgreich getauscht.', KGVVM_TEXT_DOMAIN ) );
}
/**
* Save tenant.
*
* @return void
*/
private function save_tenant() {
$this->require_cap( 'edit_paechter' );
check_admin_referer( 'kgvvm_save_tenant' );
$id = absint( isset( $_POST['id'] ) ? $_POST['id'] : 0 );
$data = $this->validator->sanitize_tenant( $_POST );
$errors = $this->validator->validate_tenant( $data );
if ( $errors->has_errors() ) {
$this->redirect_with_notice( 'kgvvm-paechter', 'error', $errors->get_error_message(), array( 'view' => 'form', 'id' => $id ) );
}
$this->tenants->save( $data, $id );
$this->redirect_with_notice( 'kgvvm-paechter', 'success', __( 'Pächter wurde gespeichert.', KGVVM_TEXT_DOMAIN ) );
}
/**
* Save a meter reading from a member or administrator.
*
* @return void
*/
private function save_meter_reading() {
if ( ! current_user_can( 'submit_meter_readings' ) && ! current_user_can( 'manage_kleingarten' ) ) {
wp_die( esc_html__( 'Sie haben keine Berechtigung für diese Aktion.', KGVVM_TEXT_DOMAIN ) );
}
check_admin_referer( 'kgvvm_save_meter_reading' );
$return_page = isset( $_POST['return_page'] ) ? sanitize_key( wp_unslash( $_POST['return_page'] ) ) : 'kgvvm-my-parcels';
$return_view = isset( $_POST['return_view'] ) ? sanitize_key( wp_unslash( $_POST['return_view'] ) ) : '';
$return_id = absint( isset( $_POST['return_id'] ) ? $_POST['return_id'] : 0 );
$return_args = array();
if ( ! in_array( $return_page, array( 'kgvvm-my-parcels', 'kgvvm-sparten', 'kgvvm-zaehler', 'kgvvm-consumption' ), true ) ) {
$return_page = 'kgvvm-my-parcels';
}
if ( $return_view ) {
$return_args['view'] = $return_view;
}
if ( $return_id > 0 ) {
$return_args['id'] = $return_id;
}
$data = $this->validator->sanitize_meter_reading( $_POST );
$errors = $this->validator->validate_meter_reading( $data );
$meter = $this->meters->find( $data['meter_id'] );
if ( ! current_user_can( 'manage_kleingarten' ) && ! $this->assignments->user_has_parcel( get_current_user_id(), $data['parcel_id'] ) ) {
$errors->add( 'forbidden_parcel', __( 'Sie dürfen nur Ablesungen für Ihre zugewiesenen Parzellen erfassen.', KGVVM_TEXT_DOMAIN ) );
}
if ( ! $meter || (int) $meter->parcel_id !== (int) $data['parcel_id'] ) {
$errors->add( 'invalid_meter_link', __( 'Der ausgewählte Zähler ist dieser Parzelle nicht zugeordnet.', KGVVM_TEXT_DOMAIN ) );
}
$latest = $meter ? $this->readings->get_latest_for_meter( $data['meter_id'] ) : null;
if ( $latest && $data['reading_date'] && strtotime( $data['reading_date'] ) < strtotime( $latest->reading_date ) ) {
$errors->add( 'reading_date_older', __( 'Das Ablesedatum darf nicht vor der letzten gespeicherten Ablesung liegen.', KGVVM_TEXT_DOMAIN ) );
}
if ( $latest && (float) $data['reading_value'] < (float) $latest->reading_value ) {
$errors->add( 'reading_too_low', __( 'Der neue Zählerstand darf nicht kleiner als die letzte erfasste Ablesung sein.', KGVVM_TEXT_DOMAIN ) );
}
if ( $errors->has_errors() ) {
$this->redirect_with_notice( $return_page, 'error', $errors->get_error_message(), $return_args );
}
$data['submitted_by'] = get_current_user_id();
$data['is_self_reading'] = ( ! current_user_can( 'manage_kleingarten' ) && current_user_can( 'submit_meter_readings' ) ) ? 1 : 0;
$warning_message = $this->build_meter_jump_warning_message( $meter, $latest, (float) $data['reading_value'] );
$this->readings->save( $data );
if ( $warning_message ) {
$this->redirect_with_notice( $return_page, 'warning', sprintf( __( 'Die Ablesung wurde gespeichert. %s', KGVVM_TEXT_DOMAIN ), $warning_message ), $return_args );
}
$this->redirect_with_notice( $return_page, 'success', __( 'Die Ablesung wurde gespeichert.', KGVVM_TEXT_DOMAIN ), $return_args );
}
/**
* Save general settings.
*
* @return void
*/
private function save_settings() {
$this->require_cap( Roles::SETTINGS_CAP );
check_admin_referer( 'kgvvm_save_settings' );
$current_settings = wp_parse_args( get_option( 'kgvvm_settings', array() ), $this->get_settings_defaults() );
$water_threshold = isset( $_POST['water_usage_alert_threshold'] ) ? (float) str_replace( ',', '.', wp_unslash( $_POST['water_usage_alert_threshold'] ) ) : 25;
$power_threshold = isset( $_POST['power_usage_alert_threshold'] ) ? (float) str_replace( ',', '.', wp_unslash( $_POST['power_usage_alert_threshold'] ) ) : 1000;
$power_unit = isset( $_POST['power_unit'] ) ? sanitize_key( wp_unslash( $_POST['power_unit'] ) ) : 'kwh';
$power_unit = in_array( $power_unit, array( 'kwh', 'mwh' ), true ) ? $power_unit : 'kwh';
$pdf_club_name = isset( $_POST['pdf_club_name'] ) ? sanitize_text_field( wp_unslash( $_POST['pdf_club_name'] ) ) : $current_settings['pdf_club_name'];
$pdf_logo_url = isset( $_POST['pdf_logo_url'] ) ? esc_url_raw( trim( wp_unslash( $_POST['pdf_logo_url'] ) ) ) : '';
$pdf_contact = isset( $_POST['pdf_contact_block'] ) ? sanitize_textarea_field( wp_unslash( $_POST['pdf_contact_block'] ) ) : '';
$pdf_intro_text = isset( $_POST['pdf_intro_text'] ) ? sanitize_textarea_field( wp_unslash( $_POST['pdf_intro_text'] ) ) : '';
$pdf_footer_text = isset( $_POST['pdf_footer_text'] ) ? sanitize_textarea_field( wp_unslash( $_POST['pdf_footer_text'] ) ) : '';
update_option(
'kgvvm_settings',
array_merge(
$current_settings,
array(
'allow_multiple_member_parcels' => ! empty( $_POST['allow_multiple_member_parcels'] ) ? 1 : 0,
'water_usage_alert_threshold' => max( 0, $water_threshold ),
'power_usage_alert_threshold' => max( 0, $power_threshold ),
'power_unit' => $power_unit,
'pdf_club_name' => $pdf_club_name,
'pdf_logo_url' => $pdf_logo_url,
'pdf_contact_block' => $pdf_contact,
'pdf_intro_text' => $pdf_intro_text,
'pdf_footer_text' => $pdf_footer_text,
)
),
false
);
$this->redirect_with_notice( 'kgvvm-settings', 'success', __( 'Einstellungen wurden gespeichert.', KGVVM_TEXT_DOMAIN ) );
}
/**
* Persist one year for the cost dropdown.
*
* @return void
*/
private function save_cost_year() {
$this->require_cap( 'manage_kleingarten' );
check_admin_referer( 'kgvvm_save_cost_year' );
$entry_year = $this->validator->sanitize_cost_year( $_POST );
$errors = $this->validator->validate_cost_year( $entry_year );
if ( $errors->has_errors() ) {
$this->redirect_with_notice( 'kgvvm-costs', 'error', $errors->get_error_message(), array( 'year' => current_time( 'Y' ) ) );
}
if ( ! $this->costs->save_year( $entry_year ) ) {
$this->redirect_with_notice( 'kgvvm-costs', 'error', __( 'Das Jahr konnte nicht gespeichert werden.', KGVVM_TEXT_DOMAIN ), array( 'year' => $entry_year ) );
}
$this->redirect_with_notice( 'kgvvm-costs', 'success', __( 'Das Jahr wurde zur Auswahlliste hinzugefügt.', KGVVM_TEXT_DOMAIN ), array( 'year' => $entry_year ) );
}
/**
* Save section-specific yearly prices.
*
* @return void
*/
private function save_cost_prices() {
$this->require_cap( 'manage_kleingarten' );
check_admin_referer( 'kgvvm_save_cost_prices' );
$data = $this->validator->sanitize_cost_year_settings( $_POST );
$errors = $this->validator->validate_cost_year_settings( $data );
if ( $errors->has_errors() ) {
$this->redirect_with_notice( 'kgvvm-costs', 'error', $errors->get_error_message(), array( 'year' => $data['entry_year'] ) );
}
if ( ! $this->costs->save_section_prices( $data ) ) {
$this->redirect_with_notice( 'kgvvm-costs', 'error', __( 'Die Preise für diese Sparte konnten nicht gespeichert werden.', KGVVM_TEXT_DOMAIN ), array( 'year' => $data['entry_year'] ) );
}
$this->redirect_with_notice( 'kgvvm-costs', 'success', __( 'Die Preise pro Sparte wurden gespeichert.', KGVVM_TEXT_DOMAIN ), array( 'year' => $data['entry_year'] ) );
}
/**
* Save one annual cost entry.
*
* @return void
*/
private function save_cost() {
$this->require_cap( 'manage_kleingarten' );
check_admin_referer( 'kgvvm_save_cost' );
$id = absint( isset( $_POST['id'] ) ? $_POST['id'] : 0 );
$data = $this->validator->sanitize_cost_entry( $_POST );
$errors = $this->validator->validate_cost_entry( $data );
if ( $errors->has_errors() ) {
$this->redirect_with_notice( 'kgvvm-costs', 'error', $errors->get_error_message(), array( 'year' => $data['entry_year'], 'id' => $id ) );
}
$active_parcels = array_values(
array_filter(
$this->parcels->search(),
static function( $parcel ) {
return isset( $parcel->status ) && 'inactive' !== $parcel->status;
}
)
);
$member_count = count( $this->assignments->get_member_users() );
$multiplier = 'member' === $data['distribution_type'] ? $member_count : count( $active_parcels );
$data['total_cost'] = (float) $data['unit_amount'] * max( 0, $multiplier );
$saved = $this->costs->save( $data, $id );
if ( ! $saved ) {
$this->redirect_with_notice( 'kgvvm-costs', 'error', __( 'Der Kostenposten konnte nicht gespeichert werden.', KGVVM_TEXT_DOMAIN ), array( 'year' => $data['entry_year'], 'id' => $id ) );
}
$this->redirect_with_notice( 'kgvvm-costs', 'success', __( 'Kostenposten wurde gespeichert.', KGVVM_TEXT_DOMAIN ), array( 'year' => $data['entry_year'] ) );
}
/**
* Render dashboard.
*
* @return void
*/
public function render_dashboard() {
$this->require_cap( 'manage_kleingarten' );
$sections_total = count( $this->sections->search() );
$parcels_total = count( $this->parcels->search() );
$meters_free = count( $this->meters->search( array( 'assignment' => 'free' ) ) );
$meters_used = count( $this->meters->search( array( 'assignment' => 'assigned' ) ) );
$tenants_total = count( $this->tenants->search() );
$members_total = count( $this->assignments->get_member_users() );
?>
<div class='wrap'>
<h1><?php echo esc_html__( 'Kleingartenverwaltung', KGVVM_TEXT_DOMAIN ); ?></h1>
<?php $this->render_notice(); ?>
<div class='kgvvm-grid'>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $sections_total ); ?></h2>
<p><?php echo esc_html__( 'Sparten', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $parcels_total ); ?></h2>
<p><?php echo esc_html__( 'Parzellen', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $meters_free ); ?></h2>
<p><?php echo esc_html__( 'Freie Zähler', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $meters_used ); ?></h2>
<p><?php echo esc_html__( 'Zugeordnete Zähler', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $tenants_total ); ?></h2>
<p><?php echo esc_html__( 'Pächter', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $members_total ); ?></h2>
<p><?php echo esc_html__( 'WordPress-Mitglieder', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html__( 'Fachmodell', KGVVM_TEXT_DOMAIN ); ?></h2>
<p><?php echo esc_html__( 'Das Plugin verwendet eigene Datenbanktabellen statt Custom Post Types, weil das Fachmodell stark relational ist: Sparten, Parzellen, Zähler, Pächter und Zuordnungen müssen performant gefiltert, eindeutig verknüpft und später um Zählerstände, Verträge oder Umlagen erweitert werden können.', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
</div>
<?php
}
/**
* Render consumption report page.
*
* @return void
*/
public function render_consumption_page() {
$this->require_cap( 'manage_kleingarten' );
$section_id = isset( $_GET['section_id'] ) ? absint( $_GET['section_id'] ) : 0;
$date_from = isset( $_GET['date_from'] ) ? sanitize_text_field( wp_unslash( $_GET['date_from'] ) ) : '';
$date_to = isset( $_GET['date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['date_to'] ) ) : '';
$order = isset( $_GET['order'] ) ? strtoupper( sanitize_key( wp_unslash( $_GET['order'] ) ) ) : 'DESC';
$order = 'ASC' === $order ? 'ASC' : 'DESC';
$sections = $this->sections->all_for_options();
if ( $date_from && $date_to && strtotime( $date_from ) > strtotime( $date_to ) ) {
$tmp = $date_from;
$date_from = $date_to;
$date_to = $tmp;
}
$rows = $this->readings->get_consumption_report( $section_id, $date_from, $date_to, $order );
$rate_lookup = $this->build_consumption_rate_lookup( $rows );
$water_total = 0.0;
$power_total = 0.0;
$water_cost_total = 0.0;
$power_cost_total = 0.0;
$consumed_count = 0;
$section_totals = array();
foreach ( $rows as $row ) {
$row->unit_price = $this->get_consumption_unit_price( $row, $rate_lookup );
$row->calculated_cost = $this->calculate_consumption_cost( $row->consumption, $row->type, $row->unit_price );
if ( null === $row->consumption ) {
continue;
}
$consumed_count++;
if ( 'power' === $row->type ) {
$power_total += (float) $row->consumption;
if ( null !== $row->calculated_cost ) {
$power_cost_total += (float) $row->calculated_cost;
}
} else {
$water_total += (float) $row->consumption;
if ( null !== $row->calculated_cost ) {
$water_cost_total += (float) $row->calculated_cost;
}
}
$section_key = $row->section_id ? (int) $row->section_id : 0;
if ( ! isset( $section_totals[ $section_key ] ) ) {
$section_totals[ $section_key ] = array(
'name' => $row->section_name ? $row->section_name : __( 'Ohne Sparte', KGVVM_TEXT_DOMAIN ),
'water' => 0.0,
'power' => 0.0,
'water_cost' => 0.0,
'power_cost' => 0.0,
);
}
$section_totals[ $section_key ][ $row->type ] += (float) $row->consumption;
if ( null !== $row->calculated_cost ) {
$section_totals[ $section_key ][ 'power' === $row->type ? 'power_cost' : 'water_cost' ] += (float) $row->calculated_cost;
}
}
?>
<div class='wrap'>
<h1><?php echo esc_html__( 'Verbrauchsauswertung', KGVVM_TEXT_DOMAIN ); ?></h1>
<?php $this->render_notice(); ?>
<form method='get' class='kgvvm-toolbar'>
<input type='hidden' name='page' value='kgvvm-consumption' />
<div class='kgvvm-filters'>
<select name='section_id'>
<option value=''><?php echo esc_html__( 'Alle Sparten', KGVVM_TEXT_DOMAIN ); ?></option>
<?php foreach ( $sections as $section ) : ?>
<option value='<?php echo esc_attr( $section->id ); ?>' <?php selected( $section_id, $section->id ); ?>><?php echo esc_html( $section->name ); ?></option>
<?php endforeach; ?>
</select>
<label>
<?php echo esc_html__( 'Von', KGVVM_TEXT_DOMAIN ); ?>
<input type='date' name='date_from' value='<?php echo esc_attr( $date_from ); ?>' />
</label>
<label>
<?php echo esc_html__( 'Bis', KGVVM_TEXT_DOMAIN ); ?>
<input type='date' name='date_to' value='<?php echo esc_attr( $date_to ); ?>' />
</label>
<select name='order'>
<option value='DESC' <?php selected( $order, 'DESC' ); ?>><?php echo esc_html__( 'Neueste zuerst', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='ASC' <?php selected( $order, 'ASC' ); ?>><?php echo esc_html__( 'Älteste zuerst', KGVVM_TEXT_DOMAIN ); ?></option>
</select>
<button class='button button-primary'><?php echo esc_html__( 'Auswerten', KGVVM_TEXT_DOMAIN ); ?></button>
</div>
</form>
<div class='kgvvm-grid'>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $this->format_meter_value_with_unit( $water_total, 'water' ) ); ?></h2>
<p><?php echo esc_html__( 'Wasserverbrauch im Zeitraum', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $this->format_meter_value_with_unit( $power_total, 'power' ) ); ?></h2>
<p><?php echo esc_html__( 'Stromverbrauch im Zeitraum', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $this->format_currency( $water_cost_total ) ); ?></h2>
<p><?php echo esc_html__( 'Wasserkosten im Zeitraum', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $this->format_currency( $power_cost_total ) ); ?></h2>
<p><?php echo esc_html__( 'Stromkosten im Zeitraum', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( number_format_i18n( $consumed_count, 0 ) ); ?></h2>
<p><?php echo esc_html__( 'Ausgewertete Ablesungen', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html__( 'Gesamtwerte je Sparte', KGVVM_TEXT_DOMAIN ); ?></h2>
<?php if ( empty( $section_totals ) ) : ?>
<p><?php echo esc_html__( 'Für den gewählten Zeitraum liegen noch keine auswertbaren Verbrauchsdaten vor.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php else : ?>
<table class='widefat striped'>
<thead>
<tr>
<th><?php echo esc_html__( 'Sparte', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Wasser', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Wasserkosten', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Strom', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Stromkosten', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $section_totals as $section_total ) : ?>
<tr>
<td><strong><?php echo esc_html( $section_total['name'] ); ?></strong></td>
<td><?php echo esc_html( $this->format_meter_value_with_unit( $section_total['water'], 'water' ) ); ?></td>
<td><?php echo esc_html( $section_total['water_cost'] > 0 ? $this->format_currency( $section_total['water_cost'] ) : '—' ); ?></td>
<td><?php echo esc_html( $this->format_meter_value_with_unit( $section_total['power'], 'power' ) ); ?></td>
<td><?php echo esc_html( $section_total['power_cost'] > 0 ? $this->format_currency( $section_total['power_cost'] ) : '—' ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<table class='widefat striped'>
<thead>
<tr>
<th><?php echo esc_html__( 'Datum', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Sparte', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Parzelle', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Zähler', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Vorheriger Stand', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Aktueller Stand', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Verbrauch seit letzter Ablesung', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Preis je Einheit', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Kosten', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php if ( empty( $rows ) ) : ?>
<tr><td colspan='9'><?php echo esc_html__( 'Für die aktuelle Auswahl wurden keine Ablesungen gefunden.', KGVVM_TEXT_DOMAIN ); ?></td></tr>
<?php else : ?>
<?php foreach ( $rows as $row ) : ?>
<tr>
<td><?php echo esc_html( wp_date( 'd.m.Y', strtotime( $row->reading_date ) ) ); ?></td>
<td><?php echo esc_html( $row->section_name ? $row->section_name : '—' ); ?></td>
<td><?php echo esc_html( $row->parcel_label ? $row->parcel_label : '—' ); ?></td>
<td><?php echo esc_html( $this->meter_type_label( $row->type ) . ' ' . $row->meter_number ); ?></td>
<td><?php echo esc_html( null !== $row->previous_value ? $this->format_meter_value_with_unit( $row->previous_value, $row->type ) : '—' ); ?></td>
<td><?php echo esc_html( $this->format_meter_value_with_unit( $row->reading_value, $row->type ) ); ?></td>
<td><?php echo esc_html( null !== $row->consumption ? $this->format_meter_value_with_unit( $row->consumption, $row->type ) : '—' ); ?></td>
<td><?php echo esc_html( null !== $row->unit_price ? $this->format_price_per_unit( $row->unit_price, 'power' === $row->type ? 'kWh' : 'm³' ) : '—' ); ?></td>
<td><?php echo esc_html( null !== $row->calculated_cost ? $this->format_currency( $row->calculated_cost ) : '—' ); ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
}
/**
* Render annual cost management.
*
* @return void
*/
public function render_costs_page() {
$this->require_cap( 'manage_kleingarten' );
$view = isset( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : 'list';
if ( 'statement' === $view ) {
$this->render_cost_statement_page();
return;
}
$selected_year = isset( $_GET['year'] ) ? absint( $_GET['year'] ) : (int) current_time( 'Y' );
$selected_year = $selected_year > 0 ? $selected_year : (int) current_time( 'Y' );
$search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
$edit_id = absint( isset( $_GET['id'] ) ? $_GET['id'] : 0 );
$edit_rate_section_id = absint( isset( $_GET['edit_rate_section_id'] ) ? $_GET['edit_rate_section_id'] : 0 );
$cost = $edit_id ? $this->costs->find( $edit_id ) : null;
if ( $cost && ! empty( $cost->entry_year ) ) {
$selected_year = (int) $cost->entry_year;
}
$years = $this->costs->get_years( $selected_year );
$sections = $this->sections->all_for_options( true );
$section_rates = $this->costs->get_section_prices( $selected_year );
$edit_rate = null;
foreach ( $section_rates as $section_rate ) {
if ( (int) $section_rate->section_id === $edit_rate_section_id ) {
$edit_rate = $section_rate;
break;
}
}
$rows = $this->costs->search(
array(
'year' => $selected_year,
's' => $search,
'orderby' => isset( $_GET['orderby'] ) ? sanitize_key( wp_unslash( $_GET['orderby'] ) ) : 'name',
'order' => isset( $_GET['order'] ) ? sanitize_key( wp_unslash( $_GET['order'] ) ) : 'ASC',
)
);
$entry_count = count( $rows );
$parcel_rows = array_values(
array_filter(
$this->parcels->search(),
static function( $parcel ) {
return isset( $parcel->status ) && 'inactive' !== $parcel->status;
}
)
);
$tenant_rows = $this->tenants->search( array( 'status' => 'active' ) );
$member_rows = $this->assignments->get_member_users();
$parcel_count = count( $parcel_rows );
$member_count = count( $member_rows );
$parcel_share = 0.0;
$member_share = 0.0;
$year_total = 0.0;
foreach ( $rows as $row ) {
$row->distribution_type = $this->get_cost_distribution_type( $row );
$row->unit_amount = $this->get_cost_unit_amount( $row, $parcel_count, $member_count );
$row->calculated_total_cost = $this->get_cost_total_amount( $row, $parcel_count, $member_count );
$year_total += (float) $row->calculated_total_cost;
if ( 'member' === $row->distribution_type ) {
$member_share += (float) $row->unit_amount;
} else {
$parcel_share += (float) $row->unit_amount;
}
}
if ( $cost ) {
$cost->distribution_type = $this->get_cost_distribution_type( $cost );
$cost->unit_amount = $this->get_cost_unit_amount( $cost, $parcel_count, $member_count );
$cost->calculated_total_cost = $this->get_cost_total_amount( $cost, $parcel_count, $member_count );
}
?>
<div class='wrap'>
<h1 class='wp-heading-inline'><?php echo esc_html__( 'Kosten', KGVVM_TEXT_DOMAIN ); ?></h1>
<?php if ( $cost ) : ?>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-costs', array( 'year' => $selected_year ) ) ); ?>' class='page-title-action'><?php echo esc_html__( 'Neuer Eintrag', KGVVM_TEXT_DOMAIN ); ?></a>
<?php endif; ?>
<?php $this->render_notice(); ?>
<form method='get' class='kgvvm-toolbar' id='kgvvm-cost-filter-form'>
<input type='hidden' name='page' value='kgvvm-costs' />
<div class='kgvvm-filters'>
<label for='kgvvm-cost-year-filter'><?php echo esc_html__( 'Jahr', KGVVM_TEXT_DOMAIN ); ?></label>
<select name='year' id='kgvvm-cost-year-filter'>
<?php foreach ( $years as $year ) : ?>
<option value='<?php echo esc_attr( $year ); ?>' <?php selected( $selected_year, $year ); ?>><?php echo esc_html( $year ); ?></option>
<?php endforeach; ?>
</select>
<input type='search' name='s' value='<?php echo esc_attr( $search ); ?>' placeholder='<?php echo esc_attr__( 'Kostenposten suchen …', KGVVM_TEXT_DOMAIN ); ?>' />
<button class='button'><?php echo esc_html__( 'Filtern', KGVVM_TEXT_DOMAIN ); ?></button>
</div>
</form>
<div class='kgvvm-grid'>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $this->format_currency( $year_total ) ); ?></h2>
<p><?php echo esc_html( sprintf( __( 'Gesamtkosten im Jahr %s', KGVVM_TEXT_DOMAIN ), $selected_year ) ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( number_format_i18n( $entry_count, 0 ) ); ?></h2>
<p><?php echo esc_html( sprintf( __( 'Erfasste Einträge in %s', KGVVM_TEXT_DOMAIN ), $selected_year ) ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $parcel_count > 0 ? $this->format_currency( $parcel_share ) : '—' ); ?></h2>
<p><?php echo esc_html__( 'Summe je Parzelle', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $member_count > 0 ? $this->format_currency( $member_share ) : '—' ); ?></h2>
<p><?php echo esc_html__( 'Summe je Mitglied', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
</div>
<div class='kgvvm-grid'>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $edit_rate ? __( 'Spartenpreise bearbeiten', KGVVM_TEXT_DOMAIN ) : __( 'Preise je Sparte', KGVVM_TEXT_DOMAIN ) ); ?></h2>
<p class='kgvvm-help'><?php echo esc_html__( 'Strom- und Wasserpreise können je Jahr und je Sparte unterschiedlich hinterlegt und später wieder bearbeitet werden.', KGVVM_TEXT_DOMAIN ); ?></p>
<form method='post'>
<?php wp_nonce_field( 'kgvvm_save_cost_prices' ); ?>
<input type='hidden' name='kgvvm_action' value='save_cost_prices' />
<table class='form-table kgvvm-form-table'>
<tr>
<th scope='row'><label for='kgvvm-price-entry-year'><?php echo esc_html__( 'Jahr', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<select name='entry_year' id='kgvvm-price-entry-year'>
<?php foreach ( $years as $year ) : ?>
<option value='<?php echo esc_attr( $year ); ?>' <?php selected( $selected_year, $year ); ?>><?php echo esc_html( $year ); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-cost-section-id'><?php echo esc_html__( 'Sparte', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<select name='section_id' id='kgvvm-cost-section-id' required>
<option value=''><?php echo esc_html__( 'Bitte wählen', KGVVM_TEXT_DOMAIN ); ?></option>
<?php foreach ( $sections as $section ) : ?>
<option value='<?php echo esc_attr( $section->id ); ?>' <?php selected( $edit_rate ? (int) $edit_rate->section_id : 0, $section->id ); ?>><?php echo esc_html( $section->name ); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-power-price'><?php echo esc_html__( 'Preis pro kWh', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input type='number' min='0' step='0.0001' name='power_price_per_kwh' id='kgvvm-power-price' value='<?php echo esc_attr( $edit_rate && null !== $edit_rate->power_price_per_kwh ? (float) $edit_rate->power_price_per_kwh : '' ); ?>' /> € / kWh</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-water-price'><?php echo esc_html__( 'Preis pro m³', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input type='number' min='0' step='0.0001' name='water_price_per_m3' id='kgvvm-water-price' value='<?php echo esc_attr( $edit_rate && null !== $edit_rate->water_price_per_m3 ? (float) $edit_rate->water_price_per_m3 : '' ); ?>' /> € / m³</td>
</tr>
</table>
<?php submit_button( $edit_rate ? __( 'Spartenpreise aktualisieren', KGVVM_TEXT_DOMAIN ) : __( 'Spartenpreise speichern', KGVVM_TEXT_DOMAIN ), 'secondary' ); ?>
<?php if ( $edit_rate ) : ?>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-costs', array( 'year' => $selected_year ) ) ); ?>' class='button-secondary'><?php echo esc_html__( 'Abbrechen', KGVVM_TEXT_DOMAIN ); ?></a>
<?php endif; ?>
</form>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $cost ? __( 'Kostenposten bearbeiten', KGVVM_TEXT_DOMAIN ) : __( 'Kostenposten anlegen', KGVVM_TEXT_DOMAIN ) ); ?></h2>
<p class='kgvvm-help'><?php echo esc_html__( 'Pro Jahr können beliebig viele Einträge mit einem Betrag pro Parzelle oder pro Mitglied hinterlegt werden.', KGVVM_TEXT_DOMAIN ); ?></p>
<form method='post'>
<?php wp_nonce_field( 'kgvvm_save_cost' ); ?>
<input type='hidden' name='kgvvm_action' value='save_cost' />
<input type='hidden' name='id' value='<?php echo esc_attr( $cost ? $cost->id : 0 ); ?>' />
<table class='form-table kgvvm-form-table'>
<tr>
<th scope='row'><label for='kgvvm-cost-entry-year'><?php echo esc_html__( 'Jahr', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<select name='entry_year' id='kgvvm-cost-entry-year'>
<?php foreach ( $years as $year ) : ?>
<option value='<?php echo esc_attr( $year ); ?>' <?php selected( $cost ? (int) $cost->entry_year : $selected_year, $year ); ?>><?php echo esc_html( $year ); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-cost-name'><?php echo esc_html__( 'Name', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='name' id='kgvvm-cost-name' type='text' class='regular-text' required value='<?php echo esc_attr( $cost ? $cost->name : '' ); ?>' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-cost-distribution'><?php echo esc_html__( 'Gilt pro', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<select name='distribution_type' id='kgvvm-cost-distribution'>
<option value='parcel' <?php selected( $cost ? $cost->distribution_type : 'parcel', 'parcel' ); ?>><?php echo esc_html__( 'Parzelle', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='member' <?php selected( $cost ? $cost->distribution_type : 'parcel', 'member' ); ?>><?php echo esc_html__( 'Mitglied', KGVVM_TEXT_DOMAIN ); ?></option>
</select>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-cost-unit-amount'><?php echo esc_html__( 'Betrag', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<input name='unit_amount' id='kgvvm-cost-unit-amount' type='number' min='0' step='0.01' required value='<?php echo esc_attr( $cost ? (float) $cost->unit_amount : '' ); ?>' />
<span>€</span>
<p class='kgvvm-help'><?php echo esc_html__( 'Betrag je ausgewählter Einheit, also pro Parzelle oder pro Mitglied.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-cost-note'><?php echo esc_html__( 'Bemerkung', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><textarea name='note' id='kgvvm-cost-note' rows='4' class='large-text'><?php echo esc_textarea( $cost ? $cost->note : '' ); ?></textarea></td>
</tr>
</table>
<?php submit_button( $cost ? __( 'Kostenposten aktualisieren', KGVVM_TEXT_DOMAIN ) : __( 'Kostenposten speichern', KGVVM_TEXT_DOMAIN ) ); ?>
</form>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html__( 'Jahresabrechnung drucken', KGVVM_TEXT_DOMAIN ); ?></h2>
<p class='kgvvm-help'><?php echo esc_html__( 'Die Übersicht kann im Browser direkt gedruckt oder als PDF gespeichert werden.', KGVVM_TEXT_DOMAIN ); ?></p>
<form method='get' target='_blank' style='margin-bottom:12px;'>
<input type='hidden' name='page' value='kgvvm-costs' />
<input type='hidden' name='view' value='statement' />
<input type='hidden' name='statement_type' value='parcel' />
<input type='hidden' name='year' value='<?php echo esc_attr( $selected_year ); ?>' />
<p>
<label for='kgvvm-statement-parcel'><?php echo esc_html__( 'Parzelle', KGVVM_TEXT_DOMAIN ); ?></label><br />
<select name='subject_id' id='kgvvm-statement-parcel' required>
<option value=''><?php echo esc_html__( 'Parzelle wählen', KGVVM_TEXT_DOMAIN ); ?></option>
<?php foreach ( $parcel_rows as $parcel ) : ?>
<option value='<?php echo esc_attr( $parcel->id ); ?>'><?php echo esc_html( $parcel->label . ( $parcel->section_name ? ' ' . $parcel->section_name : '' ) ); ?></option>
<?php endforeach; ?>
</select>
</p>
<?php submit_button( __( 'Parzellenabrechnung öffnen', KGVVM_TEXT_DOMAIN ), 'secondary', 'submit', false ); ?>
</form>
<form method='get' target='_blank'>
<input type='hidden' name='page' value='kgvvm-costs' />
<input type='hidden' name='view' value='statement' />
<input type='hidden' name='statement_type' value='tenant' />
<input type='hidden' name='year' value='<?php echo esc_attr( $selected_year ); ?>' />
<p>
<label for='kgvvm-statement-tenant'><?php echo esc_html__( 'Pächter', KGVVM_TEXT_DOMAIN ); ?></label><br />
<select name='subject_id' id='kgvvm-statement-tenant' required>
<option value=''><?php echo esc_html__( 'Pächter wählen', KGVVM_TEXT_DOMAIN ); ?></option>
<?php foreach ( $tenant_rows as $tenant ) : ?>
<option value='<?php echo esc_attr( $tenant->id ); ?>'><?php echo esc_html( $tenant->last_name . ', ' . $tenant->first_name ); ?></option>
<?php endforeach; ?>
</select>
</p>
<?php submit_button( __( 'Pächterabrechnung öffnen', KGVVM_TEXT_DOMAIN ), 'secondary', 'submit', false ); ?>
</form>
</div>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( sprintf( __( 'Preise je Sparte im Jahr %s', KGVVM_TEXT_DOMAIN ), $selected_year ) ); ?></h2>
<?php if ( empty( $section_rates ) ) : ?>
<p><?php echo esc_html__( 'Für dieses Jahr sind noch keine Spartenpreise hinterlegt.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php else : ?>
<table class='widefat striped'>
<thead>
<tr>
<th><?php echo esc_html__( 'Sparte', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Preis pro kWh', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Preis pro m³', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Aktualisiert', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Aktionen', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $section_rates as $rate ) : ?>
<tr>
<td><strong><?php echo esc_html( $rate->section_name ? $rate->section_name : '—' ); ?></strong></td>
<td><?php echo esc_html( null !== $rate->power_price_per_kwh ? $this->format_price_per_unit( $rate->power_price_per_kwh, 'kWh' ) : '—' ); ?></td>
<td><?php echo esc_html( null !== $rate->water_price_per_m3 ? $this->format_price_per_unit( $rate->water_price_per_m3, 'm³' ) : '—' ); ?></td>
<td><?php echo esc_html( ! empty( $rate->updated_at ) ? wp_date( 'd.m.Y H:i', strtotime( $rate->updated_at ) ) : '—' ); ?></td>
<td><a href='<?php echo esc_url( $this->admin_url( 'kgvvm-costs', array( 'year' => $selected_year, 'edit_rate_section_id' => $rate->section_id ) ) ); ?>'><?php echo esc_html__( 'Bearbeiten', KGVVM_TEXT_DOMAIN ); ?></a></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<table class='widefat striped'>
<thead>
<tr>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-costs', 'name' ) ); ?>'><?php echo esc_html__( 'Name', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><?php echo esc_html__( 'Verteilung', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Betrag', KGVVM_TEXT_DOMAIN ); ?></th>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-costs', 'total_cost' ) ); ?>'><?php echo esc_html__( 'Gesamt im Jahr', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><?php echo esc_html__( 'Bemerkung', KGVVM_TEXT_DOMAIN ); ?></th>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-costs', 'updated_at' ) ); ?>'><?php echo esc_html__( 'Aktualisiert', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><?php echo esc_html__( 'Aktionen', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php if ( empty( $rows ) ) : ?>
<tr><td colspan='7'><?php echo esc_html__( 'Für das gewählte Jahr sind noch keine Kostenposten vorhanden.', KGVVM_TEXT_DOMAIN ); ?></td></tr>
<?php else : ?>
<?php foreach ( $rows as $row ) : ?>
<tr>
<td><strong><?php echo esc_html( $row->name ); ?></strong></td>
<td><?php echo esc_html( $this->get_cost_distribution_label( $row->distribution_type ) ); ?></td>
<td><?php echo esc_html( $this->format_currency( $row->unit_amount ) ); ?></td>
<td><?php echo esc_html( $this->format_currency( $row->calculated_total_cost ) ); ?></td>
<td><?php echo esc_html( $row->note ? $row->note : '—' ); ?></td>
<td><?php echo esc_html( ! empty( $row->updated_at ) ? wp_date( 'd.m.Y H:i', strtotime( $row->updated_at ) ) : '—' ); ?></td>
<td>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-costs', array( 'year' => $selected_year, 'id' => $row->id ) ) ); ?>'><?php echo esc_html__( 'Bearbeiten', KGVVM_TEXT_DOMAIN ); ?></a>
|
<a href='<?php echo esc_url( wp_nonce_url( $this->admin_url( 'kgvvm-costs', array( 'kgvvm_action' => 'delete_cost', 'id' => $row->id, 'year' => $selected_year ) ), 'kgvvm_delete_cost_' . $row->id ) ); ?>' onclick='return confirm("<?php echo esc_js( __( 'Kostenposten wirklich löschen?', KGVVM_TEXT_DOMAIN ) ); ?>");'><?php echo esc_html__( 'Löschen', KGVVM_TEXT_DOMAIN ); ?></a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<div class='kgvvm-grid'>
<div class='kgvvm-card'>
<h2><?php echo esc_html__( 'Verteilung pro Parzelle', KGVVM_TEXT_DOMAIN ); ?></h2>
<?php if ( empty( $parcel_rows ) ) : ?>
<p><?php echo esc_html__( 'Aktuell sind keine aktiven Parzellen für die Verteilung vorhanden.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php else : ?>
<table class='widefat striped'>
<thead>
<tr>
<th><?php echo esc_html__( 'Parzelle', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Sparte', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Status', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Anzahl Pächter', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Jahresanteil', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $parcel_rows as $parcel ) : ?>
<tr>
<td><strong><?php echo esc_html( $parcel->label ); ?></strong></td>
<td><?php echo esc_html( $parcel->section_name ? $parcel->section_name : '—' ); ?></td>
<td><?php echo esc_html( $this->parcel_status_label( $parcel->status ) ); ?></td>
<td><?php echo esc_html( ! empty( $parcel->tenant_count ) ? number_format_i18n( (int) $parcel->tenant_count, 0 ) : '—' ); ?></td>
<td><?php echo esc_html( $this->format_currency( $parcel_share ) ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html__( 'Verteilung pro Mitglied', KGVVM_TEXT_DOMAIN ); ?></h2>
<?php if ( empty( $member_rows ) ) : ?>
<p><?php echo esc_html__( 'Aktuell sind keine Mitglieder für die Verteilung vorhanden.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php else : ?>
<table class='widefat striped'>
<thead>
<tr>
<th><?php echo esc_html__( 'Mitglied', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'E-Mail', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Jahresanteil', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $member_rows as $member ) : ?>
<tr>
<td><strong><?php echo esc_html( $member->display_name ); ?></strong></td>
<td><?php echo esc_html( $member->user_email ? $member->user_email : '—' ); ?></td>
<td><?php echo esc_html( $member_count > 0 ? $this->format_currency( $member_share ) : '—' ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
var filterForm = document.getElementById('kgvvm-cost-filter-form');
var fields = [
document.getElementById('kgvvm-cost-year-filter'),
document.getElementById('kgvvm-price-entry-year'),
document.getElementById('kgvvm-cost-entry-year')
].filter(Boolean);
if (!fields.length) {
return;
}
fields.forEach(function (field) {
field.addEventListener('change', function () {
fields.forEach(function (otherField) {
otherField.value = field.value;
});
if (field.id === 'kgvvm-cost-year-filter' && filterForm) {
filterForm.submit();
}
});
});
});
</script>
<?php
}
/**
* Render one printable annual statement for a parcel or tenant.
*
* @return void
*/
private function render_cost_statement_page() {
$settings = wp_parse_args( get_option( 'kgvvm_settings', array() ), $this->get_settings_defaults() );
$year = isset( $_GET['year'] ) ? absint( $_GET['year'] ) : (int) current_time( 'Y' );
$statement_type = isset( $_GET['statement_type'] ) ? sanitize_key( wp_unslash( $_GET['statement_type'] ) ) : 'parcel';
$output = isset( $_GET['output'] ) ? sanitize_key( wp_unslash( $_GET['output'] ) ) : 'html';
$subject_id = absint( isset( $_GET['subject_id'] ) ? $_GET['subject_id'] : 0 );
$date_from = sprintf( '%04d-01-01', $year );
$date_to = sprintf( '%04d-12-31', $year );
$all_rows = $this->readings->get_consumption_report( 0, $date_from, $date_to, 'ASC' );
$active_parcels = array_values(
array_filter(
$this->parcels->search(),
static function( $parcel ) {
return isset( $parcel->status ) && 'inactive' !== $parcel->status;
}
)
);
$active_members = $this->assignments->get_member_users();
$cost_entries = $this->costs->search( array( 'year' => $year, 'orderby' => 'name', 'order' => 'ASC' ) );
$subject_label = '';
$subject_meta = array();
$parcel_ids = array();
$subject_parcel_count = 0;
$subject_member_count = 0;
if ( 'tenant' === $statement_type ) {
$tenant = $this->tenants->find( $subject_id );
if ( ! $tenant ) {
$this->redirect_with_notice( 'kgvvm-costs', 'error', __( 'Der gewünschte Pächter wurde nicht gefunden.', KGVVM_TEXT_DOMAIN ), array( 'year' => $year ) );
}
$tenant_parcels = $this->assignments->get_parcels_for_tenant( $subject_id );
$tenant_members = $this->assignments->get_members_for_tenant( $subject_id );
$tenant_member_names = array_values( array_unique( array_filter( wp_list_pluck( $tenant_members, 'display_name' ) ) ) );
$parcel_ids = wp_list_pluck( $tenant_parcels, 'id' );
$subject_parcel_count = count( $tenant_parcels );
$subject_member_count = count( array_unique( array_filter( array_map( 'intval', wp_list_pluck( $tenant_members, 'ID' ) ) ) ) );
$subject_label = sprintf( __( 'Jahresabrechnung Pächter %1$s %2$s', KGVVM_TEXT_DOMAIN ), $tenant->last_name . ', ' . $tenant->first_name, $year );
$subject_meta[] = array(
'label' => __( 'Mitglied/Pächter', KGVVM_TEXT_DOMAIN ),
'value' => empty( $tenant_member_names ) ? '—' : implode( ', ', $tenant_member_names ),
);
$subject_meta[] = array(
'label' => __( 'Pächter', KGVVM_TEXT_DOMAIN ),
'value' => $tenant->last_name . ', ' . $tenant->first_name,
);
$subject_meta[] = array(
'label' => __( 'Parzellen', KGVVM_TEXT_DOMAIN ),
'value' => empty( $tenant_parcels ) ? '—' : implode( ', ', wp_list_pluck( $tenant_parcels, 'label' ) ),
);
} else {
$parcel = $this->parcels->find( $subject_id );
if ( ! $parcel ) {
$this->redirect_with_notice( 'kgvvm-costs', 'error', __( 'Die gewünschte Parzelle wurde nicht gefunden.', KGVVM_TEXT_DOMAIN ), array( 'year' => $year ) );
}
$section = $parcel->section_id ? $this->sections->find( $parcel->section_id ) : null;
$parcel_tenants = $this->assignments->get_tenants_for_parcel( $subject_id );
$parcel_members = $this->assignments->get_members_for_parcel( $subject_id );
$parcel_member_names = array_values( array_unique( array_filter( wp_list_pluck( $parcel_members, 'display_name' ) ) ) );
$parcel_ids = array( $subject_id );
$subject_parcel_count = 1;
$subject_member_count = count( $parcel_members );
$subject_label = sprintf( __( 'Jahresabrechnung Parzelle %1$s %2$s', KGVVM_TEXT_DOMAIN ), $parcel->label, $year );
$subject_meta[] = array(
'label' => __( 'Parzelle', KGVVM_TEXT_DOMAIN ),
'value' => $parcel->label,
);
$subject_meta[] = array(
'label' => __( 'Sparte', KGVVM_TEXT_DOMAIN ),
'value' => $section ? $section->name : '—',
);
$subject_meta[] = array(
'label' => __( 'Mitglied/Pächter', KGVVM_TEXT_DOMAIN ),
'value' => empty( $parcel_member_names ) ? '—' : implode( ', ', $parcel_member_names ),
);
$subject_meta[] = array(
'label' => __( 'Pächter', KGVVM_TEXT_DOMAIN ),
'value' => empty( $parcel_tenants ) ? '—' : implode( ', ', array_map( array( $this, 'format_tenant_full_name' ), $parcel_tenants ) ),
);
}
$statement_rows = array();
foreach ( $all_rows as $row ) {
if ( in_array( (int) $row->parcel_id, array_map( 'intval', $parcel_ids ), true ) ) {
$statement_rows[] = $row;
}
}
$rate_lookup = $this->build_consumption_rate_lookup( $statement_rows );
$water_total = 0.0;
$power_total = 0.0;
$water_cost_total = 0.0;
$power_cost_total = 0.0;
$utility_rows = array();
foreach ( $statement_rows as $row ) {
$row->unit_price = $this->get_consumption_unit_price( $row, $rate_lookup );
$row->calculated_cost = $this->calculate_consumption_cost( $row->consumption, $row->type, $row->unit_price );
if ( null === $row->consumption ) {
continue;
}
$utility_rows[] = $row;
if ( 'power' === $row->type ) {
$power_total += (float) $row->consumption;
if ( null !== $row->calculated_cost ) {
$power_cost_total += (float) $row->calculated_cost;
}
} else {
$water_total += (float) $row->consumption;
if ( null !== $row->calculated_cost ) {
$water_cost_total += (float) $row->calculated_cost;
}
}
}
$fixed_items = array();
$fixed_total = 0.0;
foreach ( $cost_entries as $entry ) {
$distribution_type = $this->get_cost_distribution_type( $entry );
$unit_amount = $this->get_cost_unit_amount( $entry, count( $active_parcels ), count( $active_members ) );
$subject_units = 'member' === $distribution_type ? $subject_member_count : $subject_parcel_count;
$share = $unit_amount * max( 0, $subject_units );
$total_amount = $this->get_cost_total_amount( $entry, count( $active_parcels ), count( $active_members ) );
$fixed_total += $share;
$fixed_items[] = array(
'name' => $entry->name,
'distribution_label' => $this->get_cost_distribution_label( $distribution_type ),
'unit_amount' => $unit_amount,
'units' => $subject_units,
'total' => $total_amount,
'share' => $share,
'note' => $entry->note,
);
}
$utility_total = $water_cost_total + $power_cost_total;
$grand_total = $fixed_total + $utility_total;
if ( 'pdf' === $output ) {
$this->render_cost_statement_pdf(
array(
'year' => $year,
'statement_type' => $statement_type,
'subject_id' => $subject_id,
'subject_label' => $subject_label,
'subject_meta' => $subject_meta,
'fixed_items' => $fixed_items,
'utility_rows' => $utility_rows,
'fixed_total' => $fixed_total,
'water_cost_total' => $water_cost_total,
'power_cost_total' => $power_cost_total,
'grand_total' => $grand_total,
'settings' => $settings,
)
);
return;
}
?>
<div class='wrap kgvvm-print-page'>
<h1><?php echo esc_html( $subject_label ); ?></h1>
<div class='kgvvm-print-actions'>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-costs', array( 'view' => 'statement', 'statement_type' => $statement_type, 'subject_id' => $subject_id, 'year' => $year, 'output' => 'pdf' ) ) ); ?>' class='button button-primary' target='_blank'><?php echo esc_html__( 'PDF erzeugen', KGVVM_TEXT_DOMAIN ); ?></a>
<a href='#' class='button button-secondary' onclick='window.print(); return false;'><?php echo esc_html__( 'Drucken / Als PDF speichern', KGVVM_TEXT_DOMAIN ); ?></a>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-costs', array( 'year' => $year ) ) ); ?>' class='button-secondary'><?php echo esc_html__( 'Zurück', KGVVM_TEXT_DOMAIN ); ?></a>
</div>
<?php if ( ! empty( $settings['pdf_club_name'] ) || ! empty( $settings['pdf_logo_url'] ) || ! empty( $settings['pdf_contact_block'] ) || ! empty( $settings['pdf_intro_text'] ) ) : ?>
<div class='kgvvm-card'>
<table style='width:100%; border-collapse:collapse;'>
<tr>
<?php if ( ! empty( $settings['pdf_logo_url'] ) ) : ?>
<td style='width:140px; vertical-align:top;'>
<img src='<?php echo esc_url( $settings['pdf_logo_url'] ); ?>' alt='' style='max-width:120px; height:auto;' />
</td>
<?php endif; ?>
<td style='vertical-align:top;'>
<?php if ( ! empty( $settings['pdf_club_name'] ) ) : ?>
<h2 style='margin-top:0;'><?php echo esc_html( $settings['pdf_club_name'] ); ?></h2>
<?php endif; ?>
<?php if ( ! empty( $settings['pdf_contact_block'] ) ) : ?>
<p><?php echo wp_kses_post( nl2br( esc_html( $settings['pdf_contact_block'] ) ) ); ?></p>
<?php endif; ?>
</td>
</tr>
</table>
<?php if ( ! empty( $settings['pdf_intro_text'] ) ) : ?>
<p><?php echo wp_kses_post( nl2br( esc_html( $settings['pdf_intro_text'] ) ) ); ?></p>
<?php endif; ?>
</div>
<?php endif; ?>
<div class='kgvvm-card'>
<ul>
<?php foreach ( $subject_meta as $meta ) : ?>
<li><strong><?php echo esc_html( $meta['label'] ); ?>:</strong> <?php echo esc_html( $meta['value'] ); ?></li>
<?php endforeach; ?>
</ul>
</div>
<div class='kgvvm-grid'>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $this->format_currency( $fixed_total ) ); ?></h2>
<p><?php echo esc_html__( 'Anteilige Grundkosten', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $this->format_currency( $water_cost_total ) ); ?></h2>
<p><?php echo esc_html__( 'Wasserkosten', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $this->format_currency( $power_cost_total ) ); ?></h2>
<p><?php echo esc_html__( 'Stromkosten', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html( $this->format_currency( $grand_total ) ); ?></h2>
<p><?php echo esc_html__( 'Gesamtbetrag', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html__( 'Grundkosten anteilig', KGVVM_TEXT_DOMAIN ); ?></h2>
<?php if ( empty( $fixed_items ) ) : ?>
<p><?php echo esc_html__( 'Für dieses Jahr sind keine allgemeinen Kostenposten hinterlegt.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php else : ?>
<table class='widefat striped'>
<thead>
<tr>
<th><?php echo esc_html__( 'Kostenposten', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Gesamtkosten', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Anteil', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $fixed_items as $item ) : ?>
<tr>
<td>
<strong><?php echo esc_html( $item['name'] ); ?></strong>
<br /><span class="kgvvm-help"><?php echo esc_html( sprintf( __( '%1$s × %2$s zu je %3$s', KGVVM_TEXT_DOMAIN ), $item['distribution_label'], number_format_i18n( (int) $item['units'], 0 ), $this->format_currency( $item['unit_amount'] ) ) ); ?></span>
<?php echo $item['note'] ? '<br /><span class="kgvvm-help">' . esc_html( $item['note'] ) . '</span>' : ''; ?>
</td>
<td><?php echo esc_html( $this->format_currency( $item['total'] ) ); ?></td>
<td><?php echo esc_html( $this->format_currency( $item['share'] ) ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html__( 'Wasserzähler', KGVVM_TEXT_DOMAIN ); ?></h2>
<?php echo $this->build_statement_meter_detail_table( $this->get_statement_rows_by_meter_type( $utility_rows, 'water' ), 'water' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
<div class='kgvvm-card'>
<h2><?php echo esc_html__( 'Stromzähler', KGVVM_TEXT_DOMAIN ); ?></h2>
<?php echo $this->build_statement_meter_detail_table( $this->get_statement_rows_by_meter_type( $utility_rows, 'power' ), 'power' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
<?php if ( ! empty( $settings['pdf_footer_text'] ) ) : ?>
<div class='kgvvm-card'>
<p><?php echo wp_kses_post( nl2br( esc_html( $settings['pdf_footer_text'] ) ) ); ?></p>
</div>
<?php endif; ?>
</div>
<?php
}
/**
* Output one annual statement as a real PDF using TCPDF.
*
* @param array $statement Statement payload.
* @return void
*/
private function render_cost_statement_pdf( $statement ) {
$tcpdf_path = KGVVM_PLUGIN_DIR . 'lib/tcpdf/tcpdf.php';
$settings = isset( $statement['settings'] ) && is_array( $statement['settings'] ) ? wp_parse_args( $statement['settings'], $this->get_settings_defaults() ) : $this->get_settings_defaults();
$document_author = ! empty( $settings['pdf_club_name'] ) ? $settings['pdf_club_name'] : 'KGV Vereinsverwaltung';
if ( function_exists( 'nocache_headers' ) ) {
nocache_headers();
}
while ( ob_get_level() > 0 ) {
ob_end_clean();
}
if ( ! class_exists( '\\TCPDF' ) && file_exists( $tcpdf_path ) ) {
require_once $tcpdf_path;
}
if ( ! class_exists( '\\TCPDF' ) ) {
wp_die( esc_html__( 'TCPDF konnte nicht geladen werden. Bitte die Bibliothek prüfen.', KGVVM_TEXT_DOMAIN ) );
}
$pdf = new \TCPDF( PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false );
$pdf->SetCreator( $document_author );
$pdf->SetAuthor( $document_author );
$pdf->SetTitle( $statement['subject_label'] );
$pdf->SetMargins( 12, 12, 12 );
$pdf->SetAutoPageBreak( true, 12 );
$pdf->setPrintHeader( false );
$pdf->setPrintFooter( false );
$pdf->AddPage();
$pdf->SetFont( 'dejavusans', '', 10 );
$pdf->writeHTML( $this->build_cost_statement_pdf_html( $statement ), true, false, true, false, '' );
$filename = sanitize_file_name( sprintf( 'jahresabrechnung-%s-%s-%s.pdf', $statement['year'], $statement['statement_type'], $statement['subject_id'] ) );
$pdf->Output( $filename, 'I' );
exit;
}
/**
* Build the HTML markup for the PDF statement.
*
* @param array $statement Statement payload.
* @return string
*/
private function build_cost_statement_pdf_html( $statement ) {
$settings = isset( $statement['settings'] ) && is_array( $statement['settings'] ) ? wp_parse_args( $statement['settings'], $this->get_settings_defaults() ) : $this->get_settings_defaults();
ob_start();
?>
<?php if ( ! empty( $settings['pdf_club_name'] ) || ! empty( $settings['pdf_logo_url'] ) || ! empty( $settings['pdf_contact_block'] ) || ! empty( $settings['pdf_intro_text'] ) ) : ?>
<table cellpadding="4" cellspacing="0" border="0">
<tr>
<?php if ( ! empty( $settings['pdf_logo_url'] ) ) : ?>
<td width="120">
<img src="<?php echo esc_url( $settings['pdf_logo_url'] ); ?>" style="max-width:100px; height:auto;" alt="" />
</td>
<?php endif; ?>
<td>
<?php if ( ! empty( $settings['pdf_club_name'] ) ) : ?>
<h2><?php echo esc_html( $settings['pdf_club_name'] ); ?></h2>
<?php endif; ?>
<?php if ( ! empty( $settings['pdf_contact_block'] ) ) : ?>
<p><?php echo nl2br( esc_html( $settings['pdf_contact_block'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
<?php endif; ?>
</td>
</tr>
</table>
<?php if ( ! empty( $settings['pdf_intro_text'] ) ) : ?>
<p><?php echo nl2br( esc_html( $settings['pdf_intro_text'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
<?php endif; ?>
<?php endif; ?>
<h1><?php echo esc_html( $statement['subject_label'] ); ?></h1>
<table cellpadding="4" cellspacing="0" border="0">
<?php foreach ( $statement['subject_meta'] as $meta ) : ?>
<tr>
<td><strong><?php echo esc_html( $meta['label'] ); ?>:</strong></td>
<td><?php echo esc_html( $meta['value'] ); ?></td>
</tr>
<?php endforeach; ?>
</table>
<h2><?php echo esc_html__( 'Zusammenfassung', KGVVM_TEXT_DOMAIN ); ?></h2>
<table cellpadding="5" cellspacing="0" border="1">
<tr>
<th><strong><?php echo esc_html__( 'Position', KGVVM_TEXT_DOMAIN ); ?></strong></th>
<th><strong><?php echo esc_html__( 'Betrag', KGVVM_TEXT_DOMAIN ); ?></strong></th>
</tr>
<tr>
<td><?php echo esc_html__( 'Anteilige Grundkosten', KGVVM_TEXT_DOMAIN ); ?></td>
<td><?php echo esc_html( $this->format_currency( $statement['fixed_total'] ) ); ?></td>
</tr>
<tr>
<td><?php echo esc_html__( 'Wasserkosten', KGVVM_TEXT_DOMAIN ); ?></td>
<td><?php echo esc_html( $this->format_currency( $statement['water_cost_total'] ) ); ?></td>
</tr>
<tr>
<td><?php echo esc_html__( 'Stromkosten', KGVVM_TEXT_DOMAIN ); ?></td>
<td><?php echo esc_html( $this->format_currency( $statement['power_cost_total'] ) ); ?></td>
</tr>
<tr>
<td><strong><?php echo esc_html__( 'Gesamtbetrag', KGVVM_TEXT_DOMAIN ); ?></strong></td>
<td><strong><?php echo esc_html( $this->format_currency( $statement['grand_total'] ) ); ?></strong></td>
</tr>
</table>
<h2><?php echo esc_html__( 'Grundkosten anteilig', KGVVM_TEXT_DOMAIN ); ?></h2>
<?php if ( empty( $statement['fixed_items'] ) ) : ?>
<p><?php echo esc_html__( 'Keine allgemeinen Kostenposten vorhanden.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php else : ?>
<table cellpadding="5" cellspacing="0" border="1">
<tr>
<th><strong><?php echo esc_html__( 'Kostenposten', KGVVM_TEXT_DOMAIN ); ?></strong></th>
<th><strong><?php echo esc_html__( 'Gesamtkosten', KGVVM_TEXT_DOMAIN ); ?></strong></th>
<th><strong><?php echo esc_html__( 'Anteil', KGVVM_TEXT_DOMAIN ); ?></strong></th>
</tr>
<?php foreach ( $statement['fixed_items'] as $item ) : ?>
<tr>
<td><?php echo esc_html( $item['name'] . ' ' . sprintf( __( '%1$s × %2$s zu je %3$s', KGVVM_TEXT_DOMAIN ), $item['distribution_label'], number_format_i18n( (int) $item['units'], 0 ), $this->format_currency( $item['unit_amount'] ) ) ); ?></td>
<td><?php echo esc_html( $this->format_currency( $item['total'] ) ); ?></td>
<td><?php echo esc_html( $this->format_currency( $item['share'] ) ); ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
<h2><?php echo esc_html__( 'Wasserzähler', KGVVM_TEXT_DOMAIN ); ?></h2>
<?php echo $this->build_statement_meter_detail_table( $this->get_statement_rows_by_meter_type( $statement['utility_rows'], 'water' ), 'water', true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<h2><?php echo esc_html__( 'Stromzähler', KGVVM_TEXT_DOMAIN ); ?></h2>
<?php echo $this->build_statement_meter_detail_table( $this->get_statement_rows_by_meter_type( $statement['utility_rows'], 'power' ), 'power', true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php if ( ! empty( $settings['pdf_footer_text'] ) ) : ?>
<p style="margin-top:16px;"><?php echo nl2br( esc_html( $settings['pdf_footer_text'] ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
<?php endif; ?>
<?php
return (string) ob_get_clean();
}
/**
* Render section admin page.
*
* @return void
*/
public function render_sections_page() {
$this->require_cap( 'edit_sparten' );
$view = isset( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : 'list';
if ( 'form' === $view ) {
$this->render_section_form();
return;
}
$rows = $this->sections->search( $_GET );
?>
<div class='wrap'>
<h1 class='wp-heading-inline'><?php echo esc_html__( 'Sparten', KGVVM_TEXT_DOMAIN ); ?></h1>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-sparten', array( 'view' => 'form' ) ) ); ?>' class='page-title-action'><?php echo esc_html__( 'Neue Sparte', KGVVM_TEXT_DOMAIN ); ?></a>
<?php $this->render_notice(); ?>
<form method='get' class='kgvvm-toolbar'>
<input type='hidden' name='page' value='kgvvm-sparten' />
<div class='kgvvm-filters'>
<input type='search' name='s' value='<?php echo esc_attr( isset( $_GET['s'] ) ? wp_unslash( $_GET['s'] ) : '' ); ?>' placeholder='<?php echo esc_attr__( 'Sparte suchen …', KGVVM_TEXT_DOMAIN ); ?>' />
<select name='status'>
<option value=''><?php echo esc_html__( 'Alle Status', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='active' <?php selected( isset( $_GET['status'] ) ? wp_unslash( $_GET['status'] ) : '', 'active' ); ?>><?php echo esc_html__( 'Aktiv', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='inactive' <?php selected( isset( $_GET['status'] ) ? wp_unslash( $_GET['status'] ) : '', 'inactive' ); ?>><?php echo esc_html__( 'Inaktiv', KGVVM_TEXT_DOMAIN ); ?></option>
</select>
<button class='button'><?php echo esc_html__( 'Filtern', KGVVM_TEXT_DOMAIN ); ?></button>
</div>
</form>
<table class='widefat striped'>
<thead>
<tr>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-sparten', 'name' ) ); ?>'><?php echo esc_html__( 'Name', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><?php echo esc_html__( 'Beschreibung', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Hauptzähler', KGVVM_TEXT_DOMAIN ); ?></th>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-sparten', 'status' ) ); ?>'><?php echo esc_html__( 'Status', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><?php echo esc_html__( 'Aktionen', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php if ( empty( $rows ) ) : ?>
<tr><td colspan='5'><?php echo esc_html__( 'Noch keine Sparten vorhanden.', KGVVM_TEXT_DOMAIN ); ?></td></tr>
<?php else : ?>
<?php foreach ( $rows as $row ) : ?>
<?php $main_meters = $this->meters->get_main_for_section( $row->id ); ?>
<tr>
<td><strong><?php echo esc_html( $row->name ); ?></strong></td>
<td><?php echo esc_html( $row->description ? $row->description : '—' ); ?></td>
<td>
<?php if ( empty( $main_meters ) ) : ?>
<?php else : ?>
<?php echo esc_html( implode( ', ', array_map( array( $this, 'format_meter_short_label' ), $main_meters ) ) ); ?>
<?php endif; ?>
</td>
<td><?php echo $this->status_badge( 'active' === $row->status ? __( 'Aktiv', KGVVM_TEXT_DOMAIN ) : __( 'Inaktiv', KGVVM_TEXT_DOMAIN ), 'active' === $row->status ? 'green' : 'gray' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
<td>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-sparten', array( 'view' => 'form', 'id' => $row->id ) ) ); ?>'><?php echo esc_html__( 'Bearbeiten', KGVVM_TEXT_DOMAIN ); ?></a>
|
<a href='<?php echo esc_url( wp_nonce_url( $this->admin_url( 'kgvvm-sparten', array( 'kgvvm_action' => 'delete_section', 'id' => $row->id ) ), 'kgvvm_delete_section_' . $row->id ) ); ?>' onclick='return confirm("<?php echo esc_js( __( 'Sparte wirklich löschen?', KGVVM_TEXT_DOMAIN ) ); ?>");'><?php echo esc_html__( 'Löschen', KGVVM_TEXT_DOMAIN ); ?></a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
}
/**
* Render section form.
*
* @return void
*/
private function render_section_form() {
$id = absint( isset( $_GET['id'] ) ? $_GET['id'] : 0 );
$section = $id ? $this->sections->find( $id ) : null;
$selected_main_meters = $id ? $this->meters->get_main_for_section( $id ) : array();
$main_meter_options = $id ? $this->meters->get_available_main_for_section( $id ) : array();
$selected_main_meter_ids = $id ? wp_list_pluck( $selected_main_meters, 'id' ) : array();
$today = current_time( 'Y-m-d' );
?>
<div class='wrap'>
<h1><?php echo esc_html( $id ? __( 'Sparte bearbeiten', KGVVM_TEXT_DOMAIN ) : __( 'Neue Sparte', KGVVM_TEXT_DOMAIN ) ); ?></h1>
<?php $this->render_notice(); ?>
<form method='post'>
<?php wp_nonce_field( 'kgvvm_save_section' ); ?>
<input type='hidden' name='kgvvm_action' value='save_section' />
<input type='hidden' name='id' value='<?php echo esc_attr( $id ); ?>' />
<table class='form-table kgvvm-form-table'>
<tr>
<th scope='row'><label for='kgvvm-section-name'><?php echo esc_html__( 'Name', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='name' id='kgvvm-section-name' type='text' class='regular-text' required value='<?php echo esc_attr( $section ? $section->name : '' ); ?>' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-section-description'><?php echo esc_html__( 'Beschreibung', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><textarea name='description' id='kgvvm-section-description' rows='5' class='large-text'><?php echo esc_textarea( $section ? $section->description : '' ); ?></textarea></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-section-main-meters'><?php echo esc_html__( 'Hauptzähler', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<?php if ( ! $id ) : ?>
<p class='kgvvm-help'><?php echo esc_html__( 'Bitte die Sparte zuerst speichern. Danach können mehrere Hauptzähler zugeordnet werden.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php elseif ( empty( $main_meter_options ) ) : ?>
<p class='kgvvm-help'><?php echo esc_html__( 'Für diese Sparte sind aktuell keine freien Zähler verfügbar. Bitte zuerst unter „Zähler“ passende Zähler anlegen.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php else : ?>
<select name='main_meter_ids[]' id='kgvvm-section-main-meters' multiple class='kgvvm-multiselect'>
<?php foreach ( $main_meter_options as $meter ) : ?>
<option value='<?php echo esc_attr( $meter->id ); ?>' <?php selected( in_array( (int) $meter->id, $selected_main_meter_ids, true ), true ); ?>><?php echo esc_html( $this->format_meter_short_label( $meter ) ); ?></option>
<?php endforeach; ?>
</select>
<p class='kgvvm-help'><?php echo esc_html__( 'Hier können mehrere freie Zähler dieser Sparte als Hauptzähler markiert werden.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php endif; ?>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-section-status'><?php echo esc_html__( 'Status', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<select name='status' id='kgvvm-section-status'>
<option value='active' <?php selected( $section ? $section->status : 'active', 'active' ); ?>><?php echo esc_html__( 'Aktiv', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='inactive' <?php selected( $section ? $section->status : 'active', 'inactive' ); ?>><?php echo esc_html__( 'Inaktiv', KGVVM_TEXT_DOMAIN ); ?></option>
</select>
</td>
</tr>
</table>
<?php submit_button( $id ? __( 'Sparte aktualisieren', KGVVM_TEXT_DOMAIN ) : __( 'Sparte anlegen', KGVVM_TEXT_DOMAIN ) ); ?>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-sparten' ) ); ?>' class='button-secondary'><?php echo esc_html__( 'Zurück', KGVVM_TEXT_DOMAIN ); ?></a>
</form>
<?php if ( $id && ! empty( $selected_main_meters ) ) : ?>
<div class='kgvvm-card' style='margin-top:24px;'>
<h2><?php echo esc_html__( 'Ablesungen der Hauptzähler', KGVVM_TEXT_DOMAIN ); ?></h2>
<p class='kgvvm-help'><?php echo esc_html__( 'Hier können für die Hauptzähler dieser Sparte direkt neue Ablesungen erfasst werden.', KGVVM_TEXT_DOMAIN ); ?></p>
<div class='kgvvm-grid'>
<?php foreach ( $selected_main_meters as $main_meter ) : ?>
<?php
$latest_main = $this->readings->get_latest_for_meter( $main_meter->id );
$recent_main = $this->readings->get_all_for_meter( $main_meter->id, 5 );
$monthly_summary = $this->readings->get_monthly_summary_for_meter( $main_meter->id );
$step = 'water' === $main_meter->type ? '0.01' : '1';
?>
<div class='kgvvm-card'>
<h3><?php echo esc_html( $this->format_meter_short_label( $main_meter ) ); ?></h3>
<p>
<strong><?php echo esc_html__( 'Letzte Ablesung:', KGVVM_TEXT_DOMAIN ); ?></strong>
<?php if ( $latest_main ) : ?>
<?php echo esc_html( $this->format_meter_value_with_unit( $latest_main->reading_value, $main_meter->type ) . ' am ' . wp_date( 'd.m.Y', strtotime( $latest_main->reading_date ) ) ); ?>
<?php else : ?>
<?php echo esc_html__( 'Noch keine Ablesung vorhanden', KGVVM_TEXT_DOMAIN ); ?>
<?php endif; ?>
</p>
<form method='post'>
<?php wp_nonce_field( 'kgvvm_save_meter_reading' ); ?>
<input type='hidden' name='kgvvm_action' value='save_meter_reading' />
<input type='hidden' name='return_page' value='kgvvm-sparten' />
<input type='hidden' name='return_view' value='form' />
<input type='hidden' name='return_id' value='<?php echo esc_attr( $id ); ?>' />
<input type='hidden' name='parcel_id' value='0' />
<input type='hidden' name='meter_id' value='<?php echo esc_attr( $main_meter->id ); ?>' />
<p>
<label><?php echo esc_html__( 'Ablesedatum', KGVVM_TEXT_DOMAIN ); ?><br />
<input type='date' name='reading_date' required value='<?php echo esc_attr( $today ); ?>' />
</label>
</p>
<p>
<label><?php echo esc_html__( 'Zählerstand', KGVVM_TEXT_DOMAIN ); ?> (<?php echo esc_html( $this->meter_unit_label( $main_meter->type ) ); ?>)<br />
<input type='number' min='0' step='<?php echo esc_attr( $step ); ?>' name='reading_value' required />
</label>
</p>
<p>
<label><?php echo esc_html__( 'Notiz', KGVVM_TEXT_DOMAIN ); ?><br />
<textarea name='note' rows='3' class='large-text'></textarea>
</label>
</p>
<?php submit_button( __( 'Ablesung speichern', KGVVM_TEXT_DOMAIN ), 'secondary', 'submit', false ); ?>
</form>
<?php if ( ! empty( $recent_main ) ) : ?>
<table class='widefat striped' style='margin-top:12px;'>
<thead>
<tr>
<th><?php echo esc_html__( 'Datum', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Stand', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Flag', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $recent_main as $reading ) : ?>
<tr>
<td><?php echo esc_html( wp_date( 'd.m.Y', strtotime( $reading->reading_date ) ) ); ?></td>
<td><?php echo esc_html( $this->format_meter_value_with_unit( $reading->reading_value, $main_meter->type ) ); ?></td>
<td><?php echo wp_kses_post( $this->reading_flag_badge( $reading ) ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<?php if ( ! empty( $monthly_summary ) ) : ?>
<h4 style='margin-top:16px;'><?php echo esc_html__( 'Monatsübersicht', KGVVM_TEXT_DOMAIN ); ?></h4>
<table class='widefat striped'>
<thead>
<tr>
<th><?php echo esc_html__( 'Monat', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Von', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Bis', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Verbrauch', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Ablesungen', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $monthly_summary as $month_row ) : ?>
<tr>
<td><?php echo esc_html( wp_date( 'm.Y', strtotime( $month_row['month'] . '-01' ) ) ); ?></td>
<td><?php echo esc_html( null !== $month_row['from_value'] ? $this->format_meter_value_with_unit( $month_row['from_value'], $main_meter->type ) : '—' ); ?></td>
<td><?php echo esc_html( $this->format_meter_value_with_unit( $month_row['to_value'], $main_meter->type ) ); ?></td>
<td><?php echo esc_html( $this->format_meter_value_with_unit( $month_row['consumption'], $main_meter->type ) ); ?></td>
<td><?php echo esc_html( (string) $month_row['readings'] ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
<?php
}
/**
* Render parcel page.
*
* @return void
*/
public function render_parcels_page() {
$this->require_cap( 'edit_parzellen' );
$view = isset( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : 'list';
if ( 'form' === $view ) {
$this->render_parcel_form();
return;
}
$paged = max( 1, absint( isset( $_GET['paged'] ) ? $_GET['paged'] : 1 ) );
$per_page = 50;
$total_items = $this->parcels->count_filtered( $_GET );
$total_pages = max( 1, (int) ceil( $total_items / $per_page ) );
$rows = $this->parcels->search( array_merge( $_GET, array( 'limit' => $per_page, 'paged' => $paged ) ) );
$sections = $this->sections->all_for_options();
?>
<div class='wrap'>
<h1 class='wp-heading-inline'><?php echo esc_html__( 'Parzellen', KGVVM_TEXT_DOMAIN ); ?></h1>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-parzellen', array( 'view' => 'form' ) ) ); ?>' class='page-title-action'><?php echo esc_html__( 'Neue Parzelle', KGVVM_TEXT_DOMAIN ); ?></a>
<?php $this->render_notice(); ?>
<form method='get' class='kgvvm-toolbar'>
<input type='hidden' name='page' value='kgvvm-parzellen' />
<div class='kgvvm-filters'>
<input type='search' name='s' value='<?php echo esc_attr( isset( $_GET['s'] ) ? wp_unslash( $_GET['s'] ) : '' ); ?>' placeholder='<?php echo esc_attr__( 'Parzelle suchen …', KGVVM_TEXT_DOMAIN ); ?>' />
<select name='section_id'>
<option value=''><?php echo esc_html__( 'Alle Sparten', KGVVM_TEXT_DOMAIN ); ?></option>
<?php foreach ( $sections as $section ) : ?>
<option value='<?php echo esc_attr( $section->id ); ?>' <?php selected( isset( $_GET['section_id'] ) ? absint( $_GET['section_id'] ) : 0, $section->id ); ?>><?php echo esc_html( $section->name ); ?></option>
<?php endforeach; ?>
</select>
<select name='status'>
<option value=''><?php echo esc_html__( 'Alle Status', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='free' <?php selected( isset( $_GET['status'] ) ? wp_unslash( $_GET['status'] ) : '', 'free' ); ?>><?php echo esc_html__( 'Frei', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='assigned' <?php selected( isset( $_GET['status'] ) ? wp_unslash( $_GET['status'] ) : '', 'assigned' ); ?>><?php echo esc_html__( 'Vergeben', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='reserved' <?php selected( isset( $_GET['status'] ) ? wp_unslash( $_GET['status'] ) : '', 'reserved' ); ?>><?php echo esc_html__( 'Reserviert', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='inactive' <?php selected( isset( $_GET['status'] ) ? wp_unslash( $_GET['status'] ) : '', 'inactive' ); ?>><?php echo esc_html__( 'Inaktiv', KGVVM_TEXT_DOMAIN ); ?></option>
</select>
<button class='button'><?php echo esc_html__( 'Filtern', KGVVM_TEXT_DOMAIN ); ?></button>
</div>
</form>
<div class='kgvvm-toolbar'>
<p class='kgvvm-help'>
<?php echo esc_html( sprintf( __( '%1$d Parzellen gesamt · Seite %2$d von %3$d', KGVVM_TEXT_DOMAIN ), (int) $total_items, (int) $paged, (int) $total_pages ) ); ?>
</p>
<?php if ( $total_items > $per_page ) : ?>
<div class='tablenav-pages'>
<?php
echo wp_kses_post(
paginate_links(
array(
'base' => add_query_arg( 'paged', '%#%' ),
'format' => '',
'current' => $paged,
'total' => $total_pages,
'prev_text' => '«',
'next_text' => '»',
)
)
);
?>
</div>
<?php endif; ?>
</div>
<table class='widefat striped'>
<thead>
<tr>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-parzellen', 'label' ) ); ?>'><?php echo esc_html__( 'Parzelle', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><?php echo esc_html__( 'Sparte', KGVVM_TEXT_DOMAIN ); ?></th>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-parzellen', 'area' ) ); ?>'><?php echo esc_html__( 'Fläche', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-parzellen', 'annual_rent' ) ); ?>'><?php echo esc_html__( 'Pacht / Jahr', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><?php echo esc_html__( 'Wasserzähler', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Stromzähler', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Mitglieder', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Pächter', KGVVM_TEXT_DOMAIN ); ?></th>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-parzellen', 'status' ) ); ?>'><?php echo esc_html__( 'Status', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><?php echo esc_html__( 'Aktionen', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php if ( empty( $rows ) ) : ?>
<tr><td colspan='10'><?php echo esc_html__( 'Noch keine Parzellen vorhanden.', KGVVM_TEXT_DOMAIN ); ?></td></tr>
<?php else : ?>
<?php foreach ( $rows as $row ) : ?>
<tr>
<td><strong><?php echo esc_html( $row->label ); ?></strong></td>
<td><?php echo esc_html( $row->section_name ); ?></td>
<td><?php echo null !== $row->area ? esc_html( number_format_i18n( (float) $row->area, 2 ) . ' m²' ) : '—'; ?></td>
<td><?php echo isset( $row->annual_rent ) && null !== $row->annual_rent ? esc_html( $this->format_currency( $row->annual_rent ) ) : '—'; ?></td>
<td><?php echo esc_html( $row->water_meter_number ? $row->water_meter_number : '—' ); ?></td>
<td><?php echo esc_html( $row->power_meter_number ? $row->power_meter_number : '—' ); ?></td>
<td><?php echo esc_html( (string) $row->member_count ); ?></td>
<td><?php echo esc_html( (string) $row->tenant_count ); ?></td>
<td><?php echo $this->status_badge( $this->parcel_status_label( $row->status ), 'inactive' === $row->status ? 'gray' : ( 'reserved' === $row->status ? 'orange' : 'green' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
<td>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-parzellen', array( 'view' => 'form', 'id' => $row->id ) ) ); ?>'><?php echo esc_html__( 'Bearbeiten', KGVVM_TEXT_DOMAIN ); ?></a>
|
<a href='<?php echo esc_url( wp_nonce_url( $this->admin_url( 'kgvvm-parzellen', array( 'kgvvm_action' => 'delete_parcel', 'id' => $row->id ) ), 'kgvvm_delete_parcel_' . $row->id ) ); ?>' onclick='return confirm("<?php echo esc_js( __( 'Parzelle wirklich löschen?', KGVVM_TEXT_DOMAIN ) ); ?>");'><?php echo esc_html__( 'Löschen', KGVVM_TEXT_DOMAIN ); ?></a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<?php if ( $total_items > $per_page ) : ?>
<div class='kgvvm-toolbar'>
<div class='tablenav-pages'>
<?php
echo wp_kses_post(
paginate_links(
array(
'base' => add_query_arg( 'paged', '%#%' ),
'format' => '',
'current' => $paged,
'total' => $total_pages,
'prev_text' => '«',
'next_text' => '»',
)
)
);
?>
</div>
</div>
<?php endif; ?>
</div>
<?php
}
/**
* Render parcel form.
*
* @return void
*/
private function render_parcel_form() {
$id = absint( isset( $_GET['id'] ) ? $_GET['id'] : 0 );
$parcel = $id ? $this->parcels->find( $id ) : null;
$sections = $this->sections->all_for_options( true );
$selected_section = $parcel ? absint( $parcel->section_id ) : 0;
$water_selected = $parcel ? $this->meters->get_assigned_to_parcel( $id, 'water' ) : null;
$power_selected = $parcel ? $this->meters->get_assigned_to_parcel( $id, 'power' ) : null;
$water_meters = $this->meters->get_free_by_type( 'water', $selected_section, $id );
$power_meters = $this->meters->get_free_by_type( 'power', $selected_section, $id );
$member_users = $this->assignments->get_member_users();
$member_ids = $id ? $this->assignments->get_member_ids_for_parcel( $id ) : array();
$tenant_ids = $id ? $this->assignments->get_tenant_ids_for_parcel( $id ) : array();
$tenants = $this->tenants->all_active();
?>
<div class='wrap'>
<h1><?php echo esc_html( $id ? __( 'Parzelle bearbeiten', KGVVM_TEXT_DOMAIN ) : __( 'Neue Parzelle', KGVVM_TEXT_DOMAIN ) ); ?></h1>
<?php $this->render_notice(); ?>
<?php if ( empty( $sections ) ) : ?>
<div class='notice notice-warning'><p><?php echo esc_html__( 'Bitte zuerst mindestens eine aktive Sparte anlegen.', KGVVM_TEXT_DOMAIN ); ?></p></div>
<?php endif; ?>
<form method='post'>
<?php wp_nonce_field( 'kgvvm_save_parcel' ); ?>
<input type='hidden' name='kgvvm_action' value='save_parcel' />
<input type='hidden' name='id' value='<?php echo esc_attr( $id ); ?>' />
<table class='form-table kgvvm-form-table'>
<tr>
<th scope='row'><label for='kgvvm-parcel-label'><?php echo esc_html__( 'Parzellennummer / Bezeichnung', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='label' id='kgvvm-parcel-label' type='text' class='regular-text' required value='<?php echo esc_attr( $parcel ? $parcel->label : '' ); ?>' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-parcel-section'><?php echo esc_html__( 'Sparte', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<select name='section_id' id='kgvvm-parcel-section' required>
<option value=''><?php echo esc_html__( 'Bitte auswählen', KGVVM_TEXT_DOMAIN ); ?></option>
<?php foreach ( $sections as $section ) : ?>
<option value='<?php echo esc_attr( $section->id ); ?>' <?php selected( $selected_section, $section->id ); ?>><?php echo esc_html( $section->name ); ?></option>
<?php endforeach; ?>
</select>
<p class='kgvvm-help'><?php echo esc_html__( 'Freie Wasser- und Stromzähler müssen zur gleichen Sparte gehören.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-parcel-area'><?php echo esc_html__( 'Fläche', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='area' id='kgvvm-parcel-area' type='number' min='0' step='0.01' value='<?php echo esc_attr( $parcel && null !== $parcel->area ? $parcel->area : '' ); ?>' /> <span class='kgvvm-help'>m²</span></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-parcel-rent'><?php echo esc_html__( 'Pacht pro Jahr', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<input name='annual_rent' id='kgvvm-parcel-rent' type='number' min='0' step='0.01' value='<?php echo esc_attr( $parcel && isset( $parcel->annual_rent ) && null !== $parcel->annual_rent ? $parcel->annual_rent : '' ); ?>' />
<span class='kgvvm-help'>€</span>
<p class='kgvvm-help'><?php echo esc_html__( 'Optionaler jährlicher Pachtbetrag für diese Parzelle.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-parcel-status'><?php echo esc_html__( 'Status', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<select name='status' id='kgvvm-parcel-status'>
<option value='free' <?php selected( $parcel ? $parcel->status : 'free', 'free' ); ?>><?php echo esc_html__( 'Frei', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='assigned' <?php selected( $parcel ? $parcel->status : 'free', 'assigned' ); ?>><?php echo esc_html__( 'Vergeben', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='reserved' <?php selected( $parcel ? $parcel->status : 'free', 'reserved' ); ?>><?php echo esc_html__( 'Reserviert', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='inactive' <?php selected( $parcel ? $parcel->status : 'free', 'inactive' ); ?>><?php echo esc_html__( 'Inaktiv', KGVVM_TEXT_DOMAIN ); ?></option>
</select>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-water-meter'><?php echo esc_html__( 'Wasserzähler', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<select name='water_meter_id' id='kgvvm-water-meter' required>
<option value=''><?php echo esc_html__( 'Bitte auswählen', KGVVM_TEXT_DOMAIN ); ?></option>
<?php foreach ( $water_meters as $meter ) : ?>
<option value='<?php echo esc_attr( $meter->id ); ?>' <?php selected( $water_selected ? $water_selected->id : 0, $meter->id ); ?>><?php echo esc_html( $meter->meter_number . ' (Sparte #' . $meter->section_id . ')' ); ?></option>
<?php endforeach; ?>
</select>
<p class='kgvvm-help'><?php echo esc_html__( 'Es werden nur freie oder bereits dieser Parzelle zugeordnete Wasserzähler angeboten.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-power-meter'><?php echo esc_html__( 'Stromzähler', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<select name='power_meter_id' id='kgvvm-power-meter' required>
<option value=''><?php echo esc_html__( 'Bitte auswählen', KGVVM_TEXT_DOMAIN ); ?></option>
<?php foreach ( $power_meters as $meter ) : ?>
<option value='<?php echo esc_attr( $meter->id ); ?>' <?php selected( $power_selected ? $power_selected->id : 0, $meter->id ); ?>><?php echo esc_html( $meter->meter_number . ' (Sparte #' . $meter->section_id . ')' ); ?></option>
<?php endforeach; ?>
</select>
<p class='kgvvm-help'><?php echo esc_html__( 'Es werden nur freie oder bereits dieser Parzelle zugeordnete Stromzähler angeboten.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-members'><?php echo esc_html__( 'Mitglieder (WordPress-User)', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<select name='member_ids[]' id='kgvvm-members' multiple class='kgvvm-multiselect'>
<?php foreach ( $member_users as $member ) : ?>
<option value='<?php echo esc_attr( $member->ID ); ?>' <?php selected( in_array( (int) $member->ID, $member_ids, true ), true ); ?>><?php echo esc_html( $member->display_name . ' (' . $member->user_email . ')' ); ?></option>
<?php endforeach; ?>
</select>
<p class='kgvvm-help'><?php echo esc_html__( 'Rolle "Mitglied" wird bei der Plugin-Aktivierung automatisch angelegt.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-tenants'><?php echo esc_html__( 'Pächter', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<select name='tenant_ids[]' id='kgvvm-tenants' multiple class='kgvvm-multiselect'>
<?php foreach ( $tenants as $tenant ) : ?>
<option value='<?php echo esc_attr( $tenant->id ); ?>' <?php selected( in_array( (int) $tenant->id, $tenant_ids, true ), true ); ?>><?php echo esc_html( $tenant->last_name . ', ' . $tenant->first_name ); ?></option>
<?php endforeach; ?>
</select>
<p class='kgvvm-help'><?php echo esc_html__( 'Mehrfachauswahl möglich, um mehrere Pächter einer Parzelle zuzuordnen.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-parcel-note'><?php echo esc_html__( 'Bemerkung', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><textarea name='note' id='kgvvm-parcel-note' rows='5' class='large-text'><?php echo esc_textarea( $parcel ? $parcel->note : '' ); ?></textarea></td>
</tr>
</table>
<?php submit_button( $id ? __( 'Parzelle aktualisieren', KGVVM_TEXT_DOMAIN ) : __( 'Parzelle anlegen', KGVVM_TEXT_DOMAIN ) ); ?>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-parzellen' ) ); ?>' class='button-secondary'><?php echo esc_html__( 'Zurück', KGVVM_TEXT_DOMAIN ); ?></a>
</form>
</div>
<?php
}
/**
* Render meter page.
*
* @return void
*/
public function render_meters_page() {
$this->require_cap( 'edit_zaehler' );
$view = isset( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : 'list';
if ( 'form' === $view ) {
$this->render_meter_form();
return;
}
if ( 'swap' === $view ) {
$this->render_meter_swap_form();
return;
}
$rows = $this->meters->search( $_GET );
$sections = $this->sections->all_for_options();
?>
<div class='wrap'>
<h1 class='wp-heading-inline'><?php echo esc_html__( 'Zähler', KGVVM_TEXT_DOMAIN ); ?></h1>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-zaehler', array( 'view' => 'form' ) ) ); ?>' class='page-title-action'><?php echo esc_html__( 'Neuer Zähler', KGVVM_TEXT_DOMAIN ); ?></a>
<?php $this->render_notice(); ?>
<form method='get' class='kgvvm-toolbar'>
<input type='hidden' name='page' value='kgvvm-zaehler' />
<div class='kgvvm-filters'>
<input type='search' name='s' value='<?php echo esc_attr( isset( $_GET['s'] ) ? wp_unslash( $_GET['s'] ) : '' ); ?>' placeholder='<?php echo esc_attr__( 'Zähler suchen …', KGVVM_TEXT_DOMAIN ); ?>' />
<select name='type'>
<option value=''><?php echo esc_html__( 'Alle Typen', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='water' <?php selected( isset( $_GET['type'] ) ? wp_unslash( $_GET['type'] ) : '', 'water' ); ?>><?php echo esc_html__( 'Wasser', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='power' <?php selected( isset( $_GET['type'] ) ? wp_unslash( $_GET['type'] ) : '', 'power' ); ?>><?php echo esc_html__( 'Strom', KGVVM_TEXT_DOMAIN ); ?></option>
</select>
<select name='section_id'>
<option value=''><?php echo esc_html__( 'Alle Sparten', KGVVM_TEXT_DOMAIN ); ?></option>
<?php foreach ( $sections as $section ) : ?>
<option value='<?php echo esc_attr( $section->id ); ?>' <?php selected( isset( $_GET['section_id'] ) ? absint( $_GET['section_id'] ) : 0, $section->id ); ?>><?php echo esc_html( $section->name ); ?></option>
<?php endforeach; ?>
</select>
<select name='assignment'>
<option value=''><?php echo esc_html__( 'Alle Zuordnungen', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='free' <?php selected( isset( $_GET['assignment'] ) ? wp_unslash( $_GET['assignment'] ) : '', 'free' ); ?>><?php echo esc_html__( 'Frei', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='assigned' <?php selected( isset( $_GET['assignment'] ) ? wp_unslash( $_GET['assignment'] ) : '', 'assigned' ); ?>><?php echo esc_html__( 'Zugeordnet', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='main' <?php selected( isset( $_GET['assignment'] ) ? wp_unslash( $_GET['assignment'] ) : '', 'main' ); ?>><?php echo esc_html__( 'Hauptzähler', KGVVM_TEXT_DOMAIN ); ?></option>
</select>
<button class='button'><?php echo esc_html__( 'Filtern', KGVVM_TEXT_DOMAIN ); ?></button>
</div>
</form>
<table class='widefat striped'>
<thead>
<tr>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-zaehler', 'meter_number' ) ); ?>'><?php echo esc_html__( 'Zählernummer', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-zaehler', 'type' ) ); ?>'><?php echo esc_html__( 'Typ', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><?php echo esc_html__( 'Sparte', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Parzelle', KGVVM_TEXT_DOMAIN ); ?></th>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-zaehler', 'installed_at' ) ); ?>'><?php echo esc_html__( 'Einbaudatum', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-zaehler', 'calibration_year' ) ); ?>'><?php echo esc_html__( 'Eichjahr', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><?php echo esc_html__( 'Status', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Aktionen', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php if ( empty( $rows ) ) : ?>
<tr><td colspan='8'><?php echo esc_html__( 'Noch keine Zähler vorhanden.', KGVVM_TEXT_DOMAIN ); ?></td></tr>
<?php else : ?>
<?php foreach ( $rows as $row ) : ?>
<?php $latest_swap_reading = $this->readings->get_latest_for_meter( $row->id ); ?>
<tr>
<td><strong><?php echo esc_html( $row->meter_number ); ?></strong></td>
<td><?php echo esc_html( $this->meter_type_label( $row->type ) ); ?></td>
<td><?php echo esc_html( $row->section_name ? $row->section_name : '—' ); ?></td>
<td><?php echo esc_html( $row->parcel_label ? $row->parcel_label : ( ! empty( $row->is_main_meter ) ? __( 'Hauptzähler der Sparte', KGVVM_TEXT_DOMAIN ) : __( 'frei', KGVVM_TEXT_DOMAIN ) ) ); ?></td>
<td><?php echo esc_html( $row->installed_at ? wp_date( 'd.m.Y', strtotime( $row->installed_at ) ) : '—' ); ?></td>
<td><?php echo esc_html( ! empty( $row->calibration_year ) ? (string) $row->calibration_year : '—' ); ?></td>
<td><?php echo $this->status_badge( (int) $row->is_active === 1 ? ( ! empty( $row->parcel_id ) ? __( 'Zugeordnet', KGVVM_TEXT_DOMAIN ) : ( ! empty( $row->is_main_meter ) ? __( 'Hauptzähler', KGVVM_TEXT_DOMAIN ) : __( 'Frei', KGVVM_TEXT_DOMAIN ) ) ) : __( 'Inaktiv', KGVVM_TEXT_DOMAIN ), (int) $row->is_active === 1 ? ( ! empty( $row->parcel_id ) ? 'orange' : ( ! empty( $row->is_main_meter ) ? 'blue' : 'green' ) ) : 'gray' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
<td>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-zaehler', array( 'view' => 'form', 'id' => $row->id ) ) ); ?>'><?php echo esc_html__( 'Bearbeiten', KGVVM_TEXT_DOMAIN ); ?></a>
<?php if ( (int) $row->is_active === 1 ) : ?>
|
<button
type='button'
class='button-link kgvvm-open-swap-modal'
data-meter-id='<?php echo esc_attr( $row->id ); ?>'
data-meter-number='<?php echo esc_attr( $row->meter_number ); ?>'
data-meter-type='<?php echo esc_attr( $this->meter_type_label( $row->type ) ); ?>'
data-parcel-label='<?php echo esc_attr( $row->parcel_label ? $row->parcel_label : ( ! empty( $row->is_main_meter ) ? __( 'Hauptzähler der Sparte', KGVVM_TEXT_DOMAIN ) : __( 'frei', KGVVM_TEXT_DOMAIN ) ) ); ?>'
data-is-assigned='<?php echo esc_attr( ! empty( $row->parcel_id ) ? '1' : '0' ); ?>'
data-last-reading='<?php echo esc_attr( $latest_swap_reading ? (string) $latest_swap_reading->reading_value : '' ); ?>'
data-last-reading-display='<?php echo esc_attr( $latest_swap_reading ? $this->format_meter_value( $latest_swap_reading->reading_value, $row->type ) : '' ); ?>'
data-last-date='<?php echo esc_attr( $latest_swap_reading ? wp_date( 'd.m.Y', strtotime( $latest_swap_reading->reading_date ) ) : '' ); ?>'
data-unit='<?php echo esc_attr( $this->meter_unit_label( $row->type ) ); ?>'
><?php echo esc_html__( 'Tauschen', KGVVM_TEXT_DOMAIN ); ?></button>
<?php endif; ?>
|
<a href='<?php echo esc_url( wp_nonce_url( $this->admin_url( 'kgvvm-zaehler', array( 'kgvvm_action' => 'delete_meter', 'id' => $row->id ) ), 'kgvvm_delete_meter_' . $row->id ) ); ?>' onclick='return confirm("<?php echo esc_js( __( 'Zähler wirklich löschen?', KGVVM_TEXT_DOMAIN ) ); ?>");'><?php echo esc_html__( 'Löschen', KGVVM_TEXT_DOMAIN ); ?></a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<?php $this->render_meter_swap_modal(); ?>
</div>
<?php
}
/**
* Render the swap modal directly inside the meter overview.
*
* @return void
*/
private function render_meter_swap_modal() {
$today = current_time( 'Y-m-d' );
?>
<div id='kgvvm-swap-modal' class='kgvvm-modal' hidden>
<div class='kgvvm-modal__backdrop' data-kgvvm-close-modal></div>
<div class='kgvvm-modal__dialog' role='dialog' aria-modal='true' aria-labelledby='kgvvm-swap-modal-title'>
<button type='button' class='kgvvm-modal__close' data-kgvvm-close-modal aria-label='<?php echo esc_attr__( 'Schließen', KGVVM_TEXT_DOMAIN ); ?>'>×</button>
<h2 id='kgvvm-swap-modal-title'><?php echo esc_html__( 'Zähler tauschen', KGVVM_TEXT_DOMAIN ); ?></h2>
<p class='kgvvm-help'><?php echo esc_html__( 'Der Altzähler wird archiviert, die Historie bleibt erhalten und der neue Zähler übernimmt bei Bedarf direkt die Parzellenzuordnung.', KGVVM_TEXT_DOMAIN ); ?></p>
<div class='kgvvm-card kgvvm-modal__summary'>
<p><strong id='kgvvm-swap-current-meter'>—</strong></p>
<p id='kgvvm-swap-current-assignment'>—</p>
<p id='kgvvm-swap-current-reading'>—</p>
</div>
<form method='post'>
<?php wp_nonce_field( 'kgvvm_swap_meter' ); ?>
<input type='hidden' name='kgvvm_action' value='swap_meter' />
<input type='hidden' name='id' id='kgvvm-swap-meter-id' value='' />
<table class='form-table kgvvm-form-table'>
<tr>
<th scope='row'><label for='kgvvm-new-meter-number'><?php echo esc_html__( 'Neue Zählernummer', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='new_meter_number' id='kgvvm-new-meter-number' type='text' class='regular-text' required value='' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-swap-date'><?php echo esc_html__( 'Tauschdatum', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='swap_date' id='kgvvm-swap-date' type='date' required value='<?php echo esc_attr( $today ); ?>' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-new-calibration-year'><?php echo esc_html__( 'Eichjahr neuer Zähler', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<input name='new_calibration_year' id='kgvvm-new-calibration-year' type='number' min='2000' max='2100' step='1' value='' />
<p class='kgvvm-help'><?php echo esc_html__( 'Optional: Jahr der Eichung des neuen Ersatz-Zählers.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tbody id='kgvvm-swap-assigned-fields'>
<tr>
<th scope='row'><label for='kgvvm-old-final-reading'><?php echo esc_html__( 'Endstand alter Zähler', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<input name='old_final_reading' id='kgvvm-old-final-reading' type='number' min='0' step='0.001' value='' />
<p class='kgvvm-help'><?php echo esc_html__( 'Optionaler Abschlussstand am Tauschdatum.', KGVVM_TEXT_DOMAIN ); ?> <span id='kgvvm-swap-final-unit'></span></p>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-new-initial-reading'><?php echo esc_html__( 'Startstand neuer Zähler', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<input name='new_initial_reading' id='kgvvm-new-initial-reading' type='number' min='0' step='0.001' value='0' />
<p class='kgvvm-help'><?php echo esc_html__( 'Dieser Wert wird direkt als erste Ablesung gespeichert.', KGVVM_TEXT_DOMAIN ); ?> <span id='kgvvm-swap-start-unit'></span></p>
</td>
</tr>
</tbody>
<tr>
<th scope='row'><label for='kgvvm-swap-note'><?php echo esc_html__( 'Bemerkung', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<textarea name='swap_note' id='kgvvm-swap-note' rows='4' class='large-text'></textarea>
</td>
</tr>
</table>
<div class='kgvvm-modal__actions'>
<?php submit_button( __( 'Zähler tauschen', KGVVM_TEXT_DOMAIN ), 'primary', 'submit', false ); ?>
<button type='button' class='button-secondary' data-kgvvm-close-modal><?php echo esc_html__( 'Abbrechen', KGVVM_TEXT_DOMAIN ); ?></button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const modal = document.getElementById('kgvvm-swap-modal');
if (!modal) {
return;
}
const form = modal.querySelector('form');
const meterIdInput = document.getElementById('kgvvm-swap-meter-id');
const meterTitle = document.getElementById('kgvvm-swap-current-meter');
const assignment = document.getElementById('kgvvm-swap-current-assignment');
const readingInfo = document.getElementById('kgvvm-swap-current-reading');
const assignedFields = document.getElementById('kgvvm-swap-assigned-fields');
const oldFinalInput = document.getElementById('kgvvm-old-final-reading');
const newInitialInput = document.getElementById('kgvvm-new-initial-reading');
const swapDateInput = document.getElementById('kgvvm-swap-date');
const finalUnit = document.getElementById('kgvvm-swap-final-unit');
const startUnit = document.getElementById('kgvvm-swap-start-unit');
const defaultSwapDate = <?php echo wp_json_encode( $today ); ?>;
const autoOpenMeterId = <?php echo wp_json_encode( isset( $_GET['open_swap'] ) ? absint( $_GET['id'] ) : 0 ); ?>;
function closeModal() {
modal.hidden = true;
modal.classList.remove('is-open');
form.reset();
meterIdInput.value = '';
swapDateInput.value = defaultSwapDate;
assignedFields.hidden = false;
newInitialInput.required = false;
}
document.querySelectorAll('.kgvvm-open-swap-modal').forEach(function (button) {
button.addEventListener('click', function () {
const isAssigned = button.dataset.isAssigned === '1';
meterIdInput.value = button.dataset.meterId || '';
meterTitle.textContent = (button.dataset.meterNumber || '—') + ' (' + (button.dataset.meterType || '') + ')';
assignment.textContent = isAssigned
? 'Parzelle: ' + (button.dataset.parcelLabel || '—')
: <?php echo wp_json_encode( __( 'Der Zähler ist aktuell frei und wird als freier Ersatzzähler angelegt.', KGVVM_TEXT_DOMAIN ) ); ?>;
if (button.dataset.lastReading) {
readingInfo.textContent = 'Letzte Ablesung: ' + (button.dataset.lastReadingDisplay || button.dataset.lastReading) + ' ' + (button.dataset.unit || '') + (button.dataset.lastDate ? ' am ' + button.dataset.lastDate : '');
} else {
readingInfo.textContent = <?php echo wp_json_encode( __( 'Für diesen Zähler liegt noch keine gespeicherte Ablesung vor.', KGVVM_TEXT_DOMAIN ) ); ?>;
}
finalUnit.textContent = button.dataset.unit ? '(' + button.dataset.unit + ')' : '';
startUnit.textContent = button.dataset.unit ? '(' + button.dataset.unit + ')' : '';
oldFinalInput.value = button.dataset.lastReading || '';
newInitialInput.value = '0';
newInitialInput.required = isAssigned;
assignedFields.hidden = !isAssigned;
swapDateInput.value = defaultSwapDate;
modal.hidden = false;
modal.classList.add('is-open');
});
});
modal.querySelectorAll('[data-kgvvm-close-modal]').forEach(function (closeButton) {
closeButton.addEventListener('click', closeModal);
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && modal.classList.contains('is-open')) {
closeModal();
}
});
if (autoOpenMeterId) {
const autoButton = document.querySelector('.kgvvm-open-swap-modal[data-meter-id="' + autoOpenMeterId + '"]');
if (autoButton) {
autoButton.click();
}
}
});
</script>
<?php
}
/**
* Render meter form.
*
* @return void
*/
private function render_meter_form() {
$id = absint( isset( $_GET['id'] ) ? $_GET['id'] : 0 );
$meter = $id ? $this->meters->find( $id ) : null;
$sections = $this->sections->all_for_options();
$history = $id ? $this->readings->get_all_for_meter( $id, 50 ) : array();
?>
<div class='wrap'>
<h1><?php echo esc_html( $id ? __( 'Zähler bearbeiten', KGVVM_TEXT_DOMAIN ) : __( 'Neuer Zähler', KGVVM_TEXT_DOMAIN ) ); ?></h1>
<?php $this->render_notice(); ?>
<form method='post'>
<?php wp_nonce_field( 'kgvvm_save_meter' ); ?>
<input type='hidden' name='kgvvm_action' value='save_meter' />
<input type='hidden' name='id' value='<?php echo esc_attr( $id ); ?>' />
<table class='form-table kgvvm-form-table'>
<tr>
<th scope='row'><label for='kgvvm-meter-type'><?php echo esc_html__( 'Typ', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<select name='type' id='kgvvm-meter-type'>
<option value='water' <?php selected( $meter ? $meter->type : 'water', 'water' ); ?>><?php echo esc_html__( 'Wasser', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='power' <?php selected( $meter ? $meter->type : 'water', 'power' ); ?>><?php echo esc_html__( 'Strom', KGVVM_TEXT_DOMAIN ); ?></option>
</select>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-meter-number'><?php echo esc_html__( 'Zählernummer', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='meter_number' id='kgvvm-meter-number' type='text' class='regular-text' required value='<?php echo esc_attr( $meter ? $meter->meter_number : '' ); ?>' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-meter-section'><?php echo esc_html__( 'Sparte', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<select name='section_id' id='kgvvm-meter-section' required>
<option value=''><?php echo esc_html__( 'Bitte auswählen', KGVVM_TEXT_DOMAIN ); ?></option>
<?php foreach ( $sections as $section ) : ?>
<option value='<?php echo esc_attr( $section->id ); ?>' <?php selected( $meter ? $meter->section_id : 0, $section->id ); ?>><?php echo esc_html( $section->name ); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-meter-installed'><?php echo esc_html__( 'Einbaudatum', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='installed_at' id='kgvvm-meter-installed' type='date' value='<?php echo esc_attr( $meter ? $meter->installed_at : '' ); ?>' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-meter-calibration-year'><?php echo esc_html__( 'Eichjahr', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<input name='calibration_year' id='kgvvm-meter-calibration-year' type='number' min='2000' max='2100' step='1' value='<?php echo esc_attr( $meter && ! empty( $meter->calibration_year ) ? $meter->calibration_year : '' ); ?>' />
<p class='kgvvm-help'><?php echo esc_html__( 'Für getauschte Wasser- und Stromzähler kann hier das Eichjahr hinterlegt werden. Das Jahr reicht aus.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><?php echo esc_html__( 'Aktiv', KGVVM_TEXT_DOMAIN ); ?></th>
<td><label><input type='checkbox' name='is_active' value='1' <?php checked( $meter ? (int) $meter->is_active : 1, 1 ); ?> /> <?php echo esc_html__( 'Zähler ist aktiv', KGVVM_TEXT_DOMAIN ); ?></label></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-meter-note'><?php echo esc_html__( 'Bemerkung', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<textarea name='note' id='kgvvm-meter-note' rows='5' class='large-text'><?php echo esc_textarea( $meter ? $meter->note : '' ); ?></textarea>
<p class='kgvvm-help'><?php echo esc_html__( 'Bei einem Zählerwechsel empfiehlt es sich, den alten Zähler inaktiv zu setzen und den neuen Zähler als eigenen Datensatz anzulegen. So bleibt die Historie nachvollziehbar.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
</table>
<?php submit_button( $id ? __( 'Zähler aktualisieren', KGVVM_TEXT_DOMAIN ) : __( 'Zähler anlegen', KGVVM_TEXT_DOMAIN ) ); ?>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-zaehler' ) ); ?>' class='button-secondary'><?php echo esc_html__( 'Zurück', KGVVM_TEXT_DOMAIN ); ?></a>
</form>
<?php if ( $id ) : ?>
<div class='kgvvm-card' style='margin-top:24px;'>
<h2><?php echo esc_html__( 'Historie dieses Zählers', KGVVM_TEXT_DOMAIN ); ?></h2>
<?php if ( empty( $history ) ) : ?>
<p><?php echo esc_html__( 'Für diesen Zähler wurden noch keine Ablesungen erfasst.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php else : ?>
<table class='widefat striped'>
<thead>
<tr>
<th><?php echo esc_html__( 'Datum', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Parzelle', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Stand', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Flag', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Notiz', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $history as $entry ) : ?>
<tr>
<td><?php echo esc_html( wp_date( 'd.m.Y', strtotime( $entry->reading_date ) ) ); ?></td>
<td><?php echo esc_html( $entry->parcel_label ? $entry->parcel_label : '—' ); ?></td>
<td><?php echo esc_html( $this->format_meter_value_with_unit( $entry->reading_value, $meter->type ) ); ?></td>
<td><?php echo wp_kses_post( $this->reading_flag_badge( $entry ) ); ?></td>
<td><?php echo esc_html( $entry->note ? $entry->note : '—' ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php
}
/**
* Render meter swap form.
*
* @return void
*/
private function render_meter_swap_form() {
$id = absint( isset( $_GET['id'] ) ? $_GET['id'] : 0 );
$meter = $id ? $this->meters->find( $id ) : null;
$parcel = $meter && ! empty( $meter->parcel_id ) ? $this->parcels->find( (int) $meter->parcel_id ) : null;
$latest = $meter ? $this->readings->get_latest_for_meter( $meter->id ) : null;
$today = current_time( 'Y-m-d' );
$unit = $meter ? $this->meter_unit_label( $meter->type ) : '';
if ( ! $meter ) {
$this->redirect_with_notice( 'kgvvm-zaehler', 'error', __( 'Der gewünschte Zähler wurde nicht gefunden.', KGVVM_TEXT_DOMAIN ) );
}
?>
<div class='wrap'>
<h1><?php echo esc_html__( 'Zähler tauschen', KGVVM_TEXT_DOMAIN ); ?></h1>
<?php $this->render_notice(); ?>
<div class='notice notice-info inline'>
<p>
<strong><?php echo esc_html( $meter->meter_number ); ?></strong>
(<?php echo esc_html( $this->meter_type_label( $meter->type ) ); ?>)
<?php echo esc_html( $parcel ? ' ' . $parcel->label : ' ' . __( 'aktuell keiner Parzelle zugeordnet', KGVVM_TEXT_DOMAIN ) ); ?>
<?php if ( $latest ) : ?>
<br /><?php echo esc_html( sprintf( __( 'Letzte Ablesung: %1$s %2$s am %3$s', KGVVM_TEXT_DOMAIN ), $this->format_meter_value( $latest->reading_value, $meter->type ), $unit, wp_date( 'd.m.Y', strtotime( $latest->reading_date ) ) ) ); ?>
<?php endif; ?>
</p>
</div>
<form method='post'>
<?php wp_nonce_field( 'kgvvm_swap_meter' ); ?>
<input type='hidden' name='kgvvm_action' value='swap_meter' />
<input type='hidden' name='id' value='<?php echo esc_attr( $id ); ?>' />
<table class='form-table kgvvm-form-table'>
<tr>
<th scope='row'><label for='kgvvm-new-meter-number'><?php echo esc_html__( 'Neue Zählernummer', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='new_meter_number' id='kgvvm-new-meter-number' type='text' class='regular-text' required value='' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-swap-date'><?php echo esc_html__( 'Tauschdatum', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='swap_date' id='kgvvm-swap-date' type='date' required value='<?php echo esc_attr( $today ); ?>' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-new-calibration-year'><?php echo esc_html__( 'Eichjahr neuer Zähler', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<input name='new_calibration_year' id='kgvvm-new-calibration-year' type='number' min='2000' max='2100' step='1' value='' />
<p class='kgvvm-help'><?php echo esc_html__( 'Optional: Jahr der Eichung des neuen Ersatz-Zählers.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<?php if ( $parcel ) : ?>
<tr>
<th scope='row'><label for='kgvvm-old-final-reading'><?php echo esc_html__( 'Endstand alter Zähler', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<input name='old_final_reading' id='kgvvm-old-final-reading' type='number' min='0' step='0.001' value='<?php echo esc_attr( $latest ? $latest->reading_value : '' ); ?>' />
<p class='kgvvm-help'><?php echo esc_html( sprintf( __( 'Optionaler Abschlussstand in %s am Tauschdatum.', KGVVM_TEXT_DOMAIN ), $unit ) ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-new-initial-reading'><?php echo esc_html__( 'Startstand neuer Zähler', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<input name='new_initial_reading' id='kgvvm-new-initial-reading' type='number' min='0' step='0.001' required value='0' />
<p class='kgvvm-help'><?php echo esc_html( sprintf( __( 'Dieser Stand wird direkt für die Parzelle mit %s gespeichert.', KGVVM_TEXT_DOMAIN ), $unit ) ); ?></p>
</td>
</tr>
<?php else : ?>
<tr>
<th scope='row'><?php echo esc_html__( 'Hinweis', KGVVM_TEXT_DOMAIN ); ?></th>
<td><?php echo esc_html__( 'Der alte Zähler ist aktuell keiner Parzelle zugeordnet. Der neue Zähler wird daher als freier Zähler angelegt.', KGVVM_TEXT_DOMAIN ); ?></td>
</tr>
<?php endif; ?>
<tr>
<th scope='row'><label for='kgvvm-swap-note'><?php echo esc_html__( 'Bemerkung', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<textarea name='swap_note' id='kgvvm-swap-note' rows='5' class='large-text'></textarea>
<p class='kgvvm-help'><?php echo esc_html__( 'Der alte Zähler wird inaktiv gesetzt, die Historie bleibt erhalten und der neue Zähler übernimmt automatisch die Zuordnung.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
</table>
<?php submit_button( __( 'Zähler tauschen', KGVVM_TEXT_DOMAIN ), 'primary', 'submit', false ); ?>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-zaehler' ) ); ?>' class='button-secondary'><?php echo esc_html__( 'Abbrechen', KGVVM_TEXT_DOMAIN ); ?></a>
</form>
</div>
<?php
}
/**
* Render tenant page.
*
* @return void
*/
public function render_tenants_page() {
$this->require_cap( 'edit_paechter' );
$view = isset( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : 'list';
if ( 'form' === $view ) {
$this->render_tenant_form();
return;
}
$rows = $this->tenants->search( $_GET );
?>
<div class='wrap'>
<h1 class='wp-heading-inline'><?php echo esc_html__( 'Pächter', KGVVM_TEXT_DOMAIN ); ?></h1>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-paechter', array( 'view' => 'form' ) ) ); ?>' class='page-title-action'><?php echo esc_html__( 'Neuer Pächter', KGVVM_TEXT_DOMAIN ); ?></a>
<?php $this->render_notice(); ?>
<form method='get' class='kgvvm-toolbar'>
<input type='hidden' name='page' value='kgvvm-paechter' />
<div class='kgvvm-filters'>
<input type='search' name='s' value='<?php echo esc_attr( isset( $_GET['s'] ) ? wp_unslash( $_GET['s'] ) : '' ); ?>' placeholder='<?php echo esc_attr__( 'Pächter suchen …', KGVVM_TEXT_DOMAIN ); ?>' />
<select name='status'>
<option value=''><?php echo esc_html__( 'Alle Status', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='active' <?php selected( isset( $_GET['status'] ) ? wp_unslash( $_GET['status'] ) : '', 'active' ); ?>><?php echo esc_html__( 'Aktiv', KGVVM_TEXT_DOMAIN ); ?></option>
<option value='inactive' <?php selected( isset( $_GET['status'] ) ? wp_unslash( $_GET['status'] ) : '', 'inactive' ); ?>><?php echo esc_html__( 'Inaktiv', KGVVM_TEXT_DOMAIN ); ?></option>
</select>
<button class='button'><?php echo esc_html__( 'Filtern', KGVVM_TEXT_DOMAIN ); ?></button>
</div>
</form>
<table class='widefat striped'>
<thead>
<tr>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-paechter', 'last_name' ) ); ?>'><?php echo esc_html__( 'Name', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><?php echo esc_html__( 'Telefon', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'E-Mail', KGVVM_TEXT_DOMAIN ); ?></th>
<th><a href='<?php echo esc_url( $this->sort_url( 'kgvvm-paechter', 'contract_start' ) ); ?>'><?php echo esc_html__( 'Vertragsbeginn', KGVVM_TEXT_DOMAIN ); ?></a></th>
<th><?php echo esc_html__( 'Parzellen', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Mitglieder', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Status', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Aktionen', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php if ( empty( $rows ) ) : ?>
<tr><td colspan='8'><?php echo esc_html__( 'Noch keine Pächter vorhanden.', KGVVM_TEXT_DOMAIN ); ?></td></tr>
<?php else : ?>
<?php foreach ( $rows as $row ) : ?>
<?php $tenant_members = $this->assignments->get_members_for_tenant( $row->id ); ?>
<tr>
<td><strong><?php echo esc_html( $row->last_name . ', ' . $row->first_name ); ?></strong></td>
<td><?php echo esc_html( $row->phone ? $row->phone : '—' ); ?></td>
<td><?php echo esc_html( $row->email ? $row->email : '—' ); ?></td>
<td><?php echo esc_html( $row->contract_start ? wp_date( 'd.m.Y', strtotime( $row->contract_start ) ) : '—' ); ?></td>
<td><?php echo esc_html( (string) $row->parcel_count ); ?></td>
<td>
<?php if ( empty( $tenant_members ) ) : ?>
<?php else : ?>
<?php echo esc_html( implode( ', ', array_map( array( $this, 'format_tenant_member_label' ), $tenant_members ) ) ); ?>
<?php endif; ?>
</td>
<td><?php echo $this->status_badge( (int) $row->is_active === 1 ? __( 'Aktiv', KGVVM_TEXT_DOMAIN ) : __( 'Inaktiv', KGVVM_TEXT_DOMAIN ), (int) $row->is_active === 1 ? 'green' : 'gray' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
<td>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-paechter', array( 'view' => 'form', 'id' => $row->id ) ) ); ?>'><?php echo esc_html__( 'Bearbeiten', KGVVM_TEXT_DOMAIN ); ?></a>
|
<a href='<?php echo esc_url( wp_nonce_url( $this->admin_url( 'kgvvm-paechter', array( 'kgvvm_action' => 'delete_tenant', 'id' => $row->id ) ), 'kgvvm_delete_tenant_' . $row->id ) ); ?>' onclick='return confirm("<?php echo esc_js( __( 'Pächter wirklich löschen?', KGVVM_TEXT_DOMAIN ) ); ?>");'><?php echo esc_html__( 'Löschen', KGVVM_TEXT_DOMAIN ); ?></a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php
}
/**
* Render tenant form.
*
* @return void
*/
private function render_tenant_form() {
$id = absint( isset( $_GET['id'] ) ? $_GET['id'] : 0 );
$tenant = $id ? $this->tenants->find( $id ) : null;
$tenant_members = $id ? $this->assignments->get_members_for_tenant( $id ) : array();
?>
<div class='wrap'>
<h1><?php echo esc_html( $id ? __( 'Pächter bearbeiten', KGVVM_TEXT_DOMAIN ) : __( 'Neuer Pächter', KGVVM_TEXT_DOMAIN ) ); ?></h1>
<?php $this->render_notice(); ?>
<form method='post'>
<?php wp_nonce_field( 'kgvvm_save_tenant' ); ?>
<input type='hidden' name='kgvvm_action' value='save_tenant' />
<input type='hidden' name='id' value='<?php echo esc_attr( $id ); ?>' />
<table class='form-table kgvvm-form-table'>
<tr>
<th scope='row'><label for='kgvvm-tenant-first-name'><?php echo esc_html__( 'Vorname', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='first_name' id='kgvvm-tenant-first-name' type='text' class='regular-text' required value='<?php echo esc_attr( $tenant ? $tenant->first_name : '' ); ?>' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-tenant-last-name'><?php echo esc_html__( 'Nachname', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='last_name' id='kgvvm-tenant-last-name' type='text' class='regular-text' required value='<?php echo esc_attr( $tenant ? $tenant->last_name : '' ); ?>' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-tenant-address'><?php echo esc_html__( 'Adresse', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><textarea name='address' id='kgvvm-tenant-address' rows='4' class='large-text'><?php echo esc_textarea( $tenant ? $tenant->address : '' ); ?></textarea></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-tenant-phone'><?php echo esc_html__( 'Telefon', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='phone' id='kgvvm-tenant-phone' type='text' class='regular-text' value='<?php echo esc_attr( $tenant ? $tenant->phone : '' ); ?>' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-tenant-email'><?php echo esc_html__( 'E-Mail', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='email' id='kgvvm-tenant-email' type='email' class='regular-text' value='<?php echo esc_attr( $tenant ? $tenant->email : '' ); ?>' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-tenant-contract-start'><?php echo esc_html__( 'Vertragsbeginn', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='contract_start' id='kgvvm-tenant-contract-start' type='date' required value='<?php echo esc_attr( $tenant ? $tenant->contract_start : '' ); ?>' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-tenant-contract-end'><?php echo esc_html__( 'Vertragsende', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input name='contract_end' id='kgvvm-tenant-contract-end' type='date' value='<?php echo esc_attr( $tenant ? $tenant->contract_end : '' ); ?>' /></td>
</tr>
<tr>
<th scope='row'><?php echo esc_html__( 'Aktiv', KGVVM_TEXT_DOMAIN ); ?></th>
<td><label><input type='checkbox' name='is_active' value='1' <?php checked( $tenant ? (int) $tenant->is_active : 1, 1 ); ?> /> <?php echo esc_html__( 'Pächter ist aktiv', KGVVM_TEXT_DOMAIN ); ?></label></td>
</tr>
<tr>
<th scope='row'><?php echo esc_html__( 'Zugeordnete Mitglieder', KGVVM_TEXT_DOMAIN ); ?></th>
<td>
<?php if ( empty( $tenant_members ) ) : ?>
<p class='kgvvm-help'><?php echo esc_html__( 'Aktuell sind über die zugeordneten Parzellen keine Mitglieder verknüpft.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php else : ?>
<ul>
<?php foreach ( $tenant_members as $member ) : ?>
<li><?php echo esc_html( $this->format_tenant_member_label( $member ) ); ?></li>
<?php endforeach; ?>
</ul>
<p class='kgvvm-help'><?php echo esc_html__( 'Nur Anzeige: Die Zuordnung der Mitglieder erfolgt bei den Parzellen und ist hier nicht bearbeitbar.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php endif; ?>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-tenant-note'><?php echo esc_html__( 'Bemerkung', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><textarea name='note' id='kgvvm-tenant-note' rows='5' class='large-text'><?php echo esc_textarea( $tenant ? $tenant->note : '' ); ?></textarea></td>
</tr>
</table>
<?php submit_button( $id ? __( 'Pächter aktualisieren', KGVVM_TEXT_DOMAIN ) : __( 'Pächter anlegen', KGVVM_TEXT_DOMAIN ) ); ?>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-paechter' ) ); ?>' class='button-secondary'><?php echo esc_html__( 'Zurück', KGVVM_TEXT_DOMAIN ); ?></a>
</form>
</div>
<?php
}
/**
* Render the member page with assigned parcels and meter reading forms.
*
* @return void
*/
public function render_my_parcels_page() {
$this->require_cap( 'view_assigned_parcels' );
$user_id = get_current_user_id();
$parcels = $this->assignments->get_parcels_for_user( $user_id );
?>
<div class='wrap'>
<div class='kgvvm-toolbar'>
<h1><?php echo esc_html__( 'Meine Parzellen', KGVVM_TEXT_DOMAIN ); ?></h1>
</div>
<?php $this->render_notice(); ?>
<?php if ( empty( $parcels ) ) : ?>
<div class='notice notice-info'><p><?php echo esc_html__( 'Ihnen ist aktuell keine Parzelle zugewiesen.', KGVVM_TEXT_DOMAIN ); ?></p></div>
<?php else : ?>
<?php foreach ( $parcels as $parcel ) : ?>
<?php
$water_meter = $this->meters->get_assigned_to_parcel( $parcel->id, 'water' );
$power_meter = $this->meters->get_assigned_to_parcel( $parcel->id, 'power' );
$water_latest = $water_meter ? $this->readings->get_latest_for_meter( $water_meter->id ) : null;
$power_latest = $power_meter ? $this->readings->get_latest_for_meter( $power_meter->id ) : null;
$recent = $this->readings->get_recent_for_parcel( $parcel->id, 8 );
?>
<div class='kgvvm-card' style='margin-bottom:20px;'>
<div class='kgvvm-toolbar'>
<h2><?php echo esc_html( $parcel->label ); ?></h2>
<a class='button button-secondary' href='<?php echo esc_url( wp_nonce_url( $this->admin_url( 'kgvvm-my-parcels', array( 'kgvvm_action' => 'export_readings_csv', 'parcel_id' => $parcel->id ) ), 'kgvvm_export_readings_' . $parcel->id ) ); ?>'><?php echo esc_html__( 'CSV-Export Ablesungen', KGVVM_TEXT_DOMAIN ); ?></a>
</div>
<p>
<strong><?php echo esc_html__( 'Sparte:', KGVVM_TEXT_DOMAIN ); ?></strong> <?php echo esc_html( $parcel->section_name ? $parcel->section_name : '—' ); ?><br />
<strong><?php echo esc_html__( 'Fläche:', KGVVM_TEXT_DOMAIN ); ?></strong> <?php echo null !== $parcel->area ? esc_html( number_format_i18n( (float) $parcel->area, 2 ) . ' m²' ) : '—'; ?><br />
<strong><?php echo esc_html__( 'Pacht:', KGVVM_TEXT_DOMAIN ); ?></strong> <?php echo isset( $parcel->annual_rent ) && null !== $parcel->annual_rent ? esc_html( $this->format_currency( $parcel->annual_rent ) ) : '—'; ?><br />
<strong><?php echo esc_html__( 'Status:', KGVVM_TEXT_DOMAIN ); ?></strong> <?php echo esc_html( $this->parcel_status_label( $parcel->status ) ); ?>
</p>
<p class='kgvvm-help'><?php echo esc_html__( 'Die Parzellendaten sind schreibgeschützt. Sie können hier nur Ihre Zählerstände erfassen und exportieren.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php if ( ! empty( $parcel->note ) ) : ?>
<p><strong><?php echo esc_html__( 'Bemerkung:', KGVVM_TEXT_DOMAIN ); ?></strong> <?php echo esc_html( $parcel->note ); ?></p>
<?php endif; ?>
<div class='kgvvm-grid'>
<?php foreach ( array(
'water' => array( 'meter' => $water_meter, 'latest' => $water_latest, 'label' => __( 'Wasserzähler', KGVVM_TEXT_DOMAIN ) ),
'power' => array( 'meter' => $power_meter, 'latest' => $power_latest, 'label' => __( 'Stromzähler', KGVVM_TEXT_DOMAIN ) ),
) as $meter_config ) : ?>
<div class='kgvvm-card'>
<h3><?php echo esc_html( $meter_config['label'] ); ?></h3>
<?php if ( empty( $meter_config['meter'] ) ) : ?>
<p><?php echo esc_html__( 'Für diese Parzelle ist aktuell kein Zähler hinterlegt.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php else : ?>
<p>
<strong><?php echo esc_html__( 'Zählernummer:', KGVVM_TEXT_DOMAIN ); ?></strong> <?php echo esc_html( $meter_config['meter']->meter_number ); ?><br />
<strong><?php echo esc_html__( 'Letzte Ablesung:', KGVVM_TEXT_DOMAIN ); ?></strong>
<?php if ( $meter_config['latest'] ) : ?>
<?php echo esc_html( $this->format_meter_value_with_unit( $meter_config['latest']->reading_value, $meter_config['meter']->type ) . ' am ' . wp_date( 'd.m.Y', strtotime( $meter_config['latest']->reading_date ) ) ); ?>
<?php else : ?>
<?php echo esc_html__( 'Noch keine Ablesung vorhanden', KGVVM_TEXT_DOMAIN ); ?>
<?php endif; ?>
</p>
<form method='post'>
<?php wp_nonce_field( 'kgvvm_save_meter_reading' ); ?>
<input type='hidden' name='kgvvm_action' value='save_meter_reading' />
<input type='hidden' name='parcel_id' value='<?php echo esc_attr( $parcel->id ); ?>' />
<input type='hidden' name='meter_id' value='<?php echo esc_attr( $meter_config['meter']->id ); ?>' />
<p>
<label><?php echo esc_html__( 'Ablesedatum', KGVVM_TEXT_DOMAIN ); ?><br />
<input type='date' name='reading_date' required value='<?php echo esc_attr( wp_date( 'Y-m-d' ) ); ?>' />
</label>
</p>
<p>
<label><?php echo esc_html__( 'Zählerstand', KGVVM_TEXT_DOMAIN ); ?> (<?php echo esc_html( $this->meter_unit_label( $meter_config['meter']->type ) ); ?>)<br />
<input type='number' min='0' step='0.001' name='reading_value' required />
</label>
</p>
<p>
<label><?php echo esc_html__( 'Notiz', KGVVM_TEXT_DOMAIN ); ?><br />
<textarea name='note' rows='3' class='large-text'></textarea>
</label>
</p>
<?php submit_button( __( 'Ablesung speichern', KGVVM_TEXT_DOMAIN ), 'secondary', 'submit', false ); ?>
</form>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<div class='kgvvm-card'>
<h3><?php echo esc_html__( 'Letzte Einträge', KGVVM_TEXT_DOMAIN ); ?></h3>
<?php if ( empty( $recent ) ) : ?>
<p><?php echo esc_html__( 'Noch keine Ablesungen für diese Parzelle erfasst.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php else : ?>
<table class='widefat striped'>
<thead>
<tr>
<th><?php echo esc_html__( 'Datum', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Typ', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Zähler', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Stand', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Flag', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $recent as $reading ) : ?>
<tr>
<td><?php echo esc_html( wp_date( 'd.m.Y', strtotime( $reading->reading_date ) ) ); ?></td>
<td><?php echo esc_html( $this->meter_type_label( $reading->type ) ); ?></td>
<td><?php echo esc_html( $reading->meter_number ); ?></td>
<td><?php echo esc_html( $this->format_meter_value_with_unit( $reading->reading_value, $reading->type ) ); ?></td>
<td><?php echo wp_kses_post( $this->reading_flag_badge( $reading ) ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php
}
/**
* Render the shared club chat for members and board.
*
* @return void
*/
public function render_chat_page() {
$this->require_chat_access();
$rooms = $this->get_chat_rooms();
$requested = isset( $_GET['room'] ) ? sanitize_key( wp_unslash( $_GET['room'] ) ) : '';
$room_keys = array_keys( $rooms );
$current_room = ( $requested && isset( $rooms[ $requested ] ) ) ? $requested : reset( $room_keys );
$messages = $this->normalize_chat_messages( $this->chat->get_recent_messages( $current_room, 60 ) );
$last_message = ! empty( $messages ) ? end( $messages ) : null;
if ( ! empty( $messages ) ) {
reset( $messages );
}
?>
<div class='wrap'>
<h1><?php echo esc_html__( 'Vereinschat', KGVVM_TEXT_DOMAIN ); ?></h1>
<?php $this->render_notice(); ?>
<div class='kgvvm-card'>
<p><?php echo esc_html__( 'Hier können Mitglieder, Vorstand und Verwaltung direkt miteinander schreiben. Mitglieder sehen nur den Vereinschat sowie die Sparte ihrer zugewiesenen Parzelle. Der Raum „Vorstand“ ist nur für berechtigte Rollen sichtbar.', KGVVM_TEXT_DOMAIN ); ?></p>
</div>
<div class='kgvvm-chat-app' data-room='<?php echo esc_attr( $current_room ); ?>' data-current-user-id='<?php echo esc_attr( get_current_user_id() ); ?>'>
<aside class='kgvvm-chat-sidebar kgvvm-card'>
<h2><?php echo esc_html__( 'Räume', KGVVM_TEXT_DOMAIN ); ?></h2>
<div class='kgvvm-chat-room-list'>
<?php foreach ( $rooms as $room_key => $room ) : ?>
<button
type='button'
class='button button-secondary kgvvm-chat-room<?php echo $current_room === $room_key ? ' is-active' : ''; ?>'
data-chat-room='<?php echo esc_attr( $room_key ); ?>'
data-chat-label='<?php echo esc_attr( $room['label'] ); ?>'
data-chat-description='<?php echo esc_attr( $room['description'] ); ?>'
>
<?php echo esc_html( $room['label'] ); ?>
</button>
<?php endforeach; ?>
</div>
</aside>
<section class='kgvvm-chat-panel kgvvm-card'>
<div class='kgvvm-chat-panel__header'>
<div>
<h2 data-chat-room-title><?php echo esc_html( $rooms[ $current_room ]['label'] ); ?></h2>
<p class='kgvvm-help' data-chat-room-description><?php echo esc_html( $rooms[ $current_room ]['description'] ); ?></p>
</div>
<span class='kgvvm-status kgvvm-status--blue'><?php echo esc_html__( 'nur intern', KGVVM_TEXT_DOMAIN ); ?></span>
</div>
<div class='kgvvm-chat-messages' data-chat-messages data-last-id='<?php echo esc_attr( $last_message ? $last_message['id'] : 0 ); ?>'>
<?php if ( empty( $messages ) ) : ?>
<p class='kgvvm-chat-empty'><?php echo esc_html__( 'Noch keine Nachrichten in diesem Raum.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php else : ?>
<?php foreach ( $messages as $message ) : ?>
<article class='kgvvm-chat-message<?php echo ! empty( $message['is_own'] ) ? ' kgvvm-chat-message--own' : ''; ?>' data-id='<?php echo esc_attr( $message['id'] ); ?>'>
<div class='kgvvm-chat-message__meta'>
<strong><?php echo esc_html( $message['user'] ); ?></strong>
<?php if ( ! empty( $message['role'] ) ) : ?>
· <?php echo esc_html( $message['role'] ); ?>
<?php endif; ?>
· <?php echo esc_html( $message['time'] ); ?>
</div>
<div class='kgvvm-chat-message__body'><?php echo nl2br( esc_html( $message['message'] ) ); ?></div>
</article>
<?php endforeach; ?>
<?php endif; ?>
</div>
<form class='kgvvm-chat-form' data-chat-form>
<label for='kgvvm-chat-input' class='screen-reader-text'><?php echo esc_html__( 'Nachricht', KGVVM_TEXT_DOMAIN ); ?></label>
<textarea id='kgvvm-chat-input' rows='3' maxlength='1000' data-chat-input placeholder='<?php echo esc_attr__( 'Nachricht an den Verein schreiben …', KGVVM_TEXT_DOMAIN ); ?>'></textarea>
<div class='kgvvm-chat-form__actions'>
<span class='kgvvm-help' data-chat-status></span>
<button type='submit' class='button button-primary'><?php echo esc_html__( 'Senden', KGVVM_TEXT_DOMAIN ); ?></button>
</div>
</form>
</section>
</div>
</div>
<?php
}
/**
* Render settings page.
*
* @return void
*/
public function render_settings_page() {
$this->require_cap( Roles::SETTINGS_CAP );
$settings = wp_parse_args( get_option( 'kgvvm_settings', array() ), $this->get_settings_defaults() );
?>
<div class='wrap'>
<h1><?php echo esc_html__( 'Einstellungen', KGVVM_TEXT_DOMAIN ); ?></h1>
<?php $this->render_notice(); ?>
<form method='post'>
<?php wp_nonce_field( 'kgvvm_save_settings' ); ?>
<input type='hidden' name='kgvvm_action' value='save_settings' />
<table class='form-table kgvvm-form-table'>
<tr>
<th scope='row'><?php echo esc_html__( 'Mitgliederregel', KGVVM_TEXT_DOMAIN ); ?></th>
<td>
<label>
<input type='checkbox' name='allow_multiple_member_parcels' value='1' <?php checked( ! empty( $settings['allow_multiple_member_parcels'] ), true ); ?> />
<?php echo esc_html__( 'Ein Mitglied darf mehreren Parzellen zugeordnet werden.', KGVVM_TEXT_DOMAIN ); ?>
</label>
<p class='kgvvm-help'><?php echo esc_html__( 'Wenn diese Option deaktiviert ist, wird eine Mehrfachzuordnung bei der Parzellenpflege validiert und blockiert.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><?php echo esc_html__( 'Plausibilitätsgrenze Wasser', KGVVM_TEXT_DOMAIN ); ?></th>
<td>
<input type='number' min='0' step='0.001' name='water_usage_alert_threshold' value='<?php echo esc_attr( $settings['water_usage_alert_threshold'] ); ?>' />
<p class='kgvvm-help'><?php echo esc_html__( 'Wenn der neue Wasserzählerstand den letzten Eintrag um mehr als diesen Wert in m³ erhöht, wird nach dem Speichern eine Warnung angezeigt.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><?php echo esc_html__( 'Strom-Einheit', KGVVM_TEXT_DOMAIN ); ?></th>
<td>
<select name='power_unit'>
<option value='kwh' <?php selected( $settings['power_unit'], 'kwh' ); ?>>kWh</option>
<option value='mwh' <?php selected( $settings['power_unit'], 'mwh' ); ?>>MWh</option>
</select>
<p class='kgvvm-help'><?php echo esc_html__( 'Wasser wird immer in m³ geführt. Für Strom kann hier die gewünschte Einheit gewählt werden.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><?php echo esc_html__( 'Plausibilitätsgrenze Strom', KGVVM_TEXT_DOMAIN ); ?></th>
<td>
<input type='number' min='0' step='0.001' name='power_usage_alert_threshold' value='<?php echo esc_attr( $settings['power_usage_alert_threshold'] ); ?>' />
<p class='kgvvm-help'><?php echo esc_html__( 'Wenn der neue Stromzählerstand den letzten Eintrag um mehr als diesen Wert in der gewählten Strom-Einheit erhöht, wird nach dem Speichern eine Warnung angezeigt.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><?php echo esc_html__( 'PDF / Briefkopf', KGVVM_TEXT_DOMAIN ); ?></th>
<td>
<p class='kgvvm-help'><?php echo esc_html__( 'Diese Angaben erscheinen in der Druckansicht und im TCPDF-Export der Jahresabrechnung.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><?php echo esc_html__( 'Vereinsname', KGVVM_TEXT_DOMAIN ); ?></th>
<td>
<input type='text' name='pdf_club_name' class='regular-text' value='<?php echo esc_attr( $settings['pdf_club_name'] ); ?>' />
<p class='kgvvm-help'><?php echo esc_html__( 'Wird als Überschrift im PDF- und Druckkopf verwendet.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><?php echo esc_html__( 'Logo-URL', KGVVM_TEXT_DOMAIN ); ?></th>
<td>
<input type='url' name='pdf_logo_url' class='regular-text code' value='<?php echo esc_attr( $settings['pdf_logo_url'] ); ?>' placeholder='https://example.de/logo.png' />
<p class='kgvvm-help'><?php echo esc_html__( 'Optional: URL zu einem Vereinslogo aus der Mediathek oder einer öffentlich erreichbaren Bilddatei.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><?php echo esc_html__( 'Adress- / Kontaktblock', KGVVM_TEXT_DOMAIN ); ?></th>
<td>
<textarea name='pdf_contact_block' rows='4' class='large-text'><?php echo esc_textarea( $settings['pdf_contact_block'] ); ?></textarea>
<p class='kgvvm-help'><?php echo esc_html__( 'Mehrzeilig möglich, z. B. Adresse, Telefon, E-Mail oder Bankverbindung.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><?php echo esc_html__( 'Einleitungstext', KGVVM_TEXT_DOMAIN ); ?></th>
<td>
<textarea name='pdf_intro_text' rows='3' class='large-text'><?php echo esc_textarea( $settings['pdf_intro_text'] ); ?></textarea>
<p class='kgvvm-help'><?php echo esc_html__( 'Optionaler Hinweistext oberhalb der Abrechnungsdetails.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><?php echo esc_html__( 'Fußtext / Hinweis', KGVVM_TEXT_DOMAIN ); ?></th>
<td>
<textarea name='pdf_footer_text' rows='3' class='large-text'><?php echo esc_textarea( $settings['pdf_footer_text'] ); ?></textarea>
<p class='kgvvm-help'><?php echo esc_html__( 'Erscheint unterhalb der Jahresabrechnung, z. B. als Zahlungs- oder Rückfragehinweis.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
</table>
<?php submit_button( __( 'Einstellungen speichern', KGVVM_TEXT_DOMAIN ) ); ?>
</form>
</div>
<?php
}
/**
* Handle AJAX polling for chat messages.
*
* @return void
*/
public function ajax_fetch_chat_messages() {
$this->require_chat_access();
check_ajax_referer( 'kgvvm_chat_nonce', 'nonce' );
$room_key = isset( $_POST['room'] ) ? sanitize_key( wp_unslash( $_POST['room'] ) ) : 'general';
$after_id = absint( isset( $_POST['after_id'] ) ? wp_unslash( $_POST['after_id'] ) : 0 );
if ( ! $this->is_chat_room_allowed( $room_key ) ) {
wp_send_json_error( array( 'message' => __( 'Sie dürfen diesen Chatraum nicht öffnen.', KGVVM_TEXT_DOMAIN ) ), 403 );
}
wp_send_json_success(
array(
'messages' => $this->normalize_chat_messages( $this->chat->get_recent_messages( $room_key, $after_id > 0 ? 100 : 60, $after_id ) ),
)
);
}
/**
* Handle AJAX submission for one new chat message.
*
* @return void
*/
public function ajax_send_chat_message() {
$this->require_chat_access();
check_ajax_referer( 'kgvvm_chat_nonce', 'nonce' );
$room_key = isset( $_POST['room'] ) ? sanitize_key( wp_unslash( $_POST['room'] ) ) : 'general';
$message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : '';
$message = trim( substr( $message, 0, 1000 ) );
if ( ! $this->is_chat_room_allowed( $room_key ) ) {
wp_send_json_error( array( 'message' => __( 'Sie dürfen in diesem Chatraum nicht schreiben.', KGVVM_TEXT_DOMAIN ) ), 403 );
}
if ( '' === $message ) {
wp_send_json_error( array( 'message' => __( 'Bitte zuerst eine Nachricht eingeben.', KGVVM_TEXT_DOMAIN ) ), 400 );
}
$created = $this->chat->save_message( $room_key, get_current_user_id(), $message );
if ( ! $created ) {
wp_send_json_error( array( 'message' => __( 'Die Nachricht konnte nicht gespeichert werden.', KGVVM_TEXT_DOMAIN ) ), 500 );
}
wp_send_json_success(
array(
'messages' => $this->normalize_chat_messages( array( $created ) ),
)
);
}
/**
* Return plugin settings defaults.
*
* @return array
*/
private function get_settings_defaults() {
return array(
'allow_multiple_member_parcels' => 1,
'water_usage_alert_threshold' => 25,
'power_usage_alert_threshold' => 1000,
'power_unit' => 'kwh',
'pdf_club_name' => get_bloginfo( 'name' ),
'pdf_logo_url' => '',
'pdf_contact_block' => '',
'pdf_intro_text' => __( 'Diese Jahresabrechnung wurde automatisch mit der KGV Vereinsverwaltung erstellt.', KGVVM_TEXT_DOMAIN ),
'pdf_footer_text' => __( 'Bitte prüfen Sie die Angaben und melden Sie Rückfragen an den Vorstand.', KGVVM_TEXT_DOMAIN ),
);
}
/**
* Export one parcel's reading history as CSV.
*
* @return void
*/
private function export_readings_csv() {
$this->require_cap( 'view_assigned_parcels' );
$parcel_id = absint( isset( $_GET['parcel_id'] ) ? $_GET['parcel_id'] : 0 );
$nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
if ( ! $parcel_id || ! wp_verify_nonce( $nonce, 'kgvvm_export_readings_' . $parcel_id ) ) {
$this->redirect_with_notice( 'kgvvm-my-parcels', 'error', __( 'Der CSV-Export wurde aus Sicherheitsgründen abgebrochen.', KGVVM_TEXT_DOMAIN ) );
}
if ( ! current_user_can( 'manage_kleingarten' ) && ! $this->assignments->user_has_parcel( get_current_user_id(), $parcel_id ) ) {
wp_die( esc_html__( 'Sie dürfen nur die Ablesungen Ihrer eigenen Parzellen exportieren.', KGVVM_TEXT_DOMAIN ) );
}
$parcel = $this->parcels->find( $parcel_id );
if ( ! $parcel ) {
$this->redirect_with_notice( 'kgvvm-my-parcels', 'error', __( 'Die gewünschte Parzelle wurde nicht gefunden.', KGVVM_TEXT_DOMAIN ) );
}
$rows = $this->readings->get_all_for_parcel( $parcel_id );
nocache_headers();
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename=ablesungen-' . sanitize_file_name( $parcel->label ) . '-' . gmdate( 'Ymd-His' ) . '.csv' );
$output = fopen( 'php://output', 'w' );
if ( false === $output ) {
exit;
}
fwrite( $output, "\xEF\xBB\xBF" );
fputcsv( $output, array( 'Parzelle', 'Datum', 'Typ', 'Zählernummer', 'Zählerstand', 'Einheit', 'Flag', 'Notiz' ), ';' );
foreach ( $rows as $row ) {
fputcsv(
$output,
array(
$parcel->label,
wp_date( 'd.m.Y', strtotime( $row->reading_date ) ),
$this->meter_type_label( $row->type ),
$row->meter_number,
$this->format_meter_value( $row->reading_value, $row->type ),
$this->meter_unit_label( $row->type ),
! empty( $row->is_self_reading ) ? __( 'Selbstablesung', KGVVM_TEXT_DOMAIN ) : '',
$row->note,
),
';'
);
}
fclose( $output );
exit;
}
/**
* Ensure the current user may access the chat.
*
* @return void
*/
private function require_chat_access() {
if ( ! current_user_can( 'view_assigned_parcels' ) && ! current_user_can( 'manage_kleingarten' ) ) {
wp_die( esc_html__( 'Sie haben keine Berechtigung für den Vereinschat.', KGVVM_TEXT_DOMAIN ) );
}
}
/**
* List all chat rooms visible for the current user.
*
* @return array
*/
private function get_chat_rooms() {
$rooms = array(
'general' => array(
'label' => __( 'Vereinschat', KGVVM_TEXT_DOMAIN ),
'description' => __( 'Offener Austausch für alle Mitglieder, Vorstand und Verwaltung.', KGVVM_TEXT_DOMAIN ),
),
);
$is_management = current_user_can( Roles::SETTINGS_CAP ) || current_user_can( 'manage_kleingarten' );
if ( $is_management ) {
$rooms['board'] = array(
'label' => __( 'Vorstand', KGVVM_TEXT_DOMAIN ),
'description' => __( 'Interner Raum für Vorstand und Verwaltung.', KGVVM_TEXT_DOMAIN ),
);
}
$sections = array();
if ( $is_management ) {
$sections = $this->sections->all_for_options();
} else {
$user_parcels = $this->assignments->get_parcels_for_user( get_current_user_id() );
$seen_sections = array();
foreach ( (array) $user_parcels as $parcel ) {
$section_id = ! empty( $parcel->section_id ) ? (int) $parcel->section_id : 0;
if ( $section_id > 0 && ! isset( $seen_sections[ $section_id ] ) ) {
$section = $this->sections->find( $section_id );
if ( $section ) {
$sections[] = $section;
}
$seen_sections[ $section_id ] = true;
}
}
}
foreach ( (array) $sections as $section ) {
$section_id = ! empty( $section->id ) ? (int) $section->id : 0;
$section_name = ! empty( $section->name ) ? $section->name : '';
if ( $section_id < 1 ) {
continue;
}
$section_room_key = 'section-' . $section_id;
$rooms[ $section_room_key ] = array(
'label' => sprintf( __( 'Sparte: %s', KGVVM_TEXT_DOMAIN ), $section_name ? $section_name : $section_id ),
'description' => sprintf( __( 'Austausch für Mitglieder der Sparte %s.', KGVVM_TEXT_DOMAIN ), $section_name ? $section_name : $section_id ),
);
}
return $rooms;
}
/**
* Check whether a chat room is available for the current user.
*
* @param string $room_key Room key.
* @return bool
*/
private function is_chat_room_allowed( $room_key ) {
$rooms = $this->get_chat_rooms();
return isset( $rooms[ sanitize_key( $room_key ) ] );
}
/**
* Normalize chat messages for HTML and AJAX responses.
*
* @param array $messages Raw database rows.
* @return array
*/
private function normalize_chat_messages( $messages ) {
$current_user_id = get_current_user_id();
$normalized = array();
foreach ( (array) $messages as $message ) {
$normalized[] = array(
'id' => isset( $message->id ) ? (int) $message->id : 0,
'user_id' => isset( $message->user_id ) ? (int) $message->user_id : 0,
'user' => ! empty( $message->display_name ) ? (string) $message->display_name : __( 'Mitglied', KGVVM_TEXT_DOMAIN ),
'role' => $this->get_chat_role_label( isset( $message->user_id ) ? (int) $message->user_id : 0 ),
'message' => isset( $message->message ) ? (string) $message->message : '',
'time' => ! empty( $message->created_at ) ? wp_date( 'd.m.Y H:i', strtotime( $message->created_at ) ) : '',
'is_own' => isset( $message->user_id ) && (int) $message->user_id === (int) $current_user_id,
);
}
return $normalized;
}
/**
* Resolve a readable sender label.
*
* @param int $user_id Sender ID.
* @return string
*/
private function get_chat_role_label( $user_id ) {
$user_id = absint( $user_id );
if ( $user_id > 0 && ( user_can( $user_id, Roles::SETTINGS_CAP ) || user_can( $user_id, 'manage_kleingarten' ) ) ) {
return __( 'Vorstand / Verwaltung', KGVVM_TEXT_DOMAIN );
}
return __( 'Mitglied', KGVVM_TEXT_DOMAIN );
}
/**
* Return a visible badge for self-readings.
*
* @param object $reading Reading row.
* @return string
*/
private function reading_flag_badge( $reading ) {
if ( empty( $reading ) || empty( $reading->is_self_reading ) ) {
return '—';
}
return $this->status_badge( __( 'Selbstablesung', KGVVM_TEXT_DOMAIN ), 'blue' );
}
/**
* Build a plausibility warning for unusual jumps.
*
* @param object $meter Meter object.
* @param object|null $latest Latest previous reading.
* @param float $new_reading New reading value.
* @return string
*/
private function build_meter_jump_warning_message( $meter, $latest, $new_reading ) {
if ( ! $meter || ! $latest ) {
return '';
}
$settings = wp_parse_args( get_option( 'kgvvm_settings', array() ), $this->get_settings_defaults() );
$threshold = 'water' === $meter->type ? (float) $settings['water_usage_alert_threshold'] : (float) $settings['power_usage_alert_threshold'];
if ( $threshold <= 0 ) {
return '';
}
$delta = (float) $new_reading - (float) $latest->reading_value;
if ( $delta <= $threshold ) {
return '';
}
return sprintf(
/* translators: 1: meter number, 2: consumption increase */
__( 'Auffälliger Verbrauchssprung beim Zähler %1$s: +%2$s seit der letzten Ablesung. Bitte prüfen.', KGVVM_TEXT_DOMAIN ),
$meter->meter_number,
$this->format_meter_value_with_unit( $delta, $meter->type )
);
}
/**
* Format one meter value for display.
*
* @param float|int|string $value Numeric value.
* @param string $type Meter type.
* @return string
*/
private function format_meter_value( $value, $type ) {
$number = (float) $value;
if ( 'water' === $type ) {
return number_format( $number, 2, ',', '' );
}
return number_format( $number, 0, '', '' );
}
/**
* Format one meter value together with its unit.
*
* @param float|int|string $value Numeric value.
* @param string $type Meter type.
* @return string
*/
private function format_meter_value_with_unit( $value, $type ) {
return $this->format_meter_value( $value, $type ) . ' ' . $this->meter_unit_label( $type );
}
/**
* Format a currency value for display.
*
* @param float|int|string $value Numeric value.
* @return string
*/
private function format_currency( $value ) {
return number_format_i18n( (float) $value, 2 ) . ' €';
}
/**
* Format a yearly unit price for display.
*
* @param float|int|string $value Numeric value.
* @param string $unit Unit label.
* @return string
*/
private function format_price_per_unit( $value, $unit ) {
return number_format_i18n( (float) $value, 4 ) . ' € / ' . $unit;
}
/**
* Normalize the saved distribution type for one cost entry.
*
* @param object|array $entry Cost entry.
* @return string
*/
private function get_cost_distribution_type( $entry ) {
$type = 'parcel';
if ( is_object( $entry ) && isset( $entry->distribution_type ) ) {
$type = sanitize_key( (string) $entry->distribution_type );
} elseif ( is_array( $entry ) && isset( $entry['distribution_type'] ) ) {
$type = sanitize_key( (string) $entry['distribution_type'] );
}
return in_array( $type, array( 'parcel', 'member' ), true ) ? $type : 'parcel';
}
/**
* Return the UI label for one cost distribution type.
*
* @param string $distribution_type Distribution type.
* @return string
*/
private function get_cost_distribution_label( $distribution_type ) {
return 'member' === $distribution_type ? __( 'Mitglied', KGVVM_TEXT_DOMAIN ) : __( 'Parzelle', KGVVM_TEXT_DOMAIN );
}
/**
* Resolve the amount per parcel/member for one cost entry.
*
* @param object|array $entry Cost entry.
* @param int $parcel_count Active parcel count.
* @param int $member_count Active member count.
* @return float
*/
private function get_cost_unit_amount( $entry, $parcel_count = 0, $member_count = 0 ) {
if ( is_object( $entry ) && isset( $entry->unit_amount ) && '' !== trim( (string) $entry->unit_amount ) ) {
return (float) $entry->unit_amount;
}
if ( is_array( $entry ) && isset( $entry['unit_amount'] ) && '' !== trim( (string) $entry['unit_amount'] ) ) {
return (float) $entry['unit_amount'];
}
$distribution_type = $this->get_cost_distribution_type( $entry );
$total_cost = 0.0;
if ( is_object( $entry ) && isset( $entry->total_cost ) ) {
$total_cost = (float) $entry->total_cost;
} elseif ( is_array( $entry ) && isset( $entry['total_cost'] ) ) {
$total_cost = (float) $entry['total_cost'];
}
$divisor = 'member' === $distribution_type ? max( 1, (int) $member_count ) : max( 1, (int) $parcel_count );
return $divisor > 0 ? $total_cost / $divisor : 0.0;
}
/**
* Calculate the overall yearly total for one cost entry.
*
* @param object|array $entry Cost entry.
* @param int $parcel_count Active parcel count.
* @param int $member_count Active member count.
* @return float
*/
private function get_cost_total_amount( $entry, $parcel_count = 0, $member_count = 0 ) {
$distribution_type = $this->get_cost_distribution_type( $entry );
$unit_amount = $this->get_cost_unit_amount( $entry, $parcel_count, $member_count );
$multiplier = 'member' === $distribution_type ? max( 0, (int) $member_count ) : max( 0, (int) $parcel_count );
if ( $multiplier < 1 ) {
if ( is_object( $entry ) && isset( $entry->total_cost ) ) {
return (float) $entry->total_cost;
}
if ( is_array( $entry ) && isset( $entry['total_cost'] ) ) {
return (float) $entry['total_cost'];
}
}
return round( $unit_amount * $multiplier, 2 );
}
/**
* Filter statement utility rows by meter type.
*
* @param array $rows Utility rows.
* @param string $type Meter type.
* @return array
*/
private function get_statement_rows_by_meter_type( $rows, $type ) {
return array_values(
array_filter(
(array) $rows,
static function( $row ) use ( $type ) {
return isset( $row->type ) && $type === $row->type;
}
)
);
}
/**
* Build the detailed meter table for HTML/PDF annual statements.
*
* @param array $rows Statement rows.
* @param string $type Meter type.
* @param bool $is_pdf Whether PDF markup is requested.
* @return string
*/
private function build_statement_meter_detail_table( $rows, $type, $is_pdf = false ) {
$rows = array_values( (array) $rows );
$unit = 'power' === $type ? 'kWh' : 'm³';
$empty_msg = 'power' === $type ? __( 'Keine auswertbaren Stromdaten vorhanden.', KGVVM_TEXT_DOMAIN ) : __( 'Keine auswertbaren Wasserdaten vorhanden.', KGVVM_TEXT_DOMAIN );
ob_start();
if ( empty( $rows ) ) {
echo '<p>' . esc_html( $empty_msg ) . '</p>';
return (string) ob_get_clean();
}
?>
<?php if ( $is_pdf ) : ?>
<table cellpadding="5" cellspacing="0" border="1">
<?php else : ?>
<table class='widefat striped'>
<?php endif; ?>
<?php if ( ! $is_pdf ) : ?>
<thead>
<?php endif; ?>
<tr>
<th><?php echo esc_html__( 'Seriennummer', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Stand alt', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Stand neu', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Verbrauch', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Einzelpreis', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Gesamtpreis', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Schwund', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
<?php if ( ! $is_pdf ) : ?>
</thead>
<tbody>
<?php endif; ?>
<?php foreach ( $rows as $row ) : ?>
<tr>
<td><?php echo esc_html( ! empty( $row->meter_number ) ? $row->meter_number : '—' ); ?></td>
<td><?php echo esc_html( null !== $row->previous_value ? $this->format_meter_value_with_unit( $row->previous_value, $type ) : '—' ); ?></td>
<td><?php echo esc_html( null !== $row->reading_value ? $this->format_meter_value_with_unit( $row->reading_value, $type ) : '—' ); ?></td>
<td><?php echo esc_html( null !== $row->consumption ? $this->format_meter_value_with_unit( $row->consumption, $type ) : '—' ); ?></td>
<td><?php echo esc_html( null !== $row->unit_price ? $this->format_price_per_unit( $row->unit_price, $unit ) : '—' ); ?></td>
<td><?php echo esc_html( null !== $row->calculated_cost ? $this->format_currency( $row->calculated_cost ) : '—' ); ?></td>
<td><?php echo esc_html( isset( $row->loss_value ) && null !== $row->loss_value ? $this->format_meter_value_with_unit( $row->loss_value, $type ) : '—' ); ?></td>
</tr>
<?php endforeach; ?>
<?php if ( ! $is_pdf ) : ?>
</tbody>
<?php endif; ?>
</table>
<?php
return (string) ob_get_clean();
}
/**
* Build a year/section lookup for saved consumption prices.
*
* @param array $rows Consumption rows.
* @return array
*/
private function build_consumption_rate_lookup( $rows ) {
$lookup = array();
$years = array();
foreach ( (array) $rows as $row ) {
if ( empty( $row->section_id ) || empty( $row->reading_date ) ) {
continue;
}
$years[] = (int) gmdate( 'Y', strtotime( $row->reading_date ) );
}
$years = array_values( array_unique( array_filter( $years ) ) );
foreach ( $years as $year ) {
$rates = $this->costs->get_section_prices( $year );
foreach ( $rates as $rate ) {
$lookup[ $year ][ (int) $rate->section_id ] = $rate;
}
}
return $lookup;
}
/**
* Resolve the saved unit price for one consumption row.
*
* @param object $row Consumption row.
* @param array $rate_lookup Saved rate map.
* @return float|null
*/
private function get_consumption_unit_price( $row, $rate_lookup ) {
$year = ! empty( $row->reading_date ) ? (int) gmdate( 'Y', strtotime( $row->reading_date ) ) : 0;
$section_id = ! empty( $row->section_id ) ? (int) $row->section_id : 0;
if ( ! $year || ! $section_id || empty( $rate_lookup[ $year ][ $section_id ] ) ) {
return null;
}
$rate = $rate_lookup[ $year ][ $section_id ];
if ( 'power' === $row->type ) {
return null !== $rate->power_price_per_kwh ? (float) $rate->power_price_per_kwh : null;
}
return null !== $rate->water_price_per_m3 ? (float) $rate->water_price_per_m3 : null;
}
/**
* Calculate the cost for a consumption delta.
*
* @param float|int|null $consumption Consumption amount.
* @param string $type Meter type.
* @param float|null $unit_price Saved price per unit.
* @return float|null
*/
private function calculate_consumption_cost( $consumption, $type, $unit_price ) {
if ( null === $consumption || null === $unit_price ) {
return null;
}
$amount = (float) $consumption;
if ( 'power' === $type ) {
$settings = wp_parse_args( get_option( 'kgvvm_settings', array() ), $this->get_settings_defaults() );
if ( 'mwh' === $settings['power_unit'] ) {
$amount *= 1000;
}
}
return $amount * (float) $unit_price;
}
/**
* Return the display unit for a meter type.
*
* @param string $type Meter type.
* @return string
*/
private function meter_unit_label( $type ) {
if ( 'water' === $type ) {
return 'm³';
}
$settings = wp_parse_args( get_option( 'kgvvm_settings', array() ), $this->get_settings_defaults() );
return 'mwh' === $settings['power_unit'] ? 'MWh' : 'kWh';
}
/**
* Build colored status badge HTML.
*
* @param string $label Visible label.
* @param string $color green|orange|gray.
* @return string
*/
private function status_badge( $label, $color ) {
return '<span class="kgvvm-status kgvvm-status--' . esc_attr( $color ) . '">' . esc_html( $label ) . '</span>';
}
/**
* Human parcel status label.
*
* @param string $status Raw status.
* @return string
*/
private function parcel_status_label( $status ) {
$map = array(
'free' => __( 'Frei', KGVVM_TEXT_DOMAIN ),
'assigned' => __( 'Vergeben', KGVVM_TEXT_DOMAIN ),
'reserved' => __( 'Reserviert', KGVVM_TEXT_DOMAIN ),
'inactive' => __( 'Inaktiv', KGVVM_TEXT_DOMAIN ),
);
return isset( $map[ $status ] ) ? $map[ $status ] : $status;
}
/**
* Human meter type label.
*
* @param string $type water|power.
* @return string
*/
private function meter_type_label( $type ) {
return 'water' === $type ? __( 'Wasser', KGVVM_TEXT_DOMAIN ) : __( 'Strom', KGVVM_TEXT_DOMAIN );
}
/**
* Format a short meter label for lists and select boxes.
*
* @param object $meter Meter record.
* @return string
*/
private function format_meter_short_label( $meter ) {
$parts = array(
$this->meter_type_label( $meter->type ),
$meter->meter_number,
);
if ( ! empty( $meter->calibration_year ) ) {
$parts[] = sprintf( __( 'Eichjahr %s', KGVVM_TEXT_DOMAIN ), $meter->calibration_year );
}
return implode( ' ', array_filter( $parts ) );
}
/**
* Format one member label for tenant views.
*
* @param object $member Member row.
* @return string
*/
private function format_tenant_member_label( $member ) {
$label = $member->display_name;
if ( ! empty( $member->parcel_label ) ) {
$label .= ' ' . sprintf( __( 'Parzelle %s', KGVVM_TEXT_DOMAIN ), $member->parcel_label );
}
return $label;
}
/**
* Format a full tenant name.
*
* @param object $tenant Tenant row.
* @return string
*/
private function format_tenant_full_name( $tenant ) {
return trim( $tenant->last_name . ', ' . $tenant->first_name, ' ,' );
}
/**
* Ensure capability.
*
* @param string $cap Capability name.
* @return void
*/
private function require_cap( $cap ) {
if ( ! current_user_can( $cap ) ) {
wp_die( esc_html__( 'Sie haben keine Berechtigung für diese Aktion.', KGVVM_TEXT_DOMAIN ) );
}
}
/**
* Render one-time admin notice.
*
* @return void
*/
private function render_notice() {
$key = 'kgvvm_notice_' . get_current_user_id();
$notice = get_transient( $key );
if ( empty( $notice['message'] ) ) {
return;
}
delete_transient( $key );
$type = ! empty( $notice['type'] ) ? $notice['type'] : 'success';
switch ( $type ) {
case 'warning':
$class = 'notice notice-warning';
break;
case 'success':
$class = 'notice notice-success is-dismissible';
break;
default:
$class = 'notice notice-error';
break;
}
echo '<div class="' . esc_attr( $class ) . '"><p>' . esc_html( $notice['message'] ) . '</p></div>';
}
/**
* Redirect with a transient notice.
*
* @param string $page Target page slug.
* @param string $type success|error.
* @param string $message Notice text.
* @param array $args Extra query args.
* @return void
*/
private function redirect_with_notice( $page, $type, $message, $args = array() ) {
set_transient(
'kgvvm_notice_' . get_current_user_id(),
array(
'type' => $type,
'message' => wp_strip_all_tags( $message ),
),
60
);
wp_safe_redirect( $this->admin_url( $page, $args ) );
exit;
}
/**
* Build admin page URL.
*
* @param string $page Page slug.
* @param array $args Additional args.
* @return string
*/
private function admin_url( $page, $args = array() ) {
return add_query_arg( array_merge( array( 'page' => $page ), $args ), admin_url( 'admin.php' ) );
}
/**
* Build sortable list URL while preserving filters.
*
* @param string $page Page slug.
* @param string $column Sort column.
* @return string
*/
private function sort_url( $page, $column ) {
$current_orderby = isset( $_GET['orderby'] ) ? sanitize_key( wp_unslash( $_GET['orderby'] ) ) : '';
$current_order = isset( $_GET['order'] ) ? strtolower( sanitize_key( wp_unslash( $_GET['order'] ) ) ) : 'asc';
$next_order = ( $current_orderby === $column && 'asc' === $current_order ) ? 'desc' : 'asc';
$args = array();
foreach ( $_GET as $key => $value ) {
if ( in_array( $key, array( '_wpnonce', 'kgvvm_action', 'view', 'id' ), true ) ) {
continue;
}
$args[ $key ] = wp_unslash( $value );
}
$args['page'] = $page;
$args['orderby'] = $column;
$args['order'] = $next_order;
return add_query_arg( $args, admin_url( 'admin.php' ) );
}
// =========================================================================
// ARBEITSSTUNDEN
// =========================================================================
/**
* Save work year configuration.
*
* @return void
*/
private function save_work_year_config() {
$this->require_cap( 'manage_kleingarten' );
check_admin_referer( 'kgvvm_save_work_year_config' );
$year = absint( isset( $_POST['entry_year'] ) ? $_POST['entry_year'] : 0 );
$required_hours = isset( $_POST['required_hours'] ) ? (float) $_POST['required_hours'] : 0;
$price_per_missing_hour = isset( $_POST['price_per_missing_hour'] ) ? (float) $_POST['price_per_missing_hour'] : 0;
if ( $year < 2000 || $year > 2100 ) {
$this->redirect_with_notice( 'kgvvm-arbeit', 'error', __( 'Bitte ein gültiges Jahr eingeben.', KGVVM_TEXT_DOMAIN ) );
}
$this->work->save_year_config( $year, $required_hours, $price_per_missing_hour );
$this->redirect_with_notice( 'kgvvm-arbeit', 'success', __( 'Jahreseinstellungen wurden gespeichert.', KGVVM_TEXT_DOMAIN ), array( 'year' => $year ) );
}
/**
* Save work job (add/update).
*
* @return void
*/
private function save_work_job() {
$this->require_cap( 'manage_kleingarten' );
check_admin_referer( 'kgvvm_save_work_job' );
$id = absint( isset( $_POST['id'] ) ? $_POST['id'] : 0 );
$name = sanitize_text_field( wp_unslash( isset( $_POST['job_name'] ) ? $_POST['job_name'] : '' ) );
if ( '' === $name ) {
$this->redirect_with_notice( 'kgvvm-arbeit', 'error', __( 'Bitte einen Namen für die Arbeitsart eingeben.', KGVVM_TEXT_DOMAIN ), array( 'tab' => 'jobs' ) );
}
if ( $this->work->job_name_exists( $name, $id ) ) {
$this->redirect_with_notice( 'kgvvm-arbeit', 'error', __( 'Eine Arbeitsart mit diesem Namen existiert bereits.', KGVVM_TEXT_DOMAIN ), array( 'tab' => 'jobs' ) );
}
$this->work->save_job(
array(
'name' => $name,
'description' => isset( $_POST['job_description'] ) ? wp_unslash( $_POST['job_description'] ) : '',
),
$id
);
$this->redirect_with_notice( 'kgvvm-arbeit', 'success', __( 'Arbeitsart wurde gespeichert.', KGVVM_TEXT_DOMAIN ), array( 'tab' => 'jobs' ) );
}
/**
* Save work log entry (add/update).
*
* @return void
*/
private function save_work_log() {
$this->require_cap( 'manage_kleingarten' );
check_admin_referer( 'kgvvm_save_work_log' );
$id = absint( isset( $_POST['id'] ) ? $_POST['id'] : 0 );
$job_id = absint( isset( $_POST['job_id'] ) ? $_POST['job_id'] : 0 );
$work_date = sanitize_text_field( wp_unslash( isset( $_POST['work_date'] ) ? $_POST['work_date'] : '' ) );
$year = $work_date ? (int) substr( $work_date, 0, 4 ) : (int) current_time( 'Y' );
if ( '' === $work_date ) {
$this->redirect_with_notice( 'kgvvm-arbeit', 'error', __( 'Bitte ein gültiges Datum eingeben.', KGVVM_TEXT_DOMAIN ), array( 'year' => $year ) );
}
$raw_members = isset( $_POST['member_hours'] ) ? (array) wp_unslash( $_POST['member_hours'] ) : array();
$members = array();
foreach ( $raw_members as $uid => $hours ) {
$uid = absint( $uid );
$hours = (float) $hours;
if ( $uid > 0 && $hours > 0 ) {
$members[ $uid ] = $hours;
}
}
if ( empty( $members ) ) {
$this->redirect_with_notice( 'kgvvm-arbeit', 'error', __( 'Bitte mindestens ein Mitglied mit Stundenzahl zuordnen.', KGVVM_TEXT_DOMAIN ), array( 'year' => $year ) );
}
$this->work->save_log(
array(
'job_id' => $job_id,
'work_date' => $work_date,
'note' => isset( $_POST['note'] ) ? wp_unslash( $_POST['note'] ) : '',
),
$id,
$members
);
$this->redirect_with_notice( 'kgvvm-arbeit', 'success', __( 'Arbeitseintrag wurde gespeichert.', KGVVM_TEXT_DOMAIN ), array( 'year' => $year ) );
}
/**
* Render the Arbeitsstunden admin page.
*
* @return void
*/
public function render_work_page() {
$this->require_cap( 'manage_kleingarten' );
$tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'logs';
$selected_year = isset( $_GET['year'] ) ? absint( $_GET['year'] ) : (int) current_time( 'Y' );
$selected_year = $selected_year > 0 ? $selected_year : (int) current_time( 'Y' );
$edit_log_id = absint( isset( $_GET['id'] ) ? $_GET['id'] : 0 );
$edit_job_id = absint( isset( $_GET['job_id'] ) ? $_GET['job_id'] : 0 );
$years = $this->work->get_years( $selected_year );
$year_config = $this->work->get_year_config( $selected_year );
$jobs = $this->work->get_jobs();
$all_members = $this->assignments->get_member_users();
$edit_log = null;
$edit_job = null;
if ( $edit_log_id ) {
$edit_log = $this->work->find_log( $edit_log_id );
}
if ( $edit_job_id ) {
$edit_job = $this->work->find_job( $edit_job_id );
}
$logs = $this->work->search_logs(
array(
'year' => $selected_year,
'orderby' => isset( $_GET['orderby'] ) ? sanitize_key( wp_unslash( $_GET['orderby'] ) ) : 'work_date',
'order' => isset( $_GET['order'] ) ? sanitize_key( wp_unslash( $_GET['order'] ) ) : 'DESC',
's' => isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '',
)
);
$summary = $this->work->get_member_summary( $selected_year, $all_members );
?>
<div class='wrap'>
<h1 class='wp-heading-inline'><?php echo esc_html__( 'Arbeitsstunden', KGVVM_TEXT_DOMAIN ); ?></h1>
<?php $this->render_notice(); ?>
<?php /* Year filter */ ?>
<form method='get' class='kgvvm-toolbar' style='margin-top:12px;'>
<input type='hidden' name='page' value='kgvvm-arbeit' />
<input type='hidden' name='tab' value='<?php echo esc_attr( $tab ); ?>' />
<div class='kgvvm-filters'>
<label for='kgvvm-work-year'><?php echo esc_html__( 'Jahr', KGVVM_TEXT_DOMAIN ); ?></label>
<select name='year' id='kgvvm-work-year'>
<?php foreach ( $years as $year ) : ?>
<option value='<?php echo esc_attr( $year ); ?>' <?php selected( $selected_year, $year ); ?>><?php echo esc_html( $year ); ?></option>
<?php endforeach; ?>
</select>
<button class='button'><?php echo esc_html__( 'Filtern', KGVVM_TEXT_DOMAIN ); ?></button>
</div>
</form>
<?php /* Year config card */ ?>
<div class='kgvvm-card' style='max-width:560px;margin-top:16px;'>
<h2><?php echo esc_html( sprintf( __( 'Jahreseinstellungen %d', KGVVM_TEXT_DOMAIN ), $selected_year ) ); ?></h2>
<form method='post'>
<?php wp_nonce_field( 'kgvvm_save_work_year_config' ); ?>
<input type='hidden' name='kgvvm_action' value='save_work_year_config' />
<input type='hidden' name='entry_year' value='<?php echo esc_attr( $selected_year ); ?>' />
<table class='form-table kgvvm-form-table'>
<tr>
<th scope='row'><label for='kgvvm-work-required-hours'><?php echo esc_html__( 'Pflichtstunden je Mitglied', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input type='number' min='0' step='0.5' name='required_hours' id='kgvvm-work-required-hours'
value='<?php echo esc_attr( $year_config ? (float) $year_config->required_hours : 0 ); ?>' /> <?php echo esc_html__( 'Std.', KGVVM_TEXT_DOMAIN ); ?></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-work-price'><?php echo esc_html__( 'Preis je fehlender Stunde', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input type='number' min='0' step='0.01' name='price_per_missing_hour' id='kgvvm-work-price'
value='<?php echo esc_attr( $year_config ? (float) $year_config->price_per_missing_hour : 0 ); ?>' /> € / Std.</td>
</tr>
</table>
<?php submit_button( __( 'Einstellungen speichern', KGVVM_TEXT_DOMAIN ), 'secondary' ); ?>
</form>
</div>
<?php /* Tabs */ ?>
<nav class='nav-tab-wrapper' style='margin-top:20px;'>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-arbeit', array( 'year' => $selected_year, 'tab' => 'logs' ) ) ); ?>'
class='nav-tab <?php echo 'logs' === $tab ? 'nav-tab-active' : ''; ?>'><?php echo esc_html__( 'Geleistete Arbeiten', KGVVM_TEXT_DOMAIN ); ?></a>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-arbeit', array( 'year' => $selected_year, 'tab' => 'summary' ) ) ); ?>'
class='nav-tab <?php echo 'summary' === $tab ? 'nav-tab-active' : ''; ?>'><?php echo esc_html__( 'Mitgliederübersicht', KGVVM_TEXT_DOMAIN ); ?></a>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-arbeit', array( 'year' => $selected_year, 'tab' => 'jobs' ) ) ); ?>'
class='nav-tab <?php echo 'jobs' === $tab ? 'nav-tab-active' : ''; ?>'><?php echo esc_html__( 'Arbeitsarten', KGVVM_TEXT_DOMAIN ); ?></a>
</nav>
<?php if ( 'jobs' === $tab ) : ?>
<?php /* ---- ARBEITSARTEN ---- */ ?>
<div class='kgvvm-card' style='margin-top:0;border-top:none;padding-top:20px;'>
<h2><?php echo esc_html( $edit_job ? __( 'Arbeitsart bearbeiten', KGVVM_TEXT_DOMAIN ) : __( 'Neue Arbeitsart', KGVVM_TEXT_DOMAIN ) ); ?></h2>
<form method='post'>
<?php wp_nonce_field( 'kgvvm_save_work_job' ); ?>
<input type='hidden' name='kgvvm_action' value='save_work_job' />
<input type='hidden' name='id' value='<?php echo esc_attr( $edit_job ? $edit_job->id : 0 ); ?>' />
<table class='form-table kgvvm-form-table'>
<tr>
<th scope='row'><label for='kgvvm-job-name'><?php echo esc_html__( 'Name', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input type='text' class='regular-text' name='job_name' id='kgvvm-job-name' required
value='<?php echo esc_attr( $edit_job ? $edit_job->name : '' ); ?>' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-job-description'><?php echo esc_html__( 'Beschreibung', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><textarea name='job_description' id='kgvvm-job-description' rows='3' class='large-text'><?php echo esc_textarea( $edit_job ? $edit_job->description : '' ); ?></textarea></td>
</tr>
</table>
<?php submit_button( $edit_job ? __( 'Arbeitsart aktualisieren', KGVVM_TEXT_DOMAIN ) : __( 'Arbeitsart anlegen', KGVVM_TEXT_DOMAIN ) ); ?>
<?php if ( $edit_job ) : ?>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-arbeit', array( 'year' => $selected_year, 'tab' => 'jobs' ) ) ); ?>' class='button'><?php echo esc_html__( 'Abbrechen', KGVVM_TEXT_DOMAIN ); ?></a>
<?php endif; ?>
</form>
</div>
<?php if ( ! empty( $jobs ) ) : ?>
<table class='wp-list-table widefat fixed striped' style='margin-top:16px;'>
<thead>
<tr>
<th><?php echo esc_html__( 'Name', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Beschreibung', KGVVM_TEXT_DOMAIN ); ?></th>
<th style='width:120px;'><?php echo esc_html__( 'Aktionen', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $jobs as $job ) : ?>
<tr>
<td><strong><?php echo esc_html( $job->name ); ?></strong></td>
<td><?php echo esc_html( $job->description ); ?></td>
<td>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-arbeit', array( 'tab' => 'jobs', 'year' => $selected_year, 'job_id' => $job->id ) ) ); ?>'><?php echo esc_html__( 'Bearbeiten', KGVVM_TEXT_DOMAIN ); ?></a>
&nbsp;|&nbsp;
<a href='<?php echo esc_url( wp_nonce_url( $this->admin_url( 'kgvvm-arbeit', array( 'kgvvm_action' => 'delete_work_job', 'id' => $job->id, 'year' => $selected_year, 'tab' => 'jobs' ) ), 'kgvvm_delete_work_job_' . $job->id ) ); ?>'
class='kgvvm-delete-link' onclick='return confirm("<?php echo esc_attr__( 'Diese Arbeitsart und alle zugehörigen Einträge wirklich löschen?', KGVVM_TEXT_DOMAIN ); ?>")'><?php echo esc_html__( 'Löschen', KGVVM_TEXT_DOMAIN ); ?></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else : ?>
<p class='kgvvm-help'><?php echo esc_html__( 'Noch keine Arbeitsarten angelegt.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php endif; ?>
<?php elseif ( 'summary' === $tab ) : ?>
<?php /* ---- MITGLIEDERÜBERSICHT ---- */ ?>
<div style='margin-top:0;'>
<?php if ( ! $year_config ) : ?>
<p class='notice notice-warning inline'><?php echo esc_html__( 'Für dieses Jahr wurden noch keine Jahreseinstellungen hinterlegt.', KGVVM_TEXT_DOMAIN ); ?></p>
<?php endif; ?>
<table class='wp-list-table widefat fixed striped' style='margin-top:16px;'>
<thead>
<tr>
<th><?php echo esc_html__( 'Mitglied', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Geleistet (Std.)', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Pflicht (Std.)', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Fehlend (Std.)', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Aufschlag (€)', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php if ( empty( $summary ) ) : ?>
<tr><td colspan='5'><?php echo esc_html__( 'Keine Mitglieder gefunden.', KGVVM_TEXT_DOMAIN ); ?></td></tr>
<?php else : ?>
<?php foreach ( $summary as $row ) : ?>
<tr>
<td><?php echo esc_html( $row->display_name ); ?></td>
<td><?php echo esc_html( number_format_i18n( $row->completed_hours, 2 ) ); ?></td>
<td><?php echo esc_html( number_format_i18n( $row->required_hours, 2 ) ); ?></td>
<td><?php echo $row->missing_hours > 0 ? '<strong style="color:#c00">' . esc_html( number_format_i18n( $row->missing_hours, 2 ) ) . '</strong>' : esc_html( number_format_i18n( 0, 2 ) ); ?></td>
<td><?php echo $row->surcharge > 0 ? '<strong style="color:#c00">' . esc_html( $this->format_currency( $row->surcharge ) ) . '</strong>' : '—'; ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php else : ?>
<?php /* ---- GELEISTETE ARBEITEN (logs) ---- */ ?>
<?php /* New/edit log form */ ?>
<div class='kgvvm-card' style='margin-top:0;border-top:none;padding-top:20px;'>
<h2><?php echo esc_html( $edit_log ? __( 'Arbeitseintrag bearbeiten', KGVVM_TEXT_DOMAIN ) : __( 'Neuer Arbeitseintrag', KGVVM_TEXT_DOMAIN ) ); ?></h2>
<form method='post'>
<?php wp_nonce_field( 'kgvvm_save_work_log' ); ?>
<input type='hidden' name='kgvvm_action' value='save_work_log' />
<input type='hidden' name='id' value='<?php echo esc_attr( $edit_log ? $edit_log->id : 0 ); ?>' />
<table class='form-table kgvvm-form-table'>
<tr>
<th scope='row'><label for='kgvvm-log-date'><?php echo esc_html__( 'Datum', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><input type='date' name='work_date' id='kgvvm-log-date' required
value='<?php echo esc_attr( $edit_log ? esc_attr( $edit_log->work_date ) : current_time( 'Y-m-d' ) ); ?>' /></td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-log-job'><?php echo esc_html__( 'Arbeitsart', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<select name='job_id' id='kgvvm-log-job'>
<option value='0'><?php echo esc_html__( '— keine Zuordnung —', KGVVM_TEXT_DOMAIN ); ?></option>
<?php foreach ( $jobs as $job ) : ?>
<option value='<?php echo esc_attr( $job->id ); ?>' <?php selected( $edit_log ? (int) $edit_log->job_id : 0, (int) $job->id ); ?>><?php echo esc_html( $job->name ); ?></option>
<?php endforeach; ?>
</select>
</td>
</tr>
<tr>
<th scope='row'><label><?php echo esc_html__( 'Mitglieder & Stunden', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td>
<?php
$existing_member_hours = array();
if ( $edit_log && ! empty( $edit_log->members ) ) {
foreach ( $edit_log->members as $m ) {
$existing_member_hours[ (int) $m->user_id ] = (float) $m->hours;
}
}
?>
<table class='kgvvm-subtable'>
<thead><tr><th><?php echo esc_html__( 'Mitglied', KGVVM_TEXT_DOMAIN ); ?></th><th><?php echo esc_html__( 'Stunden', KGVVM_TEXT_DOMAIN ); ?></th></tr></thead>
<tbody>
<?php foreach ( $all_members as $member ) : ?>
<?php
$uid = (int) $member->ID;
$hours_val = isset( $existing_member_hours[ $uid ] ) ? $existing_member_hours[ $uid ] : '';
?>
<tr>
<td><?php echo esc_html( $member->display_name ); ?></td>
<td><input type='number' min='0' step='0.25' style='width:80px;'
name='member_hours[<?php echo esc_attr( $uid ); ?>]'
value='<?php echo esc_attr( $hours_val ); ?>'
placeholder='0' /></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<p class='description'><?php echo esc_html__( 'Nur Mitglieder mit eingetragenen Stunden werden gespeichert.', KGVVM_TEXT_DOMAIN ); ?></p>
</td>
</tr>
<tr>
<th scope='row'><label for='kgvvm-log-note'><?php echo esc_html__( 'Notiz', KGVVM_TEXT_DOMAIN ); ?></label></th>
<td><textarea name='note' id='kgvvm-log-note' rows='3' class='large-text'><?php echo esc_textarea( $edit_log ? $edit_log->note : '' ); ?></textarea></td>
</tr>
</table>
<?php submit_button( $edit_log ? __( 'Arbeitseintrag aktualisieren', KGVVM_TEXT_DOMAIN ) : __( 'Arbeitseintrag speichern', KGVVM_TEXT_DOMAIN ) ); ?>
<?php if ( $edit_log ) : ?>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-arbeit', array( 'year' => $selected_year ) ) ); ?>' class='button'><?php echo esc_html__( 'Abbrechen', KGVVM_TEXT_DOMAIN ); ?></a>
<?php endif; ?>
</form>
</div>
<?php /* Log list */ ?>
<?php if ( ! empty( $logs ) ) : ?>
<table class='wp-list-table widefat fixed striped' style='margin-top:16px;'>
<thead>
<tr>
<th><?php echo esc_html__( 'Datum', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Arbeitsart', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Mitglieder', KGVVM_TEXT_DOMAIN ); ?></th>
<th><?php echo esc_html__( 'Notiz', KGVVM_TEXT_DOMAIN ); ?></th>
<th style='width:120px;'><?php echo esc_html__( 'Aktionen', KGVVM_TEXT_DOMAIN ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $logs as $log ) : ?>
<?php $log_members = $this->work->get_log_members( $log->id ); ?>
<tr>
<td><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $log->work_date ) ) ); ?></td>
<td><?php echo esc_html( $log->job_name ?: '—' ); ?></td>
<td>
<?php foreach ( $log_members as $lm ) :
$u = get_userdata( $lm->user_id );
if ( $u ) : ?>
<?php echo esc_html( $u->display_name ); ?>: <strong><?php echo esc_html( number_format_i18n( (float) $lm->hours, 2 ) ); ?> Std.</strong><br>
<?php endif; ?>
<?php endforeach; ?>
</td>
<td><?php echo esc_html( $log->note ); ?></td>
<td>
<a href='<?php echo esc_url( $this->admin_url( 'kgvvm-arbeit', array( 'year' => $selected_year, 'id' => $log->id ) ) ); ?>'><?php echo esc_html__( 'Bearbeiten', KGVVM_TEXT_DOMAIN ); ?></a>
&nbsp;|&nbsp;
<a href='<?php echo esc_url( wp_nonce_url( $this->admin_url( 'kgvvm-arbeit', array( 'kgvvm_action' => 'delete_work_log', 'id' => $log->id, 'year' => $selected_year ) ), 'kgvvm_delete_work_log_' . $log->id ) ); ?>'
class='kgvvm-delete-link' onclick='return confirm("<?php echo esc_attr__( 'Diesen Arbeitseintrag wirklich löschen?', KGVVM_TEXT_DOMAIN ); ?>")'><?php echo esc_html__( 'Löschen', KGVVM_TEXT_DOMAIN ); ?></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else : ?>
<p class='kgvvm-help' style='margin-top:12px;'><?php echo esc_html( sprintf( __( 'Keine Arbeitseinträge für %d vorhanden.', KGVVM_TEXT_DOMAIN ), $selected_year ) ); ?></p>
<?php endif; ?>
<?php endif; ?>
</div>
<?php
}
}