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(); global $wpdb; $this->data_transfer = new DataTransfer( $wpdb ); } /** * 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; case 'import_plugin_data': $this->import_plugin_data(); break; case 'toggle_parcel_cost_assignment': $this->toggle_parcel_cost_assignment(); 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_assignments_for_entry( $id ); $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 'export_plugin_data': $this->require_cap( Roles::SETTINGS_CAP ); if ( ! wp_verify_nonce( $nonce, 'kgvvm_export_plugin_data' ) ) { $this->redirect_with_notice( 'kgvvm-settings', 'error', __( 'Export wurde aus Sicherheitsgründen abgebrochen.', KGVVM_TEXT_DOMAIN ) ); } $this->data_transfer->send_download(); 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 ) ); } /** * Handle plugin data import from an uploaded JSON file. * * @return void */ private function import_plugin_data() { $this->require_cap( Roles::SETTINGS_CAP ); check_admin_referer( 'kgvvm_import_plugin_data' ); if ( empty( $_FILES['kgvvm_import_file']['tmp_name'] ) ) { $this->redirect_with_notice( 'kgvvm-settings', 'error', __( 'Bitte eine JSON-Datei auswählen.', KGVVM_TEXT_DOMAIN ) ); } $file = $_FILES['kgvvm_import_file']; // Validate MIME type. $finfo = new \finfo( FILEINFO_MIME_TYPE ); $mimetype = $finfo->file( $file['tmp_name'] ); if ( ! in_array( $mimetype, array( 'application/json', 'text/plain' ), true ) ) { $this->redirect_with_notice( 'kgvvm-settings', 'error', __( 'Die hochgeladene Datei ist keine gültige JSON-Datei.', KGVVM_TEXT_DOMAIN ) ); } $raw = file_get_contents( $file['tmp_name'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents if ( false === $raw ) { $this->redirect_with_notice( 'kgvvm-settings', 'error', __( 'Die Datei konnte nicht gelesen werden.', KGVVM_TEXT_DOMAIN ) ); } $payload = json_decode( $raw, true ); if ( ! is_array( $payload ) ) { $this->redirect_with_notice( 'kgvvm-settings', 'error', __( 'Die JSON-Datei konnte nicht verarbeitet werden. Bitte eine gültige Exportdatei hochladen.', KGVVM_TEXT_DOMAIN ) ); } $result = $this->data_transfer->import( $payload ); if ( ! empty( $result['errors'] ) ) { $this->redirect_with_notice( 'kgvvm-settings', 'error', implode( ' | ', $result['errors'] ) ); } $summary = array(); foreach ( $result['imported'] as $key => $count ) { $summary[] = $key . ': ' . $count; } $this->redirect_with_notice( 'kgvvm-settings', 'success', sprintf( /* translators: %s: summary of imported rows per table */ __( 'Import erfolgreich. Importierte Datensätze – %s', KGVVM_TEXT_DOMAIN ), implode( ', ', $summary ) ) ); } /** * 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'] ) ); } /** * Add or remove a cost entry assignment for a specific parcel. * * @return void */ private function toggle_parcel_cost_assignment() { $this->require_cap( 'manage_kleingarten' ); check_admin_referer( 'kgvvm_toggle_parcel_cost' ); $parcel_id = absint( isset( $_POST['parcel_id'] ) ? $_POST['parcel_id'] : 0 ); $cost_entry_id = absint( isset( $_POST['cost_entry_id'] ) ? $_POST['cost_entry_id'] : 0 ); $mode = isset( $_POST['assignment_mode'] ) ? sanitize_key( wp_unslash( $_POST['assignment_mode'] ) ) : ''; $year = absint( isset( $_POST['year'] ) ? $_POST['year'] : 0 ); if ( $parcel_id < 1 || $cost_entry_id < 1 || ! in_array( $mode, array( 'add', 'remove' ), true ) ) { $this->redirect_with_notice( 'kgvvm-costs', 'error', __( 'Ungültige Anfrage.', KGVVM_TEXT_DOMAIN ), array( 'view' => 'statement', 'statement_type' => 'parcel', 'subject_id' => $parcel_id, 'year' => $year ) ); } if ( 'add' === $mode ) { $this->costs->assign_to_parcel( $parcel_id, $cost_entry_id ); } else { $this->costs->unassign_from_parcel( $parcel_id, $cost_entry_id ); } wp_safe_redirect( $this->admin_url( 'kgvvm-costs', array( 'view' => 'statement', 'statement_type' => 'parcel', 'subject_id' => $parcel_id, 'year' => $year ) ) ); exit; } /** * 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() ); $all_members = $this->assignments->get_member_users(); $members_total = count( $all_members ); // Arbeitsstunden laufendes Jahr $work_year = (int) current_time( 'Y' ); $work_config = $this->work->get_year_config( $work_year ); $work_summary = $this->work->get_member_summary( $work_year, $all_members ); $members_ok = 0; $members_open = 0; $total_missing_h = 0.0; $total_surcharge = 0.0; foreach ( $work_summary as $ws ) { if ( $ws->missing_hours > 0 ) { $members_open++; $total_missing_h += $ws->missing_hours; $total_surcharge += $ws->surcharge; } else { $members_ok++; } } ?>

render_notice(); ?>

$work_year ) ) ); ?>'>

Std.

format_currency( $total_surcharge ) ); ?>

0 ) : ?>

$work_year, 'tab' => 'summary' ) ) ); ?>' class='button button-secondary'>

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; } } ?>

render_notice(); ?>

format_meter_value_with_unit( $water_total, 'water' ) ); ?>

format_meter_value_with_unit( $power_total, 'power' ) ); ?>

format_currency( $water_cost_total ) ); ?>

format_currency( $power_cost_total ) ); ?>

format_meter_value_with_unit( $section_total['water'], 'water' ) ); ?> 0 ? $this->format_currency( $section_total['water_cost'] ) : '—' ); ?> format_meter_value_with_unit( $section_total['power'], 'power' ) ); ?> 0 ? $this->format_currency( $section_total['power_cost'] ) : '—' ); ?>
reading_date ) ) ); ?> section_name ? $row->section_name : '—' ); ?> parcel_label ? $row->parcel_label : '—' ); ?> meter_type_label( $row->type ) . ' – ' . $row->meter_number ); ?> previous_value ? $this->format_meter_value_with_unit( $row->previous_value, $row->type ) : '—' ); ?> format_meter_value_with_unit( $row->reading_value, $row->type ) ); ?> consumption ? $this->format_meter_value_with_unit( $row->consumption, $row->type ) : '—' ); ?> unit_price ? $this->format_price_per_unit( $row->unit_price, 'power' === $row->type ? 'kWh' : 'm³' ) : '—' ); ?> calculated_cost ? $this->format_currency( $row->calculated_cost ) : '—' ); ?>
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 ); } ?>

$selected_year ) ) ); ?>' class='page-title-action'> render_notice(); ?>
' />

format_currency( $year_total ) ); ?>

0 ? $this->format_currency( $parcel_share ) : '—' ); ?>

0 ? $this->format_currency( $member_share ) : '—' ); ?>

' /> € / kWh
' /> € / m³
$selected_year ) ) ); ?>' class='button-secondary'>

' />
' />



section_name ? $rate->section_name : '—' ); ?> power_price_per_kwh ? $this->format_price_per_unit( $rate->power_price_per_kwh, 'kWh' ) : '—' ); ?> water_price_per_m3 ? $this->format_price_per_unit( $rate->water_price_per_m3, 'm³' ) : '—' ); ?> updated_at ) ? wp_date( 'd.m.Y H:i', strtotime( $rate->updated_at ) ) : '—' ); ?> $selected_year, 'edit_rate_section_id' => $rate->section_id ) ) ); ?>'>
'> '> '>
name ); ?> get_cost_distribution_label( $row->distribution_type ) ); ?> format_currency( $row->unit_amount ) ); ?> format_currency( $row->calculated_total_cost ) ); ?> note ? $row->note : '—' ); ?> updated_at ) ? wp_date( 'd.m.Y H:i', strtotime( $row->updated_at ) ) : '—' ); ?> $selected_year, 'id' => $row->id ) ) ); ?>'> | 'delete_cost', 'id' => $row->id, 'year' => $selected_year ) ), 'kgvvm_delete_cost_' . $row->id ) ); ?>' onclick='return confirm("");'>

label ); ?> section_name ? $parcel->section_name : '—' ); ?> parcel_status_label( $parcel->status ) ); ?> tenant_count ) ? number_format_i18n( (int) $parcel->tenant_count, 0 ) : '—' ); ?> format_currency( $parcel_share ) ); ?>

display_name ); ?> user_email ? $member->user_email : '—' ); ?> 0 ? $this->format_currency( $member_share ) : '—' ); ?>
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; // Load parcel-specific cost assignments so entries can either apply to // all parcels (mandatory) or only to explicitly assigned parcels (manual). $entries_with_assignments = $this->costs->get_entry_ids_with_assignments( $year ); $subject_assigned_ids = array(); foreach ( array_map( 'intval', $parcel_ids ) as $parcel_id ) { $subject_assigned_ids = array_merge( $subject_assigned_ids, $this->costs->get_assigned_entry_ids( $parcel_id ) ); } $subject_assigned_ids = array_values( array_unique( array_map( 'intval', $subject_assigned_ids ) ) ); foreach ( $cost_entries as $entry ) { $entry_id = (int) $entry->id; $has_assignments = in_array( $entry_id, $entries_with_assignments, true ); $is_assigned = in_array( $entry_id, $subject_assigned_ids, true ); $is_mandatory = ! isset( $entry->is_mandatory ) || (bool) $entry->is_mandatory; if ( ! $is_mandatory && ! $is_assigned ) { continue; } if ( $is_mandatory && $has_assignments && ! $is_assigned ) { continue; } $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; } ?>

'statement', 'statement_type' => $statement_type, 'subject_id' => $subject_id, 'year' => $year, 'output' => 'pdf' ) ) ); ?>' class='button button-primary' target='_blank'> $year ) ) ); ?>' class='button-secondary'>
' alt='' style='max-width:120px; height:auto;' />

  • :

format_currency( $fixed_total ) ); ?>

format_currency( $water_cost_total ) ); ?>

format_currency( $power_cost_total ) ); ?>

format_currency( $grand_total ) ); ?>


format_currency( $item['unit_amount'] ) ) ); ?> ' . esc_html( $item['note'] ) . '' : ''; ?>
format_currency( $item['total'] ) ); ?> format_currency( $item['share'] ) ); ?>

build_statement_meter_detail_table( $this->get_statement_rows_by_meter_type( $utility_rows, 'water' ), 'water' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

build_statement_meter_detail_table( $this->get_statement_rows_by_meter_type( $utility_rows, 'power' ), 'power' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

    id; $is_assigned = in_array( $entry_id, $subject_assigned_ids, true ); $has_any = in_array( $entry_id, $entries_with_assignments, true ); $is_mandatory = ! isset( $entry->is_mandatory ) || (bool) $entry->is_mandatory; ?>
  • '>✓ '>✗ '>— name ); ?>
    ' style='display:inline;'>
    ' style='display:inline;'>

   

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(); ?>

:

format_currency( $statement['fixed_total'] ) ); ?>
format_currency( $statement['water_cost_total'] ) ); ?>
format_currency( $statement['power_cost_total'] ) ); ?>
format_currency( $statement['grand_total'] ) ); ?>

format_currency( $item['unit_amount'] ) ) ); ?> format_currency( $item['total'] ) ); ?> format_currency( $item['share'] ) ); ?>

build_statement_meter_detail_table( $this->get_statement_rows_by_meter_type( $statement['utility_rows'], 'water' ), 'water', true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

build_statement_meter_detail_table( $this->get_statement_rows_by_meter_type( $statement['utility_rows'], 'power' ), 'power', true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

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 ); ?>

'form' ) ) ); ?>' class='page-title-action'> render_notice(); ?>
' placeholder='' />
meters->get_main_for_section( $row->id ); ?>
'> '>
name ); ?> description ? $row->description : '—' ); ?> status_badge( 'active' === $row->status ? __( 'Aktiv', KGVVM_TEXT_DOMAIN ) : __( 'Inaktiv', KGVVM_TEXT_DOMAIN ), 'active' === $row->status ? 'green' : 'gray' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> 'form', 'id' => $row->id ) ) ); ?>'> | 'delete_section', 'id' => $row->id ) ), 'kgvvm_delete_section_' . $row->id ) ); ?>' onclick='return confirm("");'>
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' ); ?>

render_notice(); ?>
' />

' class='button-secondary'>

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'; ?>

format_meter_short_label( $main_meter ) ); ?>

format_meter_value_with_unit( $latest_main->reading_value, $main_meter->type ) . ' am ' . wp_date( 'd.m.Y', strtotime( $latest_main->reading_date ) ) ); ?>

reading_date ) ) ); ?> format_meter_value_with_unit( $reading->reading_value, $main_meter->type ) ); ?> reading_flag_badge( $reading ) ); ?>

format_meter_value_with_unit( $month_row['from_value'], $main_meter->type ) : '—' ); ?> format_meter_value_with_unit( $month_row['to_value'], $main_meter->type ) ); ?> format_meter_value_with_unit( $month_row['consumption'], $main_meter->type ) ); ?>
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(); ?>

'form' ) ) ); ?>' class='page-title-action'> render_notice(); ?>
' placeholder='' />

$per_page ) : ?>
add_query_arg( 'paged', '%#%' ), 'format' => '', 'current' => $paged, 'total' => $total_pages, 'prev_text' => '«', 'next_text' => '»', ) ) ); ?>
'> '> '> '>
label ); ?> section_name ); ?> area ? esc_html( number_format_i18n( (float) $row->area, 2 ) . ' m²' ) : '—'; ?> annual_rent ) && null !== $row->annual_rent ? esc_html( $this->format_currency( $row->annual_rent ) ) : '—'; ?> water_meter_number ? $row->water_meter_number : '—' ); ?> power_meter_number ? $row->power_meter_number : '—' ); ?> member_count ); ?> tenant_count ); ?> status_badge( $this->parcel_status_label( $row->status ), 'inactive' === $row->status ? 'gray' : ( 'reserved' === $row->status ? 'orange' : 'green' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> 'form', 'id' => $row->id ) ) ); ?>'> | 'delete_parcel', 'id' => $row->id ) ), 'kgvvm_delete_parcel_' . $row->id ) ); ?>' onclick='return confirm("");'>
$per_page ) : ?>
add_query_arg( 'paged', '%#%' ), 'format' => '', 'current' => $paged, 'total' => $total_pages, 'prev_text' => '«', 'next_text' => '»', ) ) ); ?>
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(); ?>

render_notice(); ?>

' />

' />
' />

' class='button-secondary'>
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(); ?>

'form' ) ) ); ?>' class='page-title-action'> render_notice(); ?>
' placeholder='' />
readings->get_latest_for_meter( $row->id ); ?>
'> '> '> '>
meter_number ); ?> meter_type_label( $row->type ) ); ?> section_name ? $row->section_name : '—' ); ?> parcel_label ? $row->parcel_label : ( ! empty( $row->is_main_meter ) ? __( 'Hauptzähler der Sparte', KGVVM_TEXT_DOMAIN ) : __( 'frei', KGVVM_TEXT_DOMAIN ) ) ); ?> installed_at ? wp_date( 'd.m.Y', strtotime( $row->installed_at ) ) : '—' ); ?> calibration_year ) ? (string) $row->calibration_year : '—' ); ?> 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 ?> 'form', 'id' => $row->id ) ) ); ?>'> is_active === 1 ) : ?> | | 'delete_meter', 'id' => $row->id ) ), 'kgvvm_delete_meter_' . $row->id ) ); ?>' onclick='return confirm("");'>
render_meter_swap_modal(); ?>
meters->find( $id ) : null; $sections = $this->sections->all_for_options(); $history = $id ? $this->readings->get_all_for_meter( $id, 50 ) : array(); ?>

render_notice(); ?>
' />
' />
' />

' class='button-secondary'>

reading_date ) ) ); ?> parcel_label ? $entry->parcel_label : '—' ); ?> format_meter_value_with_unit( $entry->reading_value, $meter->type ) ); ?> reading_flag_badge( $entry ) ); ?> note ? $entry->note : '—' ); ?>
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 ) ); } ?>

render_notice(); ?>

meter_number ); ?> (meter_type_label( $meter->type ) ); ?>) label : ' – ' . __( 'aktuell keiner Parzelle zugeordnet', KGVVM_TEXT_DOMAIN ) ); ?>
format_meter_value( $latest->reading_value, $meter->type ), $unit, wp_date( 'd.m.Y', strtotime( $latest->reading_date ) ) ) ); ?>

' />

' class='button-secondary'>
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 ); ?>

'form' ) ) ); ?>' class='page-title-action'> render_notice(); ?>
' placeholder='' />
assignments->get_members_for_tenant( $row->id ); ?>
'> '>
last_name . ', ' . $row->first_name ); ?> phone ? $row->phone : '—' ); ?> email ? $row->email : '—' ); ?> contract_start ? wp_date( 'd.m.Y', strtotime( $row->contract_start ) ) : '—' ); ?> parcel_count ); ?> 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 ?> 'form', 'id' => $row->id ) ) ); ?>'> | 'delete_tenant', 'id' => $row->id ) ), 'kgvvm_delete_tenant_' . $row->id ) ); ?>' onclick='return confirm("");'>
tenants->find( $id ) : null; $tenant_members = $id ? $this->assignments->get_members_for_tenant( $id ) : array(); ?>

render_notice(); ?>
' />
' />
' />
' />
' />
' />

  • format_tenant_member_label( $member ) ); ?>

' class='button-secondary'>
require_cap( 'view_assigned_parcels' ); $user_id = get_current_user_id(); $parcels = $this->assignments->get_parcels_for_user( $user_id ); ?>

render_notice(); ?>

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 ); ?>

section_name ? $parcel->section_name : '—' ); ?>
area ? esc_html( number_format_i18n( (float) $parcel->area, 2 ) . ' m²' ) : '—'; ?>
annual_rent ) && null !== $parcel->annual_rent ? esc_html( $this->format_currency( $parcel->annual_rent ) ) : '—'; ?>
parcel_status_label( $parcel->status ) ); ?>

note ) ) : ?>

note ); ?>

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 ) : ?>

meter_number ); ?>
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 ) ) ); ?>

id ); ?>' />

reading_date ) ) ); ?> meter_type_label( $reading->type ) ); ?> meter_number ); ?> format_meter_value_with_unit( $reading->reading_value, $reading->type ) ); ?> reading_flag_badge( $reading ) ); ?>
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 ); } ?>

render_notice(); ?>

'>

' data-id=''>
· ·
require_cap( Roles::SETTINGS_CAP ); $settings = wp_parse_args( get_option( 'kgvvm_settings', array() ), $this->get_settings_defaults() ); ?>

render_notice(); ?>

' />

' />

' />

' placeholder='https://example.de/logo.png' />


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 '

' . esc_html( $empty_msg ) . '

'; return (string) ob_get_clean(); } ?>
meter_number ) ? $row->meter_number : '—' ); ?> previous_value ? $this->format_meter_value_with_unit( $row->previous_value, $type ) : '—' ); ?> reading_value ? $this->format_meter_value_with_unit( $row->reading_value, $type ) : '—' ); ?> consumption ? $this->format_meter_value_with_unit( $row->consumption, $type ) : '—' ); ?> unit_price ? $this->format_price_per_unit( $row->unit_price, $unit ) : '—' ); ?> calculated_cost ? $this->format_currency( $row->calculated_cost ) : '—' ); ?> loss_value ) && null !== $row->loss_value ? $this->format_meter_value_with_unit( $row->loss_value, $type ) : '—' ); ?>
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 '' . esc_html( $label ) . ''; } /** * 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 '

' . esc_html( $notice['message'] ) . '

'; } /** * 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 ); ?>

render_notice(); ?>

€ / Std.

' />
$selected_year, 'tab' => 'jobs' ) ) ); ?>' class='button'>
name ); ?> description ); ?> 'jobs', 'year' => $selected_year, 'job_id' => $job->id ) ) ); ?>'>  |  'delete_work_job', 'id' => $job->id, 'year' => $selected_year, 'tab' => 'jobs' ) ), 'kgvvm_delete_work_job_' . $job->id ) ); ?>' class='kgvvm-delete-link' onclick='return confirm("")'>

display_name ); ?> completed_hours, 2 ) ); ?> required_hours, 2 ) ); ?> missing_hours > 0 ? '' . esc_html( number_format_i18n( $row->missing_hours, 2 ) ) . '' : esc_html( number_format_i18n( 0, 2 ) ); ?> surcharge > 0 ? '' . esc_html( $this->format_currency( $row->surcharge ) ) . '' : '—'; ?>

' />
members ) ) { foreach ( $edit_log->members as $m ) { $existing_member_hours[ (int) $m->user_id ] = (float) $m->hours; } } ?> ID; $hours_val = isset( $existing_member_hours[ $uid ] ) ? $existing_member_hours[ $uid ] : ''; ?>
display_name ); ?>

$selected_year ) ) ); ?>' class='button'>
work->get_log_members( $log->id ); ?>
work_date ) ) ); ?> job_name ?: '—' ); ?> user_id ); if ( $u ) : ?> display_name ); ?>: hours, 2 ) ); ?> Std.
note ); ?> $selected_year, 'id' => $log->id ) ) ); ?>'>  |  'delete_work_log', 'id' => $log->id, 'year' => $selected_year ) ), 'kgvvm_delete_work_log_' . $log->id ) ); ?>' class='kgvvm-delete-link' onclick='return confirm("")'>