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