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
471 lines
14 KiB
PHP
471 lines
14 KiB
PHP
<?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;
|
|
}
|
|
}
|