Initial plugin commit

This commit is contained in:
2026-04-13 21:01:07 +02:00
commit 4367aef84a
254 changed files with 104260 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
<?php
/**
* Shared repository helpers.
*
* @package KGV\VereinManager
*/
namespace KGV\VereinManager\Repositories;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
abstract class AbstractRepository {
/**
* WordPress database object.
*
* @var \wpdb
*/
protected $wpdb;
/**
* Concrete table name.
*
* @var string
*/
protected $table = '';
/**
* Build repository instance.
*/
public function __construct() {
global $wpdb;
$this->wpdb = $wpdb;
$this->table = $this->resolve_table();
}
/**
* Resolve table name in child class.
*
* @return string
*/
abstract protected function resolve_table();
/**
* Find a single row by ID.
*
* @param int $id Record ID.
* @return object|null
*/
public function find( $id ) {
return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$this->table} WHERE id = %d", absint( $id ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Delete a record.
*
* @param int $id Record ID.
* @return bool
*/
public function delete( $id ) {
$result = $this->wpdb->delete( $this->table, array( 'id' => absint( $id ) ), array( '%d' ) );
return false !== $result;
}
/**
* Current local datetime.
*
* @return string
*/
protected function now() {
return current_time( 'mysql' );
}
/**
* Restrict sortable columns.
*
* @param string $orderby Requested column.
* @param array $allowed Allowed columns.
* @param string $default Default column.
* @return string
*/
protected function sanitize_orderby( $orderby, $allowed, $default ) {
return in_array( $orderby, $allowed, true ) ? $orderby : $default;
}
/**
* Restrict sort direction.
*
* @param string $order Requested order.
* @return string
*/
protected function sanitize_order( $order ) {
return 'ASC' === strtoupper( (string) $order ) ? 'ASC' : 'DESC';
}
}

View File

@@ -0,0 +1,279 @@
<?php
/**
* Parcel/member and parcel/tenant assignments.
*
* @package KGV\VereinManager
*/
namespace KGV\VereinManager\Repositories;
use KGV\VereinManager\Roles;
use KGV\VereinManager\Schema;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class AssignmentRepository {
/**
* WordPress database object.
*
* @var \wpdb
*/
private $wpdb;
/**
* Member assignment table.
*
* @var string
*/
private $member_table;
/**
* Tenant assignment table.
*
* @var string
*/
private $tenant_table;
/**
* Construct repository.
*/
public function __construct() {
global $wpdb;
$this->wpdb = $wpdb;
$this->member_table = Schema::table( 'parcel_members' );
$this->tenant_table = Schema::table( 'parcel_tenants' );
}
/**
* Get WordPress users with the role "Mitglied".
*
* @return array
*/
public function get_member_users() {
$query = new \WP_User_Query(
array(
'role' => Roles::MEMBER_ROLE,
'orderby' => 'display_name',
'order' => 'ASC',
'number' => 500,
)
);
return $query->get_results();
}
/**
* Get assigned member IDs for a parcel.
*
* @param int $parcel_id Parcel ID.
* @return array
*/
public function get_member_ids_for_parcel( $parcel_id ) {
$ids = $this->wpdb->get_col( $this->wpdb->prepare( "SELECT user_id FROM {$this->member_table} WHERE parcel_id = %d", $parcel_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
return array_map( 'absint', $ids );
}
/**
* Get all members assigned to one parcel.
*
* @param int $parcel_id Parcel ID.
* @return array
*/
public function get_members_for_parcel( $parcel_id ) {
$users = $this->wpdb->users;
$sql = "SELECT u.ID, u.display_name, u.user_email
FROM {$this->member_table} pm
INNER JOIN {$users} u ON u.ID = pm.user_id
WHERE pm.parcel_id = %d
ORDER BY u.display_name ASC";
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $parcel_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Get all parcels assigned to a specific user.
*
* @param int $user_id User ID.
* @return array
*/
public function get_parcels_for_user( $user_id ) {
$parcels = Schema::table( 'parcels' );
$sections = Schema::table( 'sections' );
$meters = Schema::table( 'meters' );
$sql = "SELECT p.*, s.name AS section_name,
(SELECT meter_number FROM {$meters} wm WHERE wm.parcel_id = p.id AND wm.type = 'water' LIMIT 1) AS water_meter_number,
(SELECT meter_number FROM {$meters} em WHERE em.parcel_id = p.id AND em.type = 'power' LIMIT 1) AS power_meter_number
FROM {$this->member_table} pm
INNER JOIN {$parcels} p ON p.id = pm.parcel_id
LEFT JOIN {$sections} s ON s.id = p.section_id
WHERE pm.user_id = %d
ORDER BY s.name ASC, p.label ASC";
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $user_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Check whether a parcel belongs to a given user.
*
* @param int $user_id User ID.
* @param int $parcel_id Parcel ID.
* @return bool
*/
public function user_has_parcel( $user_id, $parcel_id ) {
$sql = "SELECT COUNT(*) FROM {$this->member_table} WHERE user_id = %d AND parcel_id = %d";
return (int) $this->wpdb->get_var( $this->wpdb->prepare( $sql, $user_id, $parcel_id ) ) > 0; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Get assigned tenant IDs for a parcel.
*
* @param int $parcel_id Parcel ID.
* @return array
*/
public function get_tenant_ids_for_parcel( $parcel_id ) {
$ids = $this->wpdb->get_col( $this->wpdb->prepare( "SELECT tenant_id FROM {$this->tenant_table} WHERE parcel_id = %d", $parcel_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
return array_map( 'absint', $ids );
}
/**
* Get all members linked to a tenant via assigned parcels.
*
* @param int $tenant_id Tenant ID.
* @return array
*/
public function get_members_for_tenant( $tenant_id ) {
$parcels = Schema::table( 'parcels' );
$users = $this->wpdb->users;
$sql = "SELECT DISTINCT u.ID, u.display_name, u.user_email, p.label AS parcel_label
FROM {$this->tenant_table} pt
INNER JOIN {$parcels} p ON p.id = pt.parcel_id
INNER JOIN {$this->member_table} pm ON pm.parcel_id = pt.parcel_id
INNER JOIN {$users} u ON u.ID = pm.user_id
WHERE pt.tenant_id = %d
ORDER BY p.label ASC, u.display_name ASC";
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $tenant_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Get all tenants assigned to one parcel.
*
* @param int $parcel_id Parcel ID.
* @return array
*/
public function get_tenants_for_parcel( $parcel_id ) {
$tenants = Schema::table( 'tenants' );
$sql = "SELECT t.*
FROM {$this->tenant_table} pt
INNER JOIN {$tenants} t ON t.id = pt.tenant_id
WHERE pt.parcel_id = %d
ORDER BY t.last_name ASC, t.first_name ASC";
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $parcel_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Get all parcels assigned to one tenant.
*
* @param int $tenant_id Tenant ID.
* @return array
*/
public function get_parcels_for_tenant( $tenant_id ) {
$parcels = Schema::table( 'parcels' );
$sections = Schema::table( 'sections' );
$sql = "SELECT p.*, s.name AS section_name
FROM {$this->tenant_table} pt
INNER JOIN {$parcels} p ON p.id = pt.parcel_id
LEFT JOIN {$sections} s ON s.id = p.section_id
WHERE pt.tenant_id = %d
ORDER BY s.name ASC, p.label ASC";
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $tenant_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Synchronize member assignments.
*
* @param int $parcel_id Parcel ID.
* @param array $user_ids User IDs.
* @return void
*/
public function sync_member_ids( $parcel_id, $user_ids ) {
$user_ids = array_values( array_unique( array_filter( array_map( 'absint', (array) $user_ids ) ) ) );
$this->wpdb->delete( $this->member_table, array( 'parcel_id' => $parcel_id ), array( '%d' ) );
foreach ( $user_ids as $user_id ) {
$this->wpdb->insert(
$this->member_table,
array(
'parcel_id' => $parcel_id,
'user_id' => $user_id,
'created_at' => current_time( 'mysql' ),
),
array( '%d', '%d', '%s' )
);
}
}
/**
* Synchronize tenant assignments.
*
* @param int $parcel_id Parcel ID.
* @param array $tenant_ids Tenant IDs.
* @return void
*/
public function sync_tenant_ids( $parcel_id, $tenant_ids ) {
$tenant_ids = array_values( array_unique( array_filter( array_map( 'absint', (array) $tenant_ids ) ) ) );
$this->wpdb->delete( $this->tenant_table, array( 'parcel_id' => $parcel_id ), array( '%d' ) );
foreach ( $tenant_ids as $tenant_id ) {
$this->wpdb->insert(
$this->tenant_table,
array(
'parcel_id' => $parcel_id,
'tenant_id' => $tenant_id,
'created_at' => current_time( 'mysql' ),
),
array( '%d', '%d', '%s' )
);
}
}
/**
* Check whether a member is assigned to a different parcel.
*
* @param int $user_id User ID.
* @param int $parcel_id Current parcel ID.
* @return bool
*/
public function member_has_other_parcels( $user_id, $parcel_id ) {
$sql = "SELECT COUNT(*) FROM {$this->member_table} WHERE user_id = %d AND parcel_id != %d";
return (int) $this->wpdb->get_var( $this->wpdb->prepare( $sql, $user_id, $parcel_id ) ) > 0; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Remove all assignments for a parcel.
*
* @param int $parcel_id Parcel ID.
* @return void
*/
public function purge_parcel( $parcel_id ) {
$this->wpdb->delete( $this->member_table, array( 'parcel_id' => $parcel_id ), array( '%d' ) );
$this->wpdb->delete( $this->tenant_table, array( 'parcel_id' => $parcel_id ), array( '%d' ) );
}
/**
* Remove all tenant relations before deleting the tenant.
*
* @param int $tenant_id Tenant ID.
* @return void
*/
public function purge_tenant( $tenant_id ) {
$this->wpdb->delete( $this->tenant_table, array( 'tenant_id' => $tenant_id ), array( '%d' ) );
}
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* Chat repository for member communication.
*
* @package KGV\VereinManager
*/
namespace KGV\VereinManager\Repositories;
use KGV\VereinManager\Schema;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class ChatRepository extends AbstractRepository {
/**
* Resolve table name.
*
* @return string
*/
protected function resolve_table() {
return Schema::table( 'chat_messages' );
}
/**
* Get recent chat messages for one room.
*
* @param string $room_key Room key.
* @param int $limit Maximum result count.
* @param int $after_id Optional lower ID boundary for polling.
* @return array
*/
public function get_recent_messages( $room_key, $limit = 60, $after_id = 0 ) {
$users = $this->wpdb->users;
$room_key = sanitize_key( $room_key );
$limit = max( 1, min( 200, absint( $limit ) ) );
$after_id = absint( $after_id );
if ( $after_id > 0 ) {
$sql = "SELECT m.*, u.display_name
FROM {$this->table} m
LEFT JOIN {$users} u ON u.ID = m.user_id
WHERE m.room_key = %s AND m.id > %d
ORDER BY m.id ASC
LIMIT %d";
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $room_key, $after_id, $limit ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
$sql = "SELECT *
FROM (
SELECT m.*, u.display_name
FROM {$this->table} m
LEFT JOIN {$users} u ON u.ID = m.user_id
WHERE m.room_key = %s
ORDER BY m.id DESC
LIMIT %d
) recent_messages
ORDER BY id ASC";
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $room_key, $limit ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Store one new message.
*
* @param string $room_key Room key.
* @param int $user_id Sender user ID.
* @param string $message Message body.
* @return object|null
*/
public function save_message( $room_key, $user_id, $message ) {
$room_key = sanitize_key( $room_key );
$user_id = absint( $user_id );
$message = trim( sanitize_textarea_field( $message ) );
if ( '' === $room_key || $user_id < 1 || '' === $message ) {
return null;
}
$result = $this->wpdb->insert(
$this->table,
array(
'room_key' => $room_key,
'user_id' => $user_id,
'message' => $message,
'created_at' => $this->now(),
),
array( '%s', '%d', '%s', '%s' )
);
if ( false === $result ) {
return null;
}
return $this->find_with_author( (int) $this->wpdb->insert_id );
}
/**
* Find one message together with the author's display name.
*
* @param int $id Message ID.
* @return object|null
*/
private function find_with_author( $id ) {
$users = $this->wpdb->users;
$sql = "SELECT m.*, u.display_name
FROM {$this->table} m
LEFT JOIN {$users} u ON u.ID = m.user_id
WHERE m.id = %d
LIMIT 1";
return $this->wpdb->get_row( $this->wpdb->prepare( $sql, absint( $id ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
}

View File

@@ -0,0 +1,313 @@
<?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;
}
}

View File

@@ -0,0 +1,247 @@
<?php
/**
* Meter reading repository.
*
* @package KGV\VereinManager
*/
namespace KGV\VereinManager\Repositories;
use KGV\VereinManager\Schema;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class MeterReadingRepository extends AbstractRepository {
/**
* Resolve table name.
*
* @return string
*/
protected function resolve_table() {
return Schema::table( 'meter_readings' );
}
/**
* Store a new reading.
*
* @param array $data Reading data.
* @return int|false
*/
public function save( $data ) {
$payload = array(
'meter_id' => absint( $data['meter_id'] ),
'parcel_id' => absint( $data['parcel_id'] ),
'reading_value' => (float) $data['reading_value'],
'reading_date' => $data['reading_date'],
'note' => $data['note'],
'submitted_by' => absint( $data['submitted_by'] ),
'is_self_reading' => ! empty( $data['is_self_reading'] ) ? 1 : 0,
'created_at' => $this->now(),
);
$this->wpdb->insert(
$this->table,
$payload,
array( '%d', '%d', '%f', '%s', '%s', '%d', '%d', '%s' )
);
return $this->wpdb->insert_id;
}
/**
* Get the latest reading for one meter.
*
* @param int $meter_id Meter ID.
* @return object|null
*/
public function get_latest_for_meter( $meter_id ) {
$sql = "SELECT * FROM {$this->table} WHERE meter_id = %d ORDER BY reading_date DESC, id DESC LIMIT 1";
return $this->wpdb->get_row( $this->wpdb->prepare( $sql, $meter_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Get recent readings for a parcel.
*
* @param int $parcel_id Parcel ID.
* @param int $limit Number of rows.
* @return array
*/
public function get_recent_for_parcel( $parcel_id, $limit = 10 ) {
$meters = Schema::table( 'meters' );
$sql = "SELECT r.*, m.type, m.meter_number
FROM {$this->table} r
LEFT JOIN {$meters} m ON m.id = r.meter_id
WHERE r.parcel_id = %d
ORDER BY r.reading_date DESC, r.id DESC
LIMIT %d";
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $parcel_id, absint( $limit ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Get a filtered consumption report including deltas since the previous reading.
*
* @param int $section_id Optional section filter.
* @param string $date_from Optional start date.
* @param string $date_to Optional end date.
* @param string $order ASC|DESC by reading date.
* @return array
*/
public function get_consumption_report( $section_id = 0, $date_from = '', $date_to = '', $order = 'DESC' ) {
$meters = Schema::table( 'meters' );
$sections = Schema::table( 'sections' );
$parcels = Schema::table( 'parcels' );
$sql = "SELECT r.*, m.type, m.meter_number, m.section_id, s.name AS section_name, p.label AS parcel_label
FROM {$this->table} r
LEFT JOIN {$meters} m ON m.id = r.meter_id
LEFT JOIN {$sections} s ON s.id = m.section_id
LEFT JOIN {$parcels} p ON p.id = r.parcel_id
WHERE 1=1";
$params = array();
$order = 'ASC' === strtoupper( (string) $order ) ? 'ASC' : 'DESC';
if ( $section_id > 0 ) {
$sql .= ' AND m.section_id = %d';
$params[] = $section_id;
}
if ( '' !== $date_to ) {
$sql .= ' AND r.reading_date <= %s';
$params[] = $date_to;
}
$sql .= ' ORDER BY r.meter_id ASC, r.reading_date ASC, r.id ASC';
$rows = ! empty( $params )
? $this->wpdb->get_results( $this->wpdb->prepare( $sql, $params ) ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
: $this->wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
$report = array();
$last_by_meter = array();
foreach ( $rows as $row ) {
$previous = isset( $last_by_meter[ $row->meter_id ] ) ? $last_by_meter[ $row->meter_id ] : null;
$row->previous_value = $previous ? (float) $previous->reading_value : null;
$row->consumption = $previous && (float) $row->reading_value >= (float) $previous->reading_value ? (float) $row->reading_value - (float) $previous->reading_value : null;
$last_by_meter[$row->meter_id] = $row;
if ( '' !== $date_from && $row->reading_date < $date_from ) {
continue;
}
$report[] = $row;
}
usort(
$report,
function( $left, $right ) use ( $order ) {
$left_key = $left->reading_date . ' ' . str_pad( (string) $left->id, 10, '0', STR_PAD_LEFT );
$right_key = $right->reading_date . ' ' . str_pad( (string) $right->id, 10, '0', STR_PAD_LEFT );
if ( $left_key === $right_key ) {
return 0;
}
if ( 'ASC' === $order ) {
return $left_key < $right_key ? -1 : 1;
}
return $left_key > $right_key ? -1 : 1;
}
);
return $report;
}
/**
* Get all readings for a parcel, suitable for export.
*
* @param int $parcel_id Parcel ID.
* @return array
*/
public function get_all_for_parcel( $parcel_id ) {
$meters = Schema::table( 'meters' );
$sql = "SELECT r.*, m.type, m.meter_number
FROM {$this->table} r
LEFT JOIN {$meters} m ON m.id = r.meter_id
WHERE r.parcel_id = %d
ORDER BY r.reading_date DESC, r.id DESC";
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $parcel_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Get reading history for a specific meter.
*
* @param int $meter_id Meter ID.
* @param int $limit Max rows.
* @return array
*/
public function get_all_for_meter( $meter_id, $limit = 50 ) {
$parcels = Schema::table( 'parcels' );
$sql = "SELECT r.*, p.label AS parcel_label
FROM {$this->table} r
LEFT JOIN {$parcels} p ON p.id = r.parcel_id
WHERE r.meter_id = %d
ORDER BY r.reading_date DESC, r.id DESC
LIMIT %d";
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $meter_id, absint( $limit ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Build a monthly summary for one meter.
*
* @param int $meter_id Meter ID.
* @return array
*/
public function get_monthly_summary_for_meter( $meter_id ) {
$rows = $this->wpdb->get_results(
$this->wpdb->prepare(
"SELECT * FROM {$this->table} WHERE meter_id = %d ORDER BY reading_date ASC, id ASC",
$meter_id
)
); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( empty( $rows ) ) {
return array();
}
$monthly = array();
$last = null;
foreach ( $rows as $row ) {
$key = gmdate( 'Y-m', strtotime( $row->reading_date ) );
if ( ! isset( $monthly[ $key ] ) ) {
$monthly[ $key ] = array(
'month' => $key,
'from_value' => null,
'to_value' => (float) $row->reading_value,
'consumption' => 0.0,
'readings' => 0,
'last_date' => $row->reading_date,
);
}
$monthly[ $key ]['to_value'] = (float) $row->reading_value;
$monthly[ $key ]['last_date'] = $row->reading_date;
$monthly[ $key ]['readings']++;
if ( $last && (float) $row->reading_value >= (float) $last->reading_value ) {
if ( null === $monthly[ $key ]['from_value'] ) {
$monthly[ $key ]['from_value'] = (float) $last->reading_value;
}
$monthly[ $key ]['consumption'] += (float) $row->reading_value - (float) $last->reading_value;
}
$last = $row;
}
return array_reverse( array_values( $monthly ) );
}
}

View File

@@ -0,0 +1,304 @@
<?php
/**
* Meter repository.
*
* @package KGV\VereinManager
*/
namespace KGV\VereinManager\Repositories;
use KGV\VereinManager\Schema;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class MeterRepository extends AbstractRepository {
/**
* Resolve table name.
*
* @return string
*/
protected function resolve_table() {
return Schema::table( 'meters' );
}
/**
* Search meters.
*
* @param array $args Query arguments.
* @return array
*/
public function search( $args = array() ) {
$search = isset( $args['s'] ) ? sanitize_text_field( wp_unslash( $args['s'] ) ) : '';
$type = isset( $args['type'] ) ? sanitize_key( wp_unslash( $args['type'] ) ) : '';
$section_id = isset( $args['section_id'] ) ? absint( $args['section_id'] ) : 0;
$assignment = isset( $args['assignment'] ) ? sanitize_key( wp_unslash( $args['assignment'] ) ) : '';
$orderby = $this->sanitize_orderby( isset( $args['orderby'] ) ? sanitize_key( wp_unslash( $args['orderby'] ) ) : 'meter_number', array( 'meter_number', 'type', 'installed_at', 'calibration_year', 'is_active', 'created_at' ), 'meter_number' );
$order = $this->sanitize_order( isset( $args['order'] ) ? sanitize_key( wp_unslash( $args['order'] ) ) : 'ASC' );
$sections = Schema::table( 'sections' );
$parcels = Schema::table( 'parcels' );
$sql = "SELECT m.*, s.name AS section_name, p.label AS parcel_label
FROM {$this->table} m
LEFT JOIN {$sections} s ON s.id = m.section_id
LEFT JOIN {$parcels} p ON p.id = m.parcel_id
WHERE 1=1";
$params = array();
if ( '' !== $search ) {
$like = '%' . $this->wpdb->esc_like( $search ) . '%';
$sql .= ' AND (m.meter_number LIKE %s OR m.note LIKE %s)';
$params[] = $like;
$params[] = $like;
}
if ( in_array( $type, array( 'water', 'power' ), true ) ) {
$sql .= ' AND m.type = %s';
$params[] = $type;
}
if ( $section_id > 0 ) {
$sql .= ' AND m.section_id = %d';
$params[] = $section_id;
}
if ( 'free' === $assignment ) {
$sql .= ' AND m.parcel_id IS NULL AND m.is_main_meter = 0';
} elseif ( 'assigned' === $assignment ) {
$sql .= ' AND m.parcel_id IS NOT NULL';
} elseif ( 'main' === $assignment ) {
$sql .= ' AND m.is_main_meter = 1';
}
$sql .= " ORDER BY m.{$orderby} {$order}, m.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 meter.
*
* @param array $data Meter data.
* @param int $id Optional ID.
* @return int|false
*/
public function save( $data, $id = 0 ) {
$current = $id > 0 ? $this->find( $id ) : null;
$is_main_meter = isset( $data['is_main_meter'] ) ? absint( $data['is_main_meter'] ) : ( $current ? (int) $current->is_main_meter : 0 );
if ( empty( $data['is_active'] ) ) {
$is_main_meter = 0;
}
$payload = array(
'type' => $data['type'],
'meter_number' => $data['meter_number'],
'section_id' => $data['section_id'],
'parcel_id' => $current ? $current->parcel_id : null,
'installed_at' => $data['installed_at'] ? $data['installed_at'] : null,
'calibration_year' => isset( $data['calibration_year'] ) ? $data['calibration_year'] : null,
'is_main_meter' => $is_main_meter,
'is_active' => $data['is_active'],
'note' => $data['note'],
'updated_at' => $this->now(),
);
$formats = array( '%s', '%s', '%d', '%s', '%s', '%d', '%d', '%d', '%s', '%s' );
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( '%s', '%s', '%d', '%s', '%s', '%d', '%d', '%d', '%s', '%s', '%s' ) );
return $this->wpdb->insert_id;
}
/**
* Check if a meter number already exists for the same type.
*
* @param string $meter_number Meter number.
* @param string $type Meter type.
* @param int $exclude_id Optional ID to exclude.
* @return bool
*/
public function meter_number_exists( $meter_number, $type, $exclude_id = 0 ) {
$sql = "SELECT COUNT(*) FROM {$this->table} WHERE meter_number = %s AND type = %s";
$params = array( $meter_number, $type );
if ( $exclude_id > 0 ) {
$sql .= ' AND id != %d';
$params[] = $exclude_id;
}
return (int) $this->wpdb->get_var( $this->wpdb->prepare( $sql, $params ) ) > 0; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Return free meters for selection.
*
* @param string $type water|power.
* @param int $section_id Optional section filter.
* @param int $current_parcel_id Current parcel in edit mode.
* @return array
*/
public function get_free_by_type( $type, $section_id = 0, $current_parcel_id = 0 ) {
$sql = "SELECT * FROM {$this->table} WHERE type = %s AND is_active = 1 AND is_main_meter = 0 AND (parcel_id IS NULL OR parcel_id = %d)";
$params = array( $type, $current_parcel_id );
if ( $section_id > 0 ) {
$sql .= ' AND section_id = %d';
$params[] = $section_id;
}
$sql .= ' ORDER BY meter_number ASC';
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $params ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Get the meter assigned to a parcel by type.
*
* @param int $parcel_id Parcel ID.
* @param string $type Meter type.
* @return object|null
*/
public function get_assigned_to_parcel( $parcel_id, $type ) {
return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$this->table} WHERE parcel_id = %d AND type = %s LIMIT 1", $parcel_id, $type ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Load a meter if it can legally be assigned to a parcel.
*
* @param int $id Meter ID.
* @param string $type Expected type.
* @param int $section_id Target section ID.
* @param int $current_parcel_id Current parcel ID.
* @return object|null
*/
public function get_assignable_meter( $id, $type, $section_id, $current_parcel_id ) {
$sql = "SELECT * FROM {$this->table} WHERE id = %d AND type = %s AND section_id = %d AND is_active = 1 AND is_main_meter = 0 AND (parcel_id IS NULL OR parcel_id = %d) LIMIT 1";
return $this->wpdb->get_row( $this->wpdb->prepare( $sql, $id, $type, $section_id, $current_parcel_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Release all meters assigned to a parcel.
*
* @param int $parcel_id Parcel ID.
* @return void
*/
public function release_parcel( $parcel_id ) {
$this->wpdb->query( $this->wpdb->prepare( "UPDATE {$this->table} SET parcel_id = NULL, updated_at = %s WHERE parcel_id = %d", $this->now(), $parcel_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Assign a meter to a parcel.
*
* @param int $meter_id Meter ID.
* @param int $parcel_id Parcel ID.
* @return void
*/
public function assign_to_parcel( $meter_id, $parcel_id ) {
$this->wpdb->update(
$this->table,
array(
'parcel_id' => $parcel_id,
'is_main_meter' => 0,
'updated_at' => $this->now(),
),
array( 'id' => $meter_id ),
array( '%d', '%d', '%s' ),
array( '%d' )
);
}
/**
* Release one specific meter from its parcel.
*
* @param int $meter_id Meter ID.
* @return void
*/
public function release_meter( $meter_id ) {
$this->wpdb->update(
$this->table,
array(
'parcel_id' => null,
'updated_at' => $this->now(),
),
array( 'id' => $meter_id ),
array( '%d', '%s' ),
array( '%d' )
);
}
/**
* Return all available main meters for one section.
*
* @param int $section_id Section ID.
* @return array
*/
public function get_available_main_for_section( $section_id ) {
$sql = "SELECT * FROM {$this->table} WHERE section_id = %d AND is_active = 1 AND parcel_id IS NULL ORDER BY type ASC, meter_number ASC";
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $section_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Return all selected main meters for one section.
*
* @param int $section_id Section ID.
* @return array
*/
public function get_main_for_section( $section_id ) {
$sql = "SELECT * FROM {$this->table} WHERE section_id = %d AND is_main_meter = 1 ORDER BY type ASC, meter_number ASC";
return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $section_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Sync selected main meters for a section.
*
* @param int $section_id Section ID.
* @param array $meter_ids Selected meter IDs.
* @return void
*/
public function sync_main_meters_for_section( $section_id, $meter_ids ) {
$section_id = absint( $section_id );
$meter_ids = array_values( array_unique( array_filter( array_map( 'absint', (array) $meter_ids ) ) ) );
if ( $section_id < 1 ) {
return;
}
$this->wpdb->query( $this->wpdb->prepare( "UPDATE {$this->table} SET is_main_meter = 0, updated_at = %s WHERE section_id = %d AND parcel_id IS NULL", $this->now(), $section_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
if ( empty( $meter_ids ) ) {
return;
}
$placeholders = implode( ', ', array_fill( 0, count( $meter_ids ), '%d' ) );
$params = array_merge( array( $this->now(), $section_id ), $meter_ids );
$sql = "UPDATE {$this->table} SET is_main_meter = 1, parcel_id = NULL, updated_at = %s WHERE section_id = %d AND is_active = 1 AND parcel_id IS NULL AND id IN ({$placeholders})";
$this->wpdb->query( $this->wpdb->prepare( $sql, $params ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Check whether the meter is already assigned.
*
* @param int $id Meter ID.
* @return bool
*/
public function is_assigned( $id ) {
$meter = $this->find( $id );
return $meter && ! empty( $meter->parcel_id );
}
}

View File

@@ -0,0 +1,195 @@
<?php
/**
* Parcel repository.
*
* @package KGV\VereinManager
*/
namespace KGV\VereinManager\Repositories;
use KGV\VereinManager\Schema;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class ParcelRepository extends AbstractRepository {
/**
* Resolve table name.
*
* @return string
*/
protected function resolve_table() {
return Schema::table( 'parcels' );
}
/**
* Search parcels with relation information.
*
* @param array $args Query arguments.
* @return array
*/
public function search( $args = array() ) {
$search = isset( $args['s'] ) ? sanitize_text_field( wp_unslash( $args['s'] ) ) : '';
$status = isset( $args['status'] ) ? sanitize_key( wp_unslash( $args['status'] ) ) : '';
$section_id = isset( $args['section_id'] ) ? absint( $args['section_id'] ) : 0;
$orderby = $this->sanitize_orderby( isset( $args['orderby'] ) ? sanitize_key( wp_unslash( $args['orderby'] ) ) : 'label', array( 'label', 'status', 'area', 'annual_rent', 'created_at' ), 'label' );
$order = $this->sanitize_order( isset( $args['order'] ) ? sanitize_key( wp_unslash( $args['order'] ) ) : 'ASC' );
$limit = isset( $args['limit'] ) ? absint( $args['limit'] ) : 0;
$paged = isset( $args['paged'] ) ? max( 1, absint( $args['paged'] ) ) : 1;
$offset = $limit > 0 ? ( $paged - 1 ) * $limit : 0;
$meters = Schema::table( 'meters' );
$parcel_members = Schema::table( 'parcel_members' );
$parcel_tenants = Schema::table( 'parcel_tenants' );
$sections = Schema::table( 'sections' );
$sql = "SELECT p.*, s.name AS section_name,
MAX(CASE WHEN m.type = 'water' THEN m.meter_number ELSE NULL END) AS water_meter_number,
MAX(CASE WHEN m.type = 'power' THEN m.meter_number ELSE NULL END) AS power_meter_number,
COUNT(DISTINCT pm.user_id) AS member_count,
COUNT(DISTINCT pt.tenant_id) AS tenant_count
FROM {$this->table} p
LEFT JOIN {$sections} s ON s.id = p.section_id
LEFT JOIN {$meters} m ON m.parcel_id = p.id
LEFT JOIN {$parcel_members} pm ON pm.parcel_id = p.id
LEFT JOIN {$parcel_tenants} pt ON pt.parcel_id = p.id
WHERE 1=1";
$params = array();
if ( '' !== $search ) {
$like = '%' . $this->wpdb->esc_like( $search ) . '%';
$sql .= ' AND (p.label LIKE %s OR p.note LIKE %s)';
$params[] = $like;
$params[] = $like;
}
if ( in_array( $status, array( 'free', 'assigned', 'reserved', 'inactive' ), true ) ) {
$sql .= ' AND p.status = %s';
$params[] = $status;
}
if ( $section_id > 0 ) {
$sql .= ' AND p.section_id = %d';
$params[] = $section_id;
}
$sql .= " GROUP BY p.id ORDER BY p.{$orderby} {$order}, p.id DESC";
if ( $limit > 0 ) {
$sql .= ' LIMIT %d';
$params[] = $limit;
if ( $offset > 0 ) {
$sql .= ' OFFSET %d';
$params[] = $offset;
}
}
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
}
/**
* Count parcels for the current filter set.
*
* @param array $args Query arguments.
* @return int
*/
public function count_filtered( $args = array() ) {
$search = isset( $args['s'] ) ? sanitize_text_field( wp_unslash( $args['s'] ) ) : '';
$status = isset( $args['status'] ) ? sanitize_key( wp_unslash( $args['status'] ) ) : '';
$section_id = isset( $args['section_id'] ) ? absint( $args['section_id'] ) : 0;
$sql = "SELECT COUNT(*) FROM {$this->table} p WHERE 1=1";
$params = array();
if ( '' !== $search ) {
$like = '%' . $this->wpdb->esc_like( $search ) . '%';
$sql .= ' AND (p.label LIKE %s OR p.note LIKE %s)';
$params[] = $like;
$params[] = $like;
}
if ( in_array( $status, array( 'free', 'assigned', 'reserved', 'inactive' ), true ) ) {
$sql .= ' AND p.status = %s';
$params[] = $status;
}
if ( $section_id > 0 ) {
$sql .= ' AND p.section_id = %d';
$params[] = $section_id;
}
if ( ! empty( $params ) ) {
return (int) $this->wpdb->get_var( $this->wpdb->prepare( $sql, $params ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
return (int) $this->wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
}
/**
* Save or update parcel.
*
* @param array $data Parcel data.
* @param int $id Optional parcel ID.
* @return int|false
*/
public function save( $data, $id = 0 ) {
$payload = array(
'label' => $data['label'],
'section_id' => $data['section_id'],
'area' => $data['area'],
'annual_rent' => $data['annual_rent'],
'status' => $data['status'],
'note' => $data['note'],
'updated_at' => $this->now(),
);
$formats = array( '%s', '%d', '%f', '%f', '%s', '%s', '%s' );
if ( null === $payload['area'] ) {
$payload['area'] = null;
$formats[2] = '%s';
}
if ( null === $payload['annual_rent'] ) {
$payload['annual_rent'] = null;
$formats[3] = '%s';
}
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( '%s', '%d', $formats[2], $formats[3], '%s', '%s', '%s', '%s' ) );
return $this->wpdb->insert_id;
}
/**
* Check whether a parcel label already exists in the same section.
*
* @param string $label Parcel label.
* @param int $section_id Section ID.
* @param int $exclude_id Optional parcel ID.
* @return bool
*/
public function label_exists( $label, $section_id, $exclude_id = 0 ) {
$sql = "SELECT COUNT(*) FROM {$this->table} WHERE label = %s AND section_id = %d";
$params = array( $label, $section_id );
if ( $exclude_id > 0 ) {
$sql .= ' AND id != %d';
$params[] = $exclude_id;
}
return (int) $this->wpdb->get_var( $this->wpdb->prepare( $sql, $params ) ) > 0; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
}

View File

@@ -0,0 +1,139 @@
<?php
/**
* Section repository.
*
* @package KGV\VereinManager
*/
namespace KGV\VereinManager\Repositories;
use KGV\VereinManager\Schema;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class SectionRepository extends AbstractRepository {
/**
* Resolve table name.
*
* @return string
*/
protected function resolve_table() {
return Schema::table( 'sections' );
}
/**
* Search section list.
*
* @param array $args Query arguments.
* @return array
*/
public function search( $args = array() ) {
$search = isset( $args['s'] ) ? sanitize_text_field( wp_unslash( $args['s'] ) ) : '';
$status = isset( $args['status'] ) ? sanitize_key( wp_unslash( $args['status'] ) ) : '';
$orderby = $this->sanitize_orderby( isset( $args['orderby'] ) ? sanitize_key( wp_unslash( $args['orderby'] ) ) : 'name', array( 'name', 'status', 'created_at', 'updated_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 ( '' !== $search ) {
$like = '%' . $this->wpdb->esc_like( $search ) . '%';
$sql .= ' AND (name LIKE %s OR description LIKE %s)';
$params[] = $like;
$params[] = $like;
}
if ( in_array( $status, array( 'active', 'inactive' ), true ) ) {
$sql .= ' AND status = %s';
$params[] = $status;
}
$sql .= " ORDER BY {$orderby} {$order}";
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 section.
*
* @param array $data Section data.
* @param int $id Optional ID.
* @return int|false
*/
public function save( $data, $id = 0 ) {
$payload = array(
'name' => $data['name'],
'description' => $data['description'],
'status' => $data['status'],
'updated_at' => $this->now(),
);
$formats = array( '%s', '%s', '%s', '%s' );
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( '%s', '%s', '%s', '%s', '%s' ) );
return $this->wpdb->insert_id;
}
/**
* Check if a section name already exists.
*
* @param string $name Section name.
* @param int $exclude_id Optional ID to exclude.
* @return bool
*/
public function name_exists( $name, $exclude_id = 0 ) {
$sql = "SELECT COUNT(*) FROM {$this->table} WHERE name = %s";
if ( $exclude_id > 0 ) {
$sql .= ' AND id != %d';
return (int) $this->wpdb->get_var( $this->wpdb->prepare( $sql, $name, $exclude_id ) ) > 0; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
return (int) $this->wpdb->get_var( $this->wpdb->prepare( $sql, $name ) ) > 0; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
}
/**
* Lightweight options list.
*
* @param bool $active_only Restrict to active sections.
* @return array
*/
public function all_for_options( $active_only = false ) {
$sql = "SELECT * FROM {$this->table}";
if ( $active_only ) {
$sql .= " WHERE status = 'active'";
}
$sql .= ' ORDER BY name ASC';
return $this->wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
}
/**
* Check whether section still has related records.
*
* @param int $id Section ID.
* @return bool
*/
public function is_in_use( $id ) {
$parcel_count = (int) $this->wpdb->get_var( $this->wpdb->prepare( 'SELECT COUNT(*) FROM ' . Schema::table( 'parcels' ) . ' WHERE section_id = %d', $id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$meter_count = (int) $this->wpdb->get_var( $this->wpdb->prepare( 'SELECT COUNT(*) FROM ' . Schema::table( 'meters' ) . ' WHERE section_id = %d', $id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
return $parcel_count > 0 || $meter_count > 0;
}
}

View File

@@ -0,0 +1,112 @@
<?php
/**
* Tenant repository.
*
* @package KGV\VereinManager
*/
namespace KGV\VereinManager\Repositories;
use KGV\VereinManager\Schema;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class TenantRepository extends AbstractRepository {
/**
* Resolve table name.
*
* @return string
*/
protected function resolve_table() {
return Schema::table( 'tenants' );
}
/**
* Search tenant list.
*
* @param array $args Query arguments.
* @return array
*/
public function search( $args = array() ) {
$search = isset( $args['s'] ) ? sanitize_text_field( wp_unslash( $args['s'] ) ) : '';
$status = isset( $args['status'] ) ? sanitize_key( wp_unslash( $args['status'] ) ) : '';
$orderby = $this->sanitize_orderby( isset( $args['orderby'] ) ? sanitize_key( wp_unslash( $args['orderby'] ) ) : 'last_name', array( 'last_name', 'first_name', 'contract_start', 'is_active', 'created_at' ), 'last_name' );
$order = $this->sanitize_order( isset( $args['order'] ) ? sanitize_key( wp_unslash( $args['order'] ) ) : 'ASC' );
$parcel_tenants = Schema::table( 'parcel_tenants' );
$sql = "SELECT t.*, (SELECT COUNT(*) FROM {$parcel_tenants} pt WHERE pt.tenant_id = t.id) AS parcel_count
FROM {$this->table} t
WHERE 1=1";
$params = array();
if ( '' !== $search ) {
$like = '%' . $this->wpdb->esc_like( $search ) . '%';
$sql .= ' AND (t.first_name LIKE %s OR t.last_name LIKE %s OR t.email LIKE %s OR t.phone LIKE %s)';
$params[] = $like;
$params[] = $like;
$params[] = $like;
$params[] = $like;
}
if ( in_array( $status, array( 'active', 'inactive' ), true ) ) {
$sql .= ' AND t.is_active = ' . ( 'active' === $status ? '1' : '0' );
}
$sql .= " ORDER BY t.{$orderby} {$order}, t.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 tenant.
*
* @param array $data Tenant data.
* @param int $id Optional ID.
* @return int|false
*/
public function save( $data, $id = 0 ) {
$payload = array(
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'address' => $data['address'],
'phone' => $data['phone'],
'email' => $data['email'],
'contract_start' => $data['contract_start'],
'contract_end' => $data['contract_end'] ? $data['contract_end'] : null,
'is_active' => $data['is_active'],
'note' => $data['note'],
'updated_at' => $this->now(),
);
$formats = array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s' );
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( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s' ) );
return $this->wpdb->insert_id;
}
/**
* Return active tenants for selection lists.
*
* @return array
*/
public function all_active() {
$sql = "SELECT * FROM {$this->table} WHERE is_active = 1 ORDER BY last_name ASC, first_name ASC";
return $this->wpdb->get_results( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
}
}