Project import generated by Copybara.
GitOrigin-RevId: 63746295f1a5ab5a619056791995793d65529e62
diff --git a/src/inc/api.php b/src/inc/api.php
new file mode 100644
index 0000000..9b606be
--- /dev/null
+++ b/src/inc/api.php
@@ -0,0 +1,28 @@
+<?php
+class api {
+ public static function inputJson() {
+ $string = trim(file_get_contents("php://input"));
+
+ if (empty($string)) return false;
+
+ $json = json_decode($string, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) return false;
+
+ return $json;
+ }
+
+ public static function error($message = null) {
+ if ($message !== null) self::write([
+ 'error' => true,
+ 'message' => $message,
+ ]);
+ http_response_code(400);
+ exit();
+ }
+
+ public static function write($array) {
+ header('Content-Type: application/json');
+ echo json_encode($array);
+ }
+}
diff --git a/src/inc/calendars.php b/src/inc/calendars.php
new file mode 100644
index 0000000..a8b4a9f
--- /dev/null
+++ b/src/inc/calendars.php
@@ -0,0 +1,182 @@
+<?php
+class calendars {
+ const TYPE_FESTIU = 0;
+ const TYPE_FEINER = 1;
+ const TYPE_LECTIU = 2;
+
+ const NO_CALENDAR_APPLICABLE = 0;
+
+ public static $types = array(
+ 0 => "Festivo",
+ 2 => "Lectivo",
+ 1 => "No lectivo"
+ );
+ public static $workingTypes = [1, 2];
+ public static $days = ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado", "Domingo"];
+ public static $months = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"];
+
+ public static function parseFormCalendar($form, $ibegins, $iends, $timestamp = false) {
+ if ($timestamp) {
+ $current = new DateTime();
+ $current->setTimestamp((int)$ibegins);
+ $ends = new DateTime();
+ $ends->setTimestamp((int)$iends);
+ } else {
+ $current = new DateTime($ibegins);
+ $ends = new DateTime($iends);
+ }
+ $interval = new DateInterval("P1D");
+
+ if ($current->diff($ends)->invert === 1) {
+ return false;
+ }
+
+ $return = array(
+ "begins" => $current->getTimestamp(),
+ "ends" => $ends->getTimestamp(),
+ "calendar" => []
+ );
+
+ $possible_values = array_keys(self::$types);
+
+ $day = 0;
+ while ($current->diff($ends)->invert === 0) {
+ if (!isset($form[$day]) || !in_array($form[$day], $possible_values)) {
+ return false;
+ }
+
+ $return["calendar"][$current->getTimestamp()] = (int)$form[$day];
+
+ $day++;
+ $current->add($interval);
+ }
+
+ return $return;
+ }
+
+ public static function checkOverlap($category, $begins, $ends) {
+ global $con;
+
+ $scategory = (int)$category;
+ $sbegins = (int)$begins;
+ $sends = (int)$ends;
+
+ $query = mysqli_query($con, "SELECT * FROM calendars WHERE begins <= $sends AND ends >= $sbegins AND category = $scategory LIMIT 1");
+
+ return (mysqli_num_rows($query) > 0);
+ }
+
+ public static function add($category, $begins, $ends, $calendar) {
+ global $con;
+
+ if (self::checkOverlap($category, $begins, $ends)) {
+ return -1;
+ }
+
+ $scategory = (int)$category;
+ $sbegins = (int)$begins;
+ $sends = (int)$ends;
+ $scalendar = db::sanitize(json_encode($calendar));
+
+ return (mysqli_query($con, "INSERT INTO calendars (category, begins, ends, details) VALUES ($scategory, $sbegins, $sends, '$scalendar')") ? 0 : -2);
+ }
+
+ public static function edit($id, $calendar) {
+ global $con;
+
+ $sid = (int)$id;
+ $scalendar = db::sanitize(json_encode($calendar));
+
+ return (mysqli_query($con, "UPDATE calendars SET details = '$scalendar' WHERE id = $sid LIMIT 1"));
+ }
+
+ public static function get($id) {
+ global $con;
+
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT c.id id, c.begins begins, c.ends ends, c.details details, c.category category, ca.name categoryname FROM calendars c LEFT JOIN categories ca ON c.category = ca.id WHERE c.id = $sid");
+
+ if (!mysqli_num_rows($query)) {
+ return false;
+ }
+
+ $row = mysqli_fetch_assoc($query);
+
+ if ($row["category"] == -1) {
+ $row["categoryname"] = "Calendario por defecto";
+ }
+
+ return $row;
+ }
+
+ public static function getByCategory($id, $details = false) {
+ global $con;
+
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT id, begins, ends".($details ? ", details" : "")." FROM calendars WHERE category = $sid");
+
+ $calendars = [];
+
+ while ($row = mysqli_fetch_assoc($query)) {
+ $calendars[] = $row;
+ }
+
+ return $calendars;
+ }
+
+ public static function getAll() {
+ $categories = categories::getAll();
+ $categories[-1] = "Calendario por defecto";
+
+ $return = [];
+
+ foreach ($categories as $id => $category) {
+ $return[] = array(
+ "category" => $category,
+ "categoryid" => $id,
+ "calendars" => self::getByCategory($id)
+ );
+ }
+
+ return $return;
+ }
+
+ public static function getCurrentCalendarByCategory($category) {
+ global $con;
+
+ $scategory = (int)$category;
+ $stime = (int)time();
+
+ $query = mysqli_query($con, "SELECT id, category, begins, ends, details FROM calendars WHERE category IN (-1, $scategory) AND begins <= $stime AND ends >= $stime");
+ if ($query === false) return false;
+
+ $calendars = [];
+ while ($row = mysqli_fetch_assoc($query)) {
+ $row["details"] = json_decode($row["details"], true);
+ if (json_last_error() !== JSON_ERROR_NONE) return false;
+
+ $calendars[$row["category"]] = $row;
+ }
+
+ return $calendars[$category] ?? $calendars[-1] ?? self::NO_CALENDAR_APPLICABLE;
+ }
+
+ public static function remove($id) {
+ global $con;
+
+ $sid = (int)$id;
+
+ return mysqli_query($con, "DELETE FROM calendars WHERE id = $sid LIMIT 1");
+ }
+
+ public static function exists($id) {
+ global $con;
+
+ $sid = (int)$id;
+ $query = mysqli_query($con, "SELECT id FROM calendars WHERE id = $sid");
+
+ return (mysqli_num_rows($query) > 0);
+ }
+}
diff --git a/src/inc/calendarsView.php b/src/inc/calendarsView.php
new file mode 100644
index 0000000..eddc230
--- /dev/null
+++ b/src/inc/calendarsView.php
@@ -0,0 +1,44 @@
+<?php
+class calendarsView {
+ public static function renderCalendar($current, $ends, $selectedFunc, $disabled = false, $extra = false) {
+ $interval = new DateInterval("P1D");
+
+ echo '<div class="overflow-wrapper">';
+
+ $start = true;
+ $day = 0;
+ while ($current->diff($ends)->invert === 0) {
+ $dow = (int)$current->format("w");
+ if ($dow == 0) $dow = 7;
+ $dom = (int)$current->format("d");
+
+ if ($dow == 1) echo "</tr>";
+ if ($dom == 1) echo "</table>";
+ if ($dom == 1 || $start) echo "<div class='month'>".security::htmlsafe(ucfirst(strftime("%B %G", $current->getTimestamp())))."</div><table class='calendar'>";
+ if ($dow == 1 || $start) echo "<tr>";
+ if ($dom == 1 || $start) {
+ for ($i = 1; $i < $dow; $i++) {
+ echo "<td></td>";
+ }
+ }
+
+ echo "<td class='day'><span class='date'>".$dom."</span><br><select name='type[$day]'".($disabled ? " disabled" : "").">";
+
+ foreach (calendars::$types as $id => $type) {
+ echo "<option value='".(int)$id."'".($selectedFunc($current->getTimestamp(), $id, $dow, $dom, $extra) ? " selected" : "").">".security::htmlsafe($type)."</option>";
+ }
+
+ echo "</td>";
+
+ $start = false;
+ $day++;
+ $current->add($interval);
+ }
+
+ for ($i = $dow + 1; $i <= 7; $i++) {
+ echo "<td></td>";
+ }
+
+ echo "</tr></table></div>";
+ }
+}
diff --git a/src/inc/categories.php b/src/inc/categories.php
new file mode 100644
index 0000000..a75d998
--- /dev/null
+++ b/src/inc/categories.php
@@ -0,0 +1,161 @@
+<?php
+class categories {
+ private static function parseEmails($string) {
+ $string = str_replace(" ", "", $string);
+ $emails = explode(",", $string);
+
+ foreach ($emails as $i => &$e) {
+ if (empty($e)) {
+ unset($emails[$i]);
+ } else {
+ if (filter_var($e, FILTER_VALIDATE_EMAIL) === false) return false;
+ }
+ }
+
+ return $emails;
+ }
+
+ public static function readableEmails($emails) {
+ $array = json_decode($emails, true);
+ if (json_last_error() != JSON_ERROR_NONE) return false;
+ return implode(", ", $array);
+ }
+
+ private static function canBeParent($id) {
+ if ($id == 0) return true;
+
+ $category = self::get($id);
+ return ($category !== false && $category["parent"] == 0);
+ }
+
+ public static function add($category, $stringEmails, $parent) {
+ global $con;
+
+ $emails = (empty($stringEmails) ? [] : self::parseEmails($stringEmails));
+ if ($emails === false) return false;
+
+ if (!self::canBeParent($parent)) return false;
+
+ $scategory = db::sanitize($category);
+ $semails = db::sanitize(json_encode($emails));
+ $sparent = (int)$parent;
+
+ return mysqli_query($con, "INSERT INTO categories (name, emails, parent) VALUES ('$scategory', '$semails', $sparent)");
+ }
+
+ public static function edit($id, $name, $stringEmails, $parent) {
+ global $con;
+
+ $emails = (empty($stringEmails) ? [] : self::parseEmails($stringEmails));
+ if ($emails === false) return false;
+
+ if (!self::canBeParent($parent)) return false;
+
+ $sid = (int)$id;
+ $sname = db::sanitize($name);
+ $semails = db::sanitize(json_encode($emails));
+ $sparent = (int)$parent;
+
+ return mysqli_query($con, "UPDATE categories SET name = '$sname', emails = '$semails', parent = $sparent WHERE id = $sid LIMIT 1");
+ }
+
+ public static function get($id) {
+ global $con;
+
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT * FROM categories WHERE id = $sid");
+
+ if (!mysqli_num_rows($query)) return false;
+
+ return mysqli_fetch_assoc($query);
+ }
+
+ private static function addChilds(&$row) {
+ global $con;
+
+ $query = mysqli_query($con, "SELECT * FROM categories WHERE parent = ".(int)$row["id"]);
+
+ $row["childs"] = [];
+ while ($child = mysqli_fetch_assoc($query)) {
+ $row["childs"][] = $child;
+ }
+ }
+
+ public static function getAll($simplified = true, $withparents = true, $includechilds = false) {
+ global $con, $conf;
+
+ $query = mysqli_query($con, "SELECT ".($simplified ? "id, name" : "c.id id, c.name name, c.parent parent, c.emails emails, p.name parentname")." FROM categories c".($simplified ? "" : " LEFT JOIN categories p ON c.parent = p.id").($withparents ? "" : " WHERE c.parent = 0")." ORDER BY ".($conf["debug"] ? "id" : "name")." ASC");
+
+ $categories = [];
+
+ while ($row = mysqli_fetch_assoc($query)) {
+ if (!$simplified && $includechilds) self::addChilds($row);
+
+ if ($simplified) $categories[$row["id"]] = $row["name"];
+ else $categories[] = $row;
+ }
+
+ return $categories;
+ }
+
+ public static function getAllWithWorkers() {
+ global $con;
+
+ $categories = self::getAll(false);
+
+ foreach ($categories as &$category) {
+ $category["workers"] = [];
+
+ $query = mysqli_query($con, "SELECT w.id FROM workers w LEFT JOIN people p ON w.person = p.id WHERE p.category = ".(int)$category["id"]);
+
+ while ($row = mysqli_fetch_assoc($query)) {
+ $category["workers"][] = $row["id"];
+ }
+ }
+
+ return $categories;
+ }
+
+ public static function getChildren() {
+ global $con, $conf;
+
+ $query = mysqli_query($con, "SELECT p.id parent, c.id child FROM categories c LEFT JOIN categories p ON c.parent = p.id WHERE c.parent != 0");
+
+ $childs = [];
+
+ while ($row = mysqli_fetch_assoc($query)) {
+ if (!isset($childs[$row["parent"]])) {
+ $childs[$row["parent"]] = [];
+ }
+ $childs[$row["parent"]][] = $row["child"];
+ }
+
+ return $childs;
+ }
+
+ public static function exists($id) {
+ global $con;
+
+ if ($id == -1) return true;
+
+ $query = mysqli_query($con, "SELECT id FROM categories WHERE id = ".(int)$id);
+
+ return (mysqli_num_rows($query) > 0);
+ }
+
+ public static function getIdByName($name) {
+ global $con;
+
+ if (strtolower($name) == "sin categoría") return -1;
+
+ $sname = db::sanitize($name);
+ $query = mysqli_query($con, "SELECT id FROM categories WHERE name = '$sname'");
+
+ if (!mysqli_num_rows($query)) return false;
+
+ $row = mysqli_fetch_assoc($query);
+
+ return $row["id"];
+ }
+}
diff --git a/src/inc/common.php b/src/inc/common.php
new file mode 100644
index 0000000..26d02aa
--- /dev/null
+++ b/src/inc/common.php
@@ -0,0 +1,17 @@
+<?php
+class common {
+ public static function getDayTimestamp($originaltime) {
+ $datetime = new DateTime();
+ $datetime->setTimestamp($originaltime);
+
+ $rawdate = $datetime->format("Y-m-d")."T00:00:00";
+ $date = new DateTime($rawdate);
+
+ return $time = $date->getTimestamp();
+ }
+
+ public static function getTimestampFromRFC3339($rfc) {
+ $date = new DateTime($rfc);
+ return (int)$date->getTimestamp();
+ }
+}
diff --git a/src/inc/companies.php b/src/inc/companies.php
new file mode 100644
index 0000000..702151f
--- /dev/null
+++ b/src/inc/companies.php
@@ -0,0 +1,58 @@
+<?php
+class companies {
+ public static function add($company, $cif) {
+ global $con;
+
+ $scompany = db::sanitize($company);
+ $scif = db::sanitize($cif);
+ return mysqli_query($con, "INSERT INTO companies (name, cif) VALUES ('$scompany', '$scif')");
+ }
+
+ public static function edit($id, $name, $cif) {
+ global $con;
+
+ $sid = (int)$id;
+ $sname = db::sanitize($name);
+ $scif = db::sanitize($cif);
+
+ return mysqli_query($con, "UPDATE companies SET name = '$sname', cif = '$scif' WHERE id = $sid LIMIT 1");
+ }
+
+ public static function get($id) {
+ global $con;
+
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT * FROM companies WHERE id = $sid");
+
+ if (!mysqli_num_rows($query)) return false;
+
+ return mysqli_fetch_assoc($query);
+ }
+
+ public static function getAll($simplified = true, $mixed = false) {
+ global $con;
+
+ $query = mysqli_query($con, "SELECT * FROM companies ORDER BY id ASC");
+
+ $categories = [];
+
+ while ($row = mysqli_fetch_assoc($query)) {
+ if ($simplified) $categories[$row["id"]] = $row["name"];
+ elseif ($mixed) $categories[$row["id"]] = $row;
+ else $categories[] = $row;
+ }
+
+ return $categories;
+ }
+
+ public static function exists($id) {
+ global $con;
+
+ if ($id == -1) return true;
+
+ $query = mysqli_query($con, "SELECT id FROM companies WHERE id = ".(int)$id);
+
+ return (mysqli_num_rows($query) > 0);
+ }
+}
diff --git a/src/inc/csv.php b/src/inc/csv.php
new file mode 100644
index 0000000..ce44c81
--- /dev/null
+++ b/src/inc/csv.php
@@ -0,0 +1,31 @@
+<?php
+class csv {
+ public static $fields = ["dni", "name", "category", "email", "companies"];
+
+ public static function csv2array($filename) {
+ $file = fopen($filename, "r");
+
+ $return = [];
+
+ $i = 0;
+ while (($line = fgetcsv($file, null, ";")) !== false) {
+ if ($i == 0) {
+ if (count($line) < count(self::$fields)) return false;
+
+ for ($j = 0; $j < count(self::$fields); $j++) {
+ if ($line[$j] !== self::$fields[$j]) return false;
+ }
+ } else {
+ $return[$i] = [];
+ foreach (self::$fields as $j => $field) {
+ $return[$i][$field] = trim($line[$j]);
+ }
+ }
+ $i++;
+ }
+
+ fclose($file);
+
+ return $return;
+ }
+}
diff --git a/src/inc/db.php b/src/inc/db.php
new file mode 100644
index 0000000..293707a
--- /dev/null
+++ b/src/inc/db.php
@@ -0,0 +1,38 @@
+<?php
+class db {
+ const EXPORT_DB_FORMAT_SQL = 0;
+
+ public static function sanitize($string) {
+ global $con;
+ return mysqli_real_escape_string($con, $string);
+ }
+
+ public static function needsSetUp() {
+ global $con;
+
+ $checkquery = mysqli_query($con, "SELECT 1 FROM people LIMIT 1");
+
+ return ($checkquery === false);
+ }
+
+ public static function numRows($table) {
+ global $con;
+
+ $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+
+ $query = mysqli_query($con, "SELECT 1 FROM $stable");
+
+ if ($query === false) return -1;
+
+ return mysqli_num_rows($query);
+ }
+
+ public static function limitPagination($start, $limit) {
+ $slimit = (int)$limit;
+ $sstart = $slimit*(int)$start;
+ if ($slimit > 100 || $slimit < 0) return false;
+ if ($sstart < 0) return false;
+
+ return ($slimit == 0 ? "" : " LIMIT $sstart,$slimit");
+ }
+}
diff --git a/src/inc/export.php b/src/inc/export.php
new file mode 100644
index 0000000..6800d50
--- /dev/null
+++ b/src/inc/export.php
@@ -0,0 +1,54 @@
+<?php
+class export {
+ const FORMAT_PDF = 1;
+ const FORMAT_DETAILEDPDF = 2;
+ const FORMAT_CSV_SCHEDULES = 3;
+ const FORMAT_CSV_INCIDENTS = 4;
+
+ public static $formats = array(
+ 1 => "PDF con horario laboral",
+ 2 => "PDF detallado",
+ 3 => "CSV con horarios",
+ 4 => "CSV con incidencias"
+ );
+ public static $workerFormats = [1, 3, 4];
+
+ public static $schedulesFields = ["id", "worker", "workerid", "dni", "company", "day", "beginswork", "endswork", "beginsbreakfast", "endsbreakfast", "beginslunch", "endslunch", "state"];
+ public static $incidentsFields = ["id", "worker", "workerid", "dni", "company", "creator", "updated", "updatedby", "confirmedby", "type", "day", "allday", "begins", "ends", "details", "workerdetails", "verified", "typepresent", "typepaid", "state"];
+
+ public static function convert($str) {
+ return iconv('UTF-8', 'windows-1252', $str);
+ }
+
+ public static function getDays($worker, $begins, $ends, $showvalidated, $shownotvalidated) {
+ $return = [];
+
+ $records = registry::getRecords($worker, $begins, $ends, false, false, false, 0, 0);
+ if ($records === false) return false;
+ foreach ($records as $record) {
+ if (!$showvalidated && $record["state"] === registry::STATE_VALIDATED_BY_WORKER) continue;
+ if (!$shownotvalidated && $record["state"] === registry::STATE_REGISTERED) continue;
+ if (!isset($return[$record["day"]])) $return[$record["day"]] = [];
+ $return[$record["day"]]["schedule"] = $record;
+ }
+
+ $incidents = incidents::getAll(false, 0, 0, $worker, $begins, $ends);
+ if ($incidents === false) return false;
+ foreach ($incidents as $incident) {
+ if ($incident["state"] !== incidents::STATE_REGISTERED && $incident["state"] !== incidents::STATE_VALIDATED_BY_WORKER) continue;
+ if (!$showvalidated && $incident["state"] === incidents::STATE_VALIDATED_BY_WORKER) continue;
+ if (!$shownotvalidated && $incident["state"] === incidents::STATE_REGISTERED) continue;
+ if (!isset($return[$incident["day"]])) $return[$incident["day"]] = [];
+ if (!isset($return[$incident["day"]]["incidents"])) $return[$incident["day"]]["incidents"] = [];
+ $return[$incident["day"]]["incidents"][] = $incident;
+ }
+
+ ksort($return);
+
+ return $return;
+ }
+
+ public static function sec2hours($sec) {
+ return round((double)$sec/3600, 2)." h";
+ }
+}
diff --git a/src/inc/files.php b/src/inc/files.php
new file mode 100644
index 0000000..5f9cce2
--- /dev/null
+++ b/src/inc/files.php
@@ -0,0 +1,106 @@
+<?php
+class files {
+ const NAME_LENGTH = 16;
+ const MAX_SIZE = 6*1024*1024;
+ const READABLE_MAX_SIZE = "6 MB";
+
+ public static $acceptedFormats = ["pdf", "png", "jpg", "jpeg", "bmp", "gif"];
+ public static $mimeTypes = array(
+ "pdf" => "application/pdf",
+ "png" => "image/png",
+ "jpg" => "image/jpeg",
+ "jpeg" => "image/jpeg",
+ "bmp" => "image/bmp",
+ "gif" => "image/gif"
+ );
+ public static $readableMimeTypes = array(
+ "pdf" => "Documento PDF",
+ "png" => "Imagen PNG",
+ "jpg" => "Imagen JPG",
+ "jpeg" => "Imagen JPG",
+ "bmp" => "Imagen BMP",
+ "gif" => "Imagen GIF"
+ );
+ public static $mimeTypesIcons = array(
+ "pdf" => "insert_drive_file",
+ "png" => "image",
+ "jpg" => "image",
+ "jpeg" => "image",
+ "bmp" => "image",
+ "gif" => "image"
+ );
+
+ public static function getAcceptAttribute($pretty = false) {
+ $formats = array_map(function($el) {
+ return ".".$el;
+ }, self::$acceptedFormats);
+
+ return implode(($pretty ? ", " : ","), $formats);
+ }
+
+ // From https://stackoverflow.com/a/31107425
+ private static function randomStr(int $length = 64, string $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'): string {
+ if ($length < 1) {
+ throw new \RangeException("Length must be a positive integer");
+ }
+ $pieces = [];
+ $max = mb_strlen($keyspace, '8bit') - 1;
+ for ($i = 0; $i < $length; ++$i) {
+ $pieces []= $keyspace[random_int(0, $max)];
+ }
+ return implode('', $pieces);
+ }
+
+
+ private static function getName($ext) {
+ global $conf;
+
+ $filename = "";
+
+ do {
+ $filename = self::randomStr(self::NAME_LENGTH).".".$ext;
+ } while (file_exists($conf["attachmentsFolder"].$filename));
+
+ return $filename;
+ }
+
+ public static function getFileExtension($file) {
+ $filenameExploded = explode(".", $file);
+ return mb_strtolower($filenameExploded[count($filenameExploded) - 1]);
+ }
+
+ public static function uploadFile(&$file, &$name) {
+ global $conf;
+
+ if (!isset($file["error"]) || is_array($file["error"])) return 1;
+
+ switch ($file["error"]) {
+ case UPLOAD_ERR_OK:
+ break;
+
+ case UPLOAD_ERR_INI_SIZE:
+ case UPLOAD_ERR_FORM_SIZE:
+ return 2;
+ break;
+
+ default:
+ return 1;
+ break;
+ }
+
+ $ext = self::getFileExtension($file["name"]);
+
+ if ($file['size'] > self::MAX_SIZE) return 2;
+ if (!in_array($ext, self::$acceptedFormats)) return 3;
+
+ $name = self::getName($ext);
+
+ return (move_uploaded_file($file["tmp_name"], $conf["attachmentsFolder"].$name) ? 0 : 1);
+ }
+
+ public static function removeFile($name) {
+ global $conf;
+
+ return unlink($conf["attachmentsFolder"].$name);
+ }
+}
diff --git a/src/inc/help.php b/src/inc/help.php
new file mode 100644
index 0000000..3fe7179
--- /dev/null
+++ b/src/inc/help.php
@@ -0,0 +1,52 @@
+<?php
+class help {
+ const PLACE_INCIDENT_FORM = 0;
+ const PLACE_VALIDATION_PAGE = 1;
+ const PLACE_REGISTRY_PAGE = 2;
+ const PLACE_EXPORT_REGISTRY_PAGE = 3;
+
+ public static $places = [0, 1, 2, 3];
+ public static $placesName = [
+ 0 => "Formulario de incidencias",
+ 1 => "Página de validación",
+ 2 => "Página de listado de registros",
+ 3 => "Página de exportar registro"
+ ];
+
+ public static function exists($place) {
+ global $con;
+
+ $splace = (int)$place;
+
+ $query = mysqli_query($con, "SELECT 1 FROM help WHERE place = $splace");
+
+ return (mysqli_num_rows($query) > 0);
+ }
+
+ public static function set($place, $url) {
+ global $con;
+
+ if (!in_array($place, self::$places)) return -1;
+ if ($url !== "" && !filter_var($url, FILTER_VALIDATE_URL)) return 1;
+
+ $splace = (int)$place;
+ $surl = db::sanitize($url);
+
+ if (self::exists($place)) return (mysqli_query($con, "UPDATE help SET url = '$surl' WHERE place = $splace LIMIT 1") ? 0 : -1);
+ else return (mysqli_query($con, "INSERT INTO help (place, url) VALUES ('$splace', '$surl')") ? 0 : -1);
+ }
+
+ public static function get($place) {
+ global $con;
+
+ if (!in_array($place, self::$places)) return false;
+ $splace = (int)$place;
+
+ $query = mysqli_query($con, "SELECT url FROM help WHERE place = $splace");
+
+ if (mysqli_num_rows($query) > 0) {
+ $url = mysqli_fetch_assoc($query)["url"];
+ return ($url === "" ? false : $url);
+ } else return false;
+ }
+}
diff --git a/src/inc/helpView.php b/src/inc/helpView.php
new file mode 100644
index 0000000..d6b642a
--- /dev/null
+++ b/src/inc/helpView.php
@@ -0,0 +1,10 @@
+<?php
+class helpView {
+ public static function renderHelpButton($place, $topRight = false, $margin = false) {
+ $url = help::get($place);
+ if ($url === false) return;
+
+ echo ($topRight ? '<div class="help-btn--top-right'.($margin ? ' help-btn--top-right-margin': '').'">' : '').'<a href="'.security::htmlsafe($url).'" target="_blank" rel="noopener noreferrer" class="mdl-button mdl-button--colored mdl-button-js mdl-button--icon mdl-js-ripple-effect" id="help'.(int)$place.'"><i class="material-icons">help_outline</i><span class="mdl-ripple"></span></a>'.($topRight ? '</div>' : '');
+ echo '<div class="mdl-tooltip" for="help'.(int)$place.'">Ayuda</div>';
+ }
+}
diff --git a/src/inc/incidents.php b/src/inc/incidents.php
new file mode 100644
index 0000000..7664fe9
--- /dev/null
+++ b/src/inc/incidents.php
@@ -0,0 +1,609 @@
+<?php
+class incidents {
+ const STARTOFDAY = 0;
+ const ENDOFDAY = 60*60*24;
+
+ const PAGINATION_LIMIT = 20;
+
+ const UPDATE_STATE_NOT_UPDATED = 0;
+ const UPDATE_STATE_UPDATED = 1;
+
+ const STATE_UNVERIFIED = 0;
+ const STATE_REJECTED = 1;
+ const STATE_SCHEDULED = 2;
+ const STATE_REGISTERED = 3;
+ const STATE_MANUALLY_INVALIDATED = 4;
+ const STATE_VALIDATED_BY_WORKER = 5;
+
+ public static $stateIcons = array(
+ 0 => "new_releases",
+ 1 => "block",
+ 2 => "schedule",
+ 3 => "check",
+ 4 => "delete_forever",
+ 5 => "verified_user"
+ );
+
+ public static $stateIconColors = array(
+ 0 => "mdl-color-text--orange",
+ 1 => "mdl-color-text--red",
+ 2 => "mdl-color-text--orange",
+ 3 => "mdl-color-text--green",
+ 4 => "mdl-color-text--red",
+ 5 => "mdl-color-text--green"
+ );
+
+ public static $stateTooltips = array(
+ 0 => "Pendiente de revisión",
+ 1 => "Rechazada al revisar",
+ 2 => "Programada",
+ 3 => "Registrada",
+ 4 => "Invalidada manualmente",
+ 5 => "Validada"
+ );
+
+ public static $statesOrderForFilters = [0, 1, 4, 2, 3, 5];
+
+ public static $invalidStates = [incidents::STATE_REJECTED, incidents::STATE_MANUALLY_INVALIDATED];
+
+ public static $cannotEditCommentsStates = [];
+ public static $cannotEditStates = [self::STATE_VALIDATED_BY_WORKER, self::STATE_REGISTERED, self::STATE_MANUALLY_INVALIDATED, self::STATE_REJECTED];
+ public static $canRemoveStates = [self::STATE_SCHEDULED];
+ public static $canInvalidateStates = [self::STATE_VALIDATED_BY_WORKER, self::STATE_REGISTERED];
+ public static $workerCanEditStates = [self::STATE_UNVERIFIED];
+ public static $workerCanRemoveStates = [self::STATE_UNVERIFIED];
+
+ public static $adminPendingWhere = "i.verified = 0 AND i.confirmedby = -1";
+ public static $workerPendingWhere = "i.workervalidated = 0";
+ public static $activeWhere = "i.verified = 1 AND i.invalidated = 0";
+ public static $notInvalidatedOrRejectedWhere = "i.invalidated = 0 AND (i.verified = 1 OR i.confirmedby = -1)";
+
+ const FILTER_TYPE_ARRAY = 0;
+ const FILTER_TYPE_INT = 1;
+ const FILTER_TYPE_STRING = 2;
+
+ public static $filters = ["begins", "ends", "types", "states", "attachments", "details", "workerdetails"];
+ public static $filtersType = [
+ "begins" => self::FILTER_TYPE_STRING,
+ "ends" => self::FILTER_TYPE_STRING,
+ "types" => self::FILTER_TYPE_ARRAY,
+ "states" => self::FILTER_TYPE_ARRAY,
+ "attachments" => self::FILTER_TYPE_ARRAY,
+ "details" => self::FILTER_TYPE_ARRAY,
+ "workerdetails" => self::FILTER_TYPE_ARRAY
+ ];
+
+ public static $filtersSwitch = ["attachments", "details", "workerdetails"];
+ public static $filtersSwitchOptions = [
+ 0 => "Sin",
+ 1 => "Con"
+ ];
+ public static $filtersSwitchHelper = [
+ "attachments" => "archivo adjunto",
+ "details" => "observaciones de un administrador",
+ "workerdetails" => "observaciones del trabajador"
+ ];
+ public static $filtersSwitchMysqlField = [
+ "attachments" => "attachments",
+ "details" => "details",
+ "workerdetails" => "workerdetails"
+ ];
+
+ public static function getTypes($showHidden = true, $isForWorker = false) {
+ global $con, $conf;
+
+ $whereConditions = [];
+ if (!$showHidden) $whereConditions[] = "hidden = 0";
+ if ($isForWorker) $whereConditions[] = "workerfill = 1";
+
+ $where = (count($whereConditions) ? " WHERE ".implode(" AND ",$whereConditions) : "");
+
+ $query = mysqli_query($con, "SELECT * FROM typesincidents".$where." ORDER BY ".($conf["debug"] ? "id ASC" : "hidden ASC, name ASC"));
+
+ $incidents = [];
+
+ while ($row = mysqli_fetch_assoc($query)) {
+ $incidents[] = $row;
+ }
+
+ return $incidents;
+ }
+
+ public static function getTypesForm($isForWorker = false) {
+ return self::getTypes(false, $isForWorker);
+ }
+
+ public static function getType($id) {
+ global $con;
+
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT * FROM typesincidents WHERE id = $sid");
+
+ if (!mysqli_num_rows($query)) return false;
+
+ return mysqli_fetch_assoc($query);
+ }
+
+ public static function addType($name, $present, $paid, $workerfill, $notifies, $autovalidates, $hidden) {
+ global $con;
+
+ $sname = db::sanitize($name);
+ $spresent = (int)$present;
+ $spaid = (int)$paid;
+ $sworkerfill = (int)$workerfill;
+ $snotifies = (int)$notifies;
+ $sautovalidates = (int)$autovalidates;
+ $shidden = (int)$hidden;
+
+ return mysqli_query($con, "INSERT INTO typesincidents (name, present, paid, workerfill, notifies, autovalidates, hidden) VALUES ('$name', $spresent, $spaid, $sworkerfill, $snotifies, $sautovalidates, $shidden)");
+ }
+
+ public static function editType($id, $name, $present, $paid, $workerfill, $notifies, $autovalidates, $hidden) {
+ global $con;
+
+ $sid = (int)$id;
+ $sname = db::sanitize($name);
+ $spresent = (int)$present;
+ $spaid = (int)$paid;
+ $sworkerfill = (int)$workerfill;
+ $snotifies = (int)$notifies;
+ $sautovalidates = (int)$autovalidates;
+ $shidden = (int)$hidden;
+
+ return mysqli_query($con, "UPDATE typesincidents SET name = '$name', present = $spresent, paid = $spaid, workerfill = $sworkerfill, notifies = $snotifies, autovalidates = $sautovalidates, hidden = $shidden WHERE id = $sid LIMIT 1");
+ }
+
+ public static function addWhereConditionsHandledBySelect($select, &$whereConditions, $alreadyFilteredDates = false) {
+ if (!$alreadyFilteredDates) {
+ if ($select["enabled"]["begins"]) $whereConditions[] = "i.day >= ".(int)common::getTimestampFromRFC3339($select["selected"]["begins"]);
+ if ($select["enabled"]["ends"]) $whereConditions[] = "i.day <= ".(int)common::getTimestampFromRFC3339($select["selected"]["ends"]);
+ }
+
+ if ($select["enabled"]["types"]) {
+ $insideconditions = [];
+ foreach ($select["selected"]["types"] as $type) {
+ $insideconditions[] = "i.type = ".(int)$type;
+ }
+ $whereConditions[] = "(".implode(" OR ", $insideconditions).")";
+ }
+
+ foreach (self::$filtersSwitch as $f) {
+ if ($select["enabled"][$f]) {
+ foreach ($select["selected"][$f] as $onoff) {
+ $mysqlField = self::$filtersSwitchMysqlField[$f];
+
+ if ($onoff == "0") $insideconditions[] = "($mysqlField IS NULL OR $mysqlField = ''".($f == "attachments" ? " OR $mysqlField = '[]'" : "").")";
+ else if ($onoff == "1") $insideconditions[] = "($mysqlField IS NOT NULL AND $mysqlField <> ''".($f == "attachments" ? " AND $mysqlField <> '[]'" : "").")";
+ }
+ if (count($insideconditions) == 1) $whereConditions[] = $insideconditions[0];
+ }
+ }
+ }
+
+ public static function incidentIsWantedAccordingToSelect($select, $incident) {
+ if ($select["enabled"]["states"]) {
+ return in_array($incident["state"], $select["selected"]["states"]);
+ }
+
+ return true;
+ }
+
+ public static function todayPage($limit = self::PAGINATION_LIMIT) {
+ global $con;
+
+ $today = (int)common::getDayTimestamp(time());
+
+ $query = mysqli_query($con, "SELECT COUNT(*) count FROM incidents i WHERE i.day > $today");
+ if ($query === false) return 0;
+
+ $row = mysqli_fetch_assoc($query);
+ $first = $row["count"];
+
+ return floor($first/(int)$limit) + 1;
+ }
+
+ public static function numRows($select = null, $onlyAdminPending = false) {
+ global $con;
+
+ $whereConditions = [];
+
+ if ($select !== null) {
+ if (!$select["showResultsPaginated"]) return 0;
+
+ self::addWhereConditionsHandledBySelect($select, $whereConditions);
+ }
+
+ if ($onlyAdminPending) $whereConditions[] = self::$adminPendingWhere;
+
+ $where = (count($whereConditions) ? " WHERE ".implode(" AND ", $whereConditions) : "");
+ $query = mysqli_query($con, "SELECT COUNT(*) count FROM incidents i".$where);
+
+ if (!mysqli_num_rows($query)) return false;
+
+ $row = mysqli_fetch_assoc($query);
+
+ return (isset($row["count"]) ? (int)$row["count"] : false);
+ }
+
+ public static function numPending() {
+ return self::numRows(null, true);
+ }
+
+ public static function getStatus(&$incident, $beginsdatetime = -1) {
+ if ($beginsdatetime == -1) {
+ $date = new DateTime();
+ $date->setTimestamp($incident["day"]);
+ $dateFormat = $date->format("Y-m-d");
+
+ $begins = new DateTime($dateFormat."T".schedules::sec2time((int)$incident["begins"], false).":00");
+ $beginsdatetime = $begins->getTimestamp();
+ }
+
+ $time = time();
+
+ if ($incident["verified"] == 0 && $incident["confirmedby"] == -1) return self::STATE_UNVERIFIED;
+ elseif ($incident["verified"] == 0) return self::STATE_REJECTED;
+ elseif ($incident["invalidated"] == 1) return self::STATE_MANUALLY_INVALIDATED;
+ elseif ($beginsdatetime >= $time) return self::STATE_SCHEDULED;
+ elseif ($incident["workervalidated"] == 1) return self::STATE_VALIDATED_BY_WORKER;
+ else return self::STATE_REGISTERED;
+ }
+
+ public static function getAttachmentsFromIncident(&$incident) {
+ if (empty($incident["attachments"]) || $incident["attachments"] === null) return [];
+
+ $json = json_decode($incident["attachments"], true);
+ if (json_last_error() !== JSON_ERROR_NONE) return false;
+
+ return $json;
+ }
+
+ public static function getAttachments($id) {
+ global $con;
+
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT attachments FROM incidents WHERE id = ".$sid);
+ if (!mysqli_num_rows($query)) return false;
+
+ $incident = mysqli_fetch_assoc($query);
+
+ return self::getAttachmentsFromIncident($incident);
+ }
+
+ public static function addAttachment($id, &$file) {
+ global $con;
+
+ $name = "";
+ $status = files::uploadFile($file, $name);
+ if ($status !== 0) return $status;
+
+ $attachments = self::getAttachments($id);
+ $attachments[] = $name;
+
+ $sid = (int)$id;
+ $srawAttachments = db::sanitize(json_encode($attachments));
+
+ return (mysqli_query($con, "UPDATE incidents SET attachments = '".$srawAttachments."' WHERE id = $sid LIMIT 1") ? 0 : 1);
+ }
+
+ public static function deleteAttachment($id, $name = "ALL") {
+ global $con;
+
+ $incident = incidents::get($id, true);
+ if ($incident === false) return false;
+
+ $attachments = incidents::getAttachmentsFromIncident($incident);
+
+ if ($attachments === false) return false;
+ if (!count($attachments)) return ($name == "ALL");
+
+ $flag = false;
+
+ foreach ($attachments as $i => $attachment) {
+ if ($attachment == $name || $name === "ALL") {
+ $flag = true;
+
+ if (!files::removeFile($attachment)) return false;
+
+ unset($attachments[$i]);
+ $attachments = array_values($attachments);
+
+ $sid = (int)$id;
+ $srawAttachments = db::sanitize(json_encode($attachments));
+ }
+ }
+
+ return ($flag ? mysqli_query($con, "UPDATE incidents SET attachments = '".$srawAttachments."' WHERE id = $sid LIMIT 1") : false);
+ }
+
+ private static function magicIncident(&$row) {
+ $row["allday"] = ($row["begins"] == self::STARTOFDAY && $row["ends"] == self::ENDOFDAY);
+ $row["updatestate"] = ($row["updatedby"] == -1 ? self::UPDATE_STATE_NOT_UPDATED : self::UPDATE_STATE_UPDATED);
+
+ $date = new DateTime();
+ $date->setTimestamp($row["day"]);
+ $dateFormat = $date->format("Y-m-d");
+
+ $begins = new DateTime($dateFormat."T".schedules::sec2time((int)$row["begins"], false).":00");
+ $row["beginsdatetime"] = $begins->getTimestamp();
+ $ends = new DateTime($dateFormat."T".schedules::sec2time((int)$row["ends"], false).":00");
+ $row["endsdatetime"] = $ends->getTimestamp();
+
+ $row["state"] = self::getStatus($row, $row["beginsdatetime"]);
+ }
+
+ public static function get($id, $magic = false) {
+ global $con, $conf;
+
+ $sid = $id;
+
+ $query = mysqli_query($con, "SELECT * FROM incidents WHERE id = $sid");
+
+ if (!mysqli_num_rows($query)) return false;
+
+ $row = mysqli_fetch_assoc($query);
+ if ($magic) self::magicIncident($row);
+
+ return $row;
+ }
+
+ public static function getAll($onlyAdminPending = false, $start = 0, $limit = self::PAGINATION_LIMIT, $worker = "ALL", $begins = null, $ends = null, $onlyWorkerPending = false, $select = false) {
+ global $con, $conf;
+
+ $whereConditions = [];
+ if ($onlyAdminPending) $whereConditions[] = self::$adminPendingWhere;
+ if ($onlyWorkerPending) {
+ $whereConditions[] = self::$workerPendingWhere;
+ $whereConditions[] = self::$activeWhere;
+ }
+ if ($worker !== "ALL") $whereConditions[] = "i.worker = ".(int)$worker;
+ if ($begins !== null && $ends !== null) {
+ $whereConditions[] = "i.day <= ".(int)$ends." AND i.day >= ".(int)$begins;
+ $filteredDates = true;
+ } else $filteredDates = false;
+
+ if ($select !== false) self::addWhereConditionsHandledBySelect($select, $whereConditions, $filteredDates);
+
+ $where = (count($whereConditions) ? " WHERE ".implode(" AND ", $whereConditions) : "");
+
+ $query = mysqli_query($con, "SELECT i.*, t.id typeid, t.name typename, t.present typepresent, t.paid typepaid, t.workerfill typeworkerfill, t.notifies typenotifies, t.hidden typehidden, p.id personid, p.name workername, w.company companyid FROM incidents i LEFT JOIN typesincidents t ON i.type = t.id LEFT JOIN workers w ON i.worker = w.id LEFT JOIN people p ON w.person = p.id".$where." ORDER BY i.day DESC".db::limitPagination($start, $limit));
+
+ $return = [];
+ while ($row = mysqli_fetch_assoc($query)) {
+ self::magicIncident($row);
+ if ($onlyWorkerPending && $row["state"] !== self::STATE_REGISTERED) continue;
+ if ($select !== false && !self::incidentIsWantedAccordingToSelect($select, $row)) continue;
+ $return[] = $row;
+ }
+
+ return $return;
+ }
+
+ public static function checkOverlap($worker, $day, $begins, $ends, $sans = 0) {
+ global $con;
+
+ $sworker = (int)$worker;
+ $sday = (int)$day;
+ $sbegins = (int)$begins;
+ $sends = (int)$ends;
+ $ssans = (int)$sans;
+
+ $query = mysqli_query($con, "SELECT * FROM incidents i WHERE begins <= $sends AND ends >= $sbegins AND day = $sday AND worker = $sworker".($sans == 0 ? "" : " AND id <> $ssans")." AND ".self::$notInvalidatedOrRejectedWhere." LIMIT 1");
+
+ return (mysqli_num_rows($query) > 0);
+ }
+
+ public static function add($worker, $type, $details, $iday, $begins, $ends, $creator = "ME", $verified = 1, $alreadyTimestamp = false, $isWorkerView = false, $sendEmail = true, $forceAutoValidate = false) {
+ global $con, $conf;
+
+ // Gets information about the worker
+ $sworker = (int)$worker;
+ $workerInfo = workers::get($sworker);
+ if ($workerInfo === false) return 1;
+
+ // Sets who is the incident creator
+ if ($creator === "ME") $creator = people::userData("id");
+ $screator = (int)$creator;
+
+ // If the user is not an admin and the person to who we are going to add the incident is not the creator of the incident, do not continue
+ if (!security::isAllowed(security::ADMIN) && $workerInfo["person"] != $creator) return 5;
+
+ // Sanitizes other incident fields
+ $stype = (int)$type;
+ $sverified = (int)$verified;
+ $sdetails = db::sanitize($details);
+
+ // Gets information about the incident type
+ $incidenttype = self::getType($stype);
+ if ($incidenttype === false) return -1;
+
+ // Gets the timestamp of the incident day
+ if ($alreadyTimestamp) {
+ $sday = (int)$iday;
+ } else {
+ $day = new DateTime($iday);
+ $sday = (int)$day->getTimestamp();
+ }
+
+ // Gets the start and end times, and checks whether they are well-formed
+ $sbegins = (int)$begins;
+ $sends = (int)$ends;
+ if ($sbegins >= $sends) return 3;
+
+ // Checks whether the incident overlaps another incident
+ if (self::checkOverlap($worker, $sday, $begins, $ends)) return 2;
+
+ // Adds the incident
+ if (!mysqli_query($con, "INSERT INTO incidents (worker, creator, type, day, begins, ends, ".($isWorkerView ? "workerdetails" : "details").", verified) VALUES ($sworker, $screator, $stype, $sday, $sbegins, $sends, '$sdetails', $sverified)")) return -1;
+
+ // If the incident type is set to autovalidate or we pass the parameter to force the autovalidation, autovalidate it
+ if (($incidenttype["autovalidates"] == 1 || $forceAutoValidate) && !validations::validateIncident(mysqli_insert_id($con), validations::METHOD_AUTOVALIDATION, "ME", false, false)) {
+ return 5;
+ }
+
+ // Bonus: check whether we should send email notifications, and if applicable send them
+ if ($sendEmail && $conf["mail"]["enabled"] && ($conf["mail"]["capabilities"]["notifyOnWorkerIncidentCreation"] || $conf["mail"]["capabilities"]["notifyOnAdminIncidentCreation"] || $conf["mail"]["capabilities"]["notifyCategoryResponsiblesOnIncidentCreation"])) {
+ $workerName = people::workerData("name", $sworker);
+
+ $to = [];
+ if (($conf["mail"]["capabilities"]["notifyOnWorkerIncidentCreation"] && $isWorkerView) || ($conf["mail"]["capabilities"]["notifyOnAdminIncidentCreation"] && !$isWorkerView)) {
+ $to[] = array("email" => $conf["mail"]["adminEmail"]);
+ }
+
+ if ($conf["mail"]["capabilities"]["notifyCategoryResponsiblesOnIncidentCreation"] && $incidenttype["notifies"] == 1) {
+ $categoryid = people::workerData("category", $sworker);
+ $category = categories::get($categoryid);
+ if ($category === false) return 0;
+
+ $emails = json_decode($category["emails"], true);
+ if (json_last_error() === JSON_ERROR_NONE) {
+ foreach ($emails as $email) {
+ $to[] = array("email" => $email);
+ }
+ }
+ }
+
+ if (!count($to)) return 0;
+
+ $subject = "Incidencia del tipo \"".security::htmlsafe($incidenttype["name"])."\" creada para ".security::htmlsafe($workerName)." el ".strftime("%d %b %Y", $sday);
+ $body = mail::bodyTemplate("<p>Hola,</p>
+ <p>Este es un mensaje automático para avisarte de que ".security::htmlsafe(people::userData("name"))." ha introducido la siguiente incidencia en el sistema de registro horario:</p>
+ <ul>
+ <li><b>Trabajador:</b> ".security::htmlsafe($workerName)."</li>
+ <li><b>Motivo:</b> ".security::htmlsafe($incidenttype["name"])."</li>
+ <li><b>Fecha:</b> ".strftime("%d %b %Y", $sday)." ".(($sbegins == 0 && $sends == self::ENDOFDAY) ? "(todo el día)": schedules::sec2time($sbegins)."-".schedules::sec2time($sends))."</li>".
+ (!empty($details) ? "<li><b>Observaciones:</b> <span style='white-space: pre-wrap;'>".security::htmlsafe($details)."</span></li>" : "").
+ "</ul>
+ <p style='font-size: 11px;'>Has recibido este mensaje porque estás configurado como persona responsable de la categoría a la que pertenece este trabajador o eres el administrador del sistema.</p>");
+
+ return (mail::send($to, [], $subject, $body) ? 0 : 4);
+ }
+
+ return 0;
+ }
+
+ public static function edit($id, $type, $iday, $begins, $ends, $updatedby = "ME") {
+ global $con;
+
+ $sid = (int)$id;
+
+ $incident = incidents::get($id);
+ if ($incident === false) return 1;
+
+ $stype = (int)$type;
+ if ($updatedby === "ME") $updatedby = people::userData("id");
+ $supdatedby = (int)$updatedby;
+
+ $day = new DateTime($iday);
+ $sday = (int)$day->getTimestamp();
+
+ $sbegins = (int)$begins;
+ $sends = (int)$ends;
+ if ($sbegins >= $sends) return 3;
+ if (self::checkOverlap($incident["worker"], $sday, $begins, $ends, $id)) return 2;
+
+ return (mysqli_query($con, "UPDATE incidents SET type = $stype, day = $sday, begins = $sbegins, ends = $sends, updatedby = $supdatedby, workervalidated = 0, workervalidation = '' WHERE id = $sid LIMIT 1") ? 0 : -1);
+ }
+
+ public static function editDetails($id, $details, $updatedby = "ME") {
+ global $con;
+
+ $sid = (int)$id;
+ $sdetails = db::sanitize($details);
+ if ($updatedby === "ME") $updatedby = people::userData("id");
+ $supdatedby = (int)$updatedby;
+
+ $incident = self::get($sid);
+ if ($incident === false) return -1;
+
+ $status = self::getStatus($incident);
+ if (in_array($status, self::$cannotEditCommentsStates)) return 1;
+
+ return (mysqli_query($con, "UPDATE incidents SET details = '$sdetails', updatedby = '$supdatedby' WHERE id = $sid LIMIT 1") ? 0 : -1);
+ }
+
+ public static function verify($id, $value, $confirmedby = "ME") {
+ global $con, $conf;
+
+ $sid = (int)$id;
+ $svalue = ($value == 1 ? 1 : 0);
+ if ($confirmedby === "ME") $confirmedby = people::userData("id");
+ $sconfirmedby = (int)$confirmedby;
+
+ $incident = incidents::get($id);
+ if ($incident === false) return false;
+
+ $state = incidents::getStatus($incident);
+ if ($state != incidents::STATE_UNVERIFIED) return false;
+
+ if (!mysqli_query($con, "UPDATE incidents SET verified = $svalue, confirmedby = $sconfirmedby WHERE id = $sid LIMIT 1")) return false;
+
+ if ($conf["mail"]["enabled"] && $conf["mail"]["capabilities"]["notifyWorkerOnIncidentDecision"]) {
+ $workerEmail = people::workerData("email", $incident["worker"]);
+ if ($workerEmail !== false && !empty($workerEmail)) {
+ $workerName = people::workerData("name", $incident["worker"]);
+
+ $to = [array(
+ "email" => $workerEmail,
+ "name" => $workerName
+ )];
+ $subject = "Incidencia del ".strftime("%d %b %Y", $incident["day"])." ".($value == 1 ? "verificada" : "rechazada");
+ $body = mail::bodyTemplate("<p>Bienvenido ".security::htmlsafe($workerName).",</p>
+ <p>Este es un mensaje automático para avisarte de que la incidencia que introduciste para el día ".strftime("%d de %B de %Y", $incident["day"])." ha sido ".($value == 1 ? "aceptada" : "rechazada").".</p><p>Puedes ver el estado de todas tus incidencias en el <a href='".security::htmlsafe($conf["fullPath"])."'>aplicativo web</a>.</p>");
+
+ mail::send($to, [], $subject, $body);
+ }
+ }
+
+ return true;
+ }
+
+ public static function remove($id) {
+ global $con;
+
+ $sid = (int)$id;
+
+ $incident = incidents::get($id);
+ if ($incident === false) return false;
+
+ if (!self::deleteAttachment($id, "ALL")) return false;
+
+ return mysqli_query($con, "DELETE FROM incidents WHERE id = $sid LIMIT 1");
+ }
+
+ public static function invalidate($id, $updatedby = "ME") {
+ global $con;
+
+ $sid = (int)$id;
+ if ($updatedby === "ME") $updatedby = people::userData("id");
+ $supdatedby = (int)$updatedby;
+
+ $incident = incidents::get($id);
+ if ($incident === false) return false;
+
+ $state = incidents::getStatus($incident);
+ if (!in_array($state, self::$canInvalidateStates)) return false;
+
+ return mysqli_query($con, "UPDATE incidents SET invalidated = 1, updatedby = $supdatedby WHERE id = $sid LIMIT 1");
+ }
+
+ public static function checkIncidentIsFromPerson($incident, $person = "ME", $boolean = false) {
+ global $con;
+
+ if ($person == "ME") $person = people::userData("id");
+
+ $sincident = (int)$incident;
+ $sperson = (int)$person;
+
+ $query = mysqli_query($con, "SELECT i.id FROM incidents i INNER JOIN workers w ON i.worker = w.id INNER JOIN people p ON w.person = p.id WHERE p.id = $sperson AND i.id = $sincident LIMIT 1");
+
+ if (!mysqli_num_rows($query)) {
+ if ($boolean) return false;
+ security::denyUseMethod(security::METHOD_NOTFOUND);
+ }
+
+ return true;
+ }
+}
diff --git a/src/inc/incidentsView.php b/src/inc/incidentsView.php
new file mode 100644
index 0000000..ca646cb
--- /dev/null
+++ b/src/inc/incidentsView.php
@@ -0,0 +1,477 @@
+<?php
+class incidentsView {
+ public static $limitOptions = [10, 15, 20, 30, 40, 50];
+
+ public static $incidentsMsgs = [
+ ["added", "Se ha añadido la incidencia correctamente."],
+ ["modified", "Se ha modificado la incidencia correctamente."],
+ ["removed", "Se ha eliminado la incidencia correctamente."],
+ ["invalidated", "Se ha invalidado la incidencia correctamente."],
+ ["empty", "Faltan datos por introducir en el formulario."],
+ ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+ ["cannotmodify", "No se pueden modificar la incidencia porque ya ha sido registrada o se ha invalidado manualmente."],
+ ["verified1", "Se ha verificado la incidencia correctamente."],
+ ["verified0", "Se ha rechazado la incidencia correctamente."],
+ ["overlap", "La incidencia no se ha añadido porque se solapa con otra incidencia del mismo trabajador."],
+ ["order", "La hora de inicio debe ser anterior a la hora de fin."],
+ ["addedemailnotsent", "La incidencia se ha añadido, pero no se ha podido enviar un correo de notificación a los responsables de la categoría del trabajador. Por favor, notifica a estas personas manualmente."],
+ ["filesize", "El tamaño del archivo adjunto es demasiado grande (el límite es de ".files::READABLE_MAX_SIZE.")"],
+ ["filetype", "El formato del archivo no está soportado."],
+ ["attachmentadded", "Se ha añadido el archivo adjunto correctamente."],
+ ["attachmentdeleted", "Se ha eliminado el archivo adjunto correctamente."],
+ ["addedrecurring", "Se han añadido todas las incidencias pertinentes correctamente."],
+ ["unexpectedrecurring", "Ha habido algún problema añadiendo alguna(s) o todas las incidencias que se tenían que crear."],
+ ["addednotautovalidated", "La incidencia se ha añadido, pero no se ha podido autovalidar."],
+ ["deleteincidentsbulksuccess", "Las incidencias se han eliminado/invalidado correctamente."],
+ ["deleteincidentsbulkpartialsuccess", "Algunas incidencias (o todas) no se han podido eliminar/invalidar. Por favor, comprueba el resultado de la acción."]
+ ];
+
+ public static function renderIncidents(&$incidents, &$companies, $scrollable = false, $showPersonAndCompany = true, $isForWorker = false, $isForValidationView = false, $isForMassEdit = false, $continueUrl = "incidents.php") {
+ global $conf, $renderIncidentsAutoIncremental;
+ if (!isset($renderIncidentsAutoIncremental)) $renderIncidentsAutoIncremental = 0;
+ $menu = "";
+
+ $safeContinueUrl = security::htmlsafe(urlencode($continueUrl));
+
+ if ($isForMassEdit) {
+ ?>
+ <div class="left-actions">
+ <button id="deleteincidentsbulk" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect"><i class="material-icons icon">delete</i></button>
+ </div>
+ <?php
+ }
+ ?>
+ <div class="mdl-shadow--2dp overflow-wrapper incidents-wrapper<?=($scrollable ? " incidents-wrapper--scrollable" : "")?>">
+ <table class="incidents">
+ <?php
+ if ($isForValidationView || $isForMassEdit) {
+ ?>
+ <tr class="artificial-height">
+ <?php if ($conf["debug"]) echo "<td></td>"; ?>
+ <td class="icon-cell has-checkbox">
+ <label for="checkboxall_i_<?=$renderIncidentsAutoIncremental?>" class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" data-check-all="true">
+ <input type="checkbox" id="checkboxall_i_<?=$renderIncidentsAutoIncremental?>" class="mdl-checkbox__input" autocomplete="off">
+ </label>
+ </td>
+ <?php if ($conf["debug"]) echo "<td></td>"; ?>
+ <td></td>
+ <?php if (!$isForValidationView) echo "<td></td>"; ?>
+ <td></td>
+ <?php if ($showPersonAndCompany) echo "<td></td><td></td>"; ?>
+ <td></td>
+ </tr>
+ <?php
+ }
+
+ foreach ($incidents as $incident) {
+ $id = (int)$incident["id"]."_".(int)$renderIncidentsAutoIncremental;
+ $canEdit = (!$isForWorker && !in_array($incident["state"], incidents::$cannotEditStates)) || ($isForWorker && in_array($incident["state"], incidents::$workerCanEditStates));
+ $canRemove = (!$isForWorker && in_array($incident["state"], incidents::$canRemoveStates)) || ($isForWorker && in_array($incident["state"], incidents::$workerCanRemoveStates));
+ $canInvalidate = !$isForWorker && in_array($incident["state"], incidents::$canInvalidateStates);
+ $attachments = count(incidents::getAttachmentsFromIncident($incident));
+ ?>
+ <tr<?=(in_array($incident["state"], incidents::$invalidStates) ? ' class="mdl-color-text--grey-700 line-through"' : '')?>>
+ <?php if ($conf["debug"]) { ?><td><?=(int)$incident["id"]?></td><?php } ?>
+ <?php
+ if ($isForValidationView || $isForMassEdit) {
+ ?>
+ <td class="icon-cell has-checkbox">
+ <label for="checkbox_i_<?=$id?>" class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect">
+ <input type="checkbox" id="checkbox_i_<?=$id?>" data-incident="<?=(int)$incident["id"]?>" class="mdl-checkbox__input"<?=($isForMassEdit && !in_array($incident["state"], incidents::$canRemoveStates) && !in_array($incident["state"], incidents::$canInvalidateStates) ? " disabled" : "")?> autocomplete="off">
+ </label>
+ </td>
+ <?php
+ }
+
+ if (!$isForValidationView) {
+ ?>
+ <td class="icon-cell">
+ <i id="state<?=$id?>" class="material-icons <?=security::htmlsafe(incidents::$stateIconColors[$incident["state"]])?>"><?=security::htmlsafe(incidents::$stateIcons[$incident["state"]])?></i>
+ </td>
+ <?php
+ visual::addTooltip("state".$id, security::htmlsafe(incidents::$stateTooltips[$incident["state"]]));
+ }
+ ?>
+ <td class="can-strike"><?=security::htmlsafe($incident["typename"])?></td>
+ <?php if ($showPersonAndCompany) {
+ ?>
+ <td class="can-strike"><span data-dyndialog-href="dynamic/user.php?id=<?=(int)$incident["personid"]?>"><?=security::htmlsafe($incident["workername"])?></span></td>
+ <td class="can-strike"><?=security::htmlsafe($companies[$incident["companyid"]])?></td>
+ <?php
+ }
+ ?>
+ <td class="can-strike"><?=strftime("%d %b %Y", $incident["day"])." ".security::htmlsafe($incident["allday"] ? "(todo el día)" : schedules::sec2time($incident["begins"])."-".schedules::sec2time($incident["ends"]))?></td>
+ <td>
+ <a href="dynamic/editincidentcomment.php?id=<?=(int)$incident["id"]?>&continue=<?=$safeContinueUrl?>" data-dyndialog-href="dynamic/editincidentcomment.php?id=<?=(int)$incident["id"]?>&continue=<?=$safeContinueUrl?>" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect" title="Ver/editar las observaciones"><i class="material-icons icon"><?=(empty($incident["details"]) && empty($incident["workerdetails"]) ? "mode_" : "")?>comment</i></a>
+ <span<?=($attachments > 0 ? ' class="mdl-badge mdl-badge--overlap" data-badge="'.$attachments.'"' : '')?>><a href="dynamic/incidentattachments.php?id=<?=(int)$incident["id"]?>&continue=<?=$safeContinueUrl?>" data-dyndialog-href="dynamic/incidentattachments.php?id=<?=(int)$incident["id"]?>&continue=<?=$safeContinueUrl?>" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect" title="Ver/gestionar los archivos adjuntos"><i class="material-icons icon">attach_file</i></a></span>
+ <button id="actions<?=$id?>" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect custom-actions-btn"><i class="material-icons icon">more_vert</i></button>
+ <?php
+ $menu .= '<ul class="mdl-menu mdl-menu--unaligned mdl-js-menu mdl-js-ripple-effect" for="actions'.$id.'">';
+
+ if ($canEdit) $menu .= '<a href="dynamic/editincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'" data-dyndialog-href="dynamic/editincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'"><li class="mdl-menu__item">Editar</li></a>';
+
+ if ($canRemove) $menu .= '<a href="dynamic/deleteincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'" data-dyndialog-href="dynamic/deleteincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'"><li class="mdl-menu__item">Eliminar</li></a>';
+
+ if ($canInvalidate) $menu .= '<a href="dynamic/invalidateincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'" data-dyndialog-href="dynamic/invalidateincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'"><li class="mdl-menu__item">Invalidar</li></a>';
+
+ $menu .= '<a href="dynamic/authorsincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'" data-dyndialog-href="dynamic/authorsincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'"><li class="mdl-menu__item">Autoría</li></a></ul>';
+
+ if ($incident["state"] == incidents::STATE_UNVERIFIED && !$isForWorker) {
+ ?>
+ <form action="doverifyincident.php" method="POST" class="verification-actions">
+ <input type="hidden" name="id" value="<?=(int)$incident["id"]?>">
+ <?php visual::addContinueInput($continueUrl); ?>
+ <button name="value" value="1" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect"><i class="material-icons mdl-color-text--green">check</i></button><button name="value" value="0" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect"><i class="material-icons mdl-color-text--red">close</i></button>
+ </form>
+ <?php
+ }
+ ?>
+ </td>
+ </tr>
+ <?php
+ $renderIncidentsAutoIncremental++;
+ }
+ ?>
+ </table>
+ <?php echo $menu; ?>
+ </div>
+ <?php
+ }
+
+ public static function renderIncidentForm(&$workers, $valueFunction, $textFunction, &$companies, $isForWorker = false, $recurrent = false, $continueUrl = false) {
+ $prefix = ($recurrent ? "recurring" : "");
+ ?>
+ <script>
+ window.addEventListener("load", e1 => {
+ document.getElementById("<?=$prefix?>allday").addEventListener("change", e => {
+ var partialtime = document.getElementById("<?=$prefix?>partialtime");
+ if (e.target.checked) {
+ partialtime.classList.add("notvisible");
+ } else {
+ partialtime.classList.remove("notvisible");
+ }
+ });
+ <?php
+ if ($recurrent) {
+ ?>
+ var defaultFields = [
+ {
+ "name": "day",
+ "min": 0,
+ "max": 4
+ },
+ {
+ "name": "type",
+ "min": 1,
+ "max": 2
+ }
+ ];
+
+ defaultFields.forEach(field => {
+ for (var i = field.min; i <= field.max; i++) {
+ document.querySelector("[for=\""+field.name+"-"+i+"\"]").MaterialCheckbox.check();
+
+ var checkbox = document.getElementById(field.name+"-"+i);
+ checkbox.checked = true;
+ if ("createEvent" in document) {
+ var evt = document.createEvent("HTMLEvents");
+ evt.initEvent("change", false, true);
+ checkbox.dispatchEvent(evt);
+ }
+ }
+ });
+ <?php
+ }
+ ?>
+ });
+ </script>
+ <dialog class="mdl-dialog" id="add<?=$prefix?>incident">
+ <form action="doadd<?=$prefix?>incident.php" method="POST" autocomplete="off">
+ <?php
+ if ($continueUrl !== false) visual::addContinueInput($continueUrl);
+ helpView::renderHelpButton(help::PLACE_INCIDENT_FORM, true, true);
+ ?>
+ <h4 class="mdl-dialog__title">Añade una incidencia<?=($recurrent ? " recurrente" : "")?></h4>
+ <div class="mdl-dialog__content">
+ <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+ <select name="worker" id="<?=$prefix?>worker" class="mdlext-selectfield__select" data-required>
+ <option></option>
+ <?php
+ foreach ($workers as $worker) {
+ if ($worker["hidden"] == 1) continue;
+ echo '<option value="'.security::htmlsafe($valueFunction($worker, $companies)).'">'.security::htmlsafe($textFunction($worker, $companies)).'</option>';
+ }
+ ?>
+ </select>
+ <label for="<?=$prefix?>worker" class="mdlext-selectfield__label">Trabajador</label>
+ </div>
+ <br>
+ <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+ <select name="type" id="<?=$prefix?>type" class="mdlext-selectfield__select" data-required>
+ <option></option>
+ <?php
+ foreach (incidents::getTypesForm($isForWorker) as $i) {
+ echo '<option value="'.(int)$i["id"].'">'.security::htmlsafe($i["name"]).'</option>';
+ }
+ ?>
+ </select>
+ <label for="<?=$prefix?>type" class="mdlext-selectfield__label">Tipo</label>
+ </div>
+
+ <?php
+ if ($isForWorker) {
+ echo "<p>Para usar un tipo de incidencia que no esté en la lista, debes ponerte en contacto con RRHH para que rellenen ellos la incidencia manualmente.</p>";
+ }
+ ?>
+
+ <?php
+ if ($recurrent) {
+ ?>
+ <h5>Recurrencia</h5>
+ <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+ <input class="mdl-textfield__input" type="date" name="firstday" id="<?=$prefix?>firstday" autocomplete="off" data-required>
+ <label class="mdl-textfield__label always-focused" for="<?=$prefix?>firstday">Día de inicio</label>
+ </div>
+ <br>
+ <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+ <input class="mdl-textfield__input" type="date" name="lastday" id="<?=$prefix?>lastday" autocomplete="off" data-required>
+ <label class="mdl-textfield__label always-focused" for="<?=$prefix?>lastday">Día de fin</label>
+ </div>
+ <br>
+ <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+ <div id="dayMenu" class="mdlext-selectfield__select mdl-custom-selectfield__select" tabindex="0">-</div>
+ <ul class="mdl-menu mdl-menu--bottom mdl-js-menu mdl-custom-multiselect mdl-custom-multiselect-js" for="dayMenu">
+ <?php
+ foreach (calendars::$days as $id => $day) {
+ ?>
+ <li class="mdl-menu__item mdl-custom-multiselect__item">
+ <label class="mdl-checkbox mdl-js-checkbox" for="day-<?=(int)$id?>">
+ <input type="checkbox" id="day-<?=(int)$id?>" name="day[]" value="<?=(int)$id?>" data-value="<?=(int)$id?>" class="mdl-checkbox__input">
+ <span class="mdl-checkbox__label"><?=security::htmlsafe($day)?></span>
+ </label>
+ </li>
+ <?php
+ }
+ ?>
+ </ul>
+ <label for="day" class="mdlext-selectfield__label always-focused mdl-color-text--primary">Día de la semana</label>
+ </div>
+ <br>
+ <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+ <div id="dayType" class="mdlext-selectfield__select mdl-custom-selectfield__select" tabindex="0">-</div>
+ <ul class="mdl-menu mdl-menu--bottom mdl-js-menu mdl-custom-multiselect mdl-custom-multiselect-js" for="dayType">
+ <?php
+ foreach (calendars::$types as $id => $type) {
+ if ($id == calendars::TYPE_FESTIU) continue;
+ ?>
+ <li class="mdl-menu__item mdl-custom-multiselect__item">
+ <label class="mdl-checkbox mdl-js-checkbox" for="type-<?=(int)$id?>">
+ <input type="checkbox" id="type-<?=(int)$id?>" name="daytype[]" value="<?=(int)$id?>" data-value="<?=(int)$id?>" class="mdl-checkbox__input">
+ <span class="mdl-checkbox__label"><?=security::htmlsafe($type)?></span>
+ </label>
+ </li>
+ <?php
+ }
+ ?>
+ </ul>
+ <label for="day" class="mdlext-selectfield__label always-focused mdl-color-text--primary">Tipo de día</label>
+ </div>
+ <h5>Afectación</h5>
+ <?php
+ } else {
+ ?>
+ <h5>Afectación</h5>
+ <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+ <input class="mdl-textfield__input" type="date" name="day" id="<?=$prefix?>day" autocomplete="off" data-required>
+ <label class="mdl-textfield__label always-focused" for="<?=$prefix?>day">Día</label>
+ </div>
+ <br>
+ <?php
+ }
+ ?>
+ <p>
+ <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="<?=$prefix?>allday">
+ <input type="checkbox" id="<?=$prefix?>allday" name="allday" value="1" class="mdl-switch__input">
+ <span class="mdl-switch__label">Día entero</span>
+ </label>
+ </p>
+ <div id="<?=$prefix?>partialtime">De <input type="time" name="begins"> a <input type="time" name="ends"></div>
+
+ <h5>Detalles adicionales</h5>
+ <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+ <textarea class="mdl-textfield__input" name="details" id="<?=$prefix?>details"></textarea>
+ <label class="mdl-textfield__label" for="<?=$prefix?>details">Observaciones (opcional)</label>
+ </div>
+ <?php
+ if (!$isForWorker && !$recurrent) {
+ ?>
+ <p>
+ <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="<?=$prefix?>autoverify">
+ <input type="checkbox" id="<?=$prefix?>autoverify" name="autoverify" value="1" class="mdl-switch__input" checked>
+ <span class="mdl-switch__label">Saltar la cola de revisión</span>
+ </label>
+ </p>
+ <?php
+ }
+
+ if (!$isForWorker) echo "<p>Las observaciones aparecerán en los PDFs que se exporten.</p>";
+ else echo "<p>Las observaciones serán únicamente visibles para los administradores del sistema.</p><p>Al añadir la incidencia, se guardará tu <a href=\"https://help.gnome.org/users/gnome-help/stable/net-what-is-ip-address.html.es\" target=\"_blank\" rel=\"noopener noreferrer\">dirección IP</a> y la fecha y hora actual para autovalidar la incidencia.</p>";
+ if (!$recurrent) echo '<p>Después de crear la incidencia podrás añadir archivos adjuntos haciendo clic en el botón <i class="material-icons" style="vertical-align: middle;">attach_file</i>.</p>';
+ ?>
+ </div>
+ <div class="mdl-dialog__actions">
+ <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Añadir</button>
+ <button onclick="event.preventDefault(); document.querySelector('#add<?=$prefix?>incident').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+ </div>
+ </form>
+ </dialog>
+ <?php
+ }
+
+ public static function renderFilterDialog($select) {
+ global $_GET;
+ ?>
+ <style>
+ #filter {
+ max-width: 300px;
+ width: auto;
+ }
+
+ #filter .mdl-checkbox {
+ height: auto;
+ }
+ </style>
+ <dialog class="mdl-dialog" id="filter">
+ <form action="incidents.php" method="GET" enctype="multipart/form-data">
+ <h4 class="mdl-dialog__title">Filtrar lista</h4>
+ <div class="mdl-dialog__content">
+ <h5>Por fecha</h5>
+ <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+ <input class="mdl-textfield__input" type="date" name="begins" id="begins" autocomplete="off" <?=$select["enabled"]["begins"] ? " value=\"".security::htmlsafe($select["selected"]["begins"])."\"" : ""?>>
+ <label class="mdl-textfield__label always-focused" for="begins">Fecha inicio (opcional)</label>
+ </div>
+ <br>
+ <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+ <input class="mdl-textfield__input" type="date" name="ends" id="ends" autocomplete="off" <?=$select["enabled"]["ends"] ? " value=\"".security::htmlsafe($select["selected"]["ends"])."\"" : ""?>>
+ <label class="mdl-textfield__label always-focused" for="ends">Fecha fin (opcional)</label>
+ </div>
+ <h5>Por estado <i id="tt_incidentstatusnotpaginated" class="material-icons help">info</i></h5>
+ <div class="mdl-tooltip" for="tt_incidentstatusnotpaginated">Al filtrar por estado, la página de resultados no estará paginada y saldrán todos los resultados en una misma página. La acción podría consumir bastantes recursos y tomar más tiempo del habitual.</div>
+ <?php
+ foreach (incidents::$statesOrderForFilters as $id) {
+ ?>
+ <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="state<?=(int)$id?>">
+ <input type="checkbox" id="state<?=(int)$id?>" name="states[<?=(int)$id?>]" class="mdl-checkbox__input" value="1"<?=(($select["enabled"]["states"] && in_array($id, $select["selected"]["states"])) ? " checked" : "")?>>
+ <span class="mdl-checkbox__label"><?=security::htmlsafe(incidents::$stateTooltips[$id])?></span>
+ </label>
+ <?php
+ }
+ ?>
+ <h5>Por tipo</h5>
+ <?php
+ $types = incidents::getTypes();
+ if ($types !== false) {
+ foreach ($types as $type) {
+ ?>
+ <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="type<?=(int)$type["id"]?>">
+ <input type="checkbox" id="type<?=(int)$type["id"]?>" name="types[<?=(int)$type["id"]?>]" class="mdl-checkbox__input" value="1"<?=(($select["enabled"]["types"] && in_array($type["id"], $select["selected"]["types"])) ? " checked" : "")?>>
+ <span class="mdl-checkbox__label"><?=security::htmlsafe($type["name"])?></span>
+ </label>
+ <?php
+ }
+ }
+ ?>
+ <h5>Miscelánea</h5>
+ <?php
+ foreach (incidents::$filtersSwitchHelper as $f => $helper) {
+ foreach (incidents::$filtersSwitchOptions as $value => $option) {
+ ?>
+ <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="<?=security::htmlsafe($f).(int)$value?>">
+ <input type="checkbox" id="<?=security::htmlsafe($f).(int)$value?>" name="<?=security::htmlsafe($f)?>[<?=(int)$value?>]" class="mdl-checkbox__input" value="1"<?=(($select["enabled"][$f] && in_array($value, $select["selected"][$f])) ? " checked" : "")?>>
+ <span class="mdl-checkbox__label"><?=security::htmlsafe($option." ".$helper)?></span>
+ </label>
+ <?php
+ }
+ }
+ ?>
+ </div>
+ <div class="mdl-dialog__actions">
+ <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Filtrar</button>
+ <button onclick="event.preventDefault(); document.querySelector('#filter').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+ </div>
+ </form>
+ </dialog>
+ <?php
+ }
+
+ public static function buildSelect() {
+ global $select, $_GET;
+
+ $select = array("showPendingQueue" => false, "isAnyEnabled" => false, "pageUrl" => "", "pageUrlHasParameters" => false, "showResultsPaginated" => true, "enabled" => [], "selected" => []);
+
+ $parameters = [];
+
+ foreach (incidents::$filters as $f) {
+ $fType = incidents::$filtersType[$f];
+
+ switch ($fType) {
+ case incidents::FILTER_TYPE_ARRAY:
+ $select["enabled"][$f] = isset($_GET[$f]);
+ break;
+
+ case incidents::FILTER_TYPE_INT:
+ $select["enabled"][$f] = isset($_GET[$f]) && $_GET[$f] !== "";
+ break;
+
+ case incidents::FILTER_TYPE_STRING:
+ $select["enabled"][$f] = isset($_GET[$f]) && !empty($_GET[$f]);
+ break;
+ }
+
+ if ($select["enabled"][$f]) {
+ switch ($fType) {
+ case incidents::FILTER_TYPE_ARRAY:
+ $select["selected"][$f] = (isset($_GET[$f]) ? array_keys($_GET[$f]) : []);
+ foreach ($select["selected"][$f] as $value) {
+ $parameters[] = urlencode($f)."[".urlencode($value)."]=1";
+ }
+ break;
+
+ case incidents::FILTER_TYPE_INT:
+ $select["selected"][$f] = (int)$_GET[$f];
+ $parameters[] = urlencode($f)."=".(int)$_GET[$f];
+ break;
+
+ case incidents::FILTER_TYPE_STRING:
+ $select["selected"][$f] = $_GET[$f];
+ $parameters[] = urlencode($f)."=".urlencode($_GET[$f]);
+ break;
+ }
+ }
+ }
+
+ foreach ($select["enabled"] as $enabled) {
+ if ($enabled) {
+ $select["isAnyEnabled"] = true;
+ break;
+ }
+ }
+
+ if (!$select["isAnyEnabled"] || (isset($_GET["forceQueue"]) && $_GET["forceQueue"] == "1")) $select["showPendingQueue"] = true;
+
+ $select["pageUrlHasParameters"] = (count($parameters) > 0);
+ $select["pageUrl"] = "incidents.php".($select["pageUrlHasParameters"] ? "?".implode("&", $parameters) : "");
+ $select["showResultsPaginated"] = !$select["enabled"]["states"];
+ }
+
+ public static function handleIncidentShortcuts() {
+ global $_GET;
+ if (isset($_GET["goTo"])) {
+ switch ($_GET["goTo"]) {
+ case "today":
+ security::go("incidents.php?page=".(int)incidents::todayPage());
+ break;
+ }
+ }
+ }
+}
diff --git a/src/inc/intervals.php b/src/inc/intervals.php
new file mode 100644
index 0000000..10c31f7
--- /dev/null
+++ b/src/inc/intervals.php
@@ -0,0 +1,32 @@
+<?php
+class intervals {
+ public static function wellFormed($i) {
+ return (isset($i[0]) && isset($i[1]) && $i[0] <= $i[1]);
+ }
+
+ public static function measure($i) {
+ return ($i[1] - $i[0]);
+ }
+
+ // Does A overlap B? (with "$open = true" meaning [0, 100] does not overlap [100, 200])
+ public static function overlaps($a, $b, $open = true) {
+ return ($open ? ($a[0] < $b[1] && $a[1] > $b[0]) : ($a[0] <= $b[1] && $a[1] >= $b[0]));
+ }
+
+ // Is A inside of B?
+ public static function isSubset($a, $b) {
+ return ($a[0] >= $b[0] && $a[1] <= $b[1]);
+ }
+
+ // Intersect A and B and return the corresponding interval
+ public static function intersect($a, $b) {
+ $int = [max($a[0], $b[0]), min($a[1], $b[1])];
+
+ return (self::wellFormed($int) ? $int : false);
+ }
+
+ // Return measure of the intersection
+ public static function measureIntersection($a, $b) {
+ return self::measure(self::intersect($a, $b));
+ }
+}
diff --git a/src/inc/listings.php b/src/inc/listings.php
new file mode 100644
index 0000000..b3aa258
--- /dev/null
+++ b/src/inc/listings.php
@@ -0,0 +1,102 @@
+<?php
+class listings {
+ public static function renderFilterDialog($form, $select) {
+ global $_GET;
+ ?>
+ <dialog class="mdl-dialog" id="filter">
+ <form action="<?=$form?>" method="GET" enctype="multipart/form-data">
+ <h4 class="mdl-dialog__title">Filtrar lista</h4>
+ <div class="mdl-dialog__content">
+ <h5>Categorías</h5>
+ <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="cat-1">
+ <input type="checkbox" id="cat-1" name="categories[-1]" class="mdl-checkbox__input" value="1"<?=(($select["enabled"]["categories"] && in_array(-1, $select["selected"]["categories"])) ? " checked" : "")?>>
+ <span class="mdl-checkbox__label">Sin categoría</span>
+ </label>
+ <?php
+ foreach (categories::getAll(false, false, true) as $c) {
+ $haschilds = (count($c["childs"]) > 0);
+ if ($haschilds) {
+ $subcategories_arr = [];
+ foreach ($c["childs"] as $child) {
+ $subcategories_arr[] = "“".security::htmlsafe($child["name"])."”";
+ }
+ $subcategories = implode(", ", $subcategories_arr);
+ }
+ ?>
+ <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="cat<?=(int)$c["id"]?>">
+ <input type="checkbox" id="cat<?=(int)$c["id"]?>" name="categories[<?=(int)$c["id"]?>]" class="mdl-checkbox__input" value="1"<?=(($select["enabled"]["categories"] && in_array($c["id"], $select["selected"]["categories"])) ? " checked" : "")?>>
+ <span class="mdl-checkbox__label"><?=security::htmlsafe($c["name"])?> <?php if ($haschilds) { ?><i id="haschilds<?=(int)$c["id"]?>" class="material-icons help">info</i><?php } ?></span>
+ </label>
+ <?php
+ if ($haschilds) {
+ ?>
+ <div class="mdl-tooltip" for="haschilds<?=(int)$c["id"]?>">Esta categoría incluye la<?=(count($c["childs"]) == 1 ? "" : "s")?> subcategoría<?=(count($c["childs"]) == 1 ? "" : "s")?> <?=$subcategories?></div>
+ <?php
+ }
+ }
+ ?>
+ <h5>Empresas</h5>
+ <?php
+ foreach (companies::getAll() as $id => $company) {
+ ?>
+ <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="comp<?=(int)$id?>">
+ <input type="checkbox" id="comp<?=(int)$id?>" name="companies[<?=(int)$id?>]" class="mdl-checkbox__input" value="1"<?=(($select["enabled"]["companies"] && in_array($id, $select["selected"]["companies"])) ? " checked" : "")?>>
+ <span class="mdl-checkbox__label"><?=security::htmlsafe($company)?></span>
+ </label>
+ <?php
+ }
+ ?>
+ <h5>Tipos de usuario</h5>
+ <?php
+ foreach (security::$types as $id => $type) {
+ ?>
+ <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="type<?=(int)$id?>">
+ <input type="checkbox" id="type<?=(int)$id?>" name="types[<?=(int)$id?>]" class="mdl-checkbox__input" value="1"<?=(($select["enabled"]["types"] && in_array($id, $select["selected"]["types"])) ? " checked" : "")?>>
+ <span class="mdl-checkbox__label"><?=security::htmlsafe($type)?></span>
+ </label>
+ <?php
+ }
+
+ if ($form == "workers.php") {
+ echo "<h5>Horario actual</h5>";
+ foreach (schedules::$workerScheduleStatusShort as $id => $type) {
+ ?>
+ <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="schedulesstatus<?=(int)$id?>">
+ <input type="checkbox" id="schedulesstatus<?=(int)$id?>" name="schedulesstatus[<?=(int)$id?>]" class="mdl-checkbox__input" value="1"<?=(isset($_GET["schedulesstatus"][(int)$id]) ? " checked" : "")?>>
+ <span class="mdl-checkbox__label mdl-color-text--<?=security::htmlsafe(schedules::$workerScheduleStatusColors[$id])?>"><?=security::htmlsafe($type)?></span>
+ </label>
+ <?php
+ }
+ }
+ ?>
+ </div>
+ <div class="mdl-dialog__actions">
+ <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Filtrar</button>
+ <button onclick="event.preventDefault(); document.querySelector('#filter').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+ </div>
+ </form>
+ </dialog>
+ <?php
+ }
+
+ public static function buildSelect($form) {
+ global $select, $selectedSchedulesStatus, $_GET;
+
+ $select = array("enabled" => [], "selected" => []);
+
+ foreach (people::$filters as $f) {
+ $select["enabled"][$f] = isset($_GET[$f]);
+ if ($select["enabled"][$f]) {
+ $select["selected"][$f] = (isset($_GET[$f]) ? array_keys($_GET[$f]) : []);
+ }
+ }
+
+ if ($form == "workers.php") {
+ if (isset($_GET["schedulesstatus"]) && is_array($_GET["schedulesstatus"]) && count($_GET["schedulesstatus"])) {
+ $selectedSchedulesStatus = array_keys($_GET["schedulesstatus"]);
+ } else {
+ $selectedSchedulesStatus = false;
+ }
+ }
+ }
+}
diff --git a/src/inc/mail.php b/src/inc/mail.php
new file mode 100644
index 0000000..7f3b353
--- /dev/null
+++ b/src/inc/mail.php
@@ -0,0 +1,65 @@
+<?php
+use PHPMailer\PHPMailer\PHPMailer;
+use PHPMailer\PHPMailer\Exception;
+use PHPMailer\PHPMailer\SMTP;
+
+require __DIR__.'/../lib/PHPMailer/Exception.php';
+require __DIR__.'/../lib/PHPMailer/PHPMailer.php';
+require __DIR__.'/../lib/PHPMailer/SMTP.php';
+
+class mail {
+ public static function send($to, $attachments, $subject, $body, $isHTML = true, $plainBody = "") {
+ global $conf;
+
+ if (!$conf["mail"]["enabled"]) return false;
+
+ $mail = new PHPMailer();
+
+ try {
+ if ($conf["debug"]) {
+ $mail->SMTPDebug = SMTP::DEBUG_SERVER;
+ }
+ $mail->CharSet = "UTF-8";
+ $mail->isSMTP();
+ $mail->Host = $conf["mail"]["host"];
+ $mail->SMTPAuth = $conf["mail"]["smtpauth"];
+ $mail->Username = $conf["mail"]["username"];
+ $mail->Password = $conf["mail"]["password"];
+ $mail->SMTPSecure = 'tls';
+ $mail->Port = $conf["mail"]["port"];
+ $mail->Encoding = 'base64';
+ $mail->Timeout = 30;
+
+ $mail->setFrom($conf["mail"]["remitent"], $conf["mail"]["remitentName"]);
+
+ foreach ($to as $address) {
+ if (isset($address["name"])) $mail->addAddress($address["email"], $address["name"]);
+ else $mail->addAddress($address["email"]);
+ }
+
+ foreach ($attachments as $attachment) {
+ if (isset($attachment["name"])) $mail->addAttachment($attachment["path"], $attachment["name"]);
+ else $mail->addAttachment($attachment["email"]);
+ }
+
+ if ($isHTML) {
+ $mail->isHTML(true);
+ if (!empty($plainBody)) $mail->AltBody = $plainBody;
+ }
+
+ $mail->Subject = (!empty($conf["mail"]["subjectPrefix"]) ? $conf["mail"]["subjectPrefix"]." " : "").$subject;
+ $mail->Body = $body;
+
+ if (!$mail->send()) return false;
+ } catch (Exception $e) {
+ if ($conf["debug"]) echo $e."\n";
+ return false;
+ }
+
+ return true;
+ }
+
+ public static function bodyTemplate($msg) {
+ return "<div style=\"font-family: 'Helvetica', 'Arial', sans-serif;\">".$msg."</div>";
+ }
+}
diff --git a/src/inc/people.php b/src/inc/people.php
new file mode 100644
index 0000000..deccc10
--- /dev/null
+++ b/src/inc/people.php
@@ -0,0 +1,244 @@
+<?php
+class people {
+ public static $filters = ["categories", "types", "companies"];
+ public static $mysqlFilters = ["categories", "types"];
+ public static $mysqlFiltersFields = ["p.category", "p.type"];
+
+ public static function add($username, $name, $dni, $email, $category, $password_hash, $type) {
+ global $con;
+
+ $susername = db::sanitize($username);
+ $sname = db::sanitize($name);
+ $sdni = db::sanitize($dni);
+ $semail = db::sanitize($email);
+ $scategory = (int)$category;
+ $spassword_hash = db::sanitize($password_hash);
+ $stype = (int)$type;
+
+ if (!categories::exists($category) || !security::existsType($type)) return false;
+
+ return mysqli_query($con, "INSERT INTO people (username, name, dni, email, category, password, type) VALUES ('$susername', '$sname', '$sdni', '$semail', $scategory, '$spassword_hash', $stype)");
+ }
+
+ public static function edit($id, $username, $name, $dni, $email, $category, $type) {
+ global $con;
+
+ $sid = (int)$id;
+ $susername = db::sanitize($username);
+ $sname = db::sanitize($name);
+ $sdni = db::sanitize($dni);
+ $semail = db::sanitize($email);
+ $scategory = (int)$category;
+ $stype = (int)$type;
+
+ return mysqli_query($con, "UPDATE people SET username = '$susername', name = '$sname', dni = '$sdni', email = '$semail', category = $scategory, type = $stype WHERE id = $sid LIMIT 1");
+ }
+
+ public static function updatePassword($id, $password_hash) {
+ global $con;
+
+ $sid = (int)$id;
+ $spassword_hash = db::sanitize($password_hash);
+
+ return mysqli_query($con, "UPDATE people SET password = '$spassword_hash' WHERE id = $sid LIMIT 1");
+ }
+
+ public static function workerViewChangePassword($oldpassword, $newpassword) {
+ global $_SESSION;
+
+ if (!security::isUserPassword(false, $oldpassword)) return false;
+
+ return self::updatePassword($_SESSION["id"], password_hash($newpassword, PASSWORD_DEFAULT));
+ }
+
+ private static function addCompaniesToRow(&$row, $isWorker = false, $showHiddenCompanies = true) {
+ global $con;
+
+ $query = mysqli_query($con, "SELECT w.id id, w.company company, h.status status
+ FROM workers w ".workers::sqlAddonToGetStatusAttribute($row["id"]));
+
+ $row["baixa"] = true;
+ if ($isWorker) $row["hidden"] = true;
+ $row["companies"] = [];
+ while ($row2 = mysqli_fetch_assoc($query)) {
+ $baixa = workers::isHidden($row2["status"]);
+
+ if ($isWorker && $row2["id"] == $row["workerid"]) $row["hidden"] = $baixa;
+ if (!$baixa) $row["baixa"] = false;
+ if (!$showHiddenCompanies && $baixa) continue;
+ $row["companies"][$row2["id"]] = $row2["company"];
+ }
+ }
+
+ private static function filterCompanies($fc, $pc) { // Filter Companies, Person Companies
+ foreach ($pc as $c) {
+ if (in_array($c, $fc)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static function get($id, $showHiddenCompanies = true) {
+ global $con;
+
+ $query = mysqli_query($con, "SELECT p.id id, p.username username, p.type type, p.name name, p.dni dni, p.email email, p.category categoryid, c.name category FROM people p LEFT JOIN categories c ON p.category = c.id WHERE p.id = ".(int)$id);
+
+ if (!mysqli_num_rows($query)) {
+ return false;
+ }
+
+ $row = mysqli_fetch_assoc($query);
+ self::addCompaniesToRow($row, false, $showHiddenCompanies);
+
+ return $row;
+ }
+
+ public static function getAll($select = false, $treatCompaniesSeparated = false) {
+ global $con, $conf;
+
+ $mysqlSelect = false;
+ if ($select !== false) {
+ $mysqlSelect = true;
+ $flag = false;
+ foreach (self::$mysqlFilters as $f) {
+ if ($select["enabled"][$f]) {
+ $flag = true;
+ break;
+ }
+ }
+
+ if (!$flag) {
+ $mysqlSelect = false;
+ }
+ }
+
+ if ($mysqlSelect !== false) {
+ $categoryChilds = categories::getChildren();
+ $where = " WHERE ";
+ $conditions = [];
+ foreach (self::$mysqlFilters as $i => $f) {
+ if ($select["enabled"][$f]) {
+ $insideconditions = [];
+ foreach ($select["selected"][$f] as $value) {
+ $insideconditions[] = self::$mysqlFiltersFields[$i]." = ".(int)$value;
+ if ($f == "categories" && isset($categoryChilds[(int)$value])) {
+ foreach ($categoryChilds[(int)$value] as $child) {
+ $insideconditions[] = self::$mysqlFiltersFields[$i]." = ".(int)$child;
+ }
+ }
+ }
+ $conditions[] = "(".implode(" OR ", $insideconditions).")";
+ }
+ }
+ $where .= implode(" AND ", $conditions);
+ } else {
+ $where = "";
+ }
+
+ $query = mysqli_query($con, "SELECT
+ p.id id,
+ p.username username,
+ p.type type,
+ p.name name,
+ p.dni dni,
+ p.email email,
+ p.category categoryid,
+ c.name category
+ ".($treatCompaniesSeparated ? ", w.id workerid, w.company companyid" : "")."
+ FROM people p
+ LEFT JOIN categories c
+ ON p.category = c.id
+ ".($treatCompaniesSeparated ? " RIGHT JOIN workers w
+ ON p.id = w.person" : "").$where);
+
+ $people = [];
+
+ while ($row = mysqli_fetch_assoc($query)) {
+ self::addCompaniesToRow($row, $treatCompaniesSeparated);
+
+ if ($select === false || !$select["enabled"]["companies"] || (!$treatCompaniesSeparated && self::filterCompanies($select["selected"]["companies"], $row["companies"]) || ($treatCompaniesSeparated && in_array($row["companyid"], $select["selected"]["companies"])))) {
+ $people[] = $row;
+ }
+ }
+
+ // Order people by name and baixa
+ if ($treatCompaniesSeparated) {
+ usort($people, function($a, $b) {
+ if ($a["hidden"] == 0 && $b["hidden"] == 1) return -1;
+ if ($a["hidden"] == 1 && $b["hidden"] == 0) return 1;
+ return ($a["name"] < $b["name"] ? -1 : ($a["name"] == $b["name"] ? 0 : 1));
+ });
+ } else {
+ usort($people, function($a, $b) {
+ if ($a["baixa"] == 0 && $b["baixa"] == 1) return -1;
+ if ($a["baixa"] == 1 && $b["baixa"] == 0) return 1;
+ return ($a["name"] < $b["name"] ? -1 : ($a["name"] == $b["name"] ? 0 : 1));
+ });
+ }
+
+ return $people;
+ }
+
+ public static function exists($id) {
+ global $con;
+
+ $query = mysqli_query($con, "SELECT id FROM people WHERE id = ".(int)$id);
+
+ return (mysqli_num_rows($query) > 0);
+ }
+
+ public static function addToCompany($id, $company) {
+ global $con;
+
+ $sid = (int)$id;
+ $scompany = (int)$company;
+
+ if (!companies::exists($scompany)) return false;
+ if (!people::exists($sid)) return false;
+
+ $query = mysqli_query($con, "SELECT id FROM workers WHERE person = $sid AND company = $scompany");
+ if (mysqli_num_rows($query)) return false;
+
+ $time = (int)time();
+
+ if (!mysqli_query($con, "INSERT INTO workers (person, company) VALUES ($sid, $scompany)")) return false;
+
+ $sworkerId = (int)mysqli_insert_id($con);
+ $stime = (int)time();
+
+ return mysqli_query($con, "INSERT INTO workhistory (worker, day, status) VALUES ($sworkerId, $stime, ".(int)workers::AFFILIATION_STATUS_AUTO_WORKING.")");
+ }
+
+ public static function userData($data, $id = "ME") {
+ global $con, $_SESSION;
+
+ if ($id == "ME" && $data == "id") return $_SESSION["id"];
+ if ($id == "ME") $id = $_SESSION["id"];
+ $sdata = preg_replace("/[^A-Za-z0-9 ]/", '', $data);
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT $sdata FROM people WHERE id = $sid");
+
+ if (!mysqli_num_rows($query)) return false;
+
+ $row = mysqli_fetch_assoc($query);
+
+ return $row[$sdata];
+ }
+
+ public static function workerData($data, $id) {
+ global $con, $_SESSION;
+
+ $sdata = preg_replace("/[^A-Za-z0-9 ]/", '', $data);
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT p.$sdata $sdata FROM people p INNER JOIN workers w ON p.id = w.person WHERE w.id = $sid");
+
+ if (!mysqli_num_rows($query)) return false;
+
+ $row = mysqli_fetch_assoc($query);
+
+ return $row[$sdata];
+ }
+}
diff --git a/src/inc/recovery.php b/src/inc/recovery.php
new file mode 100644
index 0000000..1f8307b
--- /dev/null
+++ b/src/inc/recovery.php
@@ -0,0 +1,140 @@
+<?php
+class recovery {
+ const TOKEN_BYTES = 32;
+ const EXPIRATION_TIME = 60*60*24;
+ const WELCOME_EXPIRATION_TIME = 60*60*24*90;
+
+ const EMAIL_TYPE_RECOVERY = 0;
+ const EMAIL_TYPE_WELCOME = 1;
+
+ const EMAIL_NOT_SET = 0;
+
+ public static function getUser($email, $dni) {
+ global $con;
+
+ $semail = db::sanitize($email);
+ $sdni = db::sanitize($dni);
+
+ $query = mysqli_query($con, "SELECT id FROM people WHERE email = '$semail' AND dni = '$sdni' LIMIT 1");
+
+ if (!mysqli_num_rows($query)) return false;
+
+ return mysqli_fetch_assoc($query)["id"];
+ }
+
+ private static function tokenIsUnique($token) {
+ global $con;
+
+ $stoken = db::sanitize($token);
+
+ $query = mysqli_query($con, "SELECT 1 FROM recovery WHERE token = '$stoken' LIMIT 1");
+
+ return !mysqli_num_rows($query);
+ }
+
+ private static function generateRandomToken() {
+ do {
+ $token = bin2hex(random_bytes(self::TOKEN_BYTES));
+ } while (!self::tokenIsUnique($token));
+
+ return $token;
+ }
+
+ private static function invalidateAllRecoveries($id) {
+ global $con;
+
+ $sid = (int)$id;
+
+ return mysqli_query($con, "UPDATE recovery SET used = 1 WHERE user = $sid");
+ }
+
+ public static function sendRecoveryMail($user, $token, $expires, $emailType = self::EMAIL_TYPE_RECOVERY) {
+ global $conf;
+
+ $to = [
+ ["email" => $user["email"]]
+ ];
+
+ $url = security::htmlsafe($conf["fullPath"]."recovery.php?token=".$token);
+
+ switch ($emailType) {
+ case self::EMAIL_TYPE_WELCOME:
+ $subject = "Datos de acceso de ".security::htmlsafe($user["name"]);
+ $body = mail::bodyTemplate("<p>Hola ".security::htmlsafe($user["name"]).",</p>
+ <p>Para poder acceder al aplicativo de control horario, adjuntamos un enlace donde podrás configurar tu contraseña:</p>
+ <ul>
+ <li><a href=\"$url\">$url</a></li>
+ </ul>
+ <p>Una vez establezcas tu contraseña, podrás iniciar sesión con la contraseña que has rellenado y tu usuario: <b><code>".security::htmlsafe($user["username"])."</code></b></p>
+ <p>Reciba un cordial saludo.</p>");
+ break;
+
+ default:
+ $subject = "Recuperar contraseña de ".security::htmlsafe($user["name"]);
+ $body = mail::bodyTemplate("<p>Hola ".security::htmlsafe($user["name"]).",</p>
+ <p>Alguien (seguramente tú) ha rellenado el formulario de recuperación de contraseñas para el aplicativo de registro horario.</p>
+ <p>Si no has sido tú, puede que alguien esté intentando entrar en tu zona de trabajador del aplicativo, y te agredeceríamos que lo comunicaras en la mayor brevedad posible a recursos humanos.</p>
+ <p>Si has sido tú, puedes restablecer tu contraseña haciendo clic en el siguiente enlace:</p>
+ <ul>
+ <li><a href=\"$url\">$url</a></li>
+ </ul>
+ <p>Además, aprovechamos para recordarte que tu usuario en el web es <b><code>".security::htmlsafe($user["username"])."</code></b></p>
+ <p>Reciba un cordial saludo.</p>");
+ }
+
+ return mail::send($to, [], $subject, $body);
+ }
+
+ public static function recover($id, $emailType = self::EMAIL_TYPE_RECOVERY) {
+ global $con;
+
+ $user = people::get($id);
+ if ($user === false) return false;
+ if (!isset($user["email"]) || empty($user["email"])) return self::EMAIL_NOT_SET;
+
+ $token = self::generateRandomToken();
+ $stoken = db::sanitize($token);
+ $sid = (int)$id;
+ $stime = (int)time();
+ $sexpires = (int)($stime + ($emailType === self::EMAIL_TYPE_WELCOME ? self::WELCOME_EXPIRATION_TIME : self::EXPIRATION_TIME));
+
+ if (!self::invalidateAllRecoveries($id)) return false;
+
+ if (!mysqli_query($con, "INSERT INTO recovery (user, token, timecreated, expires) VALUES ($sid, '$stoken', $stime, $sexpires)")) return false;
+
+ return self::sendRecoveryMail($user, $token, $sexpires, $emailType);
+ }
+
+ public static function getRecovery($token) {
+ global $con;
+
+ $stoken = db::sanitize($token);
+
+ $query = mysqli_query($con, "SELECT * FROM recovery WHERE token = '$stoken' LIMIT 1");
+
+ if (!mysqli_num_rows($query)) return false;
+
+ return mysqli_fetch_assoc($query);
+ }
+
+ public static function getUnusedRecovery($token) {
+ $recovery = self::getRecovery($token);
+ if ($recovery === false || $recovery["used"] != 0 || $recovery["expires"] < time()) return false;
+
+ return $recovery;
+ }
+
+ public static function finishRecovery($token, $password) {
+ global $con;
+
+ $stoken = db::sanitize($token);
+ $spassword = db::sanitize(password_hash($password, PASSWORD_DEFAULT));
+
+ $recovery = recovery::getUnusedRecovery($token);
+ if ($recovery === false) return false;
+
+ if (!self::invalidateAllRecoveries($recovery["user"])) return false;
+
+ return people::updatePassword($recovery["user"], $spassword);
+ }
+}
diff --git a/src/inc/recurringIncidents.php b/src/inc/recurringIncidents.php
new file mode 100644
index 0000000..b07bf66
--- /dev/null
+++ b/src/inc/recurringIncidents.php
@@ -0,0 +1,97 @@
+<?php
+class recurringIncidents {
+ /*public static function oldAdd($worker, $type, $details, $ifirstday, $ilastday, $begins, $ends, $creator = "ME", $typedays, $days, $alreadyTimestamp = false) {
+ global $con, $conf;
+
+ $sworker = (int)$worker;
+ $workerDetails = workers::get($sworker);
+ if ($workerDetails === false) return 1;
+
+ if ($creator === "ME") $creator = people::userData("id");
+ $screator = (int)$creator;
+
+ if (!security::isAllowed(security::ADMIN) && $workerDetails["person"] != $creator) return 5;
+
+ $stype = (int)$type;
+ $sverified = (int)$verified;
+ $sdetails = db::sanitize($details);
+
+ $incidenttype = self::getType($stype);
+ if ($incidenttype === false) return -1;
+
+ if ($alreadyTimestamp) {
+ $sfirstday = (int)$ifirstday;
+ $slastday = (int)$ilastday;
+ } else {
+ $firstday = new DateTime($ifirstday);
+ $sfirstday = (int)$firstday->getTimestamp();
+ $lastday = new DateTime($ilastday);
+ $slastday = (int)$lastday->getTimestamp();
+ }
+
+ if ($sfirstday >= $slastday) return 3;
+
+ $sbegins = (int)$begins;
+ $sends = (int)$ends;
+ if ($sbegins >= $sends) return 3;
+
+ $typedays = array_unique(array_map(function($el) {
+ return (int)$el;
+ }, $typedays));
+ foreach ($typedays as $typeday) {
+ if (!in_array($typeday, $workingTypes)) return 6;
+ }
+ $stypedays = json_encode($typedays);
+
+ if (!mysqli_query($con, "INSERT INTO recurringincidents (worker, creator, type, firstday, lastday, typedays, begins, ends, details) VALUES ($sworker, $screator, $stype, $sfirstday, $slastday, '$stypedays', $sbegins, $sends, '$sdetails')")) return -1;
+
+ return 0;
+ }*/ // NOTE: This was a first idea, to allow setting up recurring incidents like schedules, but we've changed how we'll handle them and so this is no longer useful.
+
+ public static function add($worker, $type, $details, $ifirstday, $ilastday, $begins, $ends, $creator = "ME", $typeDays, $days, $alreadyTimestamp = false) {
+ if ($alreadyTimestamp) {
+ $current = new DateTime();
+ $current->setTimestamp($ifirstday);
+ $lastday = new DateTime();
+ $lastday->setTimestamp($ilastday);
+ } else {
+ $current = new DateTime($ifirstday);
+ $lastday = new DateTime($ilastday);
+ }
+
+ $oneDay = new DateInterval("P1D");
+
+ $category = registry::getWorkerCategory($worker);
+ if ($category === false) return false;
+
+ $flag = true;
+
+ for (; $current->diff($lastday)->invert === 0; $current->add($oneDay)) {
+ $currentTimestamp = $current->getTimestamp();
+ $currentDay = (int)$current->format("N") - 1;
+
+ if (!in_array($currentDay, $days)) continue;
+
+ $calendarDays = registry::getDayTypes($currentTimestamp);
+ if ($calendarDays === false) return false;
+
+ if (isset($calendarDays[$category])) {
+ $typeDay = $calendarDays[$category];
+ } else if (isset($calendarDays[-1])) {
+ $typeDay = $calendarDays[-1];
+ } else {
+ $flag = false;
+ continue;
+ }
+
+ if (!in_array($typeDay, $typeDays)) continue;
+
+ if ($status = incidents::add($worker, $type, $details, $currentTimestamp, $begins, $ends, $creator, 1, true, false, false) !== 0) {
+ $flag = false;
+ continue;
+ }
+ }
+
+ return $flag;
+ }
+}
diff --git a/src/inc/registry.php b/src/inc/registry.php
new file mode 100644
index 0000000..363968d
--- /dev/null
+++ b/src/inc/registry.php
@@ -0,0 +1,393 @@
+<?php
+class registry {
+ const LOGS_PAGINATION_LIMIT = 30;
+ const REGISTRY_PAGINATION_LIMIT = 20;
+
+ const STATE_REGISTERED = 0;
+ const STATE_MANUALLY_INVALIDATED = 1;
+ const STATE_VALIDATED_BY_WORKER = 2;
+
+ public static $stateIcons = [
+ 0 => "check",
+ 1 => "delete_forever",
+ 2 => "verified_user"
+ ];
+
+ public static $stateIconColors = [
+ 0 => "mdl-color-text--green",
+ 1 => "mdl-color-text--red",
+ 2 => "mdl-color-text--green"
+ ];
+
+ public static $stateTooltips = [
+ 0 => "Registrado",
+ 1 => "Invalidado manualmente",
+ 2 => "Validado"
+ ];
+
+ public static $workerPendingWhere = "r.workervalidated = 0";
+ public static $notInvalidatedWhere = "r.invalidated = 0";
+ public static $logsWarnings = "LOCATE('[warning]', logdetails) as warningpos, LOCATE('[error]', logdetails) as errorpos, LOCATE('[fatalerror]', logdetails) as fatalerrorpos";
+
+ private static function recordLog(&$log, $time, $executedby = -1, $quiet = false) {
+ global $con;
+
+ $slog = db::sanitize($log);
+ $sday = (int)$time;
+ $srealtime = (int)time();
+ $sexecutedby = (int)$executedby;
+
+ $status = mysqli_query($con, "INSERT INTO logs (realtime, day, executedby, logdetails) VALUES ($srealtime, $sday, $sexecutedby, '$slog')");
+
+ if (!$status) {
+ if (!$quiet) echo "[fatalerror] Couldn't record log into the database!\n";
+ return false;
+ } else {
+ if (!$quiet) echo "[success] Log recorded into the database.\n";
+ return mysqli_insert_id($con);
+ }
+ }
+
+ public static function getLogs($start = 0, $limit = self::LOGS_PAGINATION_LIMIT) {
+ global $con;
+
+ $query = mysqli_query($con, "SELECT id, realtime, day, executedby, ".self::$logsWarnings." FROM logs ORDER BY id DESC".db::limitPagination($start, $limit));
+
+ $return = [];
+ while ($row = mysqli_fetch_assoc($query)) {
+ $return[] = $row;
+ }
+
+ return $return;
+ }
+
+ public static function getLog($id) {
+ global $con;
+
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT logdetails, ".self::$logsWarnings." FROM logs WHERE id = $sid");
+
+ if (!mysqli_num_rows($query)) return false;
+
+ $row = mysqli_fetch_assoc($query);
+
+ return $row;
+ }
+
+ public static function beautifyLog($str) {
+ $str = str_replace("[info]", "<span style='font-weight: bold;' class='mdl-color-text--blue'>[info]</span>", $str);
+ $str = str_replace("[warning]", "<span style='font-weight: bold;' class='mdl-color-text--orange'>[warning]</span>", $str);
+ $str = str_replace("[error]", "<span style='font-weight: bold;' class='mdl-color-text--red'>[error]</span>", $str);
+ $str = str_replace("[fatalerror]", "<span style='font-weight: bold;' class='mdl-color-text--red-900'>[fatalerror]</span>", $str);
+ return $str;
+ }
+
+ private static function addToLog(&$log, $quiet, $msg) {
+ $log .= $msg;
+ if (!$quiet) echo $msg;
+ }
+
+ private static function alreadyRegistered($time, $worker) {
+ global $con;
+
+ $stime = (int)$time;
+ $sworker = (int)$worker;
+
+ $query = mysqli_query($con, "SELECT id FROM records WHERE worker = $sworker AND day = $stime AND invalidated = 0 LIMIT 1");
+
+ return (mysqli_num_rows($query) > 0);
+ }
+
+ private static function register($time, $worker, $schedule, $creator = -1) {
+ global $con;
+
+ $sworker = (int)$worker;
+ $stime = (int)$time;
+ $srealtime = (int)time();
+ $screator = (int)$creator;
+ $sbeginswork = (int)$schedule["beginswork"];
+ $sendswork = (int)$schedule["endswork"];
+ $sbeginsbreakfast = (int)$schedule["beginsbreakfast"];
+ $sendsbreakfast = (int)$schedule["endsbreakfast"];
+ $sbeginslunch = (int)$schedule["beginslunch"];
+ $sendslunch = (int)$schedule["endslunch"];
+
+ return mysqli_query($con, "INSERT INTO records (worker, day, created, creator, beginswork, endswork, beginsbreakfast, endsbreakfast, beginslunch, endslunch) VALUES ($sworker, $stime, $srealtime, $screator, $sbeginswork, $sendswork, $sbeginsbreakfast, $sendsbreakfast, $sbeginslunch, $sendslunch)");
+ }
+
+ public static function getWorkerCategory($id) {
+ global $con;
+
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT p.category category FROM workers w INNER JOIN people p ON w.person = p.id WHERE w.id = $sid");
+
+ if ($query === false || !mysqli_num_rows($query)) {
+ return false;
+ }
+
+ $row = mysqli_fetch_assoc($query);
+
+ return $row["category"];
+ }
+
+ public static function getDayTypes($time) {
+ global $con;
+
+ $stime = (int)$time;
+
+ $query = mysqli_query($con, "SELECT id, category, details FROM calendars WHERE begins <= $stime AND ends >= $stime");
+ if ($query === false) return false;
+
+ $calendars = [];
+ while ($row = mysqli_fetch_assoc($query)) {
+ $calendar = json_decode($row["details"], true);
+ if (json_last_error() !== JSON_ERROR_NONE) return false;
+ if (!isset($calendar[$time])) return false;
+
+ $calendars[$row["category"]] = $calendar[$time];
+ }
+
+ return $calendars;
+ }
+
+ private static function getApplicableSchedules($time) {
+ global $con;
+
+ $stime = (int)$time;
+
+ $query = mysqli_query($con, "SELECT id FROM schedules WHERE begins <= $stime AND ends >= $stime AND active = 1");
+ if ($query === false) return false;
+
+ $schedules = [];
+ while ($row = mysqli_fetch_assoc($query)) {
+ $schedules[] = $row["id"];
+ }
+
+ return $schedules;
+ }
+
+ public static function generateNow($originaltime, &$logId, $quiet = true, $executedby = -1, $workersWhitelist = false) {
+ global $con;
+
+ $log = "";
+
+ if ($workersWhitelist !== false) self::addToLog($log, $quiet, "[info] This is a partial registry generation, because a whitelist of workers was passed: [".implode(", ", $workersWhitelist)."]\n");
+
+ $datetime = new DateTime();
+ $datetime->setTimestamp($originaltime);
+ self::addToLog($log, $quiet, "[info] Time passed: ".$datetime->format("Y-m-d H:i:s")."\n");
+
+ $rawdate = $datetime->format("Y-m-d")."T00:00:00";
+ self::addToLog($log, $quiet, "[info] Working with this date: $rawdate\n");
+ $date = new DateTime($rawdate);
+
+ $time = $date->getTimestamp();
+ $dow = (int)$date->format("N") - 1;
+ self::addToLog($log, $quiet, "[info] Final date timestamp: $time, Dow: $dow\n");
+
+ $days = self::getDayTypes($time);
+ if ($days === false) {
+ self::addToLog($log, $quiet, "[fatalerror] An error occurred while loading the calendars.\n");
+ $logId = self::recordLog($log, $time, $executedby, $quiet);
+ return 1;
+ }
+
+ $schedules = self::getApplicableSchedules($time);
+ if ($schedules === false) {
+ self::addToLog($log, $quiet, "[fatalerror] An error occurred while loading the active schedules.\n");
+ $logId = self::recordLog($log, $time, $executedby, $quiet);
+ return 2;
+ }
+
+ self::addToLog($log, $quiet, "[info] Found ".count($schedules)." active schedule(s)\n");
+
+ foreach ($schedules as $scheduleid) {
+ self::addToLog($log, $quiet, "\n[info] Processing schedule $scheduleid\n");
+
+ $s = schedules::get($scheduleid);
+ if ($s === false) {
+ self::addToLog($log, $quiet, "[fatalerror] An error ocurred while loading schedule with id $scheduleid (it doesn't exist or there was an error with the SQL query)\n");
+ $logId = self::recordLog($log, $time, $executedby, $quiet);
+ return 3;
+ }
+
+ if ($workersWhitelist !== false && !in_array($s["worker"], $workersWhitelist)) {
+ self::addToLog($log, $quiet, "[info] This schedule's worker (".$s["worker"].") is not in the whitelist, so skipping\n");
+ continue;
+ }
+
+ $category = self::getWorkerCategory($s["worker"]);
+
+ if (isset($days[$category])) {
+ self::addToLog($log, $quiet, "[info] Using worker's (".$s["worker"].") category ($category) calendar\n");
+ $typeday = $days[$category];
+ } else if (isset($days[-1])) {
+ self::addToLog($log, $quiet, "[info] Using default calendar\n");
+ $typeday = $days[-1];
+ } else {
+ self::addToLog($log, $quiet, "[warning] No calendar applies, so skipping this schedule\n");
+ continue;
+ }
+
+ if (!isset($s["days"][$typeday])) {
+ self::addToLog($log, $quiet, "[info] This schedule doesn't have this type of day ($typeday) set up, so skipping\n");
+ continue;
+ }
+
+ if (!isset($s["days"][$typeday][$dow])) {
+ self::addToLog($log, $quiet, "[info] This schedule doesn't have a daily schedule for this day of the week ($dow) and type of day ($typeday), so skipping.\n");
+ continue;
+ }
+
+ self::addToLog($log, $quiet, "[info] Found matching daily schedule. We'll proceed to register it\n");
+
+ if (self::alreadyRegistered($time, $s["worker"])) {
+ self::addToLog($log, $quiet, "[warning] We're actually NOT going to register it because another registry already exists for this worker at the same day.\n");
+ } else {
+ if (self::register($time, $s["worker"], $s["days"][$typeday][$dow], $executedby)) {
+ self::addToLog($log, $quiet, "[info] Registered with id ".mysqli_insert_id($con)."\n");
+ } else {
+ self::addToLog($log, $quiet, "[error] Couldn't register this schedule because of an unknown error!\n");
+ }
+ }
+ }
+
+ $logId = self::recordLog($log, $time, $executedby, $quiet);
+
+ return 0;
+ }
+
+ public static function getStatus($row) {
+ if ($row["invalidated"] == 1) return self::STATE_MANUALLY_INVALIDATED;
+ elseif ($row["workervalidated"] == 1) return self::STATE_VALIDATED_BY_WORKER;
+ else return self::STATE_REGISTERED;
+ }
+
+ private static function magicRecord(&$row) {
+ $row["state"] = self::getStatus($row);
+ }
+
+ public static function get($id, $magic = false) {
+ global $con;
+
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT * FROM records WHERE id = $sid LIMIT 1");
+
+ if (!mysqli_num_rows($query)) return false;
+
+ $row = mysqli_fetch_assoc($query);
+ if ($magic) self::magicRecord($row);
+
+ return $row;
+ }
+
+ public static function getRecords($worker = false, $begins = false, $ends = false, $returnInvalid = false, $includeWorkerInfo = false, $sortByDateDesc = false, $start = 0, $limit = self::REGISTRY_PAGINATION_LIMIT, $treatWorkerAttributeAsUser = false, $onlyWorkerPending = false, $magic = true) {
+ global $con;
+
+ if ($treatWorkerAttributeAsUser && !$includeWorkerInfo) return false;
+
+ $where = [];
+
+ if ($worker !== false) $where[] = ($treatWorkerAttributeAsUser ? "w.person" : "r.worker")." = ".(int)$worker;
+
+ $dateLimit = ($begins !== false && $ends !== false);
+ if ($dateLimit) {
+ $where[] = "r.day >= ".(int)$begins;
+ $where[] = "r.day <= ".(int)$ends;
+ }
+
+ if (!$returnInvalid || $onlyWorkerPending) $where[] = self::$notInvalidatedWhere;
+
+ if ($onlyWorkerPending) $where[] = self::$workerPendingWhere;
+
+ $query = mysqli_query($con, "SELECT r.*".($includeWorkerInfo ? ", p.id personid, p.name workername, w.company companyid" : "")." FROM records r LEFT JOIN workers w ON r.worker = w.id".($includeWorkerInfo ? " LEFT JOIN people p ON w.person = p.id" : "").(count($where) ? " WHERE ".implode(" AND ", $where) : "")." ORDER BY".($sortByDateDesc ? " r.day DESC, w.company DESC," : "")." id DESC".db::limitPagination($start, $limit));
+
+ $return = [];
+ while ($row = mysqli_fetch_assoc($query)) {
+ if ($magic) self::magicRecord($row);
+ $return[] = $row;
+ }
+
+ return $return;
+ }
+
+ public static function getWorkerRecords($worker, $begins = false, $ends = false, $returnInvalid = false, $onlyWorkerPending = false) {
+ return self::getRecords($worker, $begins, $ends, $returnInvalid, false, true, 0, 0, false, $onlyWorkerPending);
+ }
+
+ public static function numRows($includingInvalidated = false) {
+ global $con;
+
+ $query = mysqli_query($con, "SELECT COUNT(*) count FROM records".($includingInvalidated ? "" : " WHERE invalidated = 0"));
+
+ if (!mysqli_num_rows($query)) return false;
+
+ $row = mysqli_fetch_assoc($query);
+
+ return (isset($row["count"]) ? (int)$row["count"] : false);
+ }
+
+ public static function numRowsUser($user, $includingInvalidated = true) {
+ global $con;
+
+ $where = [];
+ if (!$includingInvalidated) $where[] = "r.invlidated = 0";
+ $where[] = "w.person = ".(int)$user;
+
+ $query = mysqli_query($con, "SELECT COUNT(*) count FROM records r LEFT JOIN workers w ON r.worker = w.id".(count($where) ? " WHERE ".implode(" AND ", $where) : ""));
+
+ if (!mysqli_num_rows($query)) return false;
+
+ $row = mysqli_fetch_assoc($query);
+
+ return (isset($row["count"]) ? (int)$row["count"] : false);
+ }
+
+ public static function checkRecordIsFromPerson($record, $person = "ME", $boolean = false) {
+ global $con;
+
+ if ($person == "ME") $person = people::userData("id");
+
+ $srecord = (int)$record;
+ $sperson = (int)$person;
+
+ $query = mysqli_query($con, "SELECT r.id FROM records r INNER JOIN workers w ON r.worker = w.id INNER JOIN people p ON w.person = p.id WHERE p.id = $sperson AND r.id = $srecord LIMIT 1");
+
+ if (!mysqli_num_rows($query)) {
+ if ($boolean) return false;
+ security::denyUseMethod(security::METHOD_NOTFOUND);
+ }
+
+ return true;
+ }
+
+ public static function invalidate($id, $invalidatedby = "ME") {
+ global $con;
+
+ $sid = (int)$id;
+
+ if ($invalidatedby === "ME") $invalidatedby = people::userData("id");
+ $sinvalidatedby = (int)$invalidatedby;
+
+ return mysqli_query($con, "UPDATE records SET invalidated = 1, invalidatedby = $sinvalidatedby WHERE id = $sid LIMIT 1");
+ }
+
+ public static function invalidateAll($id, $beginsRawDate, $endsRawDate, $invalidatedby = "ME") {
+ global $con;
+
+ $beginsDate = new DateTime($beginsRawDate);
+ $sbegins = (int)$beginsDate->getTimestamp();
+ $endsDate = new DateTime($endsRawDate);
+ $sends = (int)$endsDate->getTimestamp();
+
+ $sid = (int)$id;
+
+ if ($invalidatedby === "ME") $invalidatedby = people::userData("id");
+ $sinvalidatedby = (int)$invalidatedby;
+
+ return mysqli_query($con, "UPDATE records SET invalidated = 1, invalidatedby = $sinvalidatedby WHERE worker = $sid AND invalidated = 0 AND day <= $sends AND day >= $sbegins");
+ }
+}
diff --git a/src/inc/registryView.php b/src/inc/registryView.php
new file mode 100644
index 0000000..1767bc3
--- /dev/null
+++ b/src/inc/registryView.php
@@ -0,0 +1,91 @@
+<?php
+class registryView {
+ public static function renderRegistry(&$registry, &$companies, $scrollable = false, $showPersonAndCompany = true, $isForWorker = false, $isForValidationView = false) {
+ global $conf, $renderRegistryAutoIncremental;
+ if (!isset($renderRegistryAutoIncremental)) $renderRegistryAutoIncremental = 0;
+ $menu = "";
+ ?>
+ <div class="mdl-shadow--2dp overflow-wrapper incidents-wrapper<?=($scrollable ? " incidents-wrapper--scrollable" : "")?>">
+ <table class="incidents">
+ <tr>
+ <?php if ($conf["debug"]) { ?><th>ID</th><?php } ?>
+ <th<?=($isForValidationView ? " class=\"has-checkbox\"" : "")?>>
+ <?php if ($isForValidationView) {
+ ?>
+ <label for="checkboxall_r_<?=$renderRegistryAutoIncremental?>" class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" data-check-all="true">
+ <input type="checkbox" id="checkboxall_r_<?=$renderRegistryAutoIncremental?>" class="mdl-checkbox__input" autocomplete="off">
+ </label>
+ <?php
+ }
+ ?>
+ </th>
+ <?php
+ if ($showPersonAndCompany) {
+ ?>
+ <th>Nombre</th>
+ <th>Empresa</th>
+ <?php
+ }
+ ?>
+ <th>Día</th>
+ <th>Jornada laboral</th>
+ <th>Desayuno</th>
+ <th>Comida</th>
+ <th></th>
+ </tr>
+ <?php
+ foreach ($registry as $record) {
+ $id = (int)$record["id"]."_".(int)$renderRegistryAutoIncremental;
+ $breakfastInt = [$record["beginsbreakfast"], $record["endsbreakfast"]];
+ $lunchInt = [$record["beginslunch"], $record["endslunch"]];
+ ?>
+ <tr<?=($record["invalidated"] == 1 ? ' class="mdl-color-text--grey-700 line-through"' : '')?>>
+ <?php if ($conf["debug"]) { ?><td><?=(int)$record["id"]?></td><?php } ?>
+ <td class="icon-cell<?=($isForValidationView ? " has-checkbox" : "")?>">
+ <?php
+ if ($isForValidationView) {
+ ?>
+ <label for="checkbox_r_<?=$id?>" class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect">
+ <input type="checkbox" id="checkbox_r_<?=$id?>" data-record="<?=(int)$record["id"]?>" class="mdl-checkbox__input" autocomplete="off">
+ </label>
+ <?php
+ } else {
+ ?>
+ <i id="state<?=$id?>" class="material-icons <?=security::htmlsafe(registry::$stateIconColors[$record["state"]])?>"><?=security::htmlsafe(registry::$stateIcons[$record["state"]])?></i>
+ <?php
+ visual::addTooltip("state".$id, security::htmlsafe(registry::$stateTooltips[$record["state"]]));
+ }
+ ?>
+ </td>
+ <?php
+ if ($showPersonAndCompany) {
+ ?>
+ <td class="can-strike"><span<?=($isForWorker ? '' : ' data-dyndialog-href="dynamic/user.php?id='.(int)$record["personid"].'"')?>><?=security::htmlsafe($record["workername"])?></span></td>
+ <td class="can-strike"><?=security::htmlsafe($companies[$record["companyid"]])?></td>
+ <?php
+ }
+ ?>
+ <td class="can-strike"><?=strftime("%d %b %Y", $record["day"])?></td>
+ <td class="centered can-strike"><?=schedules::sec2time($record["beginswork"])." - ".schedules::sec2time($record["endswork"])?></td>
+ <td class="centered can-strike"><?=(intervals::measure($breakfastInt) == 0 ? "-" : ($isForWorker ? export::sec2hours(intervals::measure($breakfastInt)) : schedules::sec2time($record["beginsbreakfast"])." - ".schedules::sec2time($record["endsbreakfast"])))?></td>
+ <td class="centered can-strike"><?=(intervals::measure($lunchInt) == 0 ? "-" : ($isForWorker ? export::sec2hours(intervals::measure($lunchInt)) : schedules::sec2time($record["beginslunch"])." - ".schedules::sec2time($record["endslunch"])))?></td>
+ <td><button id="actions<?=$id?>" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect custom-actions-btn"><i class="material-icons icon">more_vert</i></button></td>
+ </tr>
+ <?php
+ $menu .= '<ul class="mdl-menu mdl-menu--unaligned mdl-js-menu mdl-js-ripple-effect" for="actions'.$id.'">';
+
+ if (!$isForWorker && $record["invalidated"] == 0) {
+ $menu .= '<a href="dynamic/invalidaterecord.php?id='.(int)$record["id"].'" data-dyndialog-href="dynamic/invalidaterecord.php?id='.(int)$record["id"].'"><li class="mdl-menu__item">Invalidar</li></a>';
+ }
+
+ $menu .= '<a href="dynamic/authorsrecord.php?id='.(int)$record["id"].'" data-dyndialog-href="dynamic/authorsrecord.php?id='.(int)$record["id"].'"><li class="mdl-menu__item">Autoría</li></a></ul>';
+
+ $renderRegistryAutoIncremental++;
+ }
+ ?>
+ </table>
+ <?php echo $menu; ?>
+ </div>
+ <?php
+ }
+}
diff --git a/src/inc/schedules.php b/src/inc/schedules.php
new file mode 100644
index 0000000..87b5ed6
--- /dev/null
+++ b/src/inc/schedules.php
@@ -0,0 +1,456 @@
+<?php
+class schedules {
+ const STATUS_NO_ACTIVE_SCHEDULE = 0;
+ const STATUS_HALFWAY_CONFIGURED_SCHEDULE = 1;
+ const STATUS_ACTIVE_SCHEDULE = 2;
+
+ public static $allEvents = ["work", "breakfast", "lunch"];
+ public static $otherEvents = ["breakfast", "lunch"];
+ public static $otherEventsDescription = [
+ "breakfast" => "Desayuno",
+ "lunch" => "Comida"
+ ];
+ public static $workerScheduleStatus = [
+ 0 => "No hay ningún calendario activo",
+ 1 => "Hay un calendario activo pero no está completamente configurado",
+ 2 => "Hay un calendario activo completamente configurado"
+ ];
+ public static $workerScheduleStatusShort = [
+ 0 => "No hay un horario activo",
+ 1 => "No está configurado del todo",
+ 2 => "Existe uno configurado"
+ ];
+ public static $workerScheduleStatusColors = [
+ 0 => "red",
+ 1 => "orange",
+ 2 => "green"
+ ];
+
+ public static function time2sec($time) {
+ $e = explode(":", $time);
+ return ((int)$e[0]*60 + (int)$e[1])*60;
+ }
+
+ public static function sec2time($sec, $autoShowSeconds = true) {
+ $min = floor($sec/60);
+ $secr = $sec % 60;
+ return visual::padNum(floor($min/60), 2).":".visual::padNum(($min % 60), 2).($autoShowSeconds && $secr != 0 ? ":".visual::padNum($secr, 2) : "");
+ }
+
+ // TEMPLATES:
+
+ public static function addTemplate($name, $ibegins, $iends) {
+ global $con;
+
+ $sname = db::sanitize($name);
+ $begins = new DateTime($ibegins);
+ $sbegins = (int)$begins->getTimestamp();
+ $ends = new DateTime($iends);
+ $sends = (int)$ends->getTimestamp();
+
+ if (!intervals::wellFormed([$sbegins, $sends])) return 2;
+
+ return (mysqli_query($con, "INSERT INTO scheduletemplates (name, begins, ends) VALUES ('$sname', $sbegins, $sends)") ? 0 : 1);
+ }
+
+ private static function _addDaysToReturn(&$return, $stableDays, $sfieldSchedule, $sid) {
+ global $con;
+
+ $return["days"] = [];
+
+ $query2 = mysqli_query($con, "SELECT * FROM $stableDays WHERE $sfieldSchedule = $sid ORDER BY typeday ASC, day ASC");
+
+ while ($row = mysqli_fetch_assoc($query2)) {
+ if (!isset($return["days"][$row["typeday"]]))
+ $return["days"][$row["typeday"]] = [];
+
+ $return["days"][$row["typeday"]][$row["day"]] = $row;
+ }
+ }
+
+ private static function _get($id, $table, $tableDays, $fieldSchedule) {
+ global $con;
+
+ $sid = (int)$id;
+ $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+ $stableDays = preg_replace("/[^A-Za-z0-9 ]/", '', $tableDays);
+ $sfieldSchedule = preg_replace("/[^A-Za-z0-9 ]/", '', $fieldSchedule);
+
+ $query = mysqli_query($con, "SELECT * FROM $stable WHERE id = $sid");
+
+ if ($query === false || !mysqli_num_rows($query)) return false;
+
+ $return = mysqli_fetch_assoc($query);
+
+ self::_addDaysToReturn($return, $stableDays, $sfieldSchedule, $sid);
+
+ return $return;
+ }
+
+ public static function getTemplate($id) {
+ return self::_get($id, "scheduletemplates", "scheduletemplatesdays", "template");
+ }
+
+ public static function getTemplates() {
+ global $con, $conf;
+
+ $query = mysqli_query($con, "SELECT * FROM scheduletemplates ORDER BY ".($conf["debug"] ? "id" : "name")." ASC");
+
+ $templates = [];
+
+ while ($row = mysqli_fetch_assoc($query)) {
+ $templates[] = $row;
+ }
+
+ return $templates;
+ }
+
+ public static function editTemplate($id, $name, $ibegins, $iends) {
+ global $con;
+
+ $sid = (int)$id;
+ $sname = db::sanitize($name);
+ $begins = new DateTime($ibegins);
+ $sbegins = (int)$begins->getTimestamp();
+ $ends = new DateTime($iends);
+ $sends = (int)$ends->getTimestamp();
+
+ if (!intervals::wellFormed([$sbegins, $sends])) return 2;
+
+ return (mysqli_query($con, "UPDATE scheduletemplates SET name = '$sname', begins = $sbegins, ends = $sends WHERE id = $sid LIMIT 1") ? 0 : 1);
+ }
+
+ public static function _remove($id, $table, $tableDays, $fieldSchedule) {
+ global $con;
+
+ $sid = (int)$id;
+ $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+ $stableDays = preg_replace("/[^A-Za-z0-9 ]/", '', $tableDays);
+ $sfieldSchedule = preg_replace("/[^A-Za-z0-9 ]/", '', $fieldSchedule);
+
+ return (mysqli_query($con, "DELETE FROM $stable WHERE id = $sid LIMIT 1") && mysqli_query($con, "DELETE FROM $stableDays WHERE $fieldSchedule = $sid"));
+ }
+
+ public static function removeTemplate($id) {
+ return self::_remove($id, "scheduletemplates", "scheduletemplatesdays", "template");
+ }
+
+ private static function _exists($id, $table) {
+ global $con;
+
+ $sid = (int)$id;
+ $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+
+ $query = mysqli_query($con, "SELECT id FROM $stable WHERE id = ".(int)$id);
+
+ return (mysqli_num_rows($query) > 0);
+ }
+
+ public static function templateExists($id) {
+ return self::_exists($id, "scheduletemplates");
+ }
+
+ public static function checkAddDayGeneric($begins, $ends, $beginsb, $endsb, $beginsl, $endsl) {
+ global $con;
+
+ $times = [];
+ $times["work"] = [$begins, $ends];
+ $times["breakfast"] = [$beginsb, $endsb];
+ $times["lunch"] = [$beginsl, $endsl];
+
+ foreach ($times as $time) {
+ if (intervals::wellFormed($time) === false) return 1;
+ }
+
+ if (intervals::measure($times["work"]) == 0) return 4;
+
+ if ((!intervals::isSubset($times["breakfast"], $times["work"]) && intervals::measure($times["breakfast"]) != 0) || (!intervals::isSubset($times["lunch"], $times["work"]) && intervals::measure($times["lunch"]) != 0)) return 2;
+
+ if (intervals::overlaps($times["breakfast"], $times["lunch"]) && intervals::measure($times["breakfast"]) != 0 && intervals::measure($times["lunch"]) != 0) return 3;
+
+ return 0;
+ }
+
+ private static function _checkAddDayParticular($id, $dow, $typeday, $table, $fieldSchedule) {
+ global $con;
+
+ $sid = (int)$id;
+ $sdow = (int)$dow;
+ $stypeday = (int)$typeday;
+ $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+ $sfieldSchedule = preg_replace("/[^A-Za-z0-9 ]/", '', $fieldSchedule);
+
+ $query = mysqli_query($con, "SELECT id FROM $stable WHERE $fieldSchedule = $sid AND day = $sdow AND typeday = $stypeday");
+
+ return (!mysqli_num_rows($query));
+ }
+
+ public static function checkAddDay2TemplateParticular($id, $dow, $typeday) {
+ return self::_checkAddDayParticular($id, $dow, $typeday, "scheduletemplatesdays", "template");
+ }
+
+ private static function _addDay($id, $dow, $typeday, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl, $table, $fieldSchedule) {
+ global $con;
+
+ $sid = (int)$id;
+ $sdow = (int)$dow;
+ $stypeday = (int)$typeday;
+ $sbegins = (int)$begins;
+ $sends = (int)$ends;
+ $sbeginsb = (int)$beginsb;
+ $sendsb = (int)$endsb;
+ $sbeginsl = (int)$beginsl;
+ $sendsl = (int)$endsl;
+ $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+ $sfieldSchedule = preg_replace("/[^A-Za-z0-9 ]/", '', $fieldSchedule);
+
+ return mysqli_query($con, "INSERT INTO $stable ($sfieldSchedule, day, typeday, beginswork, endswork, beginsbreakfast, endsbreakfast, beginslunch, endslunch) VALUES ($sid, $sdow, $stypeday, $sbegins, $sends, $sbeginsb, $sendsb, $sbeginsl, $sendsl)");
+ }
+
+ public static function addDay2Template($id, $dow, $typeday, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl) {
+ return self::_addDay($id, $dow, $typeday, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl, "scheduletemplatesdays", "template");
+ }
+
+ public static function _getDay($id, $table) {
+ global $con;
+
+ $sid = (int)$id;
+ $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+
+ $query = mysqli_query($con, "SELECT * FROM $stable WHERE id = $sid");
+
+ if (!mysqli_num_rows($query)) return false;
+
+ return mysqli_fetch_assoc($query);
+ }
+
+ public static function getTemplateDay($id) {
+ return self::_getDay($id, "scheduletemplatesdays");
+ global $con;
+ }
+
+ private static function _editDay($id, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl, $table) {
+ global $con;
+
+ $sid = (int)$id;
+ $sbegins = (int)$begins;
+ $sends = (int)$ends;
+ $sbeginsb = (int)$beginsb;
+ $sendsb = (int)$endsb;
+ $sbeginsl = (int)$beginsl;
+ $sendsl = (int)$endsl;
+ $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+
+ return mysqli_query($con, "UPDATE $stable SET beginswork = $sbegins, endswork = $sends, beginsbreakfast = $sbeginsb, endsbreakfast = $sendsb, beginslunch = $sbeginsl, endslunch = $sendsl WHERE id = $sid LIMIT 1");
+ }
+
+ public static function editTemplateDay($id, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl) {
+ return self::_editDay($id, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl, "scheduletemplatesdays");
+ }
+
+ private static function _removeDay($id, $table) {
+ global $con;
+
+ $sid = (int)$id;
+ $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+
+ return mysqli_query($con, "DELETE FROM $stable WHERE id = $sid LIMIT 1");
+ }
+
+ public static function removeTemplateDay($id) {
+ return self::_removeDay($id, "scheduletemplatesdays");
+ }
+
+ private static function _dayExists($id, $table) {
+ global $con;
+
+ $sid = (int)$id;
+ $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+
+ $query = mysqli_query($con, "SELECT id FROM $stable WHERE id = $sid");
+
+ return (mysqli_num_rows($query) > 0);
+ }
+
+ public static function templateDayExists($id) {
+ return self::_dayExists($id, "scheduletemplatesdays");
+ }
+
+ // SCHEDULES:
+
+ public static function checkOverlap($worker, $begins, $ends, $sans = 0) {
+ global $con;
+
+ $sworker = (int)$worker;
+ $sbegins = (int)$begins;
+ $sends = (int)$ends;
+ $ssans = (int)$sans;
+
+ $query = mysqli_query($con, "SELECT * FROM schedules WHERE begins <= $sends AND ends >= $sbegins AND worker = $sworker".($sans == 0 ? "" : " AND id <> $ssans")." LIMIT 1");
+
+ return (mysqli_num_rows($query) > 0);
+ }
+
+ public static function get($id) {
+ return self::_get($id, "schedules", "schedulesdays", "schedule");
+ }
+
+ public static function getAll($id, $showNotActive = true) {
+ global $con, $conf;
+
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT * FROM schedules WHERE worker = $sid".($showNotActive ? "" : " AND active = 1")." ORDER BY ".($conf["debug"] ? "id ASC" : "begins DESC"));
+
+ $schedules = [];
+
+ while ($row = mysqli_fetch_assoc($query)) {
+ $schedules[] = $row;
+ }
+
+ return $schedules;
+ }
+
+ public static function getCurrent($id = "ME", $isWorker = false) {
+ global $con;
+
+ if ($id == "ME") $id = people::userData("id");
+ $sid = (int)$id;
+
+ $date = new DateTime(date("Y-m-d")."T00:00:00");
+ $timestamp = (int)$date->getTimestamp();
+
+ $query = mysqli_query($con, "SELECT s.* FROM schedules s ".($isWorker ? "WHERE s.worker = $sid" : "LEFT JOIN workers w ON s.worker = w.id WHERE w.person = $sid")." AND s.active = 1 AND s.begins <= $timestamp AND s.ends >= $timestamp");
+
+ $return = [];
+
+ while ($row = mysqli_fetch_assoc($query)) {
+ self::_addDaysToReturn($row, "schedulesdays", "schedule", $row["id"]);
+ $return[] = $row;
+ }
+
+ return $return;
+ }
+
+ public static function getWorkerScheduleStatus($id) {
+ $currentSchedules = self::getCurrent($id, true);
+ if ($currentSchedules === false || !count($currentSchedules)) return ["status" => self::STATUS_NO_ACTIVE_SCHEDULE];
+
+ $schedule =& $currentSchedules[0];
+
+ foreach (calendars::$workingTypes as $type) {
+ if (!isset($schedule["days"][$type]) || !count($schedule["days"][$type])) return ["status" => self::STATUS_HALFWAY_CONFIGURED_SCHEDULE, "schedule" => $schedule["id"]];
+ }
+
+ return ["status" => self::STATUS_ACTIVE_SCHEDULE, "schedule" => $schedule["id"]];
+ }
+
+ public static function add($worker, $ibegins, $iends, $active = 0, $alreadyTimestamp = false) {
+ global $con;
+
+ $sworker = (int)$worker;
+ $sactive = (int)$active;
+ if ($alreadyTimestamp) {
+ $sbegins = (int)$ibegins;
+ $sends = (int)$iends;
+ } else {
+ $begins = new DateTime($ibegins);
+ $sbegins = (int)$begins->getTimestamp();
+ $ends = new DateTime($iends);
+ $sends = (int)$ends->getTimestamp();
+ }
+
+ if (!intervals::wellFormed([$sbegins, $sends])) return 3;
+
+ if (self::checkOverlap($worker, $sbegins, $sends)) {
+ return 1;
+ }
+
+ return (mysqli_query($con, "INSERT INTO schedules (worker, begins, ends, active) VALUES ('$sworker', $sbegins, $sends, $sactive)") ? 0 : 2);
+ }
+
+ public static function edit($id, $ibegins, $iends) {
+ global $con;
+
+ $sid = (int)$id;
+ $begins = new DateTime($ibegins);
+ $sbegins = (int)$begins->getTimestamp();
+ $ends = new DateTime($iends);
+ $sends = (int)$ends->getTimestamp();
+
+ if (!intervals::wellFormed([$sbegins, $sends])) return 3;
+
+ $actual = self::get($sid);
+ if ($actual === false) return 4;
+
+ if (self::checkOverlap($actual["worker"], $sbegins, $sends, $sid)) {
+ return 1;
+ }
+
+ return (mysqli_query($con, "UPDATE schedules SET begins = $sbegins, ends = $sends WHERE id = $sid LIMIT 1") ? 0 : 2);
+ }
+
+ public static function remove($id) {
+ return self::_remove($id, "schedules", "schedulesdays", "schedule");
+ }
+
+ public static function switchActive($id, $value) {
+ global $con;
+
+ $sid = (int)$id;
+ $svalue = (int)$value;
+ if ($svalue > 1 || $svalue < 0) return false;
+
+ return mysqli_query($con, "UPDATE schedules SET active = $svalue WHERE id = $sid LIMIT 1");
+ }
+
+ public static function exists($id) {
+ return self::_exists($id, "schedules");
+ }
+
+ public static function checkAddDay2ScheduleParticular($id, $dow, $typeday) {
+ return self::_checkAddDayParticular($id, $dow, $typeday, "schedulesdays", "schedule");
+ }
+
+ public static function addDay2Schedule($id, $dow, $typeday, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl) {
+ return self::_addDay($id, $dow, $typeday, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl, "schedulesdays", "schedule");
+ }
+
+ public static function getDay($id) {
+ return self::_getDay($id, "schedulesdays");
+ global $con;
+ }
+
+ public static function editDay($id, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl) {
+ return self::_editDay($id, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl, "schedulesdays");
+ }
+
+ public static function dayExists($id) {
+ return self::_dayExists($id, "schedulesdays");
+ }
+
+ public static function removeDay($id) {
+ return self::_removeDay($id, "schedulesdays");
+ }
+
+ public static function copyTemplate($template, $worker, $active) {
+ global $con;
+
+ $template = self::getTemplate($template);
+ if ($template === false) return 1;
+
+ $status = self::add($worker, $template["begins"], $template["ends"], $active, true);
+ if ($status != 0) return ($status + 1);
+
+ $id = mysqli_insert_id($con);
+
+ foreach ($template["days"] as $typeday) {
+ foreach ($typeday as $day) {
+ $status2 = self::addDay2Schedule($id, $day["day"], $day["typeday"], $day["beginswork"], $day["endswork"], $day["beginsbreakfast"], $day["endsbreakfast"], $day["beginslunch"], $day["endslunch"]);
+ if (!$status2) return -1;
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/src/inc/schedulesView.php b/src/inc/schedulesView.php
new file mode 100644
index 0000000..aab3a2f
--- /dev/null
+++ b/src/inc/schedulesView.php
@@ -0,0 +1,124 @@
+<?php
+class schedulesView {
+ const HOUR_HEIGHT = 30;
+
+ public static function renderSchedule(&$week, $editEnabled = false, $editLink, $deleteLink) {
+ $secmin = 24*60*60;
+ $secmax = 0;
+
+ foreach ($week as $day) {
+ if ($day["beginswork"] < $secmin) $secmin = $day["beginswork"];
+ if ($day["endswork"] > $secmax) $secmax = $day["endswork"];
+ }
+
+ $min = max(floor($secmin/(60*60)) - 1, 0);
+ $max = min(ceil($secmax/(60*60)) + 1, 24);
+ ?>
+ <div class="overflow-wrapper overflow-wrapper--for-table" style="text-align: center;">
+ <div class="schedule mdl-shadow--2dp">
+ <div class="sidetime">
+ <?php
+ for ($hour = $min; $hour <= $max; $hour++) {
+ ?>
+ <div class="hour">
+ <div class="hour--text"><?=security::htmlsafe(visual::padNum($hour, 2).":00")?></div>
+ </div>
+ <?php
+ }
+ ?>
+ </div>
+ <?php
+ for ($day = 0; $day < 7; $day++) {
+ $isset = isset($week[$day]);
+
+ if (!$isset && $day >= 5 && !isset($week[5]) && !isset($week[6])) continue;
+
+ if ($isset) {
+ $size = [];
+ $size["work"] = [];
+ $size["work"]["top"] = (($week[$day]["beginswork"]/(60*60)) - $min)*self::HOUR_HEIGHT;
+ $size["work"]["height"] = intervals::measure([$week[$day]["beginswork"], $week[$day]["endswork"]])/(60*60)*self::HOUR_HEIGHT;
+
+ foreach (schedules::$otherEvents as $event) {
+ if (intervals::measure([$week[$day]["begins".$event], $week[$day]["ends".$event]]) > 0) {
+ $size[$event] = [];
+ $size[$event]["top"] = ($week[$day]["begins".$event] - $week[$day]["beginswork"])/(60*60)*self::HOUR_HEIGHT;
+ $size[$event]["height"] = intervals::measure([$week[$day]["begins".$event], $week[$day]["ends".$event]])/(60*60)*self::HOUR_HEIGHT;
+ }
+ }
+ }
+ ?>
+ <div class="day">
+ <div class="day--header"><?=security::htmlsafe(calendars::$days[$day])?></div>
+ <div class="day--content">
+ <?php
+ if ($isset) {
+ ?>
+ <div class="work-event" style="top: <?=(int)round($size["work"]["top"])?>px; height: <?=(int)round($size["work"]["height"])?>px;">
+ <?php
+ if ($editEnabled) {
+ ?>
+ <div class="event--actions">
+ <a href='<?=$editLink($week[$day])?>' data-dyndialog-href='<?=$editLink($week[$day])?>' title='Modificar horario'><i class='material-icons'>edit</i></a>
+ <a href='<?=$deleteLink($week[$day])?>' data-dyndialog-href='<?=$deleteLink($week[$day])?>' title='Eliminar horario'><i class='material-icons'>delete</i></a>
+ </div>
+ <?php
+ }
+ ?>
+ <div class="event--header">Trabajo</div>
+ <div class="event--body"><?=security::htmlsafe(schedules::sec2time($week[$day]["beginswork"]))?> - <?=security::htmlsafe(schedules::sec2time($week[$day]["endswork"]))?></div>
+ <?php
+ foreach (schedules::$otherEvents as $event) {
+ if (isset($size[$event])) {
+ ?>
+ <div class="inline-event" style="top: <?=(int)round($size[$event]["top"])?>px; height: <?=(int)round($size[$event]["height"])?>px;">
+ <div class="event--header"><?=security::htmlsafe(schedules::$otherEventsDescription[$event])?></div>
+ <div class="event--body"></div>
+ </div>
+ <?php
+ }
+ }
+ ?>
+ </div>
+ <?php
+ }
+
+ for ($hour = $min; $hour <= $max; $hour++) {
+ ?>
+ <div class="hour"></div>
+ <?php
+ }
+ ?>
+ </div>
+ </div>
+ <?php
+ }
+ ?>
+ </div>
+ </div>
+ <?php
+ }
+
+ public static function renderPlainSchedule($week, $editEnabled = false, $editLink, $deleteLink, &$flag) {
+ foreach ($week as $day) {
+ $flag = true;
+ ?>
+ <h4><?=security::htmlsafe(calendars::$days[$day["day"]])?> <span class="mdl-color-text--grey-600">(<?=security::htmlsafe(calendars::$types[$day["typeday"]])?>)</span>
+ <?php
+ if ($editEnabled) {
+ ?>
+ <a href='<?=$editLink($day)?>' data-dyndialog-href='<?=$editLink($day)?>' title='Modificar horario' class='mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect'><i class='material-icons icon'>edit</i></a>
+ <a href='<?=$deleteLink($day)?>' data-dyndialog-href='<?=$deleteLink($day)?>' title='Eliminar horario' class='mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect'><i class='material-icons icon'>delete</i></a>
+ <?php
+ }
+ ?>
+ </h4>
+ <ul>
+ <li><b>Horario de trabajo:</b> <?=security::htmlsafe(schedules::sec2time($day["beginswork"]))?> - <?=security::htmlsafe(schedules::sec2time($day["endswork"]))?></li>
+ <?php if (intervals::measure([$day["beginsbreakfast"], $day["endsbreakfast"]]) > 0) { ?><li><b>Desayuno:</b> <?=security::htmlsafe(schedules::sec2time($day["beginsbreakfast"]))?> - <?=security::htmlsafe(schedules::sec2time($day["endsbreakfast"]))?></li><?php } ?>
+ <?php if (intervals::measure([$day["beginslunch"], $day["endslunch"]]) > 0) { ?><li><b>Comida:</b> <?=security::htmlsafe(schedules::sec2time($day["beginslunch"]))?> - <?=security::htmlsafe(schedules::sec2time($day["endslunch"]))?></li><?php } ?>
+ </ul>
+ <?php
+ }
+ }
+}
diff --git a/src/inc/secondFactor.php b/src/inc/secondFactor.php
new file mode 100644
index 0000000..1b0de75
--- /dev/null
+++ b/src/inc/secondFactor.php
@@ -0,0 +1,288 @@
+<?php
+require_once(__DIR__."/../lib/GoogleAuthenticator/GoogleAuthenticator.php");
+require_once(__DIR__."/../lib/WebAuthn/WebAuthn.php");
+
+class secondFactor {
+ public static function isAvailable() {
+ global $conf;
+ return (($conf["secondFactor"]["enabled"] ?? false) === true);
+ }
+
+ public static function checkAvailability() {
+ global $conf;
+
+ if (!self::isAvailable()) {
+ security::notFound();
+ }
+ }
+
+ public static function isEnabled($person = "ME") {
+ if (!self::isAvailable()) return false;
+ return (people::userData("secondfactor", $person) == 1);
+ }
+
+ public static function generateSecret() {
+ $authenticator = new GoogleAuthenticator();
+ return $authenticator->generateSecret();
+ }
+
+ public static function isValidSecret($secret) {
+ return (strlen($secret) === 32);
+ }
+
+ public static function checkCode($secret, $code) {
+ $authenticator = new GoogleAuthenticator();
+ return $authenticator->checkCode($secret, $code);
+ }
+
+ public static function checkPersonCode($person, $code) {
+ global $con;
+
+ $sperson = (int)$person;
+ $query = mysqli_query($con, "SELECT secret FROM totp WHERE person = $sperson LIMIT 1");
+ if ($query === false || !mysqli_num_rows($query)) return false;
+
+ $row = mysqli_fetch_assoc($query);
+
+ return self::checkCode($row["secret"], $code);
+ }
+
+ public static function disable($person = "ME", $personDisable = true) {
+ global $con;
+
+ if ($person == "ME") $person = people::userData("id");
+ $sperson = (int)$person;
+
+ return (mysqli_query($con, "DELETE FROM totp WHERE person = $sperson") && (!$personDisable || mysqli_query($con, "UPDATE people SET secondfactor = 0 WHERE id = $sperson LIMIT 1")) && mysqli_query($con, "DELETE FROM securitykeys WHERE person = $sperson"));
+ }
+
+ public static function enable($secret, $person = "ME") {
+ global $con;
+
+ if (!self::isValidSecret($secret)) return false;
+
+ if ($person == "ME") $person = people::userData("id");
+
+ self::disable($person, false);
+
+ $sperson = (int)$person;
+ $ssecret = db::sanitize($secret);
+ return (mysqli_query($con, "INSERT INTO totp (person, secret) VALUES ($sperson, '$ssecret')") && mysqli_query($con, "UPDATE people SET secondfactor = 1 WHERE id = $sperson LIMIT 1"));
+ }
+
+ private static function completeLogin($success) {
+ if ($success === true) {
+ $_SESSION["id"] = $_SESSION["firstfactorid"];
+ unset($_SESSION["firstfactorid"]);
+ return true;
+ }
+
+ if ($success === false) {
+ unset($_SESSION["firstfactorid"]);
+ return true;
+ }
+
+ return false;
+ }
+
+ public static function completeCodeChallenge($code) {
+ global $_SESSION;
+
+ $success = self::checkPersonCode($_SESSION["firstfactorid"], $code);
+ self::completeLogin($success);
+ return $success;
+ }
+
+ private static function newWebAuthn() {
+ global $conf;
+
+ if (!isset($conf["secondFactor"]) || !isset($conf["secondFactor"]["origin"])) {
+ throw new Exception('secondFactor is not enabled (or the origin is not set) in config.php.');
+ }
+
+ return new \lbuchs\WebAuthn\WebAuthn($conf["appName"], $conf["secondFactor"]["origin"], ["none"]);
+ }
+
+ public static function createRegistrationChallenge() {
+ global $_SESSION;
+
+ $WebAuthn = self::newWebAuthn();
+
+ $credentialIds = self::getCredentialIds();
+ $createArgs = $WebAuthn->getCreateArgs(people::userData("id"), people::userData("username"), people::userData("name"), 20, false, false, ($credentialIds === false ? [] : $credentialIds));
+ $_SESSION['webauthnchallenge'] = $WebAuthn->getChallenge();
+
+ return $createArgs;
+ }
+
+ public static function addSecurityKeyToDB($credentialId, $credentialPublicKey, $name) {
+ global $con;
+
+ $person = people::userData("id");
+ if ($person === false) return false;
+ $sperson = (int)$person;
+
+ $sname = db::sanitize($name);
+ $scredentialId = db::sanitize($credentialId);
+ $scredentialPublicKey = db::sanitize($credentialPublicKey);
+ $stime = (int)time();
+
+ return mysqli_query($con, "INSERT INTO securitykeys (person, name, credentialid, credentialpublickey, added) VALUES ($sperson, '$sname', '$scredentialId', '$scredentialPublicKey', $stime)");
+ }
+
+ public static function completeRegistrationChallenge($clientDataJSON, $attestationObject, $name) {
+ global $_SESSION;
+
+ $clientDataJSON = base64_decode($clientDataJSON);
+ $attestationObject = base64_decode($attestationObject);
+
+ if (!isset($_SESSION["webauthnchallenge"])) {
+ throw new Exception('The user didn\'t start the webauthn challenge.');
+ }
+ $challenge = $_SESSION["webauthnchallenge"];
+ unset($_SESSION["webauthnchallenge"]);
+
+ $WebAuthn = self::newWebAuthn();
+ $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge);
+
+ if (!self::addSecurityKeyToDB($data->credentialId, $data->credentialPublicKey, $name)) {
+ throw new Exception('Failed adding security key to DB.');
+ }
+
+ return ["status" => "ok"];
+ }
+
+ public static function getSecurityKeys($person = "ME") {
+ global $con;
+
+ if ($person == "ME") $person = people::userData("id");
+ if ($person === false) return false;
+ $sperson = (int)$person;
+
+ $query = mysqli_query($con, "SELECT * FROM securitykeys WHERE person = $sperson");
+ if ($query === false) return false;
+
+ $securityKeys = [];
+ while ($row = mysqli_fetch_assoc($query)) {
+ $securityKeys[] = $row;
+ }
+
+ return $securityKeys;
+ }
+
+ public static function getCredentialIds($person = "ME") {
+ $securityKeys = self::getSecurityKeys($person);
+ if ($securityKeys === false) return false;
+
+ $credentials = [];
+ foreach ($securityKeys as $s) $credentials[] = $s["credentialid"];
+
+ return $credentials;
+ }
+
+ public static function hasSecurityKeys($person) {
+ global $con;
+
+ $sperson = (int)$person;
+
+ $query = mysqli_query($con, "SELECT 1 FROM securitykeys WHERE person = $sperson LIMIT 1");
+ if ($query === false) return false;
+
+ return (mysqli_num_rows($query) > 0);
+ }
+
+ public static function createValidationChallenge() {
+ global $_SESSION;
+
+ $WebAuthn = self::newWebAuthn();
+
+ if (!isset($_SESSION["firstfactorid"])) {
+ throw new Exception('User didn\'t log in with the first factor.');
+ }
+ $credentialIds = self::getCredentialIds($_SESSION["firstfactorid"]);
+ if ($credentialIds === false || empty($credentialIds)) {
+ throw new Exception('The user credentials could not be obtained.');
+ }
+
+ $getArgs = $WebAuthn->getGetArgs($credentialIds);
+ $_SESSION['webauthnvalidationchallenge'] = $WebAuthn->getChallenge();
+
+ return $getArgs;
+ }
+
+ public static function getSecurityKey($credentialId, $person) {
+ $securityKeys = self::getSecurityKeys($person);
+
+ foreach ($securityKeys as $s) {
+ if ($s["credentialid"] == $credentialId) {
+ return $s;
+ }
+ }
+
+ return null;
+ }
+
+ public static function getSecurityKeyById($id) {
+ global $con;
+
+ $query = mysqli_query($con, "SELECT * FROM securitykeys WHERE id = $id");
+ if ($query === false || !mysqli_num_rows($query)) return false;
+
+ return mysqli_fetch_assoc($query);
+ }
+
+ public static function removeSecurityKey($id) {
+ global $con;
+
+ $sid = (int)$id;
+ $sperson = (int)people::userData("id");
+ if ($sperson === false) return false;
+
+ return mysqli_query($con, "DELETE FROM securitykeys WHERE id = $sid and PERSON = $sperson LIMIT 1");
+ }
+
+ private static function recordSecurityKeyUsageToDB($id) {
+ global $con;
+
+ $sid = (int)$id;
+ $stime = (int)time();
+
+ return mysqli_query($con, "UPDATE securitykeys SET lastused = $stime WHERE id = $sid LIMIT 1");
+ }
+
+ public static function completeValidationChallenge($id, $clientDataJSON, $authenticatorData, $signature) {
+ global $_SESSION;
+
+ $id = base64_decode($id);
+ $clientDataJSON = base64_decode($clientDataJSON);
+ $authenticatorData = base64_decode($authenticatorData);
+ $signature = base64_decode($signature);
+
+ if (!isset($_SESSION["webauthnvalidationchallenge"])) {
+ throw new Exception('The user didn\'t start the webauthn challenge.');
+ }
+ $challenge = $_SESSION["webauthnvalidationchallenge"];
+ unset($_SESSION["webauthnvalidationchallenge"]);
+
+ $securityKey = self::getSecurityKey($id, $_SESSION["firstfactorid"]);
+ $credentialPublicKey = $securityKey["credentialpublickey"] ?? null;
+ if ($credentialPublicKey === null) {
+ self::completeLogin(false);
+ throw new Exception('The security key could not be found.');
+ }
+
+ try {
+ $WebAuthn = self::newWebAuthn();
+
+ $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge);
+ } catch (Throwable $ex) {
+ self::completeLogin(false);
+ throw $ex;
+ }
+
+ self::recordSecurityKeyUsageToDB($securityKey["id"]);
+ self::completeLogin(true);
+
+ return ["status" => "ok"];
+ }
+}
diff --git a/src/inc/secondFactorView.php b/src/inc/secondFactorView.php
new file mode 100644
index 0000000..7ed9712
--- /dev/null
+++ b/src/inc/secondFactorView.php
@@ -0,0 +1,9 @@
+<?php
+class secondFactorView {
+ public static function renderSecret($secret) {
+ for ($i = 0; $i < strlen($secret); $i++) {
+ if ($i != 0 && $i % 4 == 0) echo " ";
+ echo $secret[$i];
+ }
+ }
+}
diff --git a/src/inc/security.php b/src/inc/security.php
new file mode 100644
index 0000000..1d3159b
--- /dev/null
+++ b/src/inc/security.php
@@ -0,0 +1,362 @@
+<?php
+class security {
+ const HYPERADMIN = 0;
+ const ADMIN = 2;
+ /*const SYSTEM_REVIEWER = 5;
+ const LOG_REVIEWER = 7;*/
+ const WORKER = 10;
+ const UNKNOWN = 1000;
+
+ const METHOD_UNSPECIFICED = -1;
+ const METHOD_REDIRECT = 0;
+ const METHOD_NOTFOUND = 1;
+
+ const PARAM_ISSET = 1;
+ const PARAM_NEMPTY = 2;
+ const PARAM_ISEMAIL = 4;
+ const PARAM_ISDATE = 8;
+ const PARAM_ISINT = 16;
+ const PARAM_ISTIME = 32;
+ const PARAM_ISARRAY = 64;
+ const PARAM_ISEMAILOREMPTY = 128;
+
+ const SIGNIN_STATE_SIGNED_IN = 0;
+ const SIGNIN_STATE_NEEDS_SECOND_FACTOR = 1;
+ const SIGNIN_STATE_SIGNED_OUT = 2;
+ const SIGNIN_STATE_THROTTLED = 3;
+
+ public static $types = [
+ 10 => "Trabajador",
+ /*7 => "Revisor de logs",
+ 5 => "Revisor del sistema",*/
+ 2 => "Administrador",
+ 0 => "Hiperadministrador"
+ ];
+
+ public static $automatedChecks = [self::PARAM_ISSET, self::PARAM_NEMPTY, self::PARAM_ISEMAIL, self::PARAM_ISDATE, self::PARAM_ISINT, self::PARAM_ISTIME, self::PARAM_ISARRAY, self::PARAM_ISEMAILOREMPTY];
+
+ public static $passwordHelperText = "La contraseña debe tener como mínimo 8 caracteres y una letra mayúscula.";
+
+ public static function go($page) {
+ global $conf;
+
+ if ($conf["superdebug"]) {
+ echo "Redirects are not enabled. We would like to redirect you here: <a href='".self::htmlsafe($page)."'>".self::htmlsafe($page)."</a>";
+ } else {
+ header("Location: ".$page);
+ }
+
+ exit();
+ }
+
+ public static function goHome() {
+ self::go("index.php");
+ }
+
+ public static function notFound() {
+ header('HTTP/1.0 404 Not Found');
+ exit();
+ }
+
+ public static function isSignedIn() {
+ global $_SESSION;
+
+ return isset($_SESSION["id"]);
+ }
+
+ public static function check() {
+ if (!self::isSignedIn()) {
+ self::goHome();
+ }
+ }
+
+ public static function userType() {
+ global $_SESSION, $con;
+
+ if (!isset($_SESSION["id"])) {
+ return self::UNKNOWN;
+ }
+
+ $query = mysqli_query($con, "SELECT type FROM people WHERE id = ".(int)$_SESSION["id"]);
+
+ if (!mysqli_num_rows($query)) {
+ return self::UNKNOWN;
+ }
+
+ $row = mysqli_fetch_assoc($query);
+
+ return $row["type"];
+ }
+
+ public static function isAllowed($type) {
+ $userType = (self::isSignedIn() ? self::userType() : self::UNKNOWN);
+
+ return $userType <= $type;
+ }
+
+ public static function denyUseMethod($method = self::METHOD_REDIRECT) {
+ if ($method === self::METHOD_NOTFOUND) {
+ self::notFound();
+ } else { // self::METHOD_REDIRECT or anything else
+ self::goHome();
+ }
+ }
+
+ public static function checkType($type, $method = self::METHOD_REDIRECT) {
+ if (!self::isAllowed($type)) {
+ self::denyUseMethod($method);
+ }
+ }
+
+ public static function checkWorkerUIEnabled() {
+ global $conf;
+
+ if (self::userType() >= self::WORKER && !$conf["enableWorkerUI"]) {
+ self::go("index.php?msg=unsupported");
+ }
+ }
+
+ // Code from https://timoh6.github.io/2015/05/07/Rate-limiting-web-application-login-attempts.html
+ private static function getIpAddresses() {
+ $ips = [];
+ $ips["remoteIp"] = inet_pton($_SERVER['REMOTE_ADDR']); // inet_pton can handle both IPv4 and IPv6 addresses, treat IPv6 addresses as /64 or /56 blocks.
+ $ips["remoteIpBlock"] = long2ip(ip2long($_SERVER['REMOTE_ADDR']) & 0xFFFFFF00); // Something like this to turn the last octet of IPv4 address into a 0.
+ return $ips;
+ }
+
+ public static function recordLoginAttempt($username) {
+ global $con;
+
+ $susername = db::sanitize($username);
+
+ $ips = self::getIpAddresses();
+ $sremoteIp = db::sanitize($ips["remoteIp"]);
+ $sremoteIpBlock = db::sanitize($ips["remoteIpBlock"]);
+
+ return mysqli_query($con, "INSERT INTO signinattempts (username, remoteip, remoteipblock, signinattempttime) VALUES ('$susername', '$sremoteIp', '$sremoteIpBlock', NOW())");
+ }
+
+ public static function getRateLimitingCounts($username) {
+ global $con, $conf;
+
+ $susername = db::sanitize($username);
+
+ $ips = self::getIpAddresses();
+ $sremoteIp = db::sanitize($ips["remoteIp"]);
+ $sremoteIpBlock = db::sanitize($ips["remoteIpBlock"]);
+
+ $query = mysqli_query($con, "SELECT
+ COUNT(*) AS global_attempt_count,
+ IFNULL(SUM(CASE WHEN remoteip = '$sremoteIp' THEN 1 ELSE 0 END), 0) AS ip_attempt_count,
+ IFNULL(SUM(CASE WHEN (remoteipblock = '$sremoteIpBlock') THEN 1 ELSE 0 END), 0) AS ip_block_attempt_count,
+ (SELECT COUNT(DISTINCT remoteipblock) FROM signinattempts WHERE username = '$susername' AND signinattempttime >= (NOW() - INTERVAL 10 SECOND )) AS ip_blocks_per_username_attempt_count,
+ (SELECT COUNT(*) FROM signinattempts WHERE username = '$susername' AND signinattempttime >= (NOW() - INTERVAL 10 SECOND )) AS username_attempt_count
+ FROM signinattempts
+ WHERE signinattempttime >= (NOW() - INTERVAL 10 second)");
+
+ if ($query === false) return false;
+
+ return mysqli_fetch_assoc($query);
+ }
+
+ public static function isSignInThrottled($username) {
+ global $conf;
+
+ $count = self::getRateLimitingCounts($username);
+
+ if ($count["global_attempt_count"] >= $conf["signinThrottling"]["attemptCountLimit"]["global"] ||
+ $count["ip_attempt_count"] >= $conf["signinThrottling"]["attemptCountLimit"]["ip"] ||
+ $count["ip_block_attempt_count"] >= $conf["signinThrottling"]["attemptCountLimit"]["ipBlock"] ||
+ $count["ip_blocks_per_username_attempt_count"] >= $conf["signinThrottling"]["attemptCountLimit"]["ipBlocksPerUsername"] ||
+ $count["username_attempt_count"] >= $conf["signinThrottling"]["attemptCountLimit"]["username"]) {
+ return true;
+ }
+
+ if (!self::recordLoginAttempt($username)) {
+ echo "There was an unexpected error, so you could not be authenticated. Please contact me@avm99963.com and let them know of the error. (2)";
+ exit();
+ }
+
+ return false;
+ }
+
+ public static function isUserPassword($username, $password) {
+ global $con, $_SESSION;
+
+ $susername = db::sanitize($username);
+ $query = mysqli_query($con, "SELECT id, password FROM people WHERE ".($username === false ? "id = ".(int)$_SESSION["id"] : "username = '".$susername."'"));
+
+ if (!mysqli_num_rows($query)) {
+ return false;
+ }
+
+ $row = mysqli_fetch_assoc($query);
+
+ if (!password_verify($password, $row["password"])) {
+ return false;
+ }
+
+ return $row["id"];
+ }
+
+ public static function signIn($username, $password) {
+ global $_SESSION;
+
+ if (self::isSignInThrottled($username)) return self::SIGNIN_STATE_THROTTLED;
+
+ $id = self::isUserPassword($username, $password);
+
+ if ($id !== false) {
+ if (secondFactor::isEnabled($id)) {
+ $_SESSION["firstfactorid"] = $id;
+ return self::SIGNIN_STATE_NEEDS_SECOND_FACTOR;
+ } else {
+ $_SESSION["id"] = $id;
+ return self::SIGNIN_STATE_SIGNED_IN;
+ }
+ }
+
+ return self::SIGNIN_STATE_SIGNED_OUT;
+ }
+
+ public static function redirectAfterSignIn() {
+ global $conf;
+
+ if (self::isAllowed(self::ADMIN)) {
+ self::changeActiveView(visual::VIEW_ADMIN);
+ self::go("home.php");
+ } else {
+ self::changeActiveView(visual::VIEW_WORKER);
+ self::go(($conf["enableWorkerUI"] ? "workerhome.php" : "index.php?msg=unsupported"));
+ }
+ }
+
+ public static function logout() {
+ global $_SESSION;
+
+ session_destroy();
+ }
+
+ public static function htmlsafe($string) {
+ if ($string === null) return '';
+ return htmlspecialchars((string)$string);
+ }
+
+ private static function failedCheckParams($parameter, $method, $check) {
+ global $conf;
+
+ if ($conf["superdebug"]) {
+ echo "Failed check ".(int)$check." using parameter '".self::htmlsafe($parameter)."' with method ".self::htmlsafe($method).".<br>";
+ }
+ }
+
+ public static function checkParam($param, $check) {
+ if ($check == self::PARAM_NEMPTY && empty($param)) {
+ return false;
+ }
+
+ if ($check == self::PARAM_ISEMAIL && filter_var($param, FILTER_VALIDATE_EMAIL) === false) {
+ return false;
+ }
+
+ if ($check == self::PARAM_ISDATE && preg_match("/^[0-9]+-[0-9]+-[0-9]+$/", $param) !== 1) {
+ return false;
+ }
+
+ if ($check == self::PARAM_ISINT && filter_var($param, FILTER_VALIDATE_INT) === false) {
+ return false;
+ }
+
+ if ($check == self::PARAM_ISTIME) {
+ if (preg_match("/^[0-9]+:[0-9]+$/", $param) !== 1) return false;
+
+ $time = explode(":", $param);
+ if ((int)$time[0] >= 24 || (int)$time[1] >= 60) return false;
+ }
+
+ if ($check == self::PARAM_ISARRAY) {
+ // Check whether the parameter is an array
+ if (is_array($param) === false) return false;
+
+ // Check that it is not a multidimensional array (we don't want that for any parameter!)
+ foreach ($param as &$el) {
+ if (is_array($el)) return false;
+ }
+ }
+
+ if ($check == self::PARAM_ISEMAILOREMPTY && $param !== "" && filter_var($param, FILTER_VALIDATE_EMAIL) === false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static function checkParams($method, $parameters, $forceDisableDebug = false) {
+ global $_POST, $_GET;
+
+ if (!in_array($method, ["GET", "POST"])) {
+ return false;
+ }
+
+ foreach ($parameters as $p) {
+ if (!$p[1]) {
+ continue;
+ }
+
+ if (($method == "POST" && !isset($_POST[$p[0]])) || ($method == "GET" && !isset($_GET[$p[0]]))) {
+ if (!$forceDisableDebug) self::failedCheckParams($p[0], $method, self::PARAM_ISSET);
+ return false;
+ }
+
+ $value = ($method == "POST" ? $_POST[$p[0]] : $_GET[$p[0]]);
+
+ foreach (self::$automatedChecks as $check) {
+ if (($p[1] & $check) && !self::checkParam($value, $check)) {
+ if (!$forceDisableDebug) self::failedCheckParams($p[0], $method, $check);
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public static function existsType($type) {
+ $types = array_keys(self::$types);
+
+ return in_array($type, $types);
+ }
+
+ public static function getActiveView() {
+ global $_SESSION;
+
+ if (!self::isAllowed(self::ADMIN)) {
+ return visual::VIEW_WORKER;
+ }
+
+ return (!isset($_SESSION["activeView"]) ? visual::VIEW_ADMIN : $_SESSION["activeView"]);
+ }
+
+ public static function isAdminView() {
+ return (self::getActiveView() === visual::VIEW_ADMIN);
+ }
+
+ public static function changeActiveView($view) {
+ $_SESSION["activeView"] = $view;
+ }
+
+ public static function passwordIsGoodEnough($password) {
+ return (strlen($password) >= 8 && preg_match('/[A-Z]/', $password));
+ }
+
+ public static function cleanSignInAttempts($retentionDays = "DEFAULT") {
+ global $con, $conf;
+
+ if ($retentionDays === "DEFAULT") $retentionDays = $conf["signinThrottling"]["retentionDays"];
+
+ $sretentionDays = (int)$retentionDays;
+ if ($retentionDays < 0) return false;
+
+ return mysqli_query($con, "DELETE FROM signinattempts WHERE signinattempttime < (NOW() - INTERVAL $sretentionDays DAY)");
+ }
+}
diff --git a/src/inc/validations.php b/src/inc/validations.php
new file mode 100644
index 0000000..d3c310f
--- /dev/null
+++ b/src/inc/validations.php
@@ -0,0 +1,234 @@
+<?php
+class validations {
+ const DEFAULT_REMINDER_GRACE_PERIOD = 3; // When sending email notifications about pending elements to verify,
+ // only elements effective until the current day minus the grace period
+ // will be checked for validation. (value is given in days)
+
+ const METHOD_SIMPLE = 0;
+ const METHOD_AUTOVALIDATION = 1;
+
+ public static $methodCodename = [
+ 0 => "simple",
+ 1 => "autovalidation"
+ ];
+
+ public static $methodName = [
+ 0 => "Validación por dirección IP",
+ 1 => "Validación automática"
+ ];
+
+ public static $methods = [self::METHOD_SIMPLE, self::METHOD_AUTOVALIDATION];
+ public static $manualMethods = [self::METHOD_SIMPLE];
+
+ public static function reminderGracePeriod() {
+ return $conf["validation"]["gracePeriod"] ?? self::DEFAULT_REMINDER_GRACE_PERIOD;
+ }
+
+ public static function numPending($userId = "ME", $gracePeriod = 0) {
+ global $con;
+
+ if ($userId === "ME") $userId = people::userData("id");
+ $suser = (int)$userId;
+
+ $query = mysqli_query($con, "SELECT COUNT(*) count FROM incidents i INNER JOIN workers w ON i.worker = w.id WHERE w.person = ".$suser." AND ".incidents::$workerPendingWhere." AND ".incidents::$activeWhere.($gracePeriod === false ? "" : " AND (i.day + i.begins) < ".(int)(time() - (int)$gracePeriod*(24*60*60))));
+
+ if (!mysqli_num_rows($query)) return "?";
+
+ $row = mysqli_fetch_assoc($query);
+
+ $count = (int)(isset($row["count"]) ? $row["count"] : 0);
+
+ $query2 = mysqli_query($con, "SELECT COUNT(*) count FROM records r INNER JOIN workers w ON r.worker = w.id WHERE w.person = ".$suser." AND ".registry::$notInvalidatedWhere." AND ".registry::$workerPendingWhere.($gracePeriod === false ? "" : " AND r.day < ".(int)(time() - (int)$gracePeriod*(24*60*60))));
+
+ if (!mysqli_num_rows($query2)) return "?";
+
+ $row2 = mysqli_fetch_assoc($query2);
+
+ return $count + (int)(isset($row2["count"]) ? $row2["count"] : 0);
+ }
+
+ public static function getAllowedMethods() {
+ global $conf;
+
+ $allowedMethods = [];
+ foreach (self::$manualMethods as $method) {
+ if (in_array($method, $conf["validation"]["allowedMethods"])) $allowedMethods[] = $method;
+ }
+
+ return $allowedMethods;
+ }
+
+ private static function string2array($string) {
+ if (!is_string($string) || empty($string)) return [];
+ $explode = explode(",", $string);
+
+ $array = [];
+ foreach ($explode as $el) {
+ $array[] = (int)$el;
+ }
+
+ return $array;
+ }
+
+ private static function createSimpleAttestation($method, $user) {
+ $attestation = [];
+ if (!isset($_SERVER["REMOTE_ADDR"])) return false;
+ $attestation["ipAddress"] = (string)$_SERVER["REMOTE_ADDR"];
+ if ($method === self::METHOD_AUTOVALIDATION) $attestation["user"] = (int)$user;
+ return $attestation;
+ }
+
+ private static function createIncidentAttestation(&$incident, $method, $user) {
+ switch ($method) {
+ case self::METHOD_SIMPLE:
+ case self::METHOD_AUTOVALIDATION:
+ return self::createSimpleAttestation($method, $user);
+ break;
+
+ default:
+ return false;
+ }
+ }
+
+ private static function createRecordAttestation(&$record, $method, $user) {
+ switch ($method) {
+ case self::METHOD_SIMPLE:
+ case self::METHOD_AUTOVALIDATION:
+ return self::createSimpleAttestation($method, $user);
+ break;
+
+ default:
+ return false;
+ }
+ }
+
+ private static function createValidation($method, $attestation) {
+ $validation = [];
+ $validation["method"] = (int)$method;
+ $validation["timestamp"] = time();
+ $validation["attestation"] = $attestation;
+
+ return $validation;
+ }
+
+ public static function validateIncident($id, $method, $user, $userCheck = true, $stateCheck = true) {
+ global $con;
+
+ if ($user == "ME") $user = people::userData("id");
+ if ($userCheck && !incidents::checkIncidentIsFromPerson($id, "ME", true)) return false;
+
+ $incident = incidents::get($id, true);
+ if ($incident === false || ($stateCheck && $incident["state"] !== incidents::STATE_REGISTERED)) return false;
+
+ $attestation = self::createIncidentAttestation($incident, $method, $user);
+ if ($attestation === false) return false;
+
+ $validation = self::createValidation($method, $attestation);
+
+ $svalidation = db::sanitize(json_encode($validation));
+ $sid = (int)$incident["id"];
+
+ return mysqli_query($con, "UPDATE incidents SET workervalidated = 1, workervalidation = '$svalidation' WHERE id = $sid LIMIT 1");
+ }
+
+ public static function validateRecord($id, $method, $user, $userCheck = true) {
+ global $con;
+
+ if ($user == "ME") $user = people::userData("id");
+ if ($userCheck && !registry::checkRecordIsFromPerson($id, "ME", true)) return false;
+
+ $record = registry::get($id, true);
+ if ($record === false || $record["state"] !== registry::STATE_REGISTERED) return false;
+
+ $attestation = self::createRecordAttestation($record, $method, $user);
+ if ($attestation === false) return false;
+
+ $validation = self::createValidation($method, $attestation);
+
+ $svalidation = db::sanitize(json_encode($validation));
+ $sid = (int)$record["id"];
+
+ return mysqli_query($con, "UPDATE records SET workervalidated = 1, workervalidation = '$svalidation' WHERE id = $sid LIMIT 1");
+ }
+
+ public static function validate($method, $incidents, $records, $user = "ME") {
+ if (!in_array($method, self::getAllowedMethods())) return -1;
+
+ $incidents = self::string2array($incidents);
+ $records = self::string2array($records);
+
+ if ($user == "ME") $user = people::userData("id");
+
+ $flag = false;
+ foreach ($incidents as $incident) {
+ if (!self::validateIncident($incident, $method, $user)) $flag = true;
+ }
+ foreach ($records as $record) {
+ if (!self::validateRecord($record, $method, $user)) $flag = true;
+ }
+
+ return ($flag ? 1 : 0);
+ }
+
+ public static function getPeopleWithPendingValidations($gracePeriod = "DEFAULT") {
+ if ($gracePeriod === "DEFAULT") $gracePeriod = self::reminderGracePeriod();
+
+ $pendingPeople = [];
+
+ $people = people::getAll();
+ foreach ($people as $p) {
+ $numPending = self::numPending((int)$p["id"], $gracePeriod);
+
+ if ($numPending > 0) {
+ $pending = [];
+ $pending["person"] = $p;
+ $pending["numPending"] = $numPending;
+
+ $pendingPeople[] = $pending;
+ }
+ }
+
+ return $pendingPeople;
+ }
+
+ public static function sendPendingValidationsReminder() {
+ global $conf;
+
+ if (!$conf["mail"]["enabled"]) {
+ echo "[error] The mail functionality is not enabled in config.php.\n";
+ return false;
+ }
+
+ if (!$conf["mail"]["capabilities"]["sendPendingValidationsReminder"]) {
+ echo "[error] The pending validation reminders functionality is not inabled in config.php.\n";
+ return false;
+ }
+
+ $pendingPeople = self::getPeopleWithPendingValidations();
+
+ foreach ($pendingPeople as $p) {
+ if (!isset($p["person"]["email"]) || empty($p["person"]["email"])) {
+ echo "[info] ".$p["person"]["id"]." doesn't have an email address defined.\n";
+ continue;
+ }
+
+ $to = [["email" => $p["person"]["email"]]];
+
+ $subject = "[Recordatorio] Tienes validaciones pendientes en el aplicativo de control horario";
+ $body = mail::bodyTemplate("<p>Hola ".security::htmlsafe(($p["person"]["name"]) ?? "").",</p>
+ <p>Este es un mensaje automático para avisarte de que tienes ".(int)$p["numPending"]." incidencias y/o registros pendientes de validar en el aplicativo de control horario.</p>
+ <p>Para validarlos, puedes acceder al aplicativo desde la siguiente dirección:</p>
+ <ul>
+ <li><a href='".security::htmlsafe($conf["fullPath"])."'>".security::htmlsafe($conf["fullPath"])."</a></li>
+ </ul>");
+
+ if (mail::send($to, [], $subject, $body)) {
+ echo "[info] Mail sent to ".$p["person"]["id"]." successfuly.\n";
+ } else {
+ echo "[error] The email couldn't be sent to ".$p["person"]["id"]."\n";
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/inc/validationsView.php b/src/inc/validationsView.php
new file mode 100644
index 0000000..b6b2b68
--- /dev/null
+++ b/src/inc/validationsView.php
@@ -0,0 +1,105 @@
+<?php
+class validationsView {
+ public static function renderPendingValidations($userId) {
+ $workers = workers::getPersonWorkers((int)$userId);
+ $companies = companies::getAll();
+
+ if ($workers === false || $companies === false) {
+ return false;
+ }
+ ?>
+ <h4>Incidencias</h4>
+ <?php
+ $iFlag = false;
+ foreach ($workers as $w) {
+ $incidents = incidents::getAll(false, 0, 0, (int)$w["id"], null, null, true);
+ if ($incidents === false) continue;
+
+ if (count($incidents)) {
+ $iFlag = true;
+ echo "<h5>".security::htmlsafe($w["companyname"])."</h5>";
+ incidentsView::renderIncidents($incidents, $companies, false, false, true, true);
+ }
+
+ visual::printDebug("incidents::getAll(false, 0, 0, ".(int)$w["id"].", null, null, true)", $incidents);
+ }
+
+ if (!$iFlag) echo "<p>No hay incidencias pendientes para validar.</p>";
+ ?>
+
+ <h4>Registros de horario</h4>
+ <?php
+ $rFlag = false;
+ foreach ($workers as $w) {
+ $registry = registry::getWorkerRecords((int)$w["id"], false, false, false, true);
+ if ($registry === false) continue;
+
+ if (count($registry)) {
+ $rFlag = true;
+ echo "<h5>".security::htmlsafe($w["companyname"])."</h5>";
+ registryView::renderRegistry($registry, $companies, false, false, true, true);
+ }
+
+ visual::printDebug("registry::getWorkerRecords(".(int)$w["id"].", false, false, false, true)", $registry);
+ }
+
+ if (!$rFlag) echo "<p>No hay registros pendientes para validar.</p>";
+
+ if ($iFlag || $rFlag) {
+ ?>
+ <p style="margin-top: 16px;"><button id="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Validar</button></p>
+ <?php
+ }
+ }
+
+ public static function renderChallengeInstructions($method) {
+ switch ($method) {
+ case validations::METHOD_SIMPLE:
+ ?>
+ <form action="dovalidate.php" method="POST">
+ <input type="hidden" name="incidents" value="<?=security::htmlsafe($_POST["incidents"] ?? "")?>">
+ <input type="hidden" name="records" value="<?=security::htmlsafe($_POST["records"] ?? "")?>">
+ <input type="hidden" name="method" value="<?=(int)validations::METHOD_SIMPLE?>">
+ <p>Para completar la validación guardaremos tu <a href="https://help.gnome.org/users/gnome-help/stable/net-what-is-ip-address.html.es" target="_blank" rel="noopener noreferrer">dirección IP</a> y la fecha y hora actual. Para proceder, haz clic en el siguiente botón:</p>
+ <p><button id="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Confirmar validación</button></p>
+ </form>
+ <?php
+ break;
+
+ default:
+ echo "Undefined";
+ }
+ }
+
+ public static function noPIIIpAddress($validation) {
+ $ipAddress = $validation["attestation"]["ipAddress"] ?? "unknown";
+
+ if (security::isAdminView() || $validation["method"] == validations::METHOD_SIMPLE || ($validation["method"] == validations::METHOD_AUTOVALIDATION && $validation["attestation"]["user"] == people::userData("id"))) return $ipAddress;
+
+ return "oculta (solo visible para los administradores)";
+ }
+
+ public static function renderValidationInfo($validation) {
+ ?>
+ <li><b>Validada por el trabajador:</b></li>
+ <ul>
+ <li><b>Método de validación:</b> <?=security::htmlsafe(validations::$methodName[$validation["method"]])?></li>
+ <li><b>Fecha de validación:</b> <?=security::htmlsafe(date("d/m/Y H:i", $validation["timestamp"]))?></li>
+ <?php
+ switch ($validation["method"]) {
+ case validations::METHOD_SIMPLE:
+ echo "<li><b>Dirección IP:</b> ".security::htmlsafe(self::noPIIIpAddress($validation))."</li>";
+ break;
+
+ case validations::METHOD_AUTOVALIDATION:
+ echo "<li><b>Dirección IP:</b> ".security::htmlsafe(self::noPIIIpAddress($validation))."</li>";
+ echo "<li><b>Persona que autovalida la incidencia:</b> ".security::htmlsafe(people::userData("name", $validation["attestation"]["user"]))."</li>";
+ break;
+
+ default:
+ }
+ ?>
+ </ul>
+ <?php
+ }
+}
diff --git a/src/inc/visual.php b/src/inc/visual.php
new file mode 100644
index 0000000..02a3457
--- /dev/null
+++ b/src/inc/visual.php
@@ -0,0 +1,192 @@
+<?php
+class visual {
+ const VIEW_ADMIN = 0;
+ const VIEW_WORKER = 1;
+
+ // Old:
+ /*const YES = "✓";
+ const NO = "✗";*/
+
+ // New:
+ const YES = "✓";
+ const NO = "X";
+
+ public static function snackbar($msg, $timeout = 10000, $printHTML = true) {
+ if ($printHTML) echo '<div class="mdl-snackbar mdl-js-snackbar">
+ <div class="mdl-snackbar__text"></div>
+ <button type="button" class="mdl-snackbar__action"></button>
+ </div>';
+ echo '<script>
+ window.addEventListener("load", function() {
+ var notification = document.querySelector(".mdl-js-snackbar");
+ notification.MaterialSnackbar.showSnackbar(
+ {
+ message: "'.security::htmlsafe($msg).'",
+ timeout: '.(int)$timeout.'
+ }
+ );
+ });
+ </script>';
+ }
+
+ public static function smartSnackbar($msgs, $timeout = 10000, $printHTML = true) {
+ global $_GET;
+
+ if (!isset($_GET["msg"])) return;
+
+ foreach ($msgs as $msg) {
+ if ($_GET["msg"] == $msg[0]) {
+ self::snackbar($msg[1], $timeout, $printHTML);
+ return;
+ }
+ }
+ }
+
+ public static function debugJson($array) {
+ return security::htmlsafe(json_encode($array, JSON_PRETTY_PRINT));
+ }
+
+ public static function includeHead() {
+ include("includes/head.php");
+ }
+
+ public static function includeNav() {
+ global $conf, $mdHeaderRowMore, $mdHeaderMore, $mdHeaderRowBefore;
+
+ $activeView = security::getActiveView();
+ switch ($activeView) {
+ case self::VIEW_ADMIN:
+ include("includes/adminnav.php");
+ break;
+
+ case self::VIEW_WORKER:
+ include("includes/workernav.php");
+ break;
+
+ default:
+ exit();
+ }
+ }
+
+ public static function backBtn($url) {
+ return '<a class="backbtn mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect" href="'.$url.'"><i id="auto_backbtn" class="material-icons">arrow_back</i></a><div class="mdl-tooltip" for="auto_backbtn">Atrás</div><div style="width: 16px;"></div>';
+ }
+
+ public static function printDebug($function, $return, $always=false, $notjson=false) {
+ global $conf;
+
+ if ($always || $conf["debug"])
+ echo '<details class="debug margintop">
+ <summary>Debug:</summary>
+ <p><b>'.security::htmlsafe($function).'</b></p>
+ <div class="overflow-wrapper"><pre>'.($notjson ? security::htmlsafe(print_r($return, true)) : self::debugJson($return)).'</pre></div>
+ </details>';
+ }
+
+ public static function renderPagination($rows, $page, $limit = 10, $showLimitLink = false, $alreadyHasParameters = false, $limitChange = false, $highlightedPage = false) {
+ global $_GET;
+
+ $numPages = ($limit == 0 ? 1 : ceil($rows/$limit));
+ if ($numPages > 1) {
+ $currentPage = ((isset($_GET["page"]) && $_GET["page"] <= $numPages && $_GET["page"] >= 1) ? $_GET["page"] : 1);
+
+ echo '<div class="pagination">';
+ for ($i = 1; $i <= $numPages; $i++) {
+ echo ($i != $currentPage ? '<a class="page'.($i == $highlightedPage ? " mdl-color-text--green" : "").'" href="'.security::htmlsafe($page).($alreadyHasParameters ? "&" : "?").'page='.(int)$i.($showLimitLink ? '&limit='.(int)$limit : '').'">'.(int)$i.'</a> ' : '<b class="page">'.(int)$i.'</b> ');
+ }
+ echo '</div>';
+ }
+
+ if ($limitChange !== false) {
+ ?>
+ <div class="limit-change-container">Ver <select id="limit-change">
+ <?php
+ if (isset($limitChange["options"])) {
+ if (!in_array($limit, $limitChange["options"])) {
+ echo "<option value=\"".(int)$limit."\" selected>".(int)$limit."</option>";
+ }
+ foreach ($limitChange["options"] as $option) {
+ echo "<option value=\"".(int)$option."\"".($option == $limit ? " selected" : "").">".(int)$option."</option>";
+ }
+ }
+ ?>
+ </select> <?=security::htmlsafe($limitChange["elementName"])?> por página.</div>
+ <?php
+ }
+ }
+
+ public static function padNum($num, $length) {
+ return str_pad($num, $length, "0", STR_PAD_LEFT);
+ }
+
+ public static function isMDColor() {
+ global $conf;
+
+ return (($conf["backgroundColor"][0] ?? "") != "#");
+ }
+
+ public static function printBodyTag() {
+ global $conf;
+
+ $conf["backgroundColorIsDark"];
+ echo "<body ".(!visual::isMDColor() ? "style=\"background-color: ".security::htmlsafe($conf["backgroundColor"]).";\"" : "")."class=\"".(visual::isMDColor() ? "mdl-color--".security::htmlsafe($conf["backgroundColor"]) : "").($conf["backgroundColorIsDark"] ? " dark-background" : "")."\">";
+ }
+
+ // WARNING: We will not sanitize $msg, so sanitize it before calling this function!
+ public static function addTooltip($id, $msg) {
+ global $_tooltips;
+
+ if (!isset($_tooltips)) $_tooltips = "";
+ $_tooltips .= '<div class="mdl-tooltip" for="'.security::htmlsafe($id).'">'.$msg.'</div>';
+ }
+
+ public static function renderTooltips() {
+ global $_tooltips;
+
+ echo ($_tooltips ?? "");
+ }
+
+ private static function addMsgToUrl($url, $msg = false) {
+ if ($msg === false) return $url;
+ return $url.(preg_match("/\?/", $url) == 1 ? "&" : "?")."msg=".urlencode($msg);
+ }
+
+ public static function getContinueUrl($defaultUrl, $msg = false, $method = "GET") {
+ global $_GET, $_POST;
+
+ $url = "";
+
+ switch ($method) {
+ case "GET":
+ if (!isset($_GET["continue"])) return self::addMsgToUrl($defaultUrl, $msg);
+ $url = (string)$_GET["continue"];
+ break;
+
+ case "POST":
+ if (!isset($_POST["continue"])) return self::addMsgToUrl($defaultUrl, $msg);
+ $url = (string)$_POST["continue"];
+ break;
+
+ default:
+ return self::addMsgToUrl($defaultUrl, $msg);
+ }
+
+ if (!preg_match("/^[^\/\\\\]*$/", $url)) return self::addMsgToUrl($defaultUrl, $msg);
+
+ if ($msg !== false) $url = self::addMsgToUrl($url, $msg);
+
+ return $url;
+ }
+
+ public static function addContinueInput($url = false) {
+ global $_GET, $_POST;
+
+ if ($url === false) {
+ if (isset($_GET["continue"])) $url = $_GET["continue"];
+ elseif (isset($_POST["continue"])) $url = $_POST["continue"];
+ else return;
+ }
+
+ echo '<input type="hidden" name="continue" value="'.security::htmlsafe($url).'">';
+ }
+}
diff --git a/src/inc/workers.php b/src/inc/workers.php
new file mode 100644
index 0000000..6c6e1d9
--- /dev/null
+++ b/src/inc/workers.php
@@ -0,0 +1,171 @@
+<?php
+class workers {
+ const AFFILIATION_STATUS_NOTWORKING = 0;
+ const AFFILIATION_STATUS_WORKING = 1;
+ const AFFILIATION_STATUS_AUTO_NOTWORKING = 2;
+ const AFFILIATION_STATUS_AUTO_WORKING = 3;
+
+ public static $affiliationStatuses = [self::AFFILIATION_STATUS_NOTWORKING, self::AFFILIATION_STATUS_WORKING, self::AFFILIATION_STATUS_AUTO_NOTWORKING, self::AFFILIATION_STATUS_AUTO_WORKING];
+ public static $affiliationStatusesNotWorking = [self::AFFILIATION_STATUS_NOTWORKING, self::AFFILIATION_STATUS_AUTO_NOTWORKING];
+ public static $affiliationStatusesWorking = [self::AFFILIATION_STATUS_WORKING, self::AFFILIATION_STATUS_AUTO_WORKING];
+ public static $affiliationStatusesAutomatic = [self::AFFILIATION_STATUS_AUTO_WORKING, self::AFFILIATION_STATUS_AUTO_NOTWORKING];
+ public static $affiliationStatusesManual = [self::AFFILIATION_STATUS_WORKING, self::AFFILIATION_STATUS_NOTWORKING];
+
+ private static $return = "w.id id, w.person person, w.company company, c.name companyname, p.name name, p.dni dni";
+
+ public static function get($id) {
+ global $con;
+
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT ".self::$return."
+ FROM workers w
+ LEFT JOIN companies c ON w.company = c.id
+ LEFT JOIN people p ON w.person = p.id
+ WHERE w.id = $sid");
+
+ if (!mysqli_num_rows($query)) return false;
+
+ return mysqli_fetch_assoc($query);
+ }
+
+ public static function sqlAddonToGetStatusAttribute($id) {
+ $sid = (int)$id;
+
+ return "LEFT JOIN workhistory h ON w.id = h.worker
+ WHERE
+ w.person = $sid AND
+ (
+ (
+ SELECT COUNT(*)
+ FROM workhistory
+ WHERE worker = w.id AND day <= UNIX_TIMESTAMP()
+ LIMIT 1
+ ) = 0 OR
+
+ h.id = (
+ SELECT id
+ FROM workhistory
+ WHERE worker = w.id AND day <= UNIX_TIMESTAMP()
+ ORDER BY day DESC
+ LIMIT 1
+ )
+ )";
+ }
+
+ public static function getPersonWorkers($person, $simplify = false) {
+ global $con;
+
+ $query = mysqli_query($con, "SELECT ".($simplify ? "w.id id" : self::$return).", h.status status, h.day lastupdated
+ FROM workers w
+ LEFT JOIN companies c ON w.company = c.id
+ LEFT JOIN people p ON w.person = p.id
+ ".self::sqlAddonToGetStatusAttribute($person)."
+ ORDER BY
+ w.id ASC");
+
+ $results = [];
+
+ while ($row = mysqli_fetch_assoc($query)) {
+ $row["hidden"] = self::isHidden($row["status"]);
+ $results[] = ($simplify ? $row["id"] : $row);
+ }
+
+ return $results;
+ }
+
+ public static function isHidden($status) {
+ return (in_array($status, self::$affiliationStatusesWorking) ? 0 : 1);
+ }
+
+ public static function affiliationStatusHelper($status) {
+ return (self::isHidden($status) ? "Baja" : "Alta");
+ }
+
+ public static function affiliationStatusIcon($status) {
+ return (self::isHidden($status) ? "work_off" : "work");
+ }
+
+ public static function isAutomaticAffiliation($status) {
+ return in_array($status, self::$affiliationStatusesAutomatic);
+ }
+
+ public static function getWorkHistory($id) {
+ global $con;
+
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT * FROM workhistory WHERE worker = $sid ORDER BY day DESC");
+ if ($query === false) return false;
+
+ $items = [];
+ while ($row = mysqli_fetch_assoc($query)) {
+ $items[] = $row;
+ }
+
+ return $items;
+ }
+
+ public static function getWorkHistoryItem($id) {
+ global $con;
+
+ $sid = (int)$id;
+
+ $query = mysqli_query($con, "SELECT * FROM workhistory WHERE id = $sid");
+ if ($query === false || !mysqli_num_rows($query)) return false;
+
+ return mysqli_fetch_assoc($query);
+ }
+
+ public static function addWorkHistoryItem($id, $day, $status, $internal = false) {
+ global $con;
+
+ $sid = (int)$id;
+ $stime = (int)$day;
+ $sstatus = (int)$status;
+
+ if ((!$internal && !in_array($sstatus, self::$affiliationStatusesManual)) || ($internal && !in_array($affiliationStatuses))) return false;
+
+ if (!workers::exists($sid)) return false;
+
+ return mysqli_query($con, "INSERT INTO workhistory (worker, day, status) VALUES ($sid, $stime, $sstatus)");
+ }
+
+ public static function editWorkHistoryItem($id, $day, $status, $internal = false) {
+ global $con;
+
+ $sid = (int)$id;
+ $stime = (int)$day;
+ $sstatus = (int)$status;
+
+ if ((!$internal && !in_array($sstatus, self::$affiliationStatusesManual)) || ($internal && !in_array($affiliationStatuses))) return false;
+
+ if (!self::existsWorkHistoryItem($id)) return false;
+
+ return mysqli_query($con, "UPDATE workhistory SET day = $stime, status = $sstatus WHERE id = $sid LIMIT 1");
+ }
+
+ public static function deleteWorkHistoryItem($id) {
+ global $con;
+
+ $sid = (int)$id;
+
+ return mysqli_query($con, "DELETE FROM workhistory WHERE id = $sid LIMIT 1");
+ }
+
+ public static function exists($id) {
+ global $con;
+
+ $query = mysqli_query($con, "SELECT id FROM workers WHERE id = ".(int)$id);
+
+ return (mysqli_num_rows($query) > 0);
+ }
+
+ public static function existsWorkHistoryItem($id) {
+ global $con;
+
+ $query = mysqli_query($con, "SELECT 1 FROM workhistory WHERE id = ".(int)$id);
+
+ return (mysqli_num_rows($query) > 0);
+ }
+}