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