Release v1.17.10

This commit is contained in:
Ronny Grobel
2026-04-21 07:55:14 +02:00
commit 84b970b04f
258 changed files with 107458 additions and 0 deletions

View File

@@ -0,0 +1,525 @@
<?php
/**
* Cost repository.
*
* @package KGV\VereinManager
*/
namespace KGV\VereinManager\Repositories;
use KGV\VereinManager\Schema;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class CostRepository extends AbstractRepository {
/**
* Resolve table name.
*
* @return string
*/
protected function resolve_table() {
return Schema::table( 'cost_entries' );
}
/**
* Get the persistent year table name.
*
* @return string
*/
private function year_table() {
return Schema::table( 'cost_years' );
}
/**
* Get the yearly section rates table name.
*
* @return string
*/
private function rate_table() {
return Schema::table( 'cost_rates' );
}
/**
* Search cost entries.
*
* @param array $args Query arguments.
* @return array
*/
public function search( $args = array() ) {
$search = isset( $args['s'] ) ? sanitize_text_field( wp_unslash( $args['s'] ) ) : '';
$year = isset( $args['year'] ) ? absint( $args['year'] ) : 0;
$orderby = $this->sanitize_orderby( isset( $args['orderby'] ) ? sanitize_key( wp_unslash( $args['orderby'] ) ) : 'name', array( 'entry_year', 'name', 'distribution_type', 'unit_amount', 'total_cost', 'updated_at', 'created_at' ), 'name' );
$order = $this->sanitize_order( isset( $args['order'] ) ? sanitize_key( wp_unslash( $args['order'] ) ) : 'ASC' );
$sql = "SELECT * FROM {$this->table} WHERE 1=1";
$params = array();
if ( $year > 0 ) {
$this->ensure_year( $year );
$sql .= ' AND entry_year = %d';
$params[] = $year;
}
if ( '' !== $search ) {
$like = '%' . $this->wpdb->esc_like( $search ) . '%';
$sql .= ' AND (name LIKE %s OR note LIKE %s)';
$params[] = $like;
$params[] = $like;
}
$sql .= " ORDER BY {$orderby} {$order}, id DESC";
if ( ! empty( $params ) ) {
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $params ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
return $this->wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
}
/**
* Save or update a cost entry.
*
* @param array $data Entry data.
* @param int $id Optional ID.
* @return int|false
*/
public function save( $data, $id = 0 ) {
$payload = array(
'entry_year' => absint( $data['entry_year'] ),
'name' => $data['name'],
'distribution_type' => isset( $data['distribution_type'] ) ? $data['distribution_type'] : 'parcel',
'unit_amount' => isset( $data['unit_amount'] ) ? (float) $data['unit_amount'] : 0,
'total_cost' => (float) $data['total_cost'],
'is_mandatory' => isset( $data['is_mandatory'] ) ? (int) (bool) $data['is_mandatory'] : 1,
'note' => $data['note'],
'updated_at' => $this->now(),
);
$formats = array( '%d', '%s', '%s', '%f', '%f', '%d', '%s', '%s' );
$this->ensure_year( $payload['entry_year'] );
if ( $id > 0 ) {
$this->wpdb->update( $this->table, $payload, array( 'id' => $id ), $formats, array( '%d' ) );
return $id;
}
$payload['created_at'] = $this->now();
$this->wpdb->insert( $this->table, $payload, array( '%d', '%s', '%s', '%f', '%f', '%d', '%s', '%s', '%s' ) );
return $this->wpdb->insert_id;
}
/**
* Get available years for filters and dropdowns.
*
* @param int $selected_year Optional selected year.
* @return array
*/
public function get_years( $selected_year = 0 ) {
$year_table = $this->year_table();
$years = $this->wpdb->get_col( "SELECT DISTINCT entry_year FROM {$this->table} WHERE entry_year > 0 ORDER BY entry_year DESC" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
$saved_years = $this->wpdb->get_col( "SELECT entry_year FROM {$year_table} ORDER BY entry_year DESC" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
$current_year = (int) current_time( 'Y' );
$years = array_map( 'absint', array_merge( (array) $years, (array) $saved_years ) );
$years[] = $current_year;
$years[] = $current_year - 1;
$years[] = $current_year + 1;
if ( $selected_year > 0 ) {
$years[] = absint( $selected_year );
}
$years = array_values( array_unique( array_filter( $years ) ) );
rsort( $years, SORT_NUMERIC );
return $years;
}
/**
* Ensure one year exists so it remains selectable without cost entries.
*
* @param int $year Year value.
* @return bool
*/
public function ensure_year( $year ) {
$year = absint( $year );
$year_table = $this->year_table();
if ( $year < 1 || '' === $year_table ) {
return false;
}
$exists = (int) $this->wpdb->get_var( $this->wpdb->prepare( "SELECT COUNT(*) FROM {$year_table} WHERE entry_year = %d", $year ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( $exists > 0 ) {
return true;
}
$result = $this->wpdb->insert(
$year_table,
array(
'entry_year' => $year,
'power_price_per_kwh' => null,
'water_price_per_m3' => null,
'created_at' => $this->now(),
'updated_at' => $this->now(),
),
array( '%d', '%s', '%s', '%s', '%s' )
);
return false !== $result;
}
/**
* Save the configured yearly prices for electricity and water.
*
* @param int $year Year value.
* @param float|null $power_price_per_kwh Price per kWh.
* @param float|null $water_price_per_m3 Price per m³.
* @return bool
*/
public function save_year( $year, $power_price_per_kwh = null, $water_price_per_m3 = null ) {
$year = absint( $year );
$year_table = $this->year_table();
if ( ! $this->ensure_year( $year ) ) {
return false;
}
$result = $this->wpdb->update(
$year_table,
array(
'power_price_per_kwh' => null !== $power_price_per_kwh ? (float) $power_price_per_kwh : null,
'water_price_per_m3' => null !== $water_price_per_m3 ? (float) $water_price_per_m3 : null,
'updated_at' => $this->now(),
),
array( 'entry_year' => $year ),
array( '%f', '%f', '%s' ),
array( '%d' )
);
return false !== $result;
}
/**
* Load one year's saved price details.
*
* @param int $year Selected year.
* @return object|null
*/
public function get_year_details( $year ) {
$year = absint( $year );
$year_table = $this->year_table();
if ( $year < 1 || '' === $year_table ) {
return null;
}
return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$year_table} WHERE entry_year = %d", $year ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Get lock state for one statement year.
*
* @param int $year Selected year.
* @return array
*/
public function get_statement_lock_state( $year ) {
$details = $this->get_year_details( $year );
if ( ! $details ) {
return array(
'is_locked' => false,
'locked_at' => null,
'locked_by' => 0,
);
}
return array(
'is_locked' => ! empty( $details->statement_is_locked ) && 1 === (int) $details->statement_is_locked,
'locked_at' => isset( $details->statement_locked_at ) ? $details->statement_locked_at : null,
'locked_by' => isset( $details->statement_locked_by ) ? (int) $details->statement_locked_by : 0,
);
}
/**
* Check whether statement data for one year is locked.
*
* @param int $year Selected year.
* @return bool
*/
public function is_statement_locked( $year ) {
$state = $this->get_statement_lock_state( $year );
return ! empty( $state['is_locked'] );
}
/**
* Lock or unlock a statement year.
*
* @param int $year Selected year.
* @param bool $is_locked Target lock state.
* @param int $locked_by User ID that changes the state.
* @return bool
*/
public function set_statement_lock( $year, $is_locked, $locked_by = 0 ) {
$year = absint( $year );
if ( $year < 1 || ! $this->ensure_year( $year ) ) {
return false;
}
$payload = array(
'statement_is_locked' => $is_locked ? 1 : 0,
'statement_locked_at' => $is_locked ? $this->now() : null,
'statement_locked_by' => $is_locked ? absint( $locked_by ) : 0,
'updated_at' => $this->now(),
);
$result = $this->wpdb->update(
$this->year_table(),
$payload,
array( 'entry_year' => $year ),
array( '%d', '%s', '%d', '%s' ),
array( '%d' )
);
return false !== $result;
}
/**
* Save section-specific yearly prices.
*
* @param array $data Section pricing data.
* @return bool
*/
public function save_section_prices( $data ) {
$year = absint( $data['entry_year'] );
$section_id = absint( $data['section_id'] );
$rate_table = $this->rate_table();
if ( ! $this->ensure_year( $year ) || $section_id < 1 || '' === $rate_table ) {
return false;
}
$payload = array(
'entry_year' => $year,
'section_id' => $section_id,
'power_price_per_kwh' => null !== $data['power_price_per_kwh'] ? (float) $data['power_price_per_kwh'] : null,
'water_price_per_m3' => null !== $data['water_price_per_m3'] ? (float) $data['water_price_per_m3'] : null,
'updated_at' => $this->now(),
);
$exists = (int) $this->wpdb->get_var( $this->wpdb->prepare( "SELECT COUNT(*) FROM {$rate_table} WHERE entry_year = %d AND section_id = %d", $year, $section_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( $exists > 0 ) {
$result = $this->wpdb->update(
$rate_table,
array(
'power_price_per_kwh' => $payload['power_price_per_kwh'],
'water_price_per_m3' => $payload['water_price_per_m3'],
'updated_at' => $payload['updated_at'],
),
array(
'entry_year' => $year,
'section_id' => $section_id,
),
array( '%f', '%f', '%s' ),
array( '%d', '%d' )
);
return false !== $result;
}
$payload['created_at'] = $this->now();
$result = $this->wpdb->insert( $rate_table, $payload, array( '%d', '%d', '%f', '%f', '%s', '%s' ) );
return false !== $result;
}
/**
* Get all saved section prices for one year.
*
* @param int $year Selected year.
* @return array
*/
public function get_section_prices( $year ) {
$year = absint( $year );
$rate_table = $this->rate_table();
$sections = Schema::table( 'sections' );
if ( $year < 1 || '' === $rate_table ) {
return array();
}
$sql = "SELECT r.*, s.name AS section_name
FROM {$rate_table} r
LEFT JOIN {$sections} s ON s.id = r.section_id
WHERE r.entry_year = %d
ORDER BY s.name ASC, r.section_id ASC";
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $year ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Sum all costs for a year.
*
* @param int $year Selected year.
* @return float
*/
public function get_total_for_year( $year ) {
if ( $year < 1 ) {
return 0.0;
}
$total = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT COALESCE(SUM(total_cost), 0) FROM {$this->table} WHERE entry_year = %d", absint( $year ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
return (float) $total;
}
// -------------------------------------------------------------------------
// Parcel cost assignments
// -------------------------------------------------------------------------
/**
* Get the parcel cost assignments table name.
*
* @return string
*/
private function assignment_table() {
return Schema::table( 'parcel_cost_assignments' );
}
/**
* Get cost entry IDs assigned to a specific parcel.
*
* @param int $parcel_id Parcel ID.
* @return int[]
*/
public function get_assigned_entry_ids( $parcel_id ) {
$parcel_id = absint( $parcel_id );
$table = $this->assignment_table();
if ( $parcel_id < 1 || '' === $table ) {
return array();
}
$ids = $this->wpdb->get_col( $this->wpdb->prepare( "SELECT cost_entry_id FROM {$table} WHERE parcel_id = %d", $parcel_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
return array_map( 'intval', (array) $ids );
}
/**
* Get all cost entry IDs that have at least one parcel assignment.
*
* @param int $year Optional filter by entry year (0 = all years).
* @return int[]
*/
public function get_entry_ids_with_assignments( $year = 0 ) {
$table = $this->assignment_table();
if ( '' === $table ) {
return array();
}
$year = absint( $year );
if ( $year > 0 ) {
$ids = $this->wpdb->get_col( $this->wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
"SELECT DISTINCT a.cost_entry_id FROM {$table} a
INNER JOIN {$this->table} e ON e.id = a.cost_entry_id
WHERE e.entry_year = %d",
$year
) );
} else {
$ids = $this->wpdb->get_col( "SELECT DISTINCT cost_entry_id FROM {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
}
return array_map( 'intval', (array) $ids );
}
/**
* Assign a cost entry to a parcel.
*
* @param int $parcel_id Parcel ID.
* @param int $cost_entry_id Cost entry ID.
* @return bool
*/
public function assign_to_parcel( $parcel_id, $cost_entry_id ) {
$parcel_id = absint( $parcel_id );
$cost_entry_id = absint( $cost_entry_id );
$table = $this->assignment_table();
if ( $parcel_id < 1 || $cost_entry_id < 1 || '' === $table ) {
return false;
}
$exists = (int) $this->wpdb->get_var( $this->wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
"SELECT COUNT(*) FROM {$table} WHERE parcel_id = %d AND cost_entry_id = %d",
$parcel_id,
$cost_entry_id
) );
if ( $exists > 0 ) {
return true;
}
$result = $this->wpdb->insert(
$table,
array(
'parcel_id' => $parcel_id,
'cost_entry_id' => $cost_entry_id,
'created_at' => $this->now(),
),
array( '%d', '%d', '%s' )
);
return false !== $result;
}
/**
* Remove a cost entry assignment from a parcel.
*
* @param int $parcel_id Parcel ID.
* @param int $cost_entry_id Cost entry ID.
* @return bool
*/
public function unassign_from_parcel( $parcel_id, $cost_entry_id ) {
$parcel_id = absint( $parcel_id );
$cost_entry_id = absint( $cost_entry_id );
$table = $this->assignment_table();
if ( $parcel_id < 1 || $cost_entry_id < 1 || '' === $table ) {
return false;
}
$result = $this->wpdb->delete(
$table,
array(
'parcel_id' => $parcel_id,
'cost_entry_id' => $cost_entry_id,
),
array( '%d', '%d' )
);
return false !== $result;
}
/**
* Remove all parcel assignments for a cost entry (e.g. when the entry is deleted).
*
* @param int $cost_entry_id Cost entry ID.
* @return void
*/
public function delete_assignments_for_entry( $cost_entry_id ) {
$table = $this->assignment_table();
if ( '' !== $table && absint( $cost_entry_id ) > 0 ) {
$this->wpdb->delete( $table, array( 'cost_entry_id' => absint( $cost_entry_id ) ), array( '%d' ) );
}
}
}