- Neue Tabelle wp_kgvvm_parcel_cost_assignments für 1:N Zuordnungen - CostRepository erweitert: get_assigned_entry_ids(), get_entry_ids_with_assignments(), assign_to_parcel(), unassign_from_parcel(), delete_assignments_for_entry() - Admin: Neue POST-Action toggle_parcel_cost_assignment() mit Nonce-Sicherung - Jahresabrechnung Parzelle: Rechte Seitenleiste zeigt alle Kostenpositions mit Zuordnungsstatus (✓ zugeordnet, ✗ nicht zugeordnet, – alle Parzellen) - Berechnung: Kostenposten mit Beschränkung werden nur berechnet wenn Parzelle zugeordnet ist - DataTransfer.php: parcel_cost_assignments in table_keys integriert für Export/Import - DELETE-Handler bereinigt Zuordnungen beim Löschen einer Kostenposition
456 lines
13 KiB
PHP
456 lines
13 KiB
PHP
<?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'],
|
||
'note' => $data['note'],
|
||
'updated_at' => $this->now(),
|
||
);
|
||
|
||
$formats = array( '%d', '%s', '%s', '%f', '%f', '%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', '%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
|
||
}
|
||
|
||
/**
|
||
* 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' ) );
|
||
}
|
||
}
|
||
}
|