Files
anjuma-dashboard/Data.class.php
T
2026-07-22 01:12:45 +02:00

644 lines
21 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
class Data {
public $id;
private $pdo;
private $icloudCalendarUrl = "https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/";
private $icloudUser = "itzanjuma@gmail.com";
private $icloudPass = "ywce-toxv-whvv-iorx";
public function __construct($pdo) {
$this->pdo = $pdo;
}
public function renderTemplate(string $template, array $vars): string {
foreach ($vars as $key => $value) {
$template = preg_replace(
'/{{\s*' . preg_quote($key, '/') . '\s*}}/',
(string)$value,
$template
);
}
return $template;
}
public function getDataByID($id, $column){
$sql = "SELECT * FROM users WHERE id = '" . $id . "'";
foreach($this->pdo->query($sql) as $row) {
return $row[$column];
}
}
public function getTerminDataById($id, $column){
$sql = "SELECT * FROM termine WHERE id = '" . $id . "'";
foreach($this->pdo->query($sql) as $row) {
return $row[$column];
}
}
public function getSettingsData($column){
$sql = "SELECT * FROM settings_calendar WHERE id = '1'";
foreach($this->pdo->query($sql) as $row) {
return $row[$column];
}
}
public function timestampToDate($timestamp){
return date('d.m.Y H:i', $timestamp);
}
/* ======================================
KALENDER HASH BERECHNEN (zentral)
====================================== */
private function buildCalendarHashFromTermin(array $t): string {
$data = [
'start' => (int)$t['startzeit'],
'end' => (int)$t['endzeit'],
'title' => trim((string)$t['location']),
'desc' => trim((string)$t['beschreibung']),
];
return hash('sha256', json_encode($data));
}
/* ======================================
KALENDER SYNC STATUS SPEICHERN
====================================== */
private function storeCalendarSyncState(int $terminId, array $terminRow): void
{
$hash = $this->buildCalendarHashFromTermin($terminRow);
$eventId = "termin-$terminId@anjuma";
$stmt = $this->pdo->prepare("
UPDATE termine SET
calendar_event_id = :eid,
calendar_hash = :hash,
calendar_synced_at = :ts
WHERE id = :id
");
$stmt->execute([
':eid' => $eventId,
':hash' => $hash,
':ts' => time(),
':id' => $terminId
]);
}
public function getTerminLabel($id) {
$sql = "SELECT status FROM termine WHERE id = :id";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':id' => $id]);
$status = $stmt->fetchColumn();
return match ((int)$status) {
0 => "status-open", // Blau
1 => "status-ready", // Gelb
2 => "status-done", // Grün
3 => "status-cancel", // Rot
default => "",
};
}
public function getOnlineStatusLabel($id){
$sql = "SELECT * FROM users WHERE id = '" . $id . "'";
foreach($this->pdo->query($sql) as $row) {
if($row['online'] == 0){
return "badge-danger";
}else{
return "badge-success";
}
}
}
public function getTodoStatusLabel($id){
$sql = "SELECT * FROM todo WHERE id = '" . $id . "'";
foreach($this->pdo->query($sql) as $row) {
if($row['status'] == 0){
return "danger";
}
if($row['status'] == 1){
return "warning";
}
if($row['status'] == 2){
return "success";
}
}
}
private function getLastEditorInfo($terminId)
{
$sql = $this->pdo->prepare("
SELECT user_id, timestamp
FROM changes
WHERE termin_id = :id
ORDER BY timestamp DESC
LIMIT 1
");
$sql->execute([':id' => $terminId]);
$row = $sql->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return [
'name' => '—',
'date' => '—'
];
}
return [
'name' => $this->getDataByID($row['user_id'], "name"),
'date' => date("d.m.Y H:i", $row['timestamp'])
];
}
private function caldavPut($url, $ics, $isUpdate = false)
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_USERPWD, $this->icloudUser . ":" . $this->icloudPass);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_HTTPHEADER, $isUpdate ? [
"Content-Type: text/calendar"
// KEIN If-None-Match bei Updates!
] : [
"Content-Type: text/calendar",
"If-None-Match: *" // nur bei neuen Events
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $ics);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
// 🔍 DEBUG VOR dem return
$this->debugLog("PUT to $url | status=$status | error=$err | isUpdate=" . ($isUpdate ? "1" : "0"));
if ($status !== 201 && $status !== 204) {
$this->debugLog("Response body: " . substr((string)$response, 0, 500));
}
return $status === 201 || $status === 204;
}
public function createCalendarEvent($id, $startTs, $endTs, $location, $beschreibung)
{
$uid = "termin-$id@anjuma";
$tz = 'Europe/Berlin';
$dtstamp = gmdate('Ymd\THis\Z');
// Sicherheits-Fallback, falls endTs 0 ist
if (empty($endTs) || $endTs <= 0) {
$endTs = $startTs + 2 * 3600; // +2 Stunden
}
// Start/Ende in lokaler Zeit
$dtstartStr = (new DateTime("@$startTs"))
->setTimezone(new DateTimeZone($tz))
->format('Ymd\THis');
$dtendStr = (new DateTime("@$endTs"))
->setTimezone(new DateTimeZone($tz))
->format('Ymd\THis');
$location = $this->icsEscape($location);
$beschreibung = $this->icsEscape($beschreibung);
// Status-Name bestimmen
$statusRaw = $this->getTerminDataById($id, "status");
$statusName = [
0 => "Offen",
1 => "Bereit",
2 => "Erledigt",
3 => "Storniert"
][$statusRaw] ?? "Unbekannt";
// Ersteller-Name holen
$creatorId = $this->getTerminDataById($id, "user_id");
$creatorName = $this->getDataByID($creatorId, "name");
// Letzten Bearbeiter holen
$editInfo = $this->getLastEditorInfo($id);
// Gage holen
$gage = $this->getTerminDataById($id, "gage");
$vars = [
'id' => $id,
'status' => $statusName,
'locationtype' => $this->getTerminDataById($id, "locationtype"),
'location' => $this->getTerminDataById($id, "location"),
'startzeit' => date('d.m.Y H:i', $this->getTerminDataById($id, "startzeit")),
'endzeit' => date('H:i', $this->getTerminDataById($id, "endzeit")),
'gage' => $this->getTerminDataById($id, "gage"),
'desc' => $this->getTerminDataById($id, "beschreibung"),
'creator' => $creatorName,
'last_editor' => $editInfo['name'],
'last_edited' => $editInfo['date']
];
$summary = $this->renderTemplate($this->getSettingsData("title_template"), $vars);
$beschreibungFull = $this->renderTemplate($this->getSettingsData("description_template"), $vars);
$beschreibungFull = $this->icsEscape($beschreibungFull);
$ics =
"BEGIN:VCALENDAR\r\n" .
"VERSION:2.0\r\n" .
"PRODID:-//ANJUMA//DE\r\n" .
"CALSCALE:GREGORIAN\r\n" .
"BEGIN:VEVENT\r\n" .
"UID:$uid\r\n" .
"DTSTAMP:$dtstamp\r\n" .
"DTSTART;TZID=$tz:$dtstartStr\r\n" .
"DTEND;TZID=$tz:$dtendStr\r\n" .
"SUMMARY:$summary\r\n" .
"LOCATION:$location\r\n" .
"DESCRIPTION:$beschreibungFull\r\n" .
"URL:https://dashboard.anjuma-crew.de/termin_show.php?id=$id\r\n" .
// Erinnerung 1 Tag vorher
"BEGIN:VALARM\r\n" .
"TRIGGER:-P1D\r\n" .
"ACTION:DISPLAY\r\n" .
"DESCRIPTION:Erinnerung\r\n" .
"END:VALARM\r\n" .
"END:VEVENT\r\n" .
"END:VCALENDAR\r\n";
$url = $this->icloudCalendarUrl . "termin-$id.ics";
// Create = isUpdate=false
$success = $this->caldavPut($url, $ics, false);
if ($success) {
// Termin neu laden (für Hash)
$stmt = $this->pdo->prepare("SELECT * FROM termine WHERE id = :id");
$stmt->execute([':id' => $id]);
$termin = $stmt->fetch(PDO::FETCH_ASSOC);
if ($termin) {
$this->storeCalendarSyncState($id, $termin);
}
}
return $success;
}
public function updateCalendarEvent($id)
{
$stmt = $this->pdo->prepare("SELECT * FROM termine WHERE id = :id");
$stmt->execute([':id' => $id]);
$t = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$t) {
$this->debugLog("updateCalendarEvent: Termin $id nicht gefunden");
return false;
}
$startTs = (int)$t["startzeit"];
$endTs = (int)$t["endzeit"];
$location = $t["location"] ?? "";
$beschreibung = $t["beschreibung"] ?? "";
$uid = "termin-$id@anjuma";
$tz = 'Europe/Berlin';
$dtstamp = gmdate('Ymd\THis\Z');
if (empty($endTs) || $endTs <= 0) {
$endTs = $startTs + 2 * 3600;
}
$dtstartStr = (new DateTime("@$startTs"))
->setTimezone(new DateTimeZone($tz))
->format('Ymd\THis');
$dtendStr = (new DateTime("@$endTs"))
->setTimezone(new DateTimeZone($tz))
->format('Ymd\THis');
$locationEsc = $this->icsEscape($location);
$beschreibungEsc = $this->icsEscape($beschreibung);
$summary = "ANJUMA DJ $locationEsc ($id)";
// Status-Name bestimmen
$statusRaw = $this->getTerminDataById($id, "status");
$statusName = [
0 => "Offen",
1 => "Bereit",
2 => "Erledigt",
3 => "Storniert"
][$statusRaw] ?? "Unbekannt";
// Letzten Bearbeiter holen
$editInfo = $this->getLastEditorInfo($id);
// Gage holen
$gage = $this->getTerminDataById($id, "gage");
$creatorId = $this->getTerminDataById($id, "user_id");
$creatorName = $this->getDataByID($creatorId, "name");
$vars = [
'id' => $id,
'status' => $statusName,
'locationtype' => $this->getTerminDataById($id, "locationtype"),
'location' => $this->getTerminDataById($id, "location"),
'startzeit' => date('d.m.Y H:i', $this->getTerminDataById($id, "startzeit")),
'endzeit' => date('H:i', $this->getTerminDataById($id, "endzeit")),
'gage' => $this->getTerminDataById($id, "gage"),
'desc' => $this->getTerminDataById($id, "beschreibung"),
'creator' => $creatorName,
'last_editor' => $editInfo['name'],
'last_edited' => $editInfo['date']
];
$summary = $this->renderTemplate($this->getSettingsData("title_template"), $vars);
$beschreibungFull = $this->renderTemplate($this->getSettingsData("description_template"), $vars);
$beschreibungFull = $this->icsEscape($beschreibungFull);
$ics =
"BEGIN:VCALENDAR\r\n" .
"VERSION:2.0\r\n" .
"PRODID:-//ANJUMA//DE\r\n" .
"CALSCALE:GREGORIAN\r\n" .
"BEGIN:VEVENT\r\n" .
"UID:$uid\r\n" .
"DTSTAMP:$dtstamp\r\n" .
"DTSTART;TZID=$tz:$dtstartStr\r\n" .
"DTEND;TZID=$tz:$dtendStr\r\n" .
"SUMMARY:$summary\r\n" .
"LOCATION:$locationEsc\r\n" .
"DESCRIPTION:$beschreibungFull\r\n" .
"URL:https://dashboard.anjuma-crew.de/termin_show.php?id=$id\r\n" .
"BEGIN:VALARM\r\n" .
"TRIGGER:-P1D\r\n" .
"ACTION:DISPLAY\r\n" .
"DESCRIPTION:Erinnerung\r\n" .
"END:VALARM\r\n" .
"BEGIN:VALARM\r\n" .
"TRIGGER:-PT2H\r\n" .
"ACTION:DISPLAY\r\n" .
"DESCRIPTION:Erinnerung\r\n" .
"END:VALARM\r\n" .
"END:VEVENT\r\n" .
"END:VCALENDAR\r\n";
$url = $this->icloudCalendarUrl . "termin-$id.ics";
// Update = isUpdate=true
$success = $this->caldavPut($url, $ics, true);
if ($success) {
$this->storeCalendarSyncState($id, $t);
}
return $success;
}
public function deleteCalendarEvent($id)
{
$url = $this->icloudCalendarUrl . "termin-$id.ics";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
curl_setopt($ch, CURLOPT_USERPWD, $this->icloudUser . ":" . $this->icloudPass);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $status === 200 || $status === 204;
}
public function changeTermin($id, $was, $value){
$this->pdo->query("UPDATE termine SET $was = '" . $value . "' WHERE id = '" . $id . "'");
//$this->updateCalendarEvent($id, $startzeit, $endzeit, $summary, $location, $beschreibung);
return;
}
private function icsEscape($text)
{
// Normalize line endings (Windows/Mac/Linux)
$text = str_replace(["\r\n", "\r"], "\n", $text);
// ICS requires escaping BACKSLASH first!
$text = str_replace("\\", "\\\\", $text);
// Then escape comma and semicolon
$text = str_replace(",", "\\,", $text);
$text = str_replace(";", "\\;", $text);
// Only now convert \n to ICS linebreaks
$text = str_replace("\n", "\\n", $text);
return $text;
}
public function createTermin($startzeit, $endzeit, $location, $user_id, $gage, $locationtype)
{
if ($gage === "" || $gage === null) $gage = 0;
$sql = "INSERT INTO termine
(startzeit, endzeit, locationtype, location, user_id, gage, status, anwesend, beschreibung)
VALUES (:startzeit, :endzeit, :locationtype, :location, :user_id, :gage, 0, '[]', '')";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':startzeit' => $startzeit,
':endzeit' => $endzeit,
':locationtype' => $locationtype,
':location' => $location,
':user_id' => $user_id,
':gage' => $gage
]);
$id = $this->pdo->lastInsertId();
$this->createCalendarEvent($id, $startzeit, $endzeit, $location, "");
return $id;
}
public function createChanges($id, $user_id, $was, $alt, $neu)
{
// =============================================
// 1) CHANGE SPEICHERN
// =============================================
$stmt = $this->pdo->prepare("
INSERT INTO changes (termin_id, user_id, was, alt, neu, timestamp)
VALUES (:id, :user, :was, :alt, :neu, :ts)
");
$stmt->execute([
':id' => $id,
':user' => $user_id,
':was' => $was,
':alt' => $alt,
':neu' => $neu,
':ts' => time()
]);
// -------------------------------------------
// NTFY BENACHRICHTIGUNG (schön formatiert)
// -------------------------------------------
// === NTFY CHANNEL ===
/*$ntfy_url = "https://ntfy.anjuma-crew.de/anjuma-dashboard"; // <<< anpassen
// === Basic Auth ===
$auth = "Basic anVzdGluOm5ZZ0dwNnNNRWdMRWwx";
// === Nachricht bauen ===
$username = $this->getDataByID($user_id, "name");
if ($was === "anwesend") {
$altDecoded = json_decode($alt, true) ?: [];
$neuDecoded = json_decode($neu, true) ?: [];
// IDs zu Namen umwandeln
$altNames = array_map(fn($u) => $this->getDataByID($u, "name"), $altDecoded);
$neuNames = array_map(fn($u) => $this->getDataByID($u, "name"), $neuDecoded);
$alt = implode(", ", $altNames);
$neu = implode(", ", $neuNames);
}
// Mapping für schönere Bezeichnungen syfe-xapm-vcjx-dnbi
$labelMap = [
"status" => "Status",
"anwesend" => "Anwesende Personen",
"location" => "Location",
"ansprechpartner" => "Ansprechpartner",
"gage" => "Gage",
"preisprostunde" => "Preis pro Stunde",
"startzeit" => "Startzeit",
"endzeit" => "Endzeit",
"realendzeit" => "Tatsächliche Endzeit",
"create" => "Termin Erstellt"
];
$label = $labelMap[$was] ?? ucfirst($was);
// Titel
$title = "Änderung in Termin #$id $label";
// Nachricht
$body =
"$username hat $label geändert.\n\n" .
"🔻 Alt: $alt\n" .
"🔺 Neu: $neu";
// === CURL Request ===
$ch = curl_init($ntfy_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: $auth",
"X-Title: $title",
"X-Priority: 3",
"Content-Type: text/plain"
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch);*/
return;
}
public function setTerminStatus($id, $status){
$this->pdo->query("UPDATE termine SET status = '" . $status . "' WHERE id = '" . $id . "'");
return;
}
public function setTodoTitle($id, $title){
$this->pdo->query("UPDATE todo SET title = '" . $title . "' WHERE id = '" . $id . "'");
return;
}
public function setTodoDescription($id, $description){
$this->pdo->query("UPDATE todo SET description = '" . $description . "' WHERE id = '" . $id . "'");
return;
}
public function deleteTodoById($id){
$this->pdo->query("DELETE FROM todo WHERE id = '" . $id . "'");
return;
}
public function createTodo($id, $title, $description){
$this->pdo->query("INSERT INTO todo (creator, title, description) VALUES ('" . $id . "', '" . $title . "', '" . $description . "')");
return;
}
public function getTodoDataById($id, $column){
$sql = "SELECT * FROM todo WHERE id = '" . $id . "'";
foreach($this->pdo->query($sql) as $row) {
return $row[$column];
}
}
public function getAllReadyGigs(){
$nRows = $this->pdo->query('select count(*) from termine WHERE status = 1')->fetchColumn();
return $nRows;
}
public function getAllProcessingGigs(){
$nRows = $this->pdo->query('select count(*) from termine WHERE status = 0')->fetchColumn();
return $nRows;
}
public function getAllCompletedGigs(){
$nRows = $this->pdo->query('select count(*) from termine WHERE status = 2')->fetchColumn();
return $nRows;
}
private function debugLog($msg) {
$file = __DIR__ . "/debug/caldav_debug.log";
file_put_contents($file, date("[Y-m-d H:i:s] ") . $msg . "\n", FILE_APPEND);
}
}
?>