Release 1.16.0
Arbeitsstunden-Modul hinzugefuegt - Pflichtstunden pro Jahr inkl. Preis je fehlender Stunde - Arbeitsarten, Arbeitseintraege und Mehrfachzuordnung von Mitgliedern - Mitgliederuebersicht mit Berechnung fehlender Stunden und Aufschlag - Datenbankschema fuer Arbeitsstunden erweitert - Stable Tag und Changelog in README/readme.txt aktualisiert
This commit is contained in:
470
includes/Repositories/WorkRepository.php
Normal file
470
includes/Repositories/WorkRepository.php
Normal file
@@ -0,0 +1,470 @@
|
||||
<?php
|
||||
/**
|
||||
* Work hours repository.
|
||||
*
|
||||
* @package KGV\VereinManager
|
||||
*/
|
||||
|
||||
namespace KGV\VereinManager\Repositories;
|
||||
|
||||
use KGV\VereinManager\Schema;
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class WorkRepository extends AbstractRepository {
|
||||
|
||||
/**
|
||||
* Resolve main table (work_logs).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function resolve_table() {
|
||||
return Schema::table( 'work_logs' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Jobs table.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function jobs_table() {
|
||||
return Schema::table( 'work_jobs' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Year config table.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function year_config_table() {
|
||||
return Schema::table( 'work_year_config' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log members table.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function members_table() {
|
||||
return Schema::table( 'work_log_members' );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Jobs
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get all jobs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_jobs() {
|
||||
$table = $this->jobs_table();
|
||||
return $this->wpdb->get_results( "SELECT * FROM {$table} ORDER BY name ASC" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single job by ID.
|
||||
*
|
||||
* @param int $id Job ID.
|
||||
* @return object|null
|
||||
*/
|
||||
public function find_job( $id ) {
|
||||
$table = $this->jobs_table();
|
||||
return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", absint( $id ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or update a job.
|
||||
*
|
||||
* @param array $data Job data.
|
||||
* @param int $id Optional ID.
|
||||
* @return int|false
|
||||
*/
|
||||
public function save_job( $data, $id = 0 ) {
|
||||
$table = $this->jobs_table();
|
||||
$payload = array(
|
||||
'name' => sanitize_text_field( $data['name'] ),
|
||||
'description' => isset( $data['description'] ) ? sanitize_textarea_field( $data['description'] ) : '',
|
||||
'updated_at' => $this->now(),
|
||||
);
|
||||
$formats = array( '%s', '%s', '%s' );
|
||||
|
||||
if ( $id > 0 ) {
|
||||
$this->wpdb->update( $table, $payload, array( 'id' => $id ), $formats, array( '%d' ) );
|
||||
return $id;
|
||||
}
|
||||
|
||||
$payload['created_at'] = $this->now();
|
||||
$this->wpdb->insert( $table, $payload, array( '%s', '%s', '%s', '%s' ) );
|
||||
return $this->wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a job and related log members/logs.
|
||||
*
|
||||
* @param int $id Job ID.
|
||||
* @return bool
|
||||
*/
|
||||
public function delete_job( $id ) {
|
||||
$id = absint( $id );
|
||||
$logs_table = $this->table;
|
||||
$mem_table = $this->members_table();
|
||||
$jobs_table = $this->jobs_table();
|
||||
|
||||
// Delete members of all logs for this job.
|
||||
$log_ids = $this->wpdb->get_col( $this->wpdb->prepare( "SELECT id FROM {$logs_table} WHERE job_id = %d", $id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
foreach ( array_map( 'absint', (array) $log_ids ) as $log_id ) {
|
||||
$this->wpdb->delete( $mem_table, array( 'log_id' => $log_id ), array( '%d' ) );
|
||||
}
|
||||
// Delete logs.
|
||||
$this->wpdb->delete( $logs_table, array( 'job_id' => $id ), array( '%d' ) );
|
||||
// Delete job.
|
||||
$result = $this->wpdb->delete( $jobs_table, array( 'id' => $id ), array( '%d' ) );
|
||||
return false !== $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a job name already exists (for uniqueness).
|
||||
*
|
||||
* @param string $name Job name.
|
||||
* @param int $exclude_id Exclude this ID.
|
||||
* @return bool
|
||||
*/
|
||||
public function job_name_exists( $name, $exclude_id = 0 ) {
|
||||
$table = $this->jobs_table();
|
||||
$sql = $this->wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE name = %s", sanitize_text_field( $name ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
if ( $exclude_id > 0 ) {
|
||||
$sql .= $this->wpdb->prepare( ' AND id != %d', absint( $exclude_id ) );
|
||||
}
|
||||
return (int) $this->wpdb->get_var( $sql ) > 0; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Year config
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get config for a specific year (creates if missing).
|
||||
*
|
||||
* @param int $year Year.
|
||||
* @return object|null
|
||||
*/
|
||||
public function get_year_config( $year ) {
|
||||
$year = absint( $year );
|
||||
$table = $this->year_config_table();
|
||||
$row = $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$table} WHERE entry_year = %d", $year ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available configured years.
|
||||
*
|
||||
* @param int $selected_year Optional selected year to always include.
|
||||
* @return array
|
||||
*/
|
||||
public function get_years( $selected_year = 0 ) {
|
||||
$config_table = $this->year_config_table();
|
||||
$log_table = $this->table;
|
||||
$current_year = (int) current_time( 'Y' );
|
||||
|
||||
$config_years = $this->wpdb->get_col( "SELECT entry_year FROM {$config_table} ORDER BY entry_year DESC" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$log_years = $this->wpdb->get_col( "SELECT DISTINCT YEAR(work_date) FROM {$log_table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
|
||||
$years = array_map( 'absint', array_merge( (array) $config_years, (array) $log_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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save year configuration (upsert).
|
||||
*
|
||||
* @param int $year Year.
|
||||
* @param float $required_hours Required hours per member.
|
||||
* @param float $price_per_missing_hour Price per missing hour in €.
|
||||
* @return bool
|
||||
*/
|
||||
public function save_year_config( $year, $required_hours, $price_per_missing_hour ) {
|
||||
$year = absint( $year );
|
||||
$required_hours = max( 0, (float) $required_hours );
|
||||
$price_per_missing_hour = max( 0, (float) $price_per_missing_hour );
|
||||
$table = $this->year_config_table();
|
||||
$existing = $this->get_year_config( $year );
|
||||
|
||||
if ( $existing ) {
|
||||
$result = $this->wpdb->update(
|
||||
$table,
|
||||
array(
|
||||
'required_hours' => $required_hours,
|
||||
'price_per_missing_hour' => $price_per_missing_hour,
|
||||
'updated_at' => $this->now(),
|
||||
),
|
||||
array( 'entry_year' => $year ),
|
||||
array( '%f', '%f', '%s' ),
|
||||
array( '%d' )
|
||||
);
|
||||
return false !== $result;
|
||||
}
|
||||
|
||||
$result = $this->wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'entry_year' => $year,
|
||||
'required_hours' => $required_hours,
|
||||
'price_per_missing_hour' => $price_per_missing_hour,
|
||||
'created_at' => $this->now(),
|
||||
'updated_at' => $this->now(),
|
||||
),
|
||||
array( '%d', '%f', '%f', '%s', '%s' )
|
||||
);
|
||||
return false !== $result;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Work logs
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Search work logs.
|
||||
*
|
||||
* @param array $args Query arguments (year, user_id, job_id, s, orderby, order).
|
||||
* @return array
|
||||
*/
|
||||
public function search_logs( $args = array() ) {
|
||||
$year = isset( $args['year'] ) ? absint( $args['year'] ) : 0;
|
||||
$user_id = isset( $args['user_id'] ) ? absint( $args['user_id'] ) : 0;
|
||||
$job_id = isset( $args['job_id'] ) ? absint( $args['job_id'] ) : 0;
|
||||
$search = isset( $args['s'] ) ? sanitize_text_field( wp_unslash( $args['s'] ) ) : '';
|
||||
$orderby = $this->sanitize_orderby(
|
||||
isset( $args['orderby'] ) ? sanitize_key( wp_unslash( $args['orderby'] ) ) : 'work_date',
|
||||
array( 'work_date', 'job_name', 'updated_at', 'created_at' ),
|
||||
'work_date'
|
||||
);
|
||||
$order = $this->sanitize_order( isset( $args['order'] ) ? sanitize_key( wp_unslash( $args['order'] ) ) : 'DESC' );
|
||||
|
||||
$logs_table = $this->table;
|
||||
$jobs_table = $this->jobs_table();
|
||||
$mem_table = $this->members_table();
|
||||
|
||||
$sql = "SELECT l.*, COALESCE(j.name, '') AS job_name FROM {$logs_table} l
|
||||
LEFT JOIN {$jobs_table} j ON j.id = l.job_id WHERE 1=1";
|
||||
$params = array();
|
||||
|
||||
if ( $year > 0 ) {
|
||||
$sql .= ' AND YEAR(l.work_date) = %d';
|
||||
$params[] = $year;
|
||||
}
|
||||
|
||||
if ( $job_id > 0 ) {
|
||||
$sql .= ' AND l.job_id = %d';
|
||||
$params[] = $job_id;
|
||||
}
|
||||
|
||||
if ( $user_id > 0 ) {
|
||||
$sql .= " AND l.id IN (SELECT log_id FROM {$mem_table} WHERE user_id = %d)";
|
||||
$params[] = $user_id;
|
||||
}
|
||||
|
||||
if ( '' !== $search ) {
|
||||
$like = '%' . $this->wpdb->esc_like( $search ) . '%';
|
||||
$sql .= ' AND (j.name LIKE %s OR l.note LIKE %s)';
|
||||
$params[] = $like;
|
||||
$params[] = $like;
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY l.{$orderby} {$order}, l.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
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a work log with its member assignments.
|
||||
*
|
||||
* @param int $id Log ID.
|
||||
* @return object|null
|
||||
*/
|
||||
public function find_log( $id ) {
|
||||
$row = $this->find( $id );
|
||||
if ( $row ) {
|
||||
$row->members = $this->get_log_members( $id );
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or update a work log including member assignments.
|
||||
*
|
||||
* @param array $data Log data.
|
||||
* @param int $id Optional log ID.
|
||||
* @param array $members Array of [user_id => hours].
|
||||
* @return int|false
|
||||
*/
|
||||
public function save_log( $data, $id = 0, $members = array() ) {
|
||||
$payload = array(
|
||||
'job_id' => absint( $data['job_id'] ),
|
||||
'work_date' => sanitize_text_field( $data['work_date'] ),
|
||||
'note' => isset( $data['note'] ) ? sanitize_textarea_field( $data['note'] ) : '',
|
||||
'updated_at' => $this->now(),
|
||||
);
|
||||
$formats = array( '%d', '%s', '%s', '%s' );
|
||||
|
||||
if ( $id > 0 ) {
|
||||
$this->wpdb->update( $this->table, $payload, array( 'id' => $id ), $formats, array( '%d' ) );
|
||||
$log_id = $id;
|
||||
} else {
|
||||
$payload['created_at'] = $this->now();
|
||||
$this->wpdb->insert( $this->table, $payload, array( '%d', '%s', '%s', '%s', '%s' ) );
|
||||
$log_id = $this->wpdb->insert_id;
|
||||
}
|
||||
|
||||
if ( $log_id && is_array( $members ) ) {
|
||||
$this->sync_log_members( $log_id, $members );
|
||||
}
|
||||
|
||||
return $log_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a work log and its member assignments.
|
||||
*
|
||||
* @param int $id Log ID.
|
||||
* @return bool
|
||||
*/
|
||||
public function delete_log( $id ) {
|
||||
$id = absint( $id );
|
||||
$this->wpdb->delete( $this->members_table(), array( 'log_id' => $id ), array( '%d' ) );
|
||||
$result = $this->wpdb->delete( $this->table, array( 'id' => $id ), array( '%d' ) );
|
||||
return false !== $result;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Log members
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get member assignments for a log.
|
||||
*
|
||||
* @param int $log_id Log ID.
|
||||
* @return array
|
||||
*/
|
||||
public function get_log_members( $log_id ) {
|
||||
$table = $this->members_table();
|
||||
return $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$table} WHERE log_id = %d", absint( $log_id ) ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync member assignments for a log.
|
||||
*
|
||||
* @param int $log_id Log ID.
|
||||
* @param array $members Array of [user_id => hours].
|
||||
* @return void
|
||||
*/
|
||||
public function sync_log_members( $log_id, $members ) {
|
||||
$log_id = absint( $log_id );
|
||||
$table = $this->members_table();
|
||||
$now = $this->now();
|
||||
|
||||
$this->wpdb->delete( $table, array( 'log_id' => $log_id ), array( '%d' ) );
|
||||
|
||||
foreach ( $members as $user_id => $hours ) {
|
||||
$user_id = absint( $user_id );
|
||||
$hours = max( 0, (float) $hours );
|
||||
if ( $user_id < 1 ) {
|
||||
continue;
|
||||
}
|
||||
$this->wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'log_id' => $log_id,
|
||||
'user_id' => $user_id,
|
||||
'hours' => $hours,
|
||||
'created_at' => $now,
|
||||
),
|
||||
array( '%d', '%d', '%f', '%s' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Summary / statistics
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get completed hours per member for a given year.
|
||||
*
|
||||
* @param int $year Year.
|
||||
* @return array Array keyed by user_id with total hours.
|
||||
*/
|
||||
public function get_hours_per_member( $year ) {
|
||||
$year = absint( $year );
|
||||
$log_table = $this->table;
|
||||
$mem_table = $this->members_table();
|
||||
|
||||
$rows = $this->wpdb->get_results(
|
||||
$this->wpdb->prepare(
|
||||
"SELECT m.user_id, SUM(m.hours) AS total_hours
|
||||
FROM {$mem_table} m
|
||||
INNER JOIN {$log_table} l ON l.id = m.log_id
|
||||
WHERE YEAR(l.work_date) = %d
|
||||
GROUP BY m.user_id", // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
$year
|
||||
)
|
||||
);
|
||||
|
||||
$result = array();
|
||||
foreach ( $rows as $row ) {
|
||||
$result[ absint( $row->user_id ) ] = (float) $row->total_hours;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build per-member work summary for a year.
|
||||
*
|
||||
* Returns array of objects with user_id, display_name, completed_hours,
|
||||
* required_hours, missing_hours, surcharge.
|
||||
*
|
||||
* @param int $year Year.
|
||||
* @param array $all_members Array of WP_User objects.
|
||||
* @return array
|
||||
*/
|
||||
public function get_member_summary( $year, $all_members ) {
|
||||
$config = $this->get_year_config( $year );
|
||||
$required_hours = $config ? (float) $config->required_hours : 0.0;
|
||||
$price = $config ? (float) $config->price_per_missing_hour : 0.0;
|
||||
$done_map = $this->get_hours_per_member( $year );
|
||||
|
||||
$summary = array();
|
||||
foreach ( $all_members as $member ) {
|
||||
$uid = (int) $member->ID;
|
||||
$completed = isset( $done_map[ $uid ] ) ? $done_map[ $uid ] : 0.0;
|
||||
$missing = max( 0.0, $required_hours - $completed );
|
||||
$surcharge = round( $missing * $price, 2 );
|
||||
|
||||
$obj = new \stdClass();
|
||||
$obj->user_id = $uid;
|
||||
$obj->display_name = $member->display_name;
|
||||
$obj->completed_hours = $completed;
|
||||
$obj->required_hours = $required_hours;
|
||||
$obj->missing_hours = $missing;
|
||||
$obj->surcharge = $surcharge;
|
||||
|
||||
$summary[] = $obj;
|
||||
}
|
||||
return $summary;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user