diff --git a/CHANGELOG.md b/CHANGELOG.md index 45d0842..4d52d45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # CHANGELOG +## v8.2 + +* Users are now able to propose corrections to worktimes when they have been marked as for "in review". + +## v8.1 + +* Toil API release `1.13`: + * Added `editUser` endpoint + * Added `getOwnUser` endpoint + * `healthcheck` endpoint now includes the server time (ISO-8601) and API version +* Enhanced `app.json` with the `mobile` section (enable/disable app, enable/disable API token generation within Settings, per-Client rate limit, enable/disable QR-code mobile pairing) - for future mobile app release +* Fixed translations for German and English +* Added example values to `LDAP` section within `app.json` +* The ID of the worktime is now being displayed within `Worktime records` and `All worktime records`. +* Updated `README.md` +* **Update requires DB migration** (see `README.md` section `Database`) +* Added projects (read more within the `README.md`) +* Upgrading composer dependencies, please run `composer update`: + * `simple-router`: `4.3.7.2` to `5.0.0.3` +* Added function to check if current user is admin +* Added function to redirect directly to the suite page if an error occurs +* Fixed an error causing infinite redirects to the 500 HTTP Page when the database is not available. + +## v8.0.2 + +* Small fixes to the web UI +* Updated `README.md` + ## v8.0.1 * Readded the `nfclogin` button to the login page diff --git a/README.md b/README.md index aa8b019..61220cf 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,8 @@ TimeTrack aims to be an easy-to-use time recording software for small enterprise ### Requirements -- at least PHP 8.2 (`curl|gd|gmp|intl|mbstring|mysqli|openssl|xsl|gettext|dom|ldap`) -- composer (to install dependencies; phpmailer: for sending emails via smtp, parsedown: markdown parser for the `CHANGELOG.md`, simple-router: does the API routing, yaml: for reading plugin yaml files, ldaptools: for LDAP authentication, dompdf: for PDF generation, phinx: for database migrations) +- PHP 8.2 (`curl|gd|gmp|intl|mbstring|mysqli|openssl|xsl|gettext|dom|ldap`) - tested with PHP 8.2.26 +- composer (to install dependencies; phpmailer: for sending emails via smtp, parsedown: markdown parser for the `CHANGELOG.md`, simple-router: does the API routing, yaml: for reading plugin yaml files, ldaptools: for LDAP authentication, dompdf: for PDF generation, phinx: for database migrations, event-dispatcher: for handling events, contracts: for defining events and interfaces) - Apache2.4 with enabled rewrite mod (optional) This software has been tested on Debian 11/12, XAMPP, PHP internal server (e.g. `php -S 0.0.0.0:80`). @@ -40,6 +40,9 @@ Simply install the software by following these steps: - Start webserver e.g. `service apache2 stop && php -S 0.0.0.0:80` or using apache2 (then you have to configure the `sites-available` conf yourself) - You can then access TimeTrack in your browser at `http://localhost`, default login is `admin` with password `admin`. Create yourself a new admin account, login and delete the default account afterwards. +To save log files, please create the subfolder `data/logs` and make it writeable to the web server (e.g. `chown www-data:www-data data/logs && chmod 775 data/logs`). +Please also make sure that the `/data` directory is writable by the webserver, aswell as the plugins directory (default: `api/v1/class/plugins/plugins`). + ### Configure app.json In step 2, you need to configure the `app.json.sample` within the `api/v1/inc` folder: @@ -176,6 +179,13 @@ You can access the plugin by navigating to `Plugins` -> `[codeclock] View PIN`. To login with the PIN navigate to http://BASE_URL/api/v1/toil/code and enter your PIN. +## Projects + +Administrators can now create Projects within the `Projects management` tab. +Users can access their projects within the `Projects` tab. + +You can create items for their projects and map worktimes to it. The feature will be reworked in the future. + ## Updates TimeTrack has to be updated in two ways: database and application. @@ -184,6 +194,7 @@ TimeTrack has to be updated in two ways: database and application. If downloaded from GitHub you can simply pull the latest release e.g. `git pull` If downloaded any other way, just make sure to copy and paste the new files into TimeTrack's root directory. +**Check the changelogs**: They usually tell you if you need to update composer dependencies (`composer update`) or if a database migration is required. ### Database diff --git a/VERSION b/VERSION index 5210382..8d1eec6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.0.1 \ No newline at end of file +8.1 \ No newline at end of file diff --git a/api/v1/class/arbeitszeit.inc.php b/api/v1/class/arbeitszeit.inc.php index 2338780..d83c29d 100644 --- a/api/v1/class/arbeitszeit.inc.php +++ b/api/v1/class/arbeitszeit.inc.php @@ -13,6 +13,7 @@ use Arbeitszeit\ExportModule; use Arbeitszeit\Mails; use Arbeitszeit\Nodes; + use Arbeitszeit\Projects; use Arbeitszeit\StatusMessages; use Arbeitszeit\Events\EventDispatcherService; use Arbeitszeit\Events\EasymodeWorktimeAddedEvent; // "EasymodeWorktimeSTARTED" Event, actually. @@ -47,11 +48,13 @@ class Arbeitszeit private $nodes; private $statusMessages; + private $projects; + public function __construct() { $this->db = new DB(); $this->init_lang() ?? null; - if(isset($this->get_app_ini()["general"]["timezone"])){ + if (isset($this->get_app_ini()["general"]["timezone"])) { try { date_default_timezone_set($this->get_app_ini()["general"]["timezone"]); } catch (\Exception $e) { @@ -84,7 +87,7 @@ public function init_lang() */ public function delete_worktime($id) { - if(!$this->nodes()->checkNode("arbeitszeit.inc", "delete_worktime")){ + if (!$this->nodes()->checkNode("arbeitszeit.inc", "delete_worktime")) { return false; } $data = $this->db->sendQuery("DELETE FROM arbeitszeiten WHERE id = ?")->execute([$id]); @@ -97,7 +100,7 @@ public function delete_worktime($id) ] ]; } else { - EventDispatcherService::get()->dispatch(new WorktimeDeletedEvent($_SESSION["username"], (int)$id), WorktimeDeletedEvent::NAME); + EventDispatcherService::get()->dispatch(new WorktimeDeletedEvent($_SESSION["username"], (int) $id), WorktimeDeletedEvent::NAME); Exceptions::error_rep("Worktime entry with ID '{$id}' deleted successfully."); return 1; } @@ -107,7 +110,7 @@ public function delete_worktime($id) public static function add_easymode_worktime($username) { $nodes = new Nodes; - if(!$nodes->checkNode("arbeitszeit.inc", "add_easymode_worktime")) { + if (!$nodes->checkNode("arbeitszeit.inc", "add_easymode_worktime")) { return false; } Exceptions::error_rep("Creating easymode worktime entry for user '{$username}'..."); @@ -138,7 +141,7 @@ public static function add_easymode_worktime($username) public static function end_easymode_worktime($username, $id) { $nodes = new Nodes; - if(!$nodes->checkNode("arbeitszeit.inc", "end_easymode_worktime")) { + if (!$nodes->checkNode("arbeitszeit.inc", "end_easymode_worktime")) { return false; } Exceptions::error_rep("Ending easymode worktime for user '{$username}'..."); @@ -157,7 +160,7 @@ public static function end_easymode_worktime($username, $id) Exceptions::error_rep("An error occurred while ending easymode worktime. See previous message for more information."); return false; } else { - EventDispatcherService::get()->dispatch(new EasymodeWorktimeEndedEvent($username, (int)$id), EasymodeWorktimeEndedEvent::NAME); + EventDispatcherService::get()->dispatch(new EasymodeWorktimeEndedEvent($username, (int) $id), EasymodeWorktimeEndedEvent::NAME); Exceptions::error_rep("Easymode worktime ended for user '{$username}'"); return true; } @@ -166,7 +169,7 @@ public static function end_easymode_worktime($username, $id) public function start_easymode_pause_worktime($username, $id) { - if(!$this->nodes()->checkNode("arbeitszeit.inc", "start_easymode_pause_worktime")) { + if (!$this->nodes()->checkNode("arbeitszeit.inc", "start_easymode_pause_worktime")) { return false; } Exceptions::error_rep("Starting easymode pause for user '{$username}'..."); @@ -184,7 +187,7 @@ public function start_easymode_pause_worktime($username, $id) Exceptions::error_rep("An error occurred while starting user pause for worktime with ID '{$id}' for user '{$username}'. See previous message for more information."); return false; } else { - EventDispatcherService::get()->dispatch(new EasymodeWorktimePauseStartEvent($username, (int)$id), EasymodeWorktimePauseStartEvent::NAME); + EventDispatcherService::get()->dispatch(new EasymodeWorktimePauseStartEvent($username, (int) $id), EasymodeWorktimePauseStartEvent::NAME); Exceptions::error_rep("Easymode pause started for user '{$username}'"); return true; } @@ -192,7 +195,7 @@ public function start_easymode_pause_worktime($username, $id) } public function end_easymode_pause_worktime($username, $id) { - if(!$this->nodes()->checkNode("arbeitszeit.inc", "end_easymode_pause_worktime")) { + if (!$this->nodes()->checkNode("arbeitszeit.inc", "end_easymode_pause_worktime")) { return false; } Exceptions::error_rep("Ending easymode pause for user '{$username}'..."); @@ -210,7 +213,7 @@ public function end_easymode_pause_worktime($username, $id) Exceptions::error_rep("An error occurred while ending user pause for worktime with ID '{$id}' for user '{$username}'. See previous message for more information."); return false; } else { - EventDispatcherService::get()->dispatch(new EasymodeWorktimePauseEndEvent($username, (int)$id), EasymodeWorktimePauseEndEvent::NAME); + EventDispatcherService::get()->dispatch(new EasymodeWorktimePauseEndEvent($username, (int) $id), EasymodeWorktimePauseEndEvent::NAME); Exceptions::error_rep("Easymode pause ended for user '{$username}'"); return true; } @@ -219,7 +222,7 @@ public function end_easymode_pause_worktime($username, $id) public function toggle_easymode($username) { - if(!$this->nodes()->checkNode("arbeitszeit.inc", "toggle_easymode")) { + if (!$this->nodes()->checkNode("arbeitszeit.inc", "toggle_easymode")) { return false; } Exceptions::error_rep("Toggling easymode for user '{$username}'..."); @@ -254,7 +257,7 @@ public function toggle_easymode($username) public function get_easymode_status($username, $mode = 0) { - if(!$this->nodes()->checkNode("arbeitszeit.inc", "get_easymode_status")) { + if (!$this->nodes()->checkNode("arbeitszeit.inc", "get_easymode_status")) { return false; } Exceptions::error_rep("Getting easymode status for user '{$username}'..."); @@ -294,7 +297,7 @@ public function get_easymode_status($username, $mode = 0) public static function check_easymode_worktime_finished($username) { $nodes = new Nodes; - if(!$nodes->checkNode("arbeitszeit.inc", "check_easymode_worktime_finished")) { + if (!$nodes->checkNode("arbeitszeit.inc", "check_easymode_worktime_finished")) { return false; } Exceptions::error_rep("Checking easymode worktime for user '{$username}'..."); @@ -332,7 +335,7 @@ public static function check_easymode_worktime_finished($username) */ public function add_worktime($start, $end, $location, $date, $username, $type, $Wtype = 0, $pause = null, $meta = null) { - if(!$this->nodes()->checkNode("arbeitszeit.inc", "add_worktime")) { + if (!$this->nodes()->checkNode("arbeitszeit.inc", "add_worktime")) { return false; } $user = new Benutzer; @@ -346,7 +349,7 @@ public function add_worktime($start, $end, $location, $date, $username, $type, $ return false; } else { - if($this->type_from_int($Wtype) == false){ + if ($this->type_from_int($Wtype) == false) { Exceptions::error_rep("An error occurred while creating an worktime entry. The worktime type does not exist."); return false; } @@ -354,7 +357,7 @@ public function add_worktime($start, $end, $location, $date, $username, $type, $ Exceptions::error_rep("Creating worktime entry for user '{$username}'..."); $sql = "INSERT INTO `arbeitszeiten` (`name`, `id`, `email`, `username`, `schicht_tag`, `schicht_anfang`, `schicht_ende`, `ort`, `review`, `active`, `type`, `Wtype`, `pause_start`, `pause_end`, `attachements`) VALUES ( ?, '0', ?, ?, ?, ?, ?, ?, '0', '0', ?, ?, ?, ?, ?);"; $data = $this->db->sendQuery($sql); - $data->execute([$usr["name"], $usr["email"], $username, $date, $start, $end, $location, $type, $Wtype ,$pause["start"], $pause["end"], $meta]); + $data->execute([$usr["name"], $usr["email"], $username, $date, $start, $end, $location, $type, $Wtype, $pause["start"], $pause["end"], $meta]); if (!$data) { Exceptions::error_rep("An error occurred while creating an worktime entry. See previous message for more information."); return false; @@ -366,7 +369,57 @@ public function add_worktime($start, $end, $location, $date, $username, $type, $ } } - public static function type_from_int($int){ + public function update_worktime($id, $array) + { + if(!$this->check_if_for_review($id)){ + return false; + } + $allowed = [ + "schicht_anfang", + "schicht_ende", + "ort" + ]; + + $fields = []; + $values = []; + + foreach ($allowed as $col) { + if (isset($array[$col]) && $array[$col] !== "" && $array[$col] !== null && $array[$col] !== false) { + $fields[] = "$col = ?"; + $values[] = $array[$col]; + } + } + + if (empty($fields)) { + return false; + } + + $sql = "UPDATE arbeitszeiten SET " . implode(", ", $fields) . " WHERE id = ?"; + $values[] = $id; + + try { + $stmt = $this->db->sendQuery($sql); + $ok = $stmt->execute($values); + + if (!$ok) { + return false; + } + + if ($stmt->rowCount() === 0) { + return false; + } + + return true; + } catch (\PDOException $e) { + Exceptions::error_rep("[WORKTIME] Update failed: " . $e->getMessage()); + return false; + } + } + + + + public static function type_from_int($int) + { $types = json_decode(file_get_contents($_SERVER["DOCUMENT_ROOT"] . self::get_app_ini()["config"]["worktime_types"]), true); if (isset($types[$int])) { return $types[$int]; @@ -377,7 +430,8 @@ public static function type_from_int($int){ } - public static function get_all_types(){ + public static function get_all_types() + { $types = json_decode(file_get_contents($_SERVER["DOCUMENT_ROOT"] . self::get_app_ini()["config"]["worktime_types"]), true); if (isset($types)) { return $types; @@ -439,7 +493,7 @@ private static function sanitizeOutput($data) public function get_all_worktime() { - if(!$this->nodes()->checkNode("arbeitszeit.inc", "get_all_worktime")) { + if (!$this->nodes()->checkNode("arbeitszeit.inc", "get_all_worktime")) { return false; } Exceptions::error_rep("Getting all worktimes..."); @@ -459,7 +513,7 @@ public function get_all_worktime() public function get_all_user_worktime($username) { - if(!$this->nodes()->checkNode("arbeitszeit.inc", "get_all_user_worktime")) { + if (!$this->nodes()->checkNode("arbeitszeit.inc", "get_all_user_worktime")) { return false; } Exceptions::error_rep("Getting all worktimes for user '{$username}'..."); @@ -479,7 +533,7 @@ public function get_all_user_worktime($username) public function get_specific_worktime_html(int $month, int $year) { - if(!$this->nodes()->checkNode("arbeitszeit.inc", "get_specific_worktime_html")) { + if (!$this->nodes()->checkNode("arbeitszeit.inc", "get_specific_worktime_html")) { return false; } Exceptions::error_rep("Getting worktimes rendered for month '{$month}' and year '{$year}'..."); @@ -537,6 +591,7 @@ public function get_specific_worktime_html(int $month, int $year) $rpe $rum $rno $rtn + $rqw @@ -554,7 +609,7 @@ public function get_specific_worktime_html(int $month, int $year) } public function get_employee_worktime_html($username) { - if(!$this->nodes()->checkNode("arbeitszeit.inc", "get_employee_worktime_html")) { + if (!$this->nodes()->checkNode("arbeitszeit.inc", "get_employee_worktime_html")) { return false; } Exceptions::error_rep("Getting worktimes rendered for user '{$username}'..."); @@ -575,7 +630,7 @@ public function get_employee_worktime_html($username) $rqw = $row["id"]; $rtn = $this->type_from_int($row["Wtype"]) ?? "N/A"; $rps = @strftime("%H:%M", strtotime($row["pause_start"])); - $rpe = @strftime("%H.%M", strtotime($row["pause_end"])); + $rpe = @strftime("%H:%M", strtotime($row["pause_end"])); if ($rps == "01:00" || $rps == null) { $rps = "-"; @@ -584,12 +639,14 @@ public function get_employee_worktime_html($username) $rpe = "-"; } - if ($row["review"] != 0) { + if ($row["review"] == 1 || $row["review"] == '1') { $rmn = "style='color: red;'" ?? null; $rno = "⚠ {$this->i18n["to_review"]} ⚠" ?? null; + $rrr = "| {$this->i18n["propose_correction"]}" ?? null; } else { $rmn = null; $rno = null; + $rrr = null; } $data = <<$rol $rps $rpe - $rum $rno + $rum $rno $rrr $rtn + $rqw @@ -618,7 +676,7 @@ public function get_employee_worktime_html($username) public function mark_for_review($id) { - if(!$this->nodes()->checkNode("arbeitszeit.inc", "mark_for_review")) { + if (!$this->nodes()->checkNode("arbeitszeit.inc", "mark_for_review")) { return false; } Exceptions::error_rep("Marking worktime with ID '{$id}' for review..."); @@ -628,14 +686,31 @@ public function mark_for_review($id) Exceptions::error_rep("An error occurred while marking an worktime as under review, id '{$id}'. See previous message for more information."); return false; } else { - EventDispatcherService::get()->dispatch(new WorktimeMarkedForReviewEvent($_SESSION["username"], (int)$id), WorktimeMarkedForReviewEvent::NAME); + EventDispatcherService::get()->dispatch(new WorktimeMarkedForReviewEvent($_SESSION["username"], (int) $id), WorktimeMarkedForReviewEvent::NAME); return true; } } + public function check_if_for_review($id) + { + Exceptions::error_rep("Checking if worktime with ID '{$id}' is for review..."); + $sql = "SELECT * FROM `arbeitszeiten` WHERE id = ? AND review = ?"; + $res = $this->db->sendQuery($sql); + $res->execute([$id, 1]); + + if (!$res) { + Exceptions::error_rep("An error occured while checking if worktime is for review. See previous message for more information."); + return false; + } else { + if ($res->rowCount() == 1) { + return true; + } + } + } + public function unlock_for_review($id) { - if(!$this->nodes()->checkNode("arbeitszeit.inc", "unlock_for_review")) { + if (!$this->nodes()->checkNode("arbeitszeit.inc", "unlock_for_review")) { return false; } Exceptions::error_rep("Unlocking worktime from review with ID '{$id}'..."); @@ -645,7 +720,7 @@ public function unlock_for_review($id) Exceptions::error_rep("An error occurred while unlocking an worktime from review, id '{$id}'. See previous message for more information."); return false; } else { - EventDispatcherService::get()->dispatch(new WorktimeUnlockedFromReviewEvent($_SESSION["username"], (int)$id), WorktimeUnlockedFromReviewEvent::NAME); + EventDispatcherService::get()->dispatch(new WorktimeUnlockedFromReviewEvent($_SESSION["username"], (int) $id), WorktimeUnlockedFromReviewEvent::NAME); return true; } } @@ -721,22 +796,26 @@ public static function fix_easymode_worktime($username) } - public function blockIfNotAdmin(){ - if(!Benutzer::is_admin(Benutzer::get_user($_SESSION["username"]))){ - header("Location: /"); + public function blockIfNotAdmin() + { + if (!Benutzer::is_admin(Benutzer::get_user($_SESSION["username"]))) { + $this->statusMessages()->redirect("error"); } return false; } - public function global_dispatcher(): \Symfony\Component\EventDispatcher\EventDispatcher { + public function global_dispatcher(): \Symfony\Component\EventDispatcher\EventDispatcher + { return \Arbeitszeit\Events\EventDispatcherService::get(); } - public function getTimeTrackVersion(){ + public function getTimeTrackVersion() + { return file_get_contents(dirname(__DIR__, 3) . "/VERSION"); } - public function getToilVersion(){ + public function getToilVersion() + { return file_get_contents(dirname(__DIR__, 1) . "/toil/VERSION"); } @@ -830,6 +909,13 @@ public function statusMessages(): StatusMessages $this->statusMessages = new StatusMessages; return $this->statusMessages; } + + public function projects(): Projects + { + if (!$this->projects) + $this->projects = new Projects; + return $this->projects; + } } } diff --git a/api/v1/class/benutzer/benutzer.arbeit.inc.php b/api/v1/class/benutzer/benutzer.arbeit.inc.php index d7f076a..e5c410f 100644 --- a/api/v1/class/benutzer/benutzer.arbeit.inc.php +++ b/api/v1/class/benutzer/benutzer.arbeit.inc.php @@ -297,6 +297,22 @@ public static function is_admin($user) } } + public static function current_user_is_admin(){ + if(self::get_current_user()["isAdmin"] == true){ + return true; + } else { + return false; + } + } + + public static function get_current_user(){ + return self::get_user($_SESSION["username"]); + } + + public static function get_name_from_id($id){ + return self::get_user_from_id($id)["name"]; + } + public function editUserProperties(mixed $username_or_id, string $name, mixed $value): bool { if($this->nodes()->checkNode("benutzer.inc", "editUserProperties") == false){ diff --git a/api/v1/class/i18n/admin/notifications/edit/snippets_DE.json b/api/v1/class/i18n/admin/notifications/edit/snippets_DE.json index 824e062..9226b42 100644 --- a/api/v1/class/i18n/admin/notifications/edit/snippets_DE.json +++ b/api/v1/class/i18n/admin/notifications/edit/snippets_DE.json @@ -1,5 +1,5 @@ { - "title": "Benachrichtigungeneintrag bearbeiten", + "title": "Benachrichtigungseintrag bearbeiten", "label_date": "Datum", "label_time": "Uhrzeit", "label_location": "Ort", diff --git a/api/v1/class/i18n/admin/projects/admin/snippets_DE.json b/api/v1/class/i18n/admin/projects/admin/snippets_DE.json new file mode 100644 index 0000000..36fb66f --- /dev/null +++ b/api/v1/class/i18n/admin/projects/admin/snippets_DE.json @@ -0,0 +1,19 @@ +{ + "title": "Projekte - Admin", + "existing_projects": "Meine Projekte", + "th_id": "ID", + "th_name": "Name", + "th_deadline": "Deadline", + "th_owner": "Besitzer", + "th_actions": "Aktionen", + "btn_edit": "Bearbeiten", + "btn_delete": "Löschen", + "no_projects": "Keine Projekte gefunden...", + "add_project": "Füge ein neues Projekt hinzu", + "label_name": "Name", + "label_description": "Beschreibung", + "label_deadline": "Deadline", + "label_owner": "Besitzer", + "btn_add": "Projekt hinzufügen", + "delete_confirm": "Bist du sicher, dass du das Projekt löschen möchtest?" +} \ No newline at end of file diff --git a/api/v1/class/i18n/admin/projects/admin/snippets_EN.json b/api/v1/class/i18n/admin/projects/admin/snippets_EN.json new file mode 100644 index 0000000..23b3ad0 --- /dev/null +++ b/api/v1/class/i18n/admin/projects/admin/snippets_EN.json @@ -0,0 +1,19 @@ +{ + "title": "Projects - Admin", + "existing_projects": "Below is a list for which projects you are a member in.", + "th_id": "ID", + "th_name": "Name", + "th_deadline": "Deadline", + "th_owner": "Owner", + "th_actions": "Actions", + "btn_edit": "Edit", + "btn_delete": "Delete", + "no_projects": "No projects found...", + "add_project": "Add a new project", + "label_name": "Name", + "label_description": "Description", + "label_assoc": "Project short name", + "label_owner": "Owner", + "btn_add": "Add project", + "delete_confirm": "Are you sure that you want to delete this project?" +} \ No newline at end of file diff --git a/api/v1/class/i18n/admin/projects/edit/snippets_DE.json b/api/v1/class/i18n/admin/projects/edit/snippets_DE.json new file mode 100644 index 0000000..9bd8742 --- /dev/null +++ b/api/v1/class/i18n/admin/projects/edit/snippets_DE.json @@ -0,0 +1,9 @@ +{ + "title": "Projekt bearbeiten", + "label_name": "Projektname", + "label_description": "Projektbeschreibung", + "label_deadline": "Fällig bis", + "label_owner": "Projecteigentümer", + "btn_save": "Projekt bearbeiten.", + "btn_cancel": "Zurück." +} \ No newline at end of file diff --git a/api/v1/class/i18n/admin/projects/edit/snippets_EN.json b/api/v1/class/i18n/admin/projects/edit/snippets_EN.json new file mode 100644 index 0000000..f587de3 --- /dev/null +++ b/api/v1/class/i18n/admin/projects/edit/snippets_EN.json @@ -0,0 +1,9 @@ +{ + "title": "Edit Project", + "label_name": "Project name", + "label_description": "Project description", + "label_deadline": "Project deadline", + "label_owner": "Project Owner", + "btn_save": "Edit Project.", + "btn_cancel": "Cancel." +} \ No newline at end of file diff --git a/api/v1/class/i18n/admin/users/edit/snippets_DE.json b/api/v1/class/i18n/admin/users/edit/snippets_DE.json index 052a917..73dca01 100644 --- a/api/v1/class/i18n/admin/users/edit/snippets_DE.json +++ b/api/v1/class/i18n/admin/users/edit/snippets_DE.json @@ -1,7 +1,7 @@ { "title": "Benutzer bearbeiten", "add_user": "Benutzer hinzufügen", - "p1": "Hier kannst du alle Nutzer bearbeiten oder neue hinzufügen oder löschen", + "p1": "Hier kannst du alle Benutzer bearbeiten oder Neue hinzufügen oder löschen", "th1": "Aktion", "th2": "Name", "th3": "Benutzername", diff --git a/api/v1/class/i18n/admin/worktime/all/snippets_DE.json b/api/v1/class/i18n/admin/worktime/all/snippets_DE.json index bbce20a..d287f5e 100644 --- a/api/v1/class/i18n/admin/worktime/all/snippets_DE.json +++ b/api/v1/class/i18n/admin/worktime/all/snippets_DE.json @@ -13,5 +13,6 @@ "pbegin": "Pause Start", "pend": "Pause Ende", "loc": "Ort", - "type": "Typ" + "type": "Typ", + "id": "ID" } \ No newline at end of file diff --git a/api/v1/class/i18n/admin/worktime/all/snippets_EN.json b/api/v1/class/i18n/admin/worktime/all/snippets_EN.json index b83e6de..864bf82 100644 --- a/api/v1/class/i18n/admin/worktime/all/snippets_EN.json +++ b/api/v1/class/i18n/admin/worktime/all/snippets_EN.json @@ -13,5 +13,6 @@ "pbegin": "Break Start", "pend": "end of break", "loc": "location", - "type": "type" + "type": "type", + "id": "ID" } \ No newline at end of file diff --git a/api/v1/class/i18n/admin/worktime/all/snippets_NL.json b/api/v1/class/i18n/admin/worktime/all/snippets_NL.json index 3b6cc58..1949da5 100644 --- a/api/v1/class/i18n/admin/worktime/all/snippets_NL.json +++ b/api/v1/class/i18n/admin/worktime/all/snippets_NL.json @@ -13,5 +13,6 @@ "pbegin": "Breekstart", "pend": "einde einde", "loc": "locatie", - "type": "type" + "type": "type", + "id": "ID" } \ No newline at end of file diff --git a/api/v1/class/i18n/admin/worktime/sick/all/snippets_DE.json b/api/v1/class/i18n/admin/worktime/sick/all/snippets_DE.json index 571eb99..950aec6 100644 --- a/api/v1/class/i18n/admin/worktime/sick/all/snippets_DE.json +++ b/api/v1/class/i18n/admin/worktime/sick/all/snippets_DE.json @@ -1,6 +1,6 @@ { "title": "Alle Krankheiten", - "note1": "Unten siehst du eine Liste aller Krankheiten deiner Mitarbeiter.", + "note1": "Unten siehst du eine Liste aller Krankmeldungen deiner Mitarbeiter.", "note2": "Geordnet: Neu zu alt, die letzten 100 Einträge", "t1": "Mitarbeiter", "t2": "Krankheit Beginn", diff --git a/api/v1/class/i18n/admin/worktime/vacation/all/snippets_DE.json b/api/v1/class/i18n/admin/worktime/vacation/all/snippets_DE.json index 1960294..6dc12f8 100644 --- a/api/v1/class/i18n/admin/worktime/vacation/all/snippets_DE.json +++ b/api/v1/class/i18n/admin/worktime/vacation/all/snippets_DE.json @@ -1,6 +1,6 @@ { "title": "Alle Urlaube", - "note1": "Unten siehst du eine Liste aller Urlaube deiner Mitarbeiter.", + "note1": "Unten siehst du eine Liste aller Urlaubsanträge deiner Mitarbeiter.", "note2": "Geordnet: Neu zu alt, die letzten 100 Einträge", "t1": "Mitarbeiter", "t2": "Urlaub Beginn", diff --git a/api/v1/class/i18n/admin/worktime/vacation/all/snippets_EN.json b/api/v1/class/i18n/admin/worktime/vacation/all/snippets_EN.json index f20e450..71706ce 100644 --- a/api/v1/class/i18n/admin/worktime/vacation/all/snippets_EN.json +++ b/api/v1/class/i18n/admin/worktime/vacation/all/snippets_EN.json @@ -11,6 +11,7 @@ "pending": "Pending", "or": "or", "approved": "Approved", - "rejected": "Rejected" + "rejected": "Rejected", + "not_found": "No vacation reports found." } } \ No newline at end of file diff --git a/api/v1/class/i18n/suite/class/arbeitszeit/snippets_DE.json b/api/v1/class/i18n/suite/class/arbeitszeit/snippets_DE.json index 0a527bf..4369750 100644 --- a/api/v1/class/i18n/suite/class/arbeitszeit/snippets_DE.json +++ b/api/v1/class/i18n/suite/class/arbeitszeit/snippets_DE.json @@ -3,6 +3,7 @@ "easymode_disabled": "Easymode deaktiviert.", "to_review": "Zur Prüfung", "remove_review": "Prüfung aufheben", + "propose_correction": "Änderung vorschlagen", "print": "(Drucken)", "csv": "(CSV)", "delete_entry": "Eintrag löschen", diff --git a/api/v1/class/i18n/suite/class/arbeitszeit/snippets_EN.json b/api/v1/class/i18n/suite/class/arbeitszeit/snippets_EN.json index ab76592..f83beb8 100644 --- a/api/v1/class/i18n/suite/class/arbeitszeit/snippets_EN.json +++ b/api/v1/class/i18n/suite/class/arbeitszeit/snippets_EN.json @@ -3,6 +3,7 @@ "easymode_disabled": "Easymode disabled.", "to_review": "For review", "remove_review": "Remove review", + "propose_correction": "Propose correction", "print": "(Print)", "csv": "(CSV)", "delete_entry": "Delete entry", diff --git a/api/v1/class/i18n/suite/class/arbeitszeit/snippets_NL.json b/api/v1/class/i18n/suite/class/arbeitszeit/snippets_NL.json index f0c8a8c..93574a0 100644 --- a/api/v1/class/i18n/suite/class/arbeitszeit/snippets_NL.json +++ b/api/v1/class/i18n/suite/class/arbeitszeit/snippets_NL.json @@ -3,6 +3,7 @@ "easymode_disabled": "Easymode uitgeschakeld.", "to_review": "Ter beoordeling", "remove_review": "Review verwijderen", + "propose_correction": "Propose Correction", "print": "(Afdrukken)", "csv": "(CSV)", "delete_entry": "Invoer verwijderen", diff --git a/api/v1/class/i18n/suite/nav/snippets_DE.json b/api/v1/class/i18n/suite/nav/snippets_DE.json index 7ca2082..2a61043 100644 --- a/api/v1/class/i18n/suite/nav/snippets_DE.json +++ b/api/v1/class/i18n/suite/nav/snippets_DE.json @@ -3,10 +3,12 @@ "settings": "Einstellungen", "own_worktime": "Eigene Arbeitszeiten", "notifications": "Benachrichtigungen", + "projects": "Projekte", "logout": "Abmelden", "a_allworktime": "Alle Arbeitszeiten", "a_useredit": "Benutzer bearbeiten", "a_sickness": "Alle Krankheiten", "a_vacation": "Alle Urlaube", - "a_plugins": "Plugins" + "a_plugins": "Plugins", + "a_projects": "Projektmanagement" } \ No newline at end of file diff --git a/api/v1/class/i18n/suite/nav/snippets_EN.json b/api/v1/class/i18n/suite/nav/snippets_EN.json index 97a5875..9f0fc29 100644 --- a/api/v1/class/i18n/suite/nav/snippets_EN.json +++ b/api/v1/class/i18n/suite/nav/snippets_EN.json @@ -4,9 +4,11 @@ "own_worktime": "Worktime records", "notifications": "Calendar", "logout": "Logout", + "projects": "Projects", "a_allworktime": "All worktime records", "a_useredit": "Edit users", "a_sickness": "Sickness reports", "a_vacation": "Vacation reports", - "a_plugins": "Plugins" + "a_plugins": "Plugins", + "a_projects": "Projects management" } \ No newline at end of file diff --git a/api/v1/class/i18n/suite/nav/snippets_NL.json b/api/v1/class/i18n/suite/nav/snippets_NL.json index 4671610..ef441e6 100644 --- a/api/v1/class/i18n/suite/nav/snippets_NL.json +++ b/api/v1/class/i18n/suite/nav/snippets_NL.json @@ -4,9 +4,11 @@ "own_worktime": "Eigen werktijden", "notifications": "Benachrichtigungen", "logout": "Uitloggen", + "projects": "Projects", "a_allworktime": "Alle werktijden", "a_useredit": "Gebruiker bewerken", "a_sickness": "Alle ziekten", "a_vacation": "Alle vakanties", - "a_plugins": "Plug-ins" + "a_plugins": "Plug-ins", + "a_projects": "Projects management" } \ No newline at end of file diff --git a/api/v1/class/i18n/suite/projects/addUser/snippets_EN.json b/api/v1/class/i18n/suite/projects/addUser/snippets_EN.json new file mode 100644 index 0000000..aad17cb --- /dev/null +++ b/api/v1/class/i18n/suite/projects/addUser/snippets_EN.json @@ -0,0 +1,10 @@ +{ + "title": "Add User to Project", + "title2": "Add User to Project", + "label_userid": "User ID", + "label_role": "Role", + "label_permissions": "Permissions", + "tooltip_permissions": "0 = Member, 1 = Project Admin", + "btn_add": "Add.", + "btn_cancel": "Cancel." +} \ No newline at end of file diff --git a/api/v1/class/i18n/suite/projects/createItem/snippets_EN.json b/api/v1/class/i18n/suite/projects/createItem/snippets_EN.json new file mode 100644 index 0000000..0f1ceb7 --- /dev/null +++ b/api/v1/class/i18n/suite/projects/createItem/snippets_EN.json @@ -0,0 +1,7 @@ +{ + "title": "Create Project Item", + "description": "Description", + "assignee": "Assignee (UserID)", + "btn_save": "Save.", + "btn_cancel": "Cancel." +} \ No newline at end of file diff --git a/api/v1/class/i18n/suite/projects/item/snippets_EN.json b/api/v1/class/i18n/suite/projects/item/snippets_EN.json new file mode 100644 index 0000000..89fabea --- /dev/null +++ b/api/v1/class/i18n/suite/projects/item/snippets_EN.json @@ -0,0 +1,15 @@ +{ + "title": "View Item", + "no_description": "No description...", + "project": "Project", + "assignee": "Assignee", + "status": "Status", + "worktimes": "Worktimes", + "th_user": "User", + "th_hours": "Hours", + "th_date": "Date", + "no_worktimes": "No worktimes...", + "btn_edit": "Edit", + "btn_delete": "Delete", + "id": "ID" +} \ No newline at end of file diff --git a/api/v1/class/i18n/suite/projects/overview/snippets_EN.json b/api/v1/class/i18n/suite/projects/overview/snippets_EN.json new file mode 100644 index 0000000..b1db03c --- /dev/null +++ b/api/v1/class/i18n/suite/projects/overview/snippets_EN.json @@ -0,0 +1,15 @@ +{ + "title": "Overview - Projects", + "intro": "This is the overview of the projects tab.", + "your_projects": "Your projects", + "link_view_project": "View project", + "no_projects": "No projects...", + "th_item_title": "Item title", + "th_project": "Project", + "th_status": "Status", + "th_actions": "Actions", + "your_items": "Your items", + "btn_view_item": "View item", + "no_items": "No items...", + "id": "ID" +} \ No newline at end of file diff --git a/api/v1/class/i18n/suite/projects/view/snippets_EN.json b/api/v1/class/i18n/suite/projects/view/snippets_EN.json new file mode 100644 index 0000000..e0542f6 --- /dev/null +++ b/api/v1/class/i18n/suite/projects/view/snippets_EN.json @@ -0,0 +1,23 @@ +{ + "title": "View Project", + "no_description": "No description...", + "description": "Description", + "deadline": "Deadline", + "tab_items": "Items", + "tab_members": "Members", + "tab_worktimes": "Worktimes", + "items": "Items", + "th_item": "Item", + "th_assignee": "Assignee", + "th_status": "Status", + "th_actions": "Actions", + "btn_view": "View", + "no_items": "No items...", + "members": "Members", + "no_members": "No members...", + "worktimes": "Worktimes", + "th_user": "User", + "th_hours": "Hours", + "no_worktimes": "No worktimes...", + "id": "ID" +} \ No newline at end of file diff --git a/api/v1/class/i18n/suite/status/snippets_DE.json b/api/v1/class/i18n/suite/status/snippets_DE.json index 14ec08d..293e834 100644 --- a/api/v1/class/i18n/suite/status/snippets_DE.json +++ b/api/v1/class/i18n/suite/status/snippets_DE.json @@ -26,5 +26,17 @@ "statemismatch": "Fehler: Sicherheitsfehler", "ldapauth": "Fehler: Die Authentifizierung über LDAP ist fehlgeschlagen.", "ldapcreated": "Hinweis: Bitte melde dich erneut an. Dein Benutzerkonto wurde nun erstellt. (LDAP allowed self-login)", - "notification_not_found": "Fehler: Benachrichtigung nicht gefunden." + "notification_not_found": "Fehler: Benachrichtigung nicht gefunden.", + "project_deleted": "Hinweis: Das Projekt wurde erfolgreich gelöscht.", + "project_deleted_failed": "Fehler: Das Projekt konnte nicht gelöscht werden. Bitte wende dich an deinen Administrator.", + "project_edited": "Hinweis: Das Projekt wurde erfolgreich bearbeitet.", + "project_edited_error": "Fehler: Das Projekt konnte nicht gelöscht werden. Bitte wende dich an deinen Administrator.", + "project_added": "Hinweis: Das Projekt wurde erfolgreich erstellt.", + "project_added_failed": "Fehler: Das Projekt konnte nicht erstellt werden. Bitte wende dich an deinen Administrator.", + "project_userAdded": "Hinweis: Der Benutzer hat nun Zugriff auf das freigegebene Projekt.", + "project_userAddeed_failed": "Fehler: Dem Benutzer konnten die Berechtigungen nicht zugewiesen werden. Bitte wende dich an deinen Administrator.", + "projects_item_added": "Hinweis: Projektaufgabe hinzugefügt.", + "projects_item_failed": "Fehler: Die Projektaufgabe konnte nicht hinzugefügt werden. Bitte wende dich an deinen Administrator.", + "mapWorktimeToItem_success": "Hinweis: Arbeitszeit erfolgreich mit der Projektaufgabe verbunden.", + "mapWorktimeToItem_failed": "Fehler: Die Arbeitszeit konnte nicht mit der Projektaufgabe verbunden werden. Bitte wende dich an deinen Administrator." } diff --git a/api/v1/class/i18n/suite/status/snippets_EN.json b/api/v1/class/i18n/suite/status/snippets_EN.json index baf04cf..d89396f 100644 --- a/api/v1/class/i18n/suite/status/snippets_EN.json +++ b/api/v1/class/i18n/suite/status/snippets_EN.json @@ -26,5 +26,17 @@ "statemismatch": "Error: Security error", "ldapauth": "Error: LDAP authentication failed.", "ldapcreated": "Note: Please log in again. Your user account has now been created. (LDAP allowed self-login)", - "notification_not_found": "Error: Notification not found." + "notification_not_found": "Error: Notification not found.", + "project_deleted": "Note: Project successfully deleted.", + "project_deleted_failed": "Error: The project could not be deleted. Please contact your administrator.", + "project_edited": "Note: The changes to the project have been applied.", + "project_edited_error": "Error: An error occurred while applying the changes to the project. Please contact your administrator.", + "project_added": "Note: The project has been succesfully added.", + "project_added_error": "Error: An error occurred while creating the new project. Please contact your administrator.", + "project_userAdded": "Note: The user has been successfully added to the project.", + "project_userAdded_failed": "Error: An error occurred while assigning the permissions to the user. Please contact your administrator.", + "projects_item_added": "Note: The item has been added to the project.", + "projects_item_failed": "Error: An error occurred while creating the item. Please contact your administrator.", + "mapWorktimeToItem_success": "Note: Worktime successfully mapped to project item.", + "mapWorktimeToItem_failed": "Error: An error occurred while mapping the worktime to the project itme. Please contact your administrator." } diff --git a/api/v1/class/i18n/suite/worktime/all/snippets_DE.json b/api/v1/class/i18n/suite/worktime/all/snippets_DE.json index c52053f..c2aaf41 100644 --- a/api/v1/class/i18n/suite/worktime/all/snippets_DE.json +++ b/api/v1/class/i18n/suite/worktime/all/snippets_DE.json @@ -7,5 +7,6 @@ "s_pstart": "Pause Start", "s_pend": "Pause Ende", "s_location": "Ort", - "type": "Typ" + "type": "Typ", + "id": "ID" } \ No newline at end of file diff --git a/api/v1/class/i18n/suite/worktime/all/snippets_EN.json b/api/v1/class/i18n/suite/worktime/all/snippets_EN.json index 5c2322a..c13a4bc 100644 --- a/api/v1/class/i18n/suite/worktime/all/snippets_EN.json +++ b/api/v1/class/i18n/suite/worktime/all/snippets_EN.json @@ -7,5 +7,6 @@ "s_pstart": "Pause Start", "s_pend": "Pause End", "s_location": "Location", - "type": "Type" + "type": "Type", + "id": "ID" } \ No newline at end of file diff --git a/api/v1/class/i18n/suite/worktime/all/snippets_NL.json b/api/v1/class/i18n/suite/worktime/all/snippets_NL.json index b45eabf..f54406a 100644 --- a/api/v1/class/i18n/suite/worktime/all/snippets_NL.json +++ b/api/v1/class/i18n/suite/worktime/all/snippets_NL.json @@ -7,5 +7,6 @@ "s_pstart": "Start pauzeren", "s_pend": "Einde einde", "s_location": "Locatie", - "type": "Type" + "type": "Type", + "id": "ID" } \ No newline at end of file diff --git a/api/v1/class/i18n/suite/worktime/correction/snippets_DE.json b/api/v1/class/i18n/suite/worktime/correction/snippets_DE.json new file mode 100644 index 0000000..4252bb2 --- /dev/null +++ b/api/v1/class/i18n/suite/worktime/correction/snippets_DE.json @@ -0,0 +1,15 @@ +{ + "title": "Änderung an einer Arbeitszeit vorschlagen", + "h2": "Derzeitige Daten", + "start": "Start", + "end": "Ende", + "comment": "Kommentar", + "suggest_change": "Änderung vorschlagen", + "new_start": "Neue Startzeit", + "new_end": "Neues Ende", + "new_comment": "Neuer Kommentar", + "reason": "Begründung", + "tooltip_reason": "Bitte gebe eine Begründung an, warum du nun die Daten änderst. Diese wird dem Kommentarfeld hinzugefügt.", + "btn_submit": "Abschicken", + "date": "Datum" +} \ No newline at end of file diff --git a/api/v1/class/i18n/suite/worktime/correction/snippets_EN.json b/api/v1/class/i18n/suite/worktime/correction/snippets_EN.json new file mode 100644 index 0000000..3d5afdd --- /dev/null +++ b/api/v1/class/i18n/suite/worktime/correction/snippets_EN.json @@ -0,0 +1,15 @@ +{ + "title": "Propose a correction for a worktime", + "h2": "Current data", + "start": "Start", + "end": "End", + "comment": "Comment", + "suggest_change": "Propose Correction", + "new_start": "New start time", + "new_end": "New end time", + "new_comment": "New comment", + "reason": "Reason", + "tooltip_reason": "Please provide a reason why you are proposing a correction. The reason will be attached to the comment.", + "btn_submit": "Submit", + "date": "Date" +} \ No newline at end of file diff --git a/api/v1/class/plugins/PluginBuilder.plugins.arbeit.inc.php b/api/v1/class/plugins/PluginBuilder.plugins.arbeit.inc.php index a0c219d..8a264c2 100644 --- a/api/v1/class/plugins/PluginBuilder.plugins.arbeit.inc.php +++ b/api/v1/class/plugins/PluginBuilder.plugins.arbeit.inc.php @@ -129,7 +129,7 @@ final public function load_class($class, $name): void{ * * @return bool|void If everything went ok, void. If an error occurs false bool. */ - final public function initialize_plugins(): ?bool { + final public function initialize_plugins(): bool { if ($this->testing == true) { $plugins = $this->get_plugins(); if ($plugins == false) { @@ -154,10 +154,10 @@ final public function initialize_plugins(): ?bool { return true; } elseif ($this->testing == false) { - // Keine Aktion notwendig } else { return false; } + return false; } @@ -195,7 +195,7 @@ final public function read_plugin_configuration($name, $raw = false): array|stri return (array)$yaml; } else { Exceptions::error_rep("{$la} Could not read plugin configuration for plugin '{$name}' - Path: " . $_SERVER["DOCUMENT_ROOT"] . "" . $this->basepath . "/" . $name . "/plugin.yml"); - return null; + return false; } } @@ -205,7 +205,7 @@ final public function read_plugin_configuration($name, $raw = false): array|stri * * @return array|void Returns and array on success. Nothing otherwise */ - final public function get_plugins(): array{ + final public function get_plugins(): array|bool{ $this->logger("{$this->la} Getting all plugins..."); $dir = array_diff(scandir($_SERVER["DOCUMENT_ROOT"]. "" . $this->get_basepath()), array(".", "..", "data")); if($dir == false){ @@ -224,6 +224,7 @@ final public function get_plugins(): array{ return $data; } } + return false; } @@ -299,7 +300,7 @@ final public function memorize_plugin($name, $additional_payload = null): bool{ * @param string $name Class name of the plugin * @return object|bool|Exception Returns the class on success and either false or an Exception on failure */ - final public function unmemorize_plugin($name): object{ + final public function unmemorize_plugin($name): object|bool{ Exceptions::deprecated(__FUNCTION__, "This function is not supported anymore."); $this->logger("{$this->la} Unmemorizing plugin '{$name}'..."); $plugin = $this->read_plugin_configuration($name); diff --git a/api/v1/class/plugins/plugins/userdetail/plugin.yml b/api/v1/class/plugins/plugins/userdetail/plugin.yml index b860420..2a4f72a 100644 --- a/api/v1/class/plugins/plugins/userdetail/plugin.yml +++ b/api/v1/class/plugins/plugins/userdetail/plugin.yml @@ -4,7 +4,7 @@ main: Main namespace: userdetail author: Ente description: Save more information about your employees -version: "1.2" +version: "1.2.1" api: 0.1 permissions: none enabled: true diff --git a/api/v1/class/plugins/plugins/userdetail/src/Main.php b/api/v1/class/plugins/plugins/userdetail/src/Main.php index 4cd7940..15c5e16 100644 --- a/api/v1/class/plugins/plugins/userdetail/src/Main.php +++ b/api/v1/class/plugins/plugins/userdetail/src/Main.php @@ -8,7 +8,6 @@ use Arbeitszeit\Benutzer; use Arbeitszeit\Exceptions; use Arbeitszeit\PluginBuilder; -use Arbeitszeit\Hooks; use Arbeitszeit\PluginInterface; class Userdetail extends PluginBuilder implements PluginInterface { @@ -49,7 +48,6 @@ public function get_plugin_configuration(): array{ public function __construct(){ $this->set_log_append(); $this->set_plugin_configuration(); - #Hooks::addHook("create_user", "callback", function(){echo "hi";}, "userdetail"); $this->check_folder(); } @@ -102,10 +100,6 @@ public function compute_user_nav(){ return $html; } - public function add($name, $value, $user){ - - } - public function save_employee_data($payload){ $this->logger("[userdetail] Saving employee data..."); $handle = fopen(dirname(__DIR__, 1) . "/data/" . $payload["username"] . ".json", "w+"); @@ -140,10 +134,6 @@ public function check_employee_file($username){ } } - public function create_user_callback($username, $name, $email, $password, $isAdmin){ - echo "Successfully created user account..."; - } - public function check_folder(){ if(!file_exists(dirname(__DIR__, 1) . "/data/")){ mkdir(dirname(__DIR__, 1) . "/data/"); diff --git a/api/v1/class/plugins/plugins/utility/plugin.yml b/api/v1/class/plugins/plugins/utility/plugin.yml new file mode 100644 index 0000000..19c0fa4 --- /dev/null +++ b/api/v1/class/plugins/plugins/utility/plugin.yml @@ -0,0 +1,15 @@ +name: utility +src: /src +main: Main +namespace: utility +author: Ente +description: 'Export all data from an user and more.' +version: '1.0' +api: 0.1 +permissions: none +enabled: true +custom.values: + license: LIC +nav_links: + 'Open Utility Plugin': views/overview.php +path: 'C:\Users\diege\Documents\GitHub\timetrack/api/v1/class/plugins/plugins/utility/plugin.yml' diff --git a/api/v1/class/plugins/plugins/utility/src/Main.php b/api/v1/class/plugins/plugins/utility/src/Main.php new file mode 100644 index 0000000..2463bb6 --- /dev/null +++ b/api/v1/class/plugins/plugins/utility/src/Main.php @@ -0,0 +1,172 @@ +read_plugin_configuration("utility")["version"]; + $this->log_append = "[Utility] v{$v}"; + } + + public function get_log_append(): string + { + return $this->log_append; + } + + public function set_plugin_configuration(): void + { + $this->plugin_configuration = $this->read_plugin_configuration("userdetail"); + } + + public function get_plugin_configuration(): array + { + return $this->plugin_configuration; + } + + public function __construct() + { + $this->set_log_append(); + $this->set_plugin_configuration(); + $this->db = new DB(); + } + + public function onLoad(): void + { + $lga = $this->get_log_append(); + $this->logger($lga . " Loading utility plugin..."); + } + + public function onEnable(): void + { + + } + + public function onDisable(): void + { + + } + + public function set_userData($data): void { + $this->userData = $data; + } + + public function checkIfuserDataisSet(){ + try{ + if(isset($this->userData)){ + return true; + } else { + return false; + } + } catch(\Exception $e){ + StatusMessages::redirect("error"); + } + } + + public function exportAll($username): self{ + $userData = Benutzer::get_user($username); + $this->userData = $userData; + $arr = []; + $arr["worktimes"] = $this->exportUserWorktimes(); + $arr["sickness"] = $this->exportUserSickness(); + $arr["vacations"] = $this->exportUserVacations(); + + $this->data = json_encode($arr, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + $this->filename = "export_user_" . $userData["username"] . "_" . date("Y-m-d_H-i-s") . ".json"; + + return $this; + } + + public function download(): void{ + if(!isset($this->data)){ + StatusMessages::redirect("error"); + } + + header('Content-Type: application/json; charset=utf-8'); + header('Content-Disposition: attachment; filename="' . $this->filename . '"'); + header('Content-Length: ' . strlen($this->data)); + + echo $this->data; + exit; + } + + public function getString(): string{ + return $this->data; + } + + public function exportUserWorktimes(): array|bool{ + $this->checkIfuserDataisSet(); + $sql = "SELECT * FROM `arbeitszeiten` WHERE name = ?"; + $res = $this->db->sendQuery($sql); + $res->execute([$this->userData["username"]]); + + $worktimes = []; + if($res->rowCount() >= 1){ + while($data = $res->fetch(\PDO::FETCH_ASSOC)){ + array_push($worktimes, $data); + } + return $worktimes; + } else { + return false; + } + } + + public function exportUserSickness(): array|bool{ + $this->checkIfuserDataisSet(); + $sql = "SELECT * FROM `sick` WHERE username = ?"; + $res = $this->db->sendQuery($sql); + $res->execute([$this->userData["username"]]); + + $sickness = []; + + if($res->rowCount() >= 1){ + while($data = $res->fetch(\PDO::FETCH_ASSOC)){ + array_push($sickness, $data); + } + return $sickness; + } else { + return false; + } + } + + public function exportUserVacations(): array|bool{ + $this->checkIfuserDataisSet(); + $sql = "SELECT * FROM `vacation` WHERE username = ?"; + $res = $this->db->sendQuery($sql); + $res->execute([$this->userData["username"]]); + + $vacation = []; + + if($res->rowCount() >= 1){ + while($data = $res->fetch(\PDO::FETCH_ASSOC)){ + array_push($vacation, $data); + } + return $vacation; + } else { + return false; + } + } +} \ No newline at end of file diff --git a/api/v1/class/plugins/plugins/utility/views/download.php b/api/v1/class/plugins/plugins/utility/views/download.php new file mode 100644 index 0000000..96482d7 --- /dev/null +++ b/api/v1/class/plugins/plugins/utility/views/download.php @@ -0,0 +1,23 @@ +auth()->login_validation(); +$a->benutzer()->current_user_is_admin(); + +if(!isset($_POST["username"])){ + $main->logger("[utility] Username not found. Aborting export..."); + $a->statusMessages()->redirect("error"); +} + +$main->exportAll($_POST["username"])->download(); \ No newline at end of file diff --git a/api/v1/class/plugins/plugins/utility/views/overview.php b/api/v1/class/plugins/plugins/utility/views/overview.php new file mode 100644 index 0000000..c63bac0 --- /dev/null +++ b/api/v1/class/plugins/plugins/utility/views/overview.php @@ -0,0 +1,28 @@ +auth()->login_validation(); +$a->benutzer()->current_user_is_admin(); +?> + +

User Export

+
+

Please input the username of the user you want to export all data of below.

+

You can find the list here: Edit Users.

+
+ + + +

After clicking on "Download JSON export" your browser will open a dialog to save the generated export file.

+
+
diff --git a/api/v1/class/projects/projects.arbeit.inc.php b/api/v1/class/projects/projects.arbeit.inc.php index 34499aa..d6b5282 100644 --- a/api/v1/class/projects/projects.arbeit.inc.php +++ b/api/v1/class/projects/projects.arbeit.inc.php @@ -1,65 +1,203 @@ db = new DB; } - public function addProjectE($name, $description, $note, $users = null){ - Exceptions::error_rep("[PROJECTS] Adding project '{$name}'..."); - $sql = "INSERT INTO projects (id, name, users, description, note) VALUES (0, ?, ?, ?, ?);"; - $res = $this->db->sendQuery($sql)->execute([$name, $description, $note, $this->computeProjectUserArray("json", $users)]); - if(!$res){ - Exceptions::error_rep("[PROJECTS] An error occurred while creating an project. See previous message for more information."); + public function addProject($name, $description = null, $items_assoc, $deadline = null, $owner = null) + { + Exceptions::error_rep("[PROJECTS] Adding project..."); + if ($items_assoc == null) { + $items_assoc = rand(0, 9999999); + } + $sql = "INSERT INTO `projects` (name, description, items_assoc, deadline, owner) VALUES (?, ?, ?, ?, ?);"; + + if ($owner == null) { + $owner = Benutzer::get_user($_SESSION["username"])["id"]; + } + + $res = $this->db->sendQuery($sql)->execute([$name, $description, $items_assoc, $deadline, $owner]); + + if (!$res) { + Exceptions::error_rep("[PROJECTS] An error occured while creating new project. See previous message for more information..."); return false; } else { - Exceptions::error_rep("[PROJECTS] Successfully created project '{$name}'."); return true; } } - public function deleteProject($id){ + public function editProject($projectId, $changes) + { + Exceptions::error_rep("[PROJECTS] Editing project '{$projectId}'..."); + $changes = [ + "name" => $changes["name"], + "description" => $changes["description"], + "deadline" => $changes["deadline"], + "owner" => $changes["owner"] + ]; + + foreach ($changes as $change => $val) { + $sql = "UPDATE `projects` SET `$change` = ?"; + $res = $this->db->sendQuery($sql)->execute([$val]); + Exceptions::error_rep("[PROJECTS] Changing project '{$projectId}'. Attribute: '{$change}' | Value: '{$val}'"); + if (!$res) { + Exceptions::error_rep("An error returned while editing project '{$projectId}'. See previous message for more information."); + return false; + } + } + Exceptions::error_rep("[PROJECTS] Successfully edited project '{$projectId}'."); + return true; + + } + + public function deleteProject($id) + { Exceptions::error_rep("[PROJECTS] Deleting project '{$id}'..."); $sql = "DELETE FROM projects WHERE id = ?"; $res = $this->db->sendQuery($sql)->execute([$id]); - if(!$res){ + if (!$res) { Exceptions::error_rep("[PROJECTS] An error occurred while deleting an project. See previous message for more information."); return false; } else { - Exceptions::error_rep("[PROJECTS] Successfully deleted project '{$id}'."); + Exceptions::error_rep(message: "[PROJECTS] Successfully deleted project '{$id}'."); + return true; + } + } + + + public function getProjectUsers($project_id): array|bool + { + Exceptions::error_rep("[PROJECTS] Getting users for project '{$project_id}'..."); + + $sql = "SELECT * FROM projects_users WHERE projectid = ?"; + $stmt = $this->db->sendQuery($sql); + $res = $stmt->execute([$project_id]); + + if (!$res) { + Exceptions::error_rep("[PROJECTS] Error while getting project users for project '{$project_id}'"); + return false; + } + + $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + if (empty($rows)) { + return false; + } + + $arr = []; + foreach ($rows as $row) { + $arr[$row["id"]] = ["name" => $this->benutzer()->get_user_from_id($row["userid"])["name"], "role" => $row["role"], "userid" => $row["userid"]]; // key by id + } + + return $arr; + } + + + public function addProjectMember($project_id, $user_id, $permissions = 0, $role = "Member") + { + Exceptions::error_rep("[PROJECTS] Adding user to project '{$project_id}'..."); + // check if user exists + if (!$this->benutzer()->get_current_user()) { + return false; + } + if (!$this->getProject($project_id)) { + return false; + } + // PERMS: 0 = member, 1 = project admin, 2 = admin + $sql = "INSERT INTO `projects_users` (userid, projectid, permissions, role, is_owner) VALUES (?, ?, ?, ?, ?)"; + $res = $this->db->sendQuery($sql)->execute([$user_id, $project_id, $permissions, $role, 0]); + + if (!$res) { + return false; + } else { return true; } } - public function computeProjectUserArray($type = "array", $usernames = null){ - Exceptions::error_rep("[PROJECTS] Computing user array..."); - $user = []; - $i = 0; - foreach($usernames as $user){ - $user[$i] = $user; - ++$i; + public function getCurrentUserProjects() + { + if (!$this->benutzer()->get_current_user()) { + return false; + } + + $id = $this->benutzer()->get_current_user()["id"]; + #$sql = "SELECT * FROM projects_users WHERE userid = ?"; + if ($this->benutzer()->is_admin($this->benutzer()->get_current_user())) { + $sql = "SELECT * FROM projects"; + $res = $this->db->simpleQuery($sql); + } else { + $sql = "SELECT * FROM projects_users WHERE userid = ?"; + $res1 = $this->db->sendQuery($sql)->execute([$id]); + if (!$res1) { + return false; + } else { + if ($res1->rowCount == 0) { + return false; + } + $user_projects = []; + while ($row = $res1->fetch(\PDO::FETCH_ASSOC)) { + array_push($user_projects, $row["projectid"]); + } + $placeholder = implode(",", array_fill(0, count($user_projects), "?")); + $sql = "SELECT * FROM projects WHERE id IN ($placeholder)"; + $res = $this->db->sendQuery($sql)->execute([$user_projects]); + } + } + + if (!$res) { + return false; + } else { + $arr = []; + while ($row = $res->fetch(\PDO::FETCH_ASSOC)) { + $arr[$row["id"]] = $row; + } + return $arr; + } - if($type != "array"){ - return json_encode($user); + } + + public function addProjectItem($project_id, $title, $description, $assignee = null) + { + $sql = "INSERT INTO `projects_items` (id, title, description, assignee) VALUES (?, ?, ?, ?)"; + $res = $this->db->sendQuery($sql)->execute([$project_id, $title, $description, $assignee]); + + if (!$res) { + return false; } else { - Exceptions::error_rep("[PROJECTS] Could not compute user array. Returning default."); - return $user; + return true; } } - public function getProjectUsers($id){ - Exceptions::error_rep("[PROJECTS] Getting users for project '{$id}'..."); - return json_decode($this->getProject($id)["users"]); + public function mapWorktimeToItem($worktime_id, $item_id, $user_id = null) + { + if ($user_id = null) { + if (!$this->benutzer()->get_current_user()) { + return false; + } + + $sql = "INSERT INTO `projects_worktimes` (itemid, worktimeid, user) VALUES (?, ?, ?)"; + $res = $this->db->sendQuery($sql)->execute([$item_id, $worktime_id, $user_id]); + + if (!$res) { + return false; + } else { + return true; + } + } } - public function getProjects(){ + public function getProjects() + { Exceptions::error_rep("[PROJECTS] Fetching projects..."); $sql = "SELECT id FROM projects"; $res = $this->db->sendQuery($sql); $res->execute(); - if(!$res){ + if (!$res) { Exceptions::error_rep("[PROJECTS] An error occurred while fetching projects - there just may be none. See previous message for more information."); return false; } else { @@ -68,12 +206,13 @@ public function getProjects(){ } } - public function getProject($id){ + public function getProject($id) + { Exceptions::error_rep("[PROJECTS] Fetching project '{$id}'..."); $sql = "SELECT * FROM projects WHERE id = ?"; $res = $this->db->sendQuery($sql); $res->execute([$id]); - if(!$res){ + if (!$res) { Exceptions::error_rep("[PROJECTS] An error occurred while fetching project '{$id}'. See previous message for more information."); return false; } else { @@ -82,24 +221,182 @@ public function getProject($id){ } } - public function getUserProjects($username){ - Exceptions::error_rep("[PROJECTS] Fetching projects for user '{$username}'..."); - $projects = $this->getProjects(); - $userProjects = []; - $i = 0; - foreach($projects as $pr){ - $pr1 = $this->getProject($pr); - $users = json_decode($pr1["users"]); - foreach($users as $user){ - if($user == $username){ - $userProjects[$i] = $pr1["id"]; - } + public function checkUserisOwner($project_id) + { + $project = $this->getProject($project_id); + $user = $this->benutzer()->get_current_user(); + + if ($user["id"] == $project["owner"] || $user["isAdmin"] == true) { + return true; + } else { + return false; + } + } + + public function checkUserHasProjectAccess($user_id, $project_id, $required = 0) + { + if (isset($project_id)) { + $sql = "SELECT projectid, permissions FROM projects_users WHERE userid = ? AND projectid = ?"; + $res = $this->db->sendQuery($sql)->execute([$user_id, $project_id]); + } else { + $sql = "SELECT projectid, permissions FROM projects_users WHERE userid = ?"; + $res = $this->db->sendQuery($sql)->execute([$user_id]); + } + + if (!$res) { + return false; + } + + if (count($res->fetch(\PDO::FETCH_ASSOC)) > 1) { + Exceptions::error_rep("[PROJECTS] An error occured while checking user project permissions. There might be a duplicate entry for the permissions of project '{$project_id}' and user '{$user_id}'"); + return false; + } + + while ($data = $res->fetch(\PDO::FETCH_ASSOC)) { + if ($data["projectid"] == $project_id && $data["permissions"] >= 0) { + return true; + } + } + return false; + + } + + public function getProjectByName($name) + { + Exceptions::error_rep("[PROJECTS] Fetching project by name '{$name}'."); + $sql = "SELECT * FROM projects WHERE name = ?"; + $res = $this->db->sendQuery($sql)->execute([$name]); + + if (!$res) { + Exceptions::error_rep("[PROJECTS] An error occured while fetching project. See previous messages."); + return false; + } else { + return $res->fetch(\PDO::FETCH_ASSOC); + } + } + + public function getUserProjects($user) + { + Exceptions::error_rep("[PROJECTS] Fetching projects for user ID '{$user}'..."); + $sql = "SELECT * FROM projects WHERE id = ?"; + $res = $this->db->sendQuery($sql)->execute([$user]); + if (!$res) { + Exceptions::error_rep("[PROJECTS] An error occured while fetching user projects for user ID '{$user}'. User might also have no projects... See previous message for more information."); + return false; + } else { + $projects = []; + Exceptions::error_rep("[PROJECTS] Successfully found projects for user ID '{$user}'."); + while ($data = $res->fetch(\PDO::FETCH_ASSOC)) { + array_push($items, $data); } - ++$i; + return $projects; + } + } + + public function getProjectItems($project_id): array|bool + { + Exceptions::error_rep("[PROJECTS] Fetching project items for project '{$project_id}'..."); + + $sql = "SELECT * FROM projects_items WHERE id = ?"; + $stmt = $this->db->sendQuery($sql); + $res = $stmt->execute([$project_id]); + + if (!$res) { + Exceptions::error_rep("[PROJECTS] Error fetching project items for '{$project_id}'."); + return false; + } + + $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + if (empty($rows)) { + return false; } - Exceptions::error_rep("[PROJECTS] Returning found projects for user '{$username}'."); - return $userProjects; + + Exceptions::error_rep("[PROJECTS] Successfully fetched project items for '{$project_id}'."); + return $rows; } + + + public function getUserProjectItems($project_id, $user) + { + Exceptions::error_rep("[PROJECTS] Fetching project items for user '{$user}' and project '{$project_id}'..."); + $sql = "SELECT * FROM projects_items WHERE assignee = ? AND id = ?"; + $stmt = $this->db->sendQuery($sql); + + if (!$stmt->execute([$user, $project_id])) { + Exceptions::error_rep("[PROJECTS] Error while fetching user project items."); + return false; + } + + $items = $stmt->fetchAll(\PDO::FETCH_ASSOC); + Exceptions::error_rep("[PROJECTS] Successfully fetched " . count($items) . " items."); + return $items; + } + + + public function getItem($id): array|bool + { + $sql = "SELECT * FROM projects_items WHERE id = ?"; + $stmt = $this->db->sendQuery($sql); + $res = $stmt->execute([$id]); + + if (!$res) { + return false; + } + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + return $row ?: false; + } + + public function getUserProjectWorktimes($item_id): array|bool + { + Exceptions::error_rep("[PROJECTS] Fetching worktimes for item '{$item_id}'..."); + + $sql = "SELECT * FROM projects_worktimes WHERE itemid = ?"; + $stmt = $this->db->sendQuery($sql); + $res = $stmt->execute([$item_id]); + + if (!$res) { + Exceptions::error_rep("[PROJECTS] Error while fetching worktimes for item '{$item_id}'."); + return false; + } + + $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + if (empty($rows)) { + return false; + } + + Exceptions::error_rep("[PROJECTS] Successfully fetched " . count($rows) . " worktimes for item '{$item_id}'."); + return $rows; + } + + public function getProjectWorktimes($project_id): array|bool + { + Exceptions::error_rep("[PROJECTS] Fetching worktimes for project '{$project_id}'..."); + + $sql = "SELECT * FROM projects_worktimes WHERE projectid = ?"; + $stmt = $this->db->sendQuery($sql); + $res = $stmt->execute([$project_id]); + + if (!$res) { + Exceptions::error_rep("[PROJECTS] Error while fetching worktimes for project '{$project_id}'."); + return false; + } + + $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC); + + if (empty($rows)) { + return false; + } + + Exceptions::error_rep("[PROJECTS] Successfully fetched " . count($rows) . " worktimes for project '{$project_id}'."); + return $rows; + } + + + } } diff --git a/api/v1/class/status/statusMessages.arbeit.inc.php b/api/v1/class/status/statusMessages.arbeit.inc.php index cd7bddd..b9f737a 100644 --- a/api/v1/class/status/statusMessages.arbeit.inc.php +++ b/api/v1/class/status/statusMessages.arbeit.inc.php @@ -108,7 +108,7 @@ public function convertBase64Key($key) * @param mixed $path Optional path to the status message, if needed. * @return string Returns the URI string with the status message encoded in base64 ("status=....", without URI begin (?) or separator (&)). */ - public function URIBuilder($key, $path = null) + public static function URIBuilder($key, $path = null) { // This function is used to build the URI for the status message. $uri = "status=" . base64_encode(json_encode([ @@ -119,6 +119,12 @@ public function URIBuilder($key, $path = null) return $uri; } + public static function redirect($message, $suite = true){ + Exceptions::error_rep("Attempting redirect for user with message '{$message}'"); + $base_url = self::get_app_ini()["general"]["base_url"]; + header("Location: http://{$base_url}/suite/?" . self::URIBuilder($message)); + } + public function hook() { // This function is used in the arbeitszeit class to hook it into the system directly, ensuring it will be displayed at the top and exactly as before v7.13. diff --git a/api/v1/inc/app.json.sample b/api/v1/inc/app.json.sample index 629adbb..6241a4a 100644 --- a/api/v1/inc/app.json.sample +++ b/api/v1/inc/app.json.sample @@ -30,13 +30,13 @@ }, "ldap": { "ldap": "false", - "ldap_user": "USER", + "ldap_user": "myuser", "ldap_password": "BASE64-ENCODED-PASSWORD", - "ldap_host": "HOST", + "ldap_host": "dc.domain.tld", "ldap_ip": "IP", - "ldap_domain": "DOMAIN", - "ldap_basedn": "BASEDN", - "ldap_group": "GROUP", + "ldap_domain": "domain.tld", + "ldap_basedn": "DC=domain,DC=tld", + "ldap_group": "MYGROUP", "saf": "true", "saf_user": "USER", "saf_password": "BASE64-ENCODED-PASSWORD", @@ -51,5 +51,12 @@ "worktime_types": "/api/v1/inc/config/worktime_types.json", "vacation_types": "/api/v1/inc/config/vacation_types.json", "sickness_types": "/api/v1/inc/config/sickness_types.json" + }, + "mobile": { + "allow_app_use": false, + "allow_api_token_generation_w_settings": true, + "enable_rate_limit_client_requests": false, + "rate_limit_client_requests_p_minute": 80, + "enable_qr_code_pairing": false } } \ No newline at end of file diff --git a/api/v1/inc/arbeit.inc.php b/api/v1/inc/arbeit.inc.php index 6117c64..db59a69 100644 --- a/api/v1/inc/arbeit.inc.php +++ b/api/v1/inc/arbeit.inc.php @@ -12,7 +12,9 @@ require_once dirname(__DIR__, 1) . "/class/events/loader.events.arbeit.inc.php"; require_once dirname(__DIR__, 1) . "/class/arbeitszeit.inc.php"; require_once dirname(__DIR__, 1) . "/class/nodes/nodes.arbeit.inc.php"; -require_once dirname(__DIR__, 1) . "/class/db/db.arbeit.inc.php"; +if (!defined("NO_DB")) { + require_once dirname(__DIR__, 1) . "/class/db/db.arbeit.inc.php"; +} require_once dirname(__DIR__, 1) . "/class/notifications/notifications.arbeit.inc.php"; require_once dirname(__DIR__, 1) . "/class/i18n/i18n.arbeit.inc.php"; require_once dirname(__DIR__, 1) . "/class/benutzer/benutzer.arbeit.inc.php"; @@ -24,6 +26,7 @@ require_once dirname(__DIR__, 1) . "/class/status/runHook.php"; require_once dirname(__DIR__, 1) . "/class/vacation/vacation.arbeit.inc.php"; require_once dirname(__DIR__, 1) . "/class/sickness/sickness.arbeit.inc.php"; +require_once dirname(__DIR__, 1) . "/class/projects/projects.arbeit.inc.php"; require_once dirname(__DIR__, 1) . "/class/exports/ExportModule.arbeit.inc.php"; require_once dirname(__DIR__, 1) . "/class/exports/modules/ExportModuleInterface.em.arbeit.inc.php"; diff --git a/api/v1/inc/config/sickness_types.json b/api/v1/inc/config/sickness_types.json new file mode 100644 index 0000000..5589514 --- /dev/null +++ b/api/v1/inc/config/sickness_types.json @@ -0,0 +1,5 @@ +{ + "0": "Initial medical certificate", + "1": "Follow-up medical certificate", + "2": "Other" +} \ No newline at end of file diff --git a/api/v1/inc/config/vacation_types.json b/api/v1/inc/config/vacation_types.json new file mode 100644 index 0000000..6e5cc2d --- /dev/null +++ b/api/v1/inc/config/vacation_types.json @@ -0,0 +1,8 @@ +{ + "0": "Vacation", + "1": "Public Holiday", + "2": "Unpaid leave", + "3": "Overtime", + "4": "Parental Leave", + "5": "Other" +} \ No newline at end of file diff --git a/api/v1/inc/config/worktime_types.json b/api/v1/inc/config/worktime_types.json index 1ac310a..9cf9b69 100644 --- a/api/v1/inc/config/worktime_types.json +++ b/api/v1/inc/config/worktime_types.json @@ -3,5 +3,6 @@ "1": "Office", "2": "Public Holiday", "3": "Business Trip", - "4": "Mobile / On the Go" + "4": "Mobile / On the Go", + "5": "Other" } \ No newline at end of file diff --git a/api/v1/toil/README.md b/api/v1/toil/README.md index 84e6304..5a1192a 100644 --- a/api/v1/toil/README.md +++ b/api/v1/toil/README.md @@ -18,9 +18,11 @@ Most endpoints are using `GET` method to either change, delete, view or add data * `getOwnWorktime` - Returns the worktime entries of the authenticated API user (user) * `addOwnWorktime` - Allows you to add a worktime entry for the authenticated API user (user) * `addOwnVacation` - Allows you to request vacation for the authenticated API user (user) +* `getOwnUser` - Returns user information for the authenticated API user (user) * `getUserWorktimes` - Returns a JSON array of all worktime entries of a specified user (admin) * `addProject` - Allows you to add a project (admin) * `addUser` - Create a new user (admin) +* `editUser` - Edit a user (admin) * `deleteUser` - Delete a desired user (admin) * `getUserDetails` - Get a JSON array of the user's details (admin) * `getVersion` - Returns the current installed TimeTrack version (user) @@ -92,5 +94,7 @@ You can also use the `CustomRoutes::getCustomRoutes()` function to get all custo ## Tokens +**Tokens can only be used by local users** + You can use the `createToken` endpoint to create a new token for yourself. The token will be returned in the response along with other useful information you may want to note down. The token will be valid by default for 2 hours. You can set the expiration time in the `Tokens.routes.toil.arbeit.inc.php` file. When a token is expired you can use the `refreshToken` enpoint to refresh the token. When a token expires it is not deleted from the database to allow you refreshing it as the refresh token does not expire. To delete a token entirely you can use the `deleteToken` endpoint (revoke). Tokens cannot be used to login to the web interface, you can only use them to access the API. You have to set the `Authorization` header to `Bearer {token}` to use the token. diff --git a/api/v1/toil/VERSION b/api/v1/toil/VERSION index 6eef684..40cbe04 100644 --- a/api/v1/toil/VERSION +++ b/api/v1/toil/VERSION @@ -1 +1 @@ -v1.12 +v1.13 diff --git a/api/v1/toil/permissions.json b/api/v1/toil/permissions.json index 7c22d9e..65d4e05 100644 --- a/api/v1/toil/permissions.json +++ b/api/v1/toil/permissions.json @@ -25,5 +25,6 @@ "getNotifications": 0, "createToken": 0, "deleteToken": 0, - "refreshToken": 0 + "refreshToken": 0, + "editUser": 1 } \ No newline at end of file diff --git a/api/v1/toil/resources/editUser.ep.toil.arbeit.inc.php b/api/v1/toil/resources/editUser.ep.toil.arbeit.inc.php new file mode 100644 index 0000000..a178f17 --- /dev/null +++ b/api/v1/toil/resources/editUser.ep.toil.arbeit.inc.php @@ -0,0 +1,87 @@ +arbeitzeit = new Arbeitszeit; + } + + public function __set($name, $value) + { + $this->$name = $value; + } + + public function __get($name) + { + return $this->$name; + } + + public function get() + { + header('Content-Type: application/json'); + $res = $this->arbeitszeit->benutzer()->editUserProperties( + $_GET["username"], + $_GET["name"], + $_GET["value"] + ); + + if($res){ + $r = array( + "status" => "success", + "username" => $_GET["username"], + "name" => $_GET["name"], + "value" => $_GET["value"], + "updated" => true + ); + } else { + $r = array( + "status" => "error", + "username" => $_GET["username"], + "name" => $_GET["name"], + "value" => $_GET["value"], + "updated" => false, + "message" => "Please review the log for additional information" + ); + } + + echo json_encode($r); + + } + + public function post($post = null) + { + /** + * Empty, required by interface + * + */ + } + + public function delete() + { + /** + * Empty, required by interface + * + */ + } + + public function put() + { + /** + * Empty, required by interface + * + */ + } + } +} + + +?> \ No newline at end of file diff --git a/api/v1/toil/resources/getOwnUser.ep.toil.arbeit.inc.php b/api/v1/toil/resources/getOwnUser.ep.toil.arbeit.inc.php new file mode 100644 index 0000000..fbab24e --- /dev/null +++ b/api/v1/toil/resources/getOwnUser.ep.toil.arbeit.inc.php @@ -0,0 +1,79 @@ +arbeitzeit = new Arbeitszeit; + } + + public function __set($name, $value) + { + $this->$name = $value; + } + + public function __get($name) + { + return $this->$name; + } + + public function get() + { + header('Content-Type: application/json'); + $user = $_SERVER["PHP_AUTH_USER"]; + + $data = $this->arbeitszeit->benutzer()->get_user($user); + + unset($data["password"]); + unset($data["state"]); + unset($data[4], $data[7]); + + if($data == false){ + $r = [ + "error" => "An error occured while fetching user data" + ]; + echo json_encode($r); + die(); + } + + $r = $data; + echo json_encode($r); + + } + + public function post($post = null) + { + /** + * Empty, required by interface + * + */ + } + + public function delete() + { + /** + * Empty, required by interface + * + */ + } + + public function put() + { + /** + * Empty, required by interface + * + */ + } + } +} + + +?> \ No newline at end of file diff --git a/api/v1/toil/resources/healthcheck.ep.toil.arbeit.inc.php b/api/v1/toil/resources/healthcheck.ep.toil.arbeit.inc.php index 12436a5..ec79750 100644 --- a/api/v1/toil/resources/healthcheck.ep.toil.arbeit.inc.php +++ b/api/v1/toil/resources/healthcheck.ep.toil.arbeit.inc.php @@ -26,7 +26,7 @@ public function __get($name) public function get() { header('Content-Type: application/json'); - echo json_encode(array('status' => "alive")); + echo json_encode(array('status' => "alive", "time" => date("c"))); } public function post($post = null) diff --git a/assets/css/v8.css b/assets/css/v8.css index db3a365..3b1115d 100644 --- a/assets/css/v8.css +++ b/assets/css/v8.css @@ -27,7 +27,7 @@ h1, h2, h3 { font-weight: 600; } -input, button { +input, button, textarea { border-radius: var(--radius); border: none; padding: 0.75rem; @@ -514,3 +514,31 @@ td:first-child { .card form .button:hover { background: var(--accent-dark, #333); } + +.tooltip { + position: relative; + display: inline-block; +} + +.tooltip .tooltip-text { + visibility: hidden; + background-color: #333; + color: #fff; + font-size: 0.85rem; + text-align: center; + border-radius: 6px; + padding: 5px 8px; + position: absolute; + z-index: 1; + bottom: 125%; + left: 50%; + transform: translateX(-50%); + opacity: 0; + transition: opacity 0.3s; + white-space: nowrap; +} + +.tooltip:hover .tooltip-text { + visibility: visible; + opacity: 1; +} \ No newline at end of file diff --git a/assets/gui/standard_nav.php b/assets/gui/standard_nav.php index 393ee8f..267e03d 100644 --- a/assets/gui/standard_nav.php +++ b/assets/gui/standard_nav.php @@ -20,6 +20,7 @@ + is_Admin($user->get_user(@$_SESSION["username"]))) : $v = file_get_contents($_SERVER["DOCUMENT_ROOT"] . "/VERSION"); ?> @@ -27,6 +28,7 @@ + ADMIN | diff --git a/composer.json b/composer.json index 9c0b299..857d0ea 100644 --- a/composer.json +++ b/composer.json @@ -3,18 +3,18 @@ "description": "TimeTrack is a PHP-written time recording tool for small businesses", "type": "software", "license": "GNU GPL", - "version": "8.0", + "version": "8.1", "authors": [ { - "name": "Ente", - "email": "github@openducks.org", + "name": "Bryan Boehnke-Avan", + "email": "bryan@openducks.org", "homepage": "https://openducks.org" } ], "require": { "phpmailer/phpmailer": "^6.1", "erusev/parsedown": "^1.7", - "pecee/simple-router": "4.3.7.2", + "pecee/simple-router": "5.0.0.3", "symfony/yaml": "^7.1", "cweagans/composer-patches": "^1.7", "ldaptools/ldaptools": "dev-master", diff --git a/errors/500.php b/errors/500.php index a62eb03..6f928bd 100644 --- a/errors/500.php +++ b/errors/500.php @@ -1,4 +1,5 @@ hasTable("projects"); + if ($exists) { + echo "\nSkipping. Table already exists.\n"; + return; + } + + $this->execute("SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';"); + $this->execute("SET time_zone = '+00:00';"); + + $this->table("projects", ["id" => false, "primary_key" => "id"]) + ->addColumn("id", "integer", ["identity" => true]) + ->addColumn("name", "string", ["limit" => 255]) + ->addColumn("description", "text", ["null" => true]) + ->addColumn("members", "text", ["null" => true]) + ->addColumn("items_assoc", "string", ["limit" => 64]) + ->addColumn("deadline", "datetime", ["null" => true]) + ->addColumn("owner", "integer") + ->create(); + } +} diff --git a/migrations/migrations/20250905190421_init_projects_users_scheme.php b/migrations/migrations/20250905190421_init_projects_users_scheme.php new file mode 100644 index 0000000..b4a5325 --- /dev/null +++ b/migrations/migrations/20250905190421_init_projects_users_scheme.php @@ -0,0 +1,29 @@ +hasTable("projects_users"); + if ($exists) { + echo "\nSkipping. Table already exists.\n"; + return; + } + + $this->execute("SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';"); + $this->execute("SET time_zone = '+00:00';"); + + $this->table("projects_users", ["id" => false, "primary_key" => "id"]) + ->addColumn("id", "integer", ["identity" => true]) + ->addColumn("userid", "integer", ["limit" => 255]) + ->addColumn("projectid", "integer", ["null" => true]) + ->addColumn("permissions", "integer") + ->addColumn("role", "text") + ->addColumn("is_owner", "boolean") + ->create(); + } +} diff --git a/migrations/migrations/20250905190436_init_projects_items_scheme.php b/migrations/migrations/20250905190436_init_projects_items_scheme.php new file mode 100644 index 0000000..894ceb2 --- /dev/null +++ b/migrations/migrations/20250905190436_init_projects_items_scheme.php @@ -0,0 +1,29 @@ +hasTable("projects_items"); + if ($exists) { + echo "\nSkipping. Table already exists.\n"; + return; + } + + $this->execute("SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';"); + $this->execute("SET time_zone = '+00:00';"); + + $this->table("projects_items", ["id" => false]) + ->addColumn("id", "integer") // PROJECT ID + ->addColumn("title", "string", ["limit" => 255]) + ->addColumn("description", "text", ["null" => true]) + ->addColumn("assignee", "integer") // USER ID + ->addColumn("itemid", "integer", ["id" => true]) + ->addColumn("deadline", "date") + ->create(); + } +} diff --git a/migrations/migrations/20250905190447_init_projects_worktimes_scheme.php b/migrations/migrations/20250905190447_init_projects_worktimes_scheme.php new file mode 100644 index 0000000..424b4f0 --- /dev/null +++ b/migrations/migrations/20250905190447_init_projects_worktimes_scheme.php @@ -0,0 +1,26 @@ +hasTable("projects_worktimes"); + if ($exists) { + echo "\nSkipping. Table already exists.\n"; + return; + } + + $this->execute("SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';"); + $this->execute("SET time_zone = '+00:00';"); + + $this->table("projects_worktimes", ["id" => false]) + ->addColumn("itemid", "integer") + ->addColumn("worktimeid", "integer") + ->addColumn("user", "integer") + ->create(); + } +} diff --git a/suite/actions/projects/createItem.php b/suite/actions/projects/createItem.php new file mode 100644 index 0000000..8244149 --- /dev/null +++ b/suite/actions/projects/createItem.php @@ -0,0 +1,19 @@ +get_app_ini()["general"]["base_url"]; +$arbeit->auth()->login_validation(); + +if(!isset($_POST["title"], $_POST["description"], $_POST["project_id"])){ + $arbeit->statusMessages()->redirect("error"); +} + +if(($arbeit->projects()->checkUserisOwner($_POST["project_id"]) || $arbeit->projects()->checkUserHasProjectAccess($arbeit->benutzer()->get_current_user()["id"], 0))){ + if($arbeit->projects()->addProjectItem($_POST["project_id"], $_POST["title"], $_POST["description"], $_POST["assignee"])){ + $arbeit->statusMessages()->redirect("projects_item_added"); + } else { + $arbeit->statusMessages()->redirect("project_item_failed"); + } +} \ No newline at end of file diff --git a/suite/actions/projects/mapWorktimeToItem.php b/suite/actions/projects/mapWorktimeToItem.php new file mode 100644 index 0000000..70f7362 --- /dev/null +++ b/suite/actions/projects/mapWorktimeToItem.php @@ -0,0 +1,23 @@ +get_app_ini()["general"]["base_url"]; +$arbeit->auth()->login_validation(); + +if(!isset($_POST["worktime_id"], $_POST["item_id"])){ + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("error")); +} + +if($_POST["user_id"] == null){ + $user_id = $arbeit->benutzer()->get_current_user()["id"]; +} + +if(($arbeit->projects()->checkUserisOwner($_POST["project_id"]) || $arbeit->projects()->checkUserHasProjectAccess($arbeit->benutzer()->get_current_user()["id"], $_POST["project_id"], 0))){ + if($arbeit->projects()->mapWorktimeToItem($_POST["worktime_id"], $_POST["item_id"])){ + $arbeit->statusMessages()->redirect("mapWorktimeToItem_success"); + } else { + $arbeit->statusMessages()->redirect("mapWorktimeToItem_failed"); + } +} \ No newline at end of file diff --git a/suite/actions/worktime/correction.php b/suite/actions/worktime/correction.php new file mode 100644 index 0000000..7442035 --- /dev/null +++ b/suite/actions/worktime/correction.php @@ -0,0 +1,17 @@ +get_app_ini()["general"]["base_url"]; +$worktime->auth()->login_validation(); +$id = $_POST["worktime_id"]; +$work = $worktime->update_worktime($id, ["schicht_anfang" => $_POST["new_start"], "schicht_ende" => $_POST["new_end"], "ort" => $_POST["new_comment"] . " - SYS: " . $_POST["reason"]]); +if(!$work){ + header("Location: http://{$base_url}/suite/?" . $worktime->statusMessages()->URIBuilder('error_worktime_update')); +} else { + header("Location: http://{$base_url}/suite/?" . $worktime->statusMessages()->URIBuilder('worktime_updated')); +} + + + +?> \ No newline at end of file diff --git a/suite/admin/actions/projects/add.php b/suite/admin/actions/projects/add.php new file mode 100644 index 0000000..d35ae0a --- /dev/null +++ b/suite/admin/actions/projects/add.php @@ -0,0 +1,19 @@ +auth()->login_validation(); + +if(!isset($_POST["name"], $_POST["items_assoc"], $_POST["owner"])){ + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("error")); +} + +if($arbeit->benutzer()->is_admin($arbeit->benutzer()->get_user($_SESSION["username"]))){ + if($arbeit->projects()->addProject($_POST["name"], $_POST["description"], $_POST["items_assoc"], null, $_POST["owner"] ?? $arbeit->benutzer()->get_current_user()["id"])){ + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("project_added")); + } else { + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("project_added_error")); + } +} \ No newline at end of file diff --git a/suite/admin/actions/projects/delete.php b/suite/admin/actions/projects/delete.php new file mode 100644 index 0000000..45890d2 --- /dev/null +++ b/suite/admin/actions/projects/delete.php @@ -0,0 +1,19 @@ +get_app_ini()["general"]["base_url"]; +$arbeit->auth()->login_validation(); + +if(!isset($_GET["id"])){ + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("error")); +} + +if($arbeit->benutzer()->is_admin($arbeit->benutzer()->get_user($_SESSION["username"])) && $arbeit->projects()->checkUserisOwner($_GET["id"])){ + if($arbeit->projects()->deleteProject($_GET["id"])){ + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("project_deleted")); + } else { + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("project_deleted_failed")); + } +} \ No newline at end of file diff --git a/suite/admin/actions/projects/edit.php b/suite/admin/actions/projects/edit.php new file mode 100644 index 0000000..55131e0 --- /dev/null +++ b/suite/admin/actions/projects/edit.php @@ -0,0 +1,25 @@ +get_app_ini()["general"]["base_url"]; + +$arbeit->auth()->login_validation(); + +if(!isset($_POST["name"], $_POST["owner"]) || !isset($_POST["id"])){ + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("error")); +} + +if($arbeit->benutzer()->is_admin($arbeit->benutzer()->get_current_user()) && ($arbeit->benutzer()->current_user_is_admin() || $arbeit->projects()->checkUserisOwner($_POST["id"]))){ + if($arbeit->projects()->editProject($_POST["id"], [ + "name" => $_POST["name"], + "description" => $_POST["description"], + "deadline" => $_POST["deadline"], + "owner" => $_POST["owner"] + ])){ + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("project_edited")); + } else { + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("project_edited_error")); + } +} \ No newline at end of file diff --git a/suite/admin/actions/projects/userEdit.php b/suite/admin/actions/projects/userEdit.php new file mode 100644 index 0000000..cb684f0 --- /dev/null +++ b/suite/admin/actions/projects/userEdit.php @@ -0,0 +1,21 @@ +get_app_ini()["general"]["base_url"]; +$arbeit->auth()->login_validation(); + +if(!isset($_POST["projectId"], $_POST["userId"])){ + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("error")); +} + +if($arbeit->projects()->checkUserisOwner($_POST["projectId"])){ + if($arbeit->projects()->addProjectMember($_POST["projectId"], $_POST["userId"], $_POST["permissions"], $_POST["role"])){ + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("project_userAdded")); + } else { + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("project_userAdded_failed")); + } +} + +?> \ No newline at end of file diff --git a/suite/admin/projects/addUser.php b/suite/admin/projects/addUser.php new file mode 100644 index 0000000..baf8af8 --- /dev/null +++ b/suite/admin/projects/addUser.php @@ -0,0 +1,49 @@ +get_app_ini(); + +$arbeit->auth()->login_validation(); +if(!$arbeit->benutzer()->is_admin($arbeit->benutzer()->get_current_user())){ + die("No permissions"); +} + +$projectId = $_GET["project"] ?? null; +$language = $arbeit->i18n()->loadLanguage(null, "projects/addUser"); +?> + + + + + <?= $language["title"]; ?> | <?= $ini["general"]["app_name"]; ?> + + + + + +
+

+
+ + +
+ " hidden> + +

+
+ +

+
+
+ +

+
+ +
+
+ + diff --git a/suite/admin/projects/admin.php b/suite/admin/projects/admin.php new file mode 100644 index 0000000..1c89cef --- /dev/null +++ b/suite/admin/projects/admin.php @@ -0,0 +1,112 @@ +get_app_ini(); +$base_url = $ini["general"]["base_url"]; + +$language = $arbeit->i18n()->loadLanguage(null, "projects/admin", "admin"); + +$arbeit->auth()->login_validation(); +if (!$arbeit->benutzer()->is_admin($arbeit->benutzer()->get_current_user())) { + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("noperms")); + exit; +} + +$allProjects = $arbeit->projects()->getCurrentUserProjects(); +?> + + + + + + <?= $language["title"]; ?> | <?= $ini["general"]["app_name"]; ?> + + + + + + +
+

+ +
+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
benutzer()->get_name_from_id($proj["owner"]); ?> + " + class="v8-button"> + " onclick="return confirmDelete(event, )" + class="v8-button danger"> + + +
+ +
+
+ + +
+

+
+ + +
+
+ +
+ + +
+ + "> +
+ +
+
+
+ + + \ No newline at end of file diff --git a/suite/admin/projects/edit.php b/suite/admin/projects/edit.php new file mode 100644 index 0000000..9d0e721 --- /dev/null +++ b/suite/admin/projects/edit.php @@ -0,0 +1,62 @@ +get_app_ini(); +$base_url = $ini["general"]["base_url"]; + +$language = $arbeit->i18n()->loadLanguage(null, "projects/edit", "admin"); + +$arbeit->auth()->login_validation(); +if(!$arbeit->benutzer()->is_admin($arbeit->benutzer()->get_current_user())){ + header("Location: http://{$base_url}/suite/?" . $arbeit->statusMessages()->URIBuilder("noperms")); + exit; +} + +$projectId = $_GET["id"] ?? null; + +$project = $arbeit->projects()->getProject($projectId); +?> + + + + + <?= $language["title"]; ?> | <?= $ini["general"]["app_name"]; ?> + + + + + +
+

:

+ +
+
+ + "> + +
+ " required> +

+
+ +

+
+ "> +

+
+ " placeholder="UserID"> +
+

+ +
+
+
+ + diff --git a/suite/admin/users/edit.php b/suite/admin/users/edit.php index 4b239b3..a140057 100644 --- a/suite/admin/users/edit.php +++ b/suite/admin/users/edit.php @@ -41,7 +41,7 @@ -
+

@@ -61,11 +61,11 @@ -
+

-
+ diff --git a/suite/notifications/all.php b/suite/notifications/all.php index f4fa79f..3e45379 100644 --- a/suite/notifications/all.php +++ b/suite/notifications/all.php @@ -39,20 +39,20 @@ -
+
- + " name="datum" required> - - +
+ " name="uhrzeit" required> - - -
- + + +

+
- +
diff --git a/suite/projects/createItem.php b/suite/projects/createItem.php new file mode 100644 index 0000000..554ab39 --- /dev/null +++ b/suite/projects/createItem.php @@ -0,0 +1,44 @@ +get_app_ini(); + +$arbeit->auth()->login_validation(); + +$projectId = $_GET["project"] ?? null; +$language = $arbeit->i18n()->loadLanguage(null, "projects/createItem"); +?> + + + + + <?= $language["title"]; ?> | <?= $ini["general"]["app_name"]; ?> + + + + + +
+

+
+ + +
+ +

+
+ +

+
+ +

+
+ + +
+ + diff --git a/suite/projects/item.php b/suite/projects/item.php new file mode 100644 index 0000000..0c72132 --- /dev/null +++ b/suite/projects/item.php @@ -0,0 +1,77 @@ +get_app_ini(); +$base_url = $ini["general"]["base_url"]; + +$language = $arbeit->i18n()->loadLanguage(null, "projects/item"); + +$arbeit->auth()->login_validation(); + +$user = $arbeit->benutzer()->get_current_user(); + +$itemId = $_GET["id"] ?? null; + +$item = $arbeit->projects()->getItem($itemId); +$project = $arbeit->projects()->getProject($item["id"]); +$worktimes = $arbeit->projects()->getUserProjectWorktimes($project["id"], $user); + +?> + + + + + <?= $language["title"]; ?> | <?= $ini["general"]["app_name"]; ?> + + + + + +
+

+

+ +

:

+

: benutzer()->get_user_from_id($item["assignee"])["name"] ?? "-"; ?>

+

:

+

:

+ +
+

+
+ + + + + + + + + + + + + + + + + + + +
+
+ +
+ + +
+ + + diff --git a/suite/projects/mapWorktimeToItem.php b/suite/projects/mapWorktimeToItem.php new file mode 100644 index 0000000..761d30c --- /dev/null +++ b/suite/projects/mapWorktimeToItem.php @@ -0,0 +1,43 @@ +get_app_ini(); + +$arbeit->auth()->login_validation(); + +$projectId = $_GET["project"] ?? null; +?> + + + + + Map Worktime | <?= $ini["general"]["app_name"]; ?> + + + + + +
+

Map Worktime to Item

+
+ + +
+ +

+
+ +

+
+ +

+
+ Cancel +
+
+ + diff --git a/suite/projects/overview.php b/suite/projects/overview.php new file mode 100644 index 0000000..aa0220b --- /dev/null +++ b/suite/projects/overview.php @@ -0,0 +1,99 @@ +get_app_ini(); +$base_url = $ini["general"]["base_url"]; + +$language = $arbeit->i18n()->loadLanguage(null, "projects/overview"); + +$arbeit->auth()->login_validation(); + +$user = $arbeit->benutzer()->get_current_user(); + +$userProjects = $arbeit->projects()->getCurrentUserProjects(); +$userItems = $arbeit->projects()->getUserProjectItems($userProjects[1]["id"], $user["id"]); +?> + + + + + <?= $language["title"]; ?> | <?= $ini["general"]["app_name"]; ?> + + + + + +
+

+ +
+

+
+ +
+

+ +
+ +
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ projects()->getProject($item["id"])["name"]; ?> + + " class="v8-button"> +
+ +
+
+
+
+ + diff --git a/suite/projects/view.php b/suite/projects/view.php new file mode 100644 index 0000000..a4a3ed4 --- /dev/null +++ b/suite/projects/view.php @@ -0,0 +1,143 @@ +get_app_ini(); +$base_url = $ini["general"]["base_url"]; + +$language = $arbeit->i18n()->loadLanguage(null, "projects/view"); + +$arbeit->auth()->login_validation(); + +$user = $arbeit->benutzer()->get_current_user(); + +$projectId = $_GET["id"] ?? null; +$project = $arbeit->projects()->getProject($projectId); + +$items = $arbeit->projects()->getProjectItems($projectId); +$members = $arbeit->projects()->getProjectUsers($projectId); +$worktimes = $arbeit->projects()->getUserProjectWorktimes($items) ?? false; +$arbeit->statusMessages()->blockIfNotAdmin(); +?> + + + + + <?= $language["title"]; ?> | <?= $ini["general"]["app_name"]; ?> + + + + + + +
+

+

:

+

:

+ + +
+ + + +
+ +
+
+

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
benutzer()->get_user_from_id($i["assignee"])["name"] ?? "-"; ?>" class="v8-button">
+
+ + + + +
+ + + + diff --git a/suite/worktime/all.php b/suite/worktime/all.php index 2a1616c..4857f4e 100644 --- a/suite/worktime/all.php +++ b/suite/worktime/all.php @@ -48,6 +48,7 @@ + diff --git a/suite/worktime/correction.php b/suite/worktime/correction.php new file mode 100644 index 0000000..c946fa8 --- /dev/null +++ b/suite/worktime/correction.php @@ -0,0 +1,68 @@ +get_app_ini(); +$base_url = $ini["general"]["base_url"]; + +$language = $arbeit->i18n()->loadLanguage(null, "worktime/correction"); + +$arbeit->auth()->login_validation(); + +$user = $arbeit->benutzer()->get_current_user(); + +$worktimeId = $_GET["id"] ?? null; +$worktime = $arbeit->get_worktime_by_id($worktimeId); +if(!$arbeit->check_if_for_review($worktimeId)){ + $arbeit->statusMessages()->redirect("error"); +} +?> + + + + + <?= $language["title"]; ?> | <?= $ini["general"]["app_name"]; ?> + + + + + +
+

+ +
+

+

:

+

:

+

:

+

:

+
+ +
+

+
+ + +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ +