Project import generated by Copybara.

GitOrigin-RevId: 63746295f1a5ab5a619056791995793d65529e62
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;
+  }
+}