initial commit
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 953 KiB |
+644
@@ -0,0 +1,644 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
require_once "../config/config.php";
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new RuntimeException('Method not allowed');
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) throw new RuntimeException('id fehlt');
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM finance_expenses WHERE id=:id");
|
||||
$stmt->execute([':id'=>$id]);
|
||||
|
||||
echo json_encode(['ok'=>true], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok'=>false,'error'=>$e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
require_once "../config/config.php";
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
$from = $_GET['from'] ?? date('Y-m-01');
|
||||
$to = $_GET['to'] ?? date('Y-m-t');
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, date, category, title, amount_eur, method, notes
|
||||
FROM finance_expenses
|
||||
WHERE date BETWEEN :from AND :to
|
||||
ORDER BY date DESC, id DESC
|
||||
");
|
||||
$stmt->execute([':from' => $from, ':to' => $to]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$sum = 0;
|
||||
foreach ($rows as $r) $sum += (int)$r['amount_eur'];
|
||||
|
||||
echo json_encode(['ok'=>true,'from'=>$from,'to'=>$to,'sum_eur'=>$sum,'items'=>$rows], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok'=>false,'error'=>$e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
require_once "../config/config.php";
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new RuntimeException('Method not allowed');
|
||||
|
||||
$id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
|
||||
$date = trim($_POST['date'] ?? '');
|
||||
$category = trim($_POST['category'] ?? '');
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$amount = (int)($_POST['amount_eur'] ?? 0);
|
||||
$method = trim($_POST['method'] ?? 'bank');
|
||||
$notes = trim($_POST['notes'] ?? '');
|
||||
|
||||
if (!$date || !$category || !$title) throw new RuntimeException('Pflichtfelder fehlen');
|
||||
if ($amount <= 0) throw new RuntimeException('Betrag muss > 0 sein');
|
||||
|
||||
$allowedMethods = ['cash','bank','paypal','card','other'];
|
||||
if (!in_array($method, $allowedMethods, true)) $method = 'bank';
|
||||
|
||||
if ($id > 0) {
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE finance_expenses
|
||||
SET date=:date, category=:category, title=:title, amount_eur=:amount, method=:method, notes=:notes
|
||||
WHERE id=:id
|
||||
");
|
||||
$stmt->execute([
|
||||
':date'=>$date, ':category'=>$category, ':title'=>$title, ':amount'=>$amount, ':method'=>$method, ':notes'=>$notes, ':id'=>$id
|
||||
]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO finance_expenses (date, category, title, amount_eur, method, notes)
|
||||
VALUES (:date,:category,:title,:amount,:method,:notes)
|
||||
");
|
||||
$stmt->execute([
|
||||
':date'=>$date, ':category'=>$category, ':title'=>$title, ':amount'=>$amount, ':method'=>$method, ':notes'=>$notes
|
||||
]);
|
||||
$id = (int)$pdo->lastInsertId();
|
||||
}
|
||||
|
||||
echo json_encode(['ok'=>true,'id'=>$id], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok'=>false,'error'=>$e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
require_once "../config/config.php";
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
// TODO: Statuswert anpassen:
|
||||
// Beispiel: 2 = erledigt/abgeschlossen
|
||||
$STATUS_DONE = 2;
|
||||
|
||||
$from = $_GET['from'] ?? null; // YYYY-MM-DD
|
||||
$to = $_GET['to'] ?? null;
|
||||
|
||||
// Default: aktueller Monat
|
||||
if (!$from || !$to) {
|
||||
$from = date('Y-m-01');
|
||||
$to = date('Y-m-t');
|
||||
}
|
||||
|
||||
$fromTs = strtotime($from . ' 00:00:00');
|
||||
$toTs = strtotime($to . ' 23:59:59');
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
id,
|
||||
startzeit,
|
||||
endzeit,
|
||||
locationtype,
|
||||
location,
|
||||
gage,
|
||||
status,
|
||||
beschreibung
|
||||
FROM termine
|
||||
WHERE startzeit BETWEEN :fromTs AND :toTs
|
||||
AND status = :done
|
||||
AND gage > 0
|
||||
ORDER BY startzeit DESC
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute([
|
||||
':fromTs' => $fromTs,
|
||||
':toTs' => $toTs,
|
||||
':done' => $STATUS_DONE,
|
||||
]);
|
||||
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$sum = 0;
|
||||
foreach ($rows as $r) $sum += (int)$r['gage'];
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'sum_eur' => $sum,
|
||||
'items' => array_map(function($r){
|
||||
return [
|
||||
'id' => (int)$r['id'],
|
||||
'date' => date('Y-m-d', (int)$r['startzeit']),
|
||||
'time' => date('H:i', (int)$r['startzeit']) . '–' . date('H:i', (int)$r['endzeit']),
|
||||
'location' => trim(($r['locationtype'] ?? '') . ' ' . ($r['location'] ?? '')),
|
||||
'gage' => (int)$r['gage'],
|
||||
'desc' => (string)($r['beschreibung'] ?? ''),
|
||||
];
|
||||
}, $rows),
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
require_once "../config/config.php";
|
||||
require_once __DIR__ . '/../lib/InvoiceNo.php';
|
||||
|
||||
global $pdo;
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
throw new RuntimeException('Method not allowed');
|
||||
}
|
||||
|
||||
$terminId = (int)($_POST['termin_id'] ?? 0);
|
||||
if ($terminId <= 0) {
|
||||
throw new RuntimeException('termin_id fehlt');
|
||||
}
|
||||
|
||||
// Termin laden
|
||||
$st = $pdo->prepare("SELECT * FROM termine WHERE id=:id");
|
||||
$st->execute([':id' => $terminId]);
|
||||
$t = $st->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$t) {
|
||||
throw new RuntimeException('Termin nicht gefunden');
|
||||
}
|
||||
|
||||
// Prüfen ob Rechnung existiert
|
||||
$st = $pdo->prepare("SELECT id FROM invoices WHERE termine_id=:tid LIMIT 1");
|
||||
$st->execute([':tid' => $terminId]);
|
||||
|
||||
if ($st->fetch()) {
|
||||
throw new RuntimeException('Für diesen Termin existiert bereits eine Rechnung.');
|
||||
}
|
||||
|
||||
$serviceDate = date('Y-m-d', (int)$t['startzeit']);
|
||||
$invoiceDate = date('Y-m-d');
|
||||
$dueDate = date('Y-m-d', strtotime($invoiceDate . ' +14 days'));
|
||||
|
||||
$year = (int)date('Y');
|
||||
$invoiceNo = nextInvoiceNo($pdo, $year);
|
||||
|
||||
$customerName = trim($_POST['customer_name'] ?? 'Kunde (bitte bearbeiten)');
|
||||
$customerStreet = trim($_POST['customer_street'] ?? '');
|
||||
$customerZip = trim($_POST['customer_zip'] ?? '');
|
||||
$customerCity = trim($_POST['customer_city'] ?? '');
|
||||
$customerCountry = trim($_POST['customer_country'] ?? 'DE');
|
||||
|
||||
$gage = (int)($t['gage'] ?? 0);
|
||||
|
||||
$desc = "DJ-Leistung am " . date('d.m.Y', (int)$t['startzeit']);
|
||||
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$pdo->prepare("
|
||||
INSERT INTO invoices
|
||||
(invoice_no, invoice_date, service_date, due_date, status,
|
||||
customer_name, customer_street, customer_zip, customer_city, customer_country,
|
||||
termine_id, amount_net_eur)
|
||||
VALUES
|
||||
(:no, :invdate, :servdate, :duedate, 'draft',
|
||||
:cname, :cstreet, :czip, :ccity, :ccountry,
|
||||
:tid, :amount)
|
||||
")->execute([
|
||||
':no' => $invoiceNo,
|
||||
':invdate' => $invoiceDate,
|
||||
':servdate' => $serviceDate,
|
||||
':duedate' => $dueDate,
|
||||
':cname' => $customerName,
|
||||
':cstreet' => $customerStreet,
|
||||
':czip' => $customerZip,
|
||||
':ccity' => $customerCity,
|
||||
':ccountry' => $customerCountry,
|
||||
':tid' => $terminId,
|
||||
':amount' => $gage
|
||||
]);
|
||||
|
||||
$invoiceId = (int)$pdo->lastInsertId();
|
||||
|
||||
$pdo->prepare("
|
||||
INSERT INTO invoice_items
|
||||
(invoice_id, pos, description, qty, unit_price_eur, line_total_eur)
|
||||
VALUES
|
||||
(:iid, 1, :d, 1, :p, :p)
|
||||
")->execute([
|
||||
':iid' => $invoiceId,
|
||||
':d' => $desc,
|
||||
':p' => $gage
|
||||
]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'invoice_id' => $invoiceId,
|
||||
'invoice_no' => $invoiceNo
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
if ($pdo && $pdo->inTransaction()) $pdo->rollBack();
|
||||
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => $e->getMessage()
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
require_once "../config/config.php";
|
||||
global $pdo;
|
||||
|
||||
try {
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if ($id <= 0) throw new RuntimeException('id fehlt');
|
||||
|
||||
$st = $pdo->prepare("SELECT * FROM invoices WHERE id=:id");
|
||||
$st->execute([':id'=>$id]);
|
||||
$inv = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$inv) throw new RuntimeException('Rechnung nicht gefunden');
|
||||
|
||||
$it = $pdo->prepare("SELECT * FROM invoice_items WHERE invoice_id=:id ORDER BY pos ASC, id ASC");
|
||||
$it->execute([':id'=>$id]);
|
||||
$items = $it->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['ok'=>true,'invoice'=>$inv,'items'=>$items], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok'=>false,'error'=>$e->getMessage()], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
require_once "../config/config.php";
|
||||
global $pdo;
|
||||
|
||||
try {
|
||||
$from = $_GET['from'] ?? date('Y-m-01');
|
||||
$to = $_GET['to'] ?? date('Y-m-t');
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, invoice_no, invoice_date, service_date, due_date, status,
|
||||
customer_name, amount_net_eur, termine_id, pdf_path
|
||||
FROM invoices
|
||||
WHERE invoice_date BETWEEN :from AND :to
|
||||
ORDER BY invoice_date DESC, id DESC
|
||||
");
|
||||
$stmt->execute([':from'=>$from, ':to'=>$to]);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['ok'=>true,'from'=>$from,'to'=>$to,'items'=>$rows], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok'=>false,'error'=>$e->getMessage()], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
<?php
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
set_exception_handler(function(Throwable $e){
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine()
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
});
|
||||
set_error_handler(function($severity, $message, $file, $line){
|
||||
throw new ErrorException($message, 0, $severity, $file, $line);
|
||||
});
|
||||
|
||||
|
||||
require_once "../config/config.php";
|
||||
global $pdo;
|
||||
|
||||
$config = require __DIR__ . '/../config/invoice.php';
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
use Dompdf\Dompdf;
|
||||
use Dompdf\Options;
|
||||
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
|
||||
function eur($n){ return number_format((int)$n, 0, ',', '.') . " €"; }
|
||||
|
||||
function imgToDataUri(string $path): string {
|
||||
if (!file_exists($path)) {
|
||||
throw new RuntimeException("Logo nicht gefunden: $path");
|
||||
}
|
||||
if (!is_readable($path)) {
|
||||
throw new RuntimeException("Logo nicht lesbar (Rechte): $path");
|
||||
}
|
||||
|
||||
$data = file_get_contents($path);
|
||||
if ($data === false) {
|
||||
throw new RuntimeException("Logo konnte nicht gelesen werden: $path");
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
$mime = match($ext) {
|
||||
'png' => 'image/png',
|
||||
'jpg', 'jpeg' => 'image/jpeg',
|
||||
'svg' => 'image/svg+xml',
|
||||
default => throw new RuntimeException("Logo-Format nicht unterstützt: .$ext (bitte png/jpg/svg)")
|
||||
};
|
||||
|
||||
return "data:$mime;base64," . base64_encode($data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
try {
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if ($id <= 0) throw new RuntimeException('id fehlt');
|
||||
|
||||
// Load invoice + items
|
||||
$st = $pdo->prepare("SELECT * FROM invoices WHERE id=:id");
|
||||
$st->execute([':id'=>$id]);
|
||||
$inv = $st->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$inv) throw new RuntimeException('Rechnung nicht gefunden');
|
||||
|
||||
$it = $pdo->prepare("SELECT * FROM invoice_items WHERE invoice_id=:id ORDER BY pos ASC, id ASC");
|
||||
$it->execute([':id'=>$id]);
|
||||
$items = $it->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Ensure pdf dir
|
||||
$pdfDir = $config['pdf_dir'];
|
||||
if (!is_dir($pdfDir)) {
|
||||
if (!mkdir($pdfDir, 0775, true)) throw new RuntimeException('pdf_dir kann nicht erstellt werden');
|
||||
}
|
||||
|
||||
$issuer = $config['issuer'];
|
||||
$pay = $config['payment'];
|
||||
|
||||
$branding = $config['branding'] ?? [];
|
||||
$accent = $branding['accent'] ?? '#111';
|
||||
$logoData = imgToDataUri($branding['logo_path'] ?? null);
|
||||
|
||||
|
||||
$logoHtml = '';
|
||||
if ($logoData) {
|
||||
$logoHtml = '<img src="'.h($logoData).'" style="height:52px;" alt="ANJUMA Logo">';
|
||||
}
|
||||
|
||||
$html = '
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
@page { margin: 26px 30px; }
|
||||
|
||||
body {
|
||||
font-family: DejaVu Sans, Arial, sans-serif;
|
||||
font-size: 11px;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
/* ===== HEADER ===== */
|
||||
.header {
|
||||
width:100%;
|
||||
}
|
||||
.header-left {
|
||||
float:left;
|
||||
width:60%;
|
||||
}
|
||||
.header-right {
|
||||
float:right;
|
||||
width:38%;
|
||||
text-align:right;
|
||||
}
|
||||
.clear { clear:both; }
|
||||
|
||||
.header-name {
|
||||
font-size:14px;
|
||||
font-weight:700;
|
||||
}
|
||||
.header-address {
|
||||
font-size:10px;
|
||||
color:#666;
|
||||
}
|
||||
|
||||
.line {
|
||||
height:4px;
|
||||
background: '.$accent.';
|
||||
margin: 12px 0 18px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size:28px;
|
||||
font-weight:800;
|
||||
margin-bottom:10px;
|
||||
}
|
||||
|
||||
/* ===== META ===== */
|
||||
.meta {
|
||||
width:100%;
|
||||
margin-bottom:14px;
|
||||
}
|
||||
.meta td {
|
||||
padding:2px 0;
|
||||
}
|
||||
.meta .k {
|
||||
width:140px;
|
||||
color:#666;
|
||||
}
|
||||
.meta .v {
|
||||
font-weight:600;
|
||||
}
|
||||
|
||||
/* ===== SECTIONS ===== */
|
||||
.section {
|
||||
margin-top:18px;
|
||||
}
|
||||
.section-title {
|
||||
font-size:10px;
|
||||
letter-spacing:.06em;
|
||||
text-transform:uppercase;
|
||||
color:#666;
|
||||
margin-bottom:6px;
|
||||
}
|
||||
.section-content {
|
||||
border-top:2px solid #111;
|
||||
padding-top:8px;
|
||||
}
|
||||
|
||||
/* ===== ADDRESS ===== */
|
||||
.addr-left {
|
||||
float:left;
|
||||
width:48%;
|
||||
}
|
||||
.addr-right {
|
||||
float:right;
|
||||
width:48%;
|
||||
}
|
||||
|
||||
/* ===== ITEMS TABLE ===== */
|
||||
table.items {
|
||||
width:100%;
|
||||
border-collapse:collapse;
|
||||
margin-top:20px;
|
||||
}
|
||||
table.items th {
|
||||
background:#f3f3f3;
|
||||
padding:8px;
|
||||
font-size:10px;
|
||||
text-transform:uppercase;
|
||||
border-top:1px solid #ccc;
|
||||
border-bottom:1px solid #ccc;
|
||||
}
|
||||
table.items td {
|
||||
padding:10px 8px;
|
||||
border-bottom:1px solid #eee;
|
||||
}
|
||||
.num {
|
||||
text-align:right;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
/* ===== SUMMARY ===== */
|
||||
.summary {
|
||||
margin-top:16px;
|
||||
}
|
||||
.summary-note {
|
||||
float:left;
|
||||
width:58%;
|
||||
font-size:10px;
|
||||
color:#555;
|
||||
line-height:1.4;
|
||||
}
|
||||
.summary-bar {
|
||||
float:right;
|
||||
width:40%;
|
||||
background:#f3f3f3;
|
||||
border-top:3px solid #111;
|
||||
padding:10px 12px;
|
||||
}
|
||||
.summary-bar table {
|
||||
width:100%;
|
||||
}
|
||||
.summary-bar td {
|
||||
padding:4px 0;
|
||||
}
|
||||
.summary-bar .grand {
|
||||
font-size:18px;
|
||||
font-weight:800;
|
||||
}
|
||||
|
||||
/* ===== PAYMENT ===== */
|
||||
.payment {
|
||||
margin-top:20px;
|
||||
border-top:2px solid #111;
|
||||
padding-top:8px;
|
||||
}
|
||||
.payment td {
|
||||
padding:3px 0;
|
||||
}
|
||||
.muted { color:#666; }
|
||||
|
||||
/* ===== FOOTER ===== */
|
||||
.footer {
|
||||
position: fixed;
|
||||
left:30px;
|
||||
right:30px;
|
||||
bottom:18px;
|
||||
font-size:9px;
|
||||
color:#666;
|
||||
border-top:1px solid #eee;
|
||||
padding-top:6px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- HEADER -->
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<div class="header-name">'.h($issuer['name']).'</div>
|
||||
<div class="header-address">
|
||||
'.h($issuer['street']).' • '.h($issuer['zip']).' '.h($issuer['city']).'
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
'.$logoHtml.'
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
|
||||
<div class="line"></div>
|
||||
|
||||
<div class="title">Rechnung</div>
|
||||
|
||||
<table class="meta">
|
||||
<tr><td class="k">Rechnungsnummer</td><td class="v">'.h($inv['invoice_no']).'</td></tr>
|
||||
<tr><td class="k">Rechnungsdatum</td><td class="v">'.h(date('d.m.Y', strtotime($inv['invoice_date']))).'</td></tr>
|
||||
<tr><td class="k">Leistungsdatum</td><td class="v">'.h(date('d.m.Y', strtotime($inv['service_date']))).'</td></tr>
|
||||
<tr><td class="k">Fällig am</td><td class="v">'.h(date('d.m.Y', strtotime($inv['due_date']))).'</td></tr>
|
||||
</table>
|
||||
|
||||
<!-- ADDRESSES -->
|
||||
<div class="section">
|
||||
<div class="addr-left">
|
||||
<div class="section-title">Rechnung an</div>
|
||||
<div class="section-content">
|
||||
<b>'.h($inv['customer_name']).'</b><br>
|
||||
'.($inv['customer_street'] ? h($inv['customer_street']).'<br>' : '').'
|
||||
'.h($inv['customer_zip']).' '.h($inv['customer_city']).'<br>
|
||||
'.h($inv['customer_country']).'
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="addr-right">
|
||||
<div class="section-title">Aussteller</div>
|
||||
<div class="section-content">
|
||||
<b>'.h($issuer['name']).'</b><br>
|
||||
'.h($issuer['street']).'<br>
|
||||
'.h($issuer['zip']).' '.h($issuer['city']).'<br>
|
||||
'.h($issuer['country']).'<br><br>
|
||||
<span class="muted">Steuernr.:</span> '.h($issuer['tax_no']).'<br>
|
||||
<span class="muted">E-Mail:</span> '.h($issuer['email']).'<br>
|
||||
<span class="muted">Tel.:</span> '.h($issuer['phone']).'
|
||||
</div>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
|
||||
<!-- ITEMS -->
|
||||
<table class="items">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px;">Pos</th>
|
||||
<th>Beschreibung</th>
|
||||
<th class="num" style="width:70px;">Menge</th>
|
||||
<th class="num" style="width:110px;">Einzelpreis</th>
|
||||
<th class="num" style="width:110px;">Gesamt</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
|
||||
|
||||
|
||||
// --- ITEMS LOOP (wie gehabt) ---
|
||||
$pos = 1;
|
||||
$sum = 0;
|
||||
|
||||
foreach ($items as $row) {
|
||||
$line = (int)$row['line_total_eur'];
|
||||
$sum += $line;
|
||||
|
||||
$html .= '
|
||||
<tr>
|
||||
<td>'.h($row['pos'] ?? $pos).'</td>
|
||||
<td>'.h($row['description']).'</td>
|
||||
<td class="num">'.h($row['qty']).'</td>
|
||||
<td class="num">'.h(eur($row['unit_price_eur'])).'</td>
|
||||
<td class="num"><b>'.h(eur($line)).'</b></td>
|
||||
</tr>';
|
||||
$pos++;
|
||||
}
|
||||
|
||||
// --- CLOSE TABLE + SUMMARY + PAYMENT + FOOTER ---
|
||||
$html .= '
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- SUMMARY -->
|
||||
<div class="summary">
|
||||
<div class="summary-note">
|
||||
'.h($config['kleinunternehmer_text']).'
|
||||
'.(!empty($inv['notes']) ? '<br><br>'.nl2br(h($inv['notes'])) : '').'
|
||||
</div>
|
||||
|
||||
<div class="summary-bar">
|
||||
<table>
|
||||
<tr>
|
||||
<td class="muted">Zwischensumme</td>
|
||||
<td class="num">'.h(eur($sum)).'</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="muted">Umsatzsteuer</td>
|
||||
<td class="num">0 €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-top:8px;"><b>Gesamtbetrag</b></td>
|
||||
<td class="num grand">'.h(eur($sum)).'</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="clear"></div>
|
||||
</div>
|
||||
|
||||
<!-- PAYMENT -->
|
||||
<div class="payment">
|
||||
<div class="section-title">Zahlungsinformationen</div>
|
||||
<div class="section-content" style="border-top:0; padding-top:0;">
|
||||
<div style="margin-bottom:8px;">
|
||||
Bitte überweisen Sie den Betrag innerhalb von '.h($pay['pay_within_days']).' Tagen.
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td style="width:70px;" class="muted">IBAN</td>
|
||||
<td><b>'.h($pay['iban']).'</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="muted">BIC</td>
|
||||
<td><b>'.h($pay['bic']).'</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="muted">Bank</td>
|
||||
<td><b>'.h($pay['bank']).'</b></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<div class="footer">
|
||||
'.h($issuer['name']).' • '.h($issuer['street']).', '.h($issuer['zip']).' '.h($issuer['city']).' • Steuernr.: '.h($issuer['tax_no']).'
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>';
|
||||
|
||||
|
||||
|
||||
|
||||
// Dompdf options
|
||||
$options = new Options();
|
||||
$options->set('isRemoteEnabled', false);
|
||||
$options->set('defaultFont', 'DejaVu Sans');
|
||||
|
||||
$dompdf = new Dompdf($options);
|
||||
$dompdf->loadHtml($html, 'UTF-8');
|
||||
$dompdf->setPaper('A4', 'portrait');
|
||||
$dompdf->render();
|
||||
|
||||
$pdfOutput = $dompdf->output();
|
||||
|
||||
// Save file
|
||||
$filename = $inv['invoice_no'] . '.pdf';
|
||||
$fullPath = rtrim($pdfDir, '/').'/'.$filename;
|
||||
$relativePath = 'storage/invoices/' . $filename;
|
||||
|
||||
if (file_put_contents($fullPath, $pdfOutput) === false) {
|
||||
throw new RuntimeException('PDF konnte nicht gespeichert werden: ' . $fullPath);
|
||||
}
|
||||
|
||||
// Store pdf_path in DB (relative is nicer)
|
||||
$pdo->prepare("UPDATE invoices SET pdf_path=:p WHERE id=:id")
|
||||
->execute([':p'=>$relativePath, ':id'=>$id]);
|
||||
|
||||
// Output download
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: inline; filename="'.basename($filename).'"');
|
||||
echo $pdfOutput;
|
||||
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(400);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['ok'=>false,'error'=>$e->getMessage()], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/PveApi.php';
|
||||
$cfg = require __DIR__ . '/../config/pve.php';
|
||||
$pve = new PveApi($cfg);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['ok' => false, 'error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$vmid = (int)($_POST['vmid'] ?? 0);
|
||||
$action = (string)($_POST['action'] ?? '');
|
||||
|
||||
// optional: hier deine Dashboard-Auth checken!
|
||||
if ($vmid <= 0) throw new InvalidArgumentException("vmid fehlt/ungültig");
|
||||
|
||||
$upid = $pve->powerAction($vmid, $action);
|
||||
echo json_encode(['ok' => true, 'vmid' => $vmid, 'action' => $action, 'upid' => $upid]);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../lib/PveApi.php';
|
||||
$cfg = require __DIR__ . '/../config/pve.php';
|
||||
$pve = new PveApi($cfg);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
$lxcs = $pve->listLxc(); // array mit vmid, name, status, cpu, mem, etc.
|
||||
$vms = $pve->listQemu(); // array mit vmid, name, status, cpu, mem, etc.
|
||||
|
||||
$all = [];
|
||||
|
||||
foreach ($lxcs as $c) {
|
||||
$vmid = (int)($c['vmid'] ?? 0);
|
||||
if ($vmid >= 100 && $vmid <= 199) {
|
||||
$all[] = [
|
||||
'vmid' => $vmid,
|
||||
'type' => 'lxc',
|
||||
'name' => (string)($c['name'] ?? "LXC {$vmid}"),
|
||||
'status' => (string)($c['status'] ?? 'unknown'),
|
||||
'uptime' => (int)($c['uptime'] ?? 0),
|
||||
'cpu' => (float)($c['cpu'] ?? 0),
|
||||
'mem' => (int)($c['mem'] ?? 0),
|
||||
'maxmem' => (int)($c['maxmem'] ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($vms as $v) {
|
||||
$vmid = (int)($v['vmid'] ?? 0);
|
||||
if ($vmid >= 200 && $vmid <= 299) {
|
||||
$all[] = [
|
||||
'vmid' => $vmid,
|
||||
'type' => 'qemu',
|
||||
'name' => (string)($v['name'] ?? "VM {$vmid}"),
|
||||
'status' => (string)($v['status'] ?? 'unknown'),
|
||||
'uptime' => (int)($v['uptime'] ?? 0),
|
||||
'cpu' => (float)($v['cpu'] ?? 0),
|
||||
'mem' => (int)($v['mem'] ?? 0),
|
||||
'maxmem' => (int)($v['maxmem'] ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: erst LXCs, dann VMs oder nach vmid
|
||||
usort($all, fn($a, $b) => $a['vmid'] <=> $b['vmid']);
|
||||
|
||||
echo json_encode(['ok' => true, 'items' => $all], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/PveApi.php';
|
||||
$cfg = require __DIR__ . '/../config/pve.php';
|
||||
$pve = new PveApi($cfg);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
$vmid = (int)($_GET['vmid'] ?? 0);
|
||||
if ($vmid <= 0) throw new InvalidArgumentException("vmid fehlt/ungültig");
|
||||
|
||||
$data = $pve->statusCurrent($vmid);
|
||||
echo json_encode(['ok' => true, 'vmid' => $vmid, 'data' => $data]);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
require_once "../config/config.php";
|
||||
global $pdo;
|
||||
|
||||
if (!($pdo instanceof PDO)) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok'=>false,'error'=>'PDO nicht initialisiert']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// Filter
|
||||
$from = $_GET['from'] ?? null; // YYYY-MM-DD
|
||||
$to = $_GET['to'] ?? null; // YYYY-MM-DD
|
||||
$status = $_GET['status'] ?? ''; // optional
|
||||
|
||||
// Default: aktueller Monat
|
||||
if (!$from || !$to) {
|
||||
$from = date('Y-m-01');
|
||||
$to = date('Y-m-t');
|
||||
}
|
||||
|
||||
$fromTs = strtotime($from . ' 00:00:00');
|
||||
$toTs = strtotime($to . ' 23:59:59');
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
id,
|
||||
startzeit,
|
||||
endzeit,
|
||||
locationtype,
|
||||
location,
|
||||
gage,
|
||||
status,
|
||||
beschreibung,
|
||||
user_id,
|
||||
anwesend
|
||||
FROM termine
|
||||
WHERE startzeit BETWEEN :fromTs AND :toTs
|
||||
";
|
||||
|
||||
$params = [
|
||||
':fromTs' => $fromTs,
|
||||
':toTs' => $toTs,
|
||||
];
|
||||
|
||||
if ($status !== '' && is_numeric($status)) {
|
||||
$sql .= " AND status = :status ";
|
||||
$params[':status'] = (int)$status;
|
||||
}
|
||||
|
||||
$sql .= " ORDER BY startzeit DESC ";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'count' => count($rows),
|
||||
'items' => array_map(function($r){
|
||||
return [
|
||||
'id' => (int)$r['id'],
|
||||
'startzeit' => (int)$r['startzeit'],
|
||||
'endzeit' => (int)$r['endzeit'],
|
||||
'locationtype' => (string)$r['locationtype'],
|
||||
'location' => (string)$r['location'],
|
||||
'gage' => (int)$r['gage'],
|
||||
'status' => (int)$r['status'],
|
||||
'beschreibung' => (string)($r['beschreibung'] ?? ''),
|
||||
'user_id' => (int)$r['user_id'],
|
||||
'anwesend' => $r['anwesend'], // JSON
|
||||
];
|
||||
}, $rows),
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok'=>false,'error'=>$e->getMessage()]);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) { http_response_code(401); echo json_encode(['ok'=>false]); exit; }
|
||||
if (($_SESSION['role'] ?? 'user') !== 'admin') { http_response_code(403); echo json_encode(['ok'=>false]); exit; }
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) { http_response_code(400); echo json_encode(['ok'=>false,'error'=>'Missing id']); exit; }
|
||||
if ($id === (int)($_SESSION['id'] ?? 0)) { http_response_code(400); echo json_encode(['ok'=>false,'error'=>'Du kannst dich nicht selbst löschen']); exit; }
|
||||
|
||||
$st = $pdo->prepare("DELETE FROM users WHERE id=:id");
|
||||
$st->execute([':id'=>$id]);
|
||||
|
||||
echo json_encode(['ok'=>true]);
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) { http_response_code(401); echo json_encode(['ok'=>false]); exit; }
|
||||
if (($_SESSION['role'] ?? 'user') !== 'admin') { http_response_code(403); echo json_encode(['ok'=>false]); exit; }
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) { http_response_code(400); echo json_encode(['ok'=>false,'error'=>'Missing id']); exit; }
|
||||
if ($id === (int)($_SESSION['id'] ?? 0)) { http_response_code(400); echo json_encode(['ok'=>false,'error'=>'Du kannst dein eigenes Passwort hier nicht resetten']); exit; }
|
||||
|
||||
$st = $pdo->prepare("UPDATE users SET password_hash=NULL, first_login=1 WHERE id=:id");
|
||||
$st->execute([':id'=>$id]);
|
||||
|
||||
echo json_encode(['ok'=>true]);
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
require_once __DIR__ . '/../Data.class.php';
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['ok'=>false,'error'=>'Not logged in']); exit;
|
||||
}
|
||||
if (($_SESSION['role'] ?? 'user') !== 'admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['ok'=>false,'error'=>'Admin only']); exit;
|
||||
}
|
||||
|
||||
$stmt = $pdo->query("SELECT id, name, username, role, first_login, last_login FROM users ORDER BY role DESC, name ASC");
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['ok'=>true,'items'=>$rows], JSON_UNESCAPED_UNICODE);
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
require_once __DIR__ . "/../config/session.php";
|
||||
require_once __DIR__ . "/../config/config.php";
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Not logged in']); exit;
|
||||
}
|
||||
if (($_SESSION["role"] ?? "user") !== "admin") {
|
||||
echo json_encode(['ok' => false, 'error' => 'Forbidden']); exit;
|
||||
}
|
||||
|
||||
// CSRF check
|
||||
$csrf = $_POST['csrf'] ?? '';
|
||||
if (empty($_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $csrf)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'CSRF']); exit;
|
||||
}
|
||||
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id <= 0) { echo json_encode(['ok'=>false,'error'=>'Invalid id']); exit; }
|
||||
|
||||
// Optional: sich selbst nicht
|
||||
if ($id === (int)($_SESSION['id'] ?? 0)) {
|
||||
echo json_encode(['ok'=>false,'error'=>'Not allowed for yourself']); exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("DELETE FROM remember_tokens WHERE user_id = :uid");
|
||||
$stmt->execute([':uid' => $id]);
|
||||
echo json_encode(['ok' => true]);
|
||||
} catch (Throwable $e) {
|
||||
echo json_encode(['ok' => false, 'error' => 'DB error']);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
ini_set('display_errors', 0);
|
||||
error_reporting(E_ALL);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
require_once __DIR__ . '/../config/config.php';
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['ok'=>false,'error'=>'Not logged in']); exit;
|
||||
}
|
||||
if (($_SESSION['role'] ?? 'user') !== 'admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(['ok'=>false,'error'=>'Admin only']); exit;
|
||||
}
|
||||
|
||||
$id = isset($_POST['id']) && $_POST['id'] !== '' ? (int)$_POST['id'] : null;
|
||||
$name = trim((string)($_POST['name'] ?? ''));
|
||||
$username = trim((string)($_POST['username'] ?? ''));
|
||||
$role = (($_POST['role'] ?? 'user') === 'admin') ? 'admin' : 'user';
|
||||
$password = (string)($_POST['password'] ?? '');
|
||||
|
||||
if ($name === '' || $username === '') {
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok'=>false,'error'=>'Name und Username sind Pflicht']); exit;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($id === null) {
|
||||
// CREATE: Passwort optional -> wenn leer: first_login=1 und password_hash=NULL
|
||||
if ($password === '') {
|
||||
$st = $pdo->prepare("INSERT INTO users (name, username, password_hash, role, first_login, last_login)
|
||||
VALUES (:n,:u,NULL,:r,1,NULL)");
|
||||
$st->execute([':n'=>$name, ':u'=>$username, ':r'=>$role]);
|
||||
} else {
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
$st = $pdo->prepare("INSERT INTO users (name, username, password_hash, role, first_login, last_login)
|
||||
VALUES (:n,:u,:p,:r,0,NULL)");
|
||||
$st->execute([':n'=>$name, ':u'=>$username, ':p'=>$hash, ':r'=>$role]);
|
||||
}
|
||||
|
||||
echo json_encode(['ok'=>true,'id'=>(int)$pdo->lastInsertId()], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
// UPDATE
|
||||
if ($password !== '') {
|
||||
// Admin setzt neues Passwort -> first_login=0
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
$st = $pdo->prepare("UPDATE users SET name=:n, username=:u, role=:r, password_hash=:p, first_login=0 WHERE id=:id");
|
||||
$st->execute([':n'=>$name, ':u'=>$username, ':r'=>$role, ':p'=>$hash, ':id'=>$id]);
|
||||
} else {
|
||||
$st = $pdo->prepare("UPDATE users SET name=:n, username=:u, role=:r WHERE id=:id");
|
||||
$st->execute([':n'=>$name, ':u'=>$username, ':r'=>$role, ':id'=>$id]);
|
||||
}
|
||||
|
||||
echo json_encode(['ok'=>true,'id'=>$id], JSON_UNESCAPED_UNICODE);
|
||||
} catch (PDOException $e) {
|
||||
if ((int)($e->errorInfo[1] ?? 0) === 1062) {
|
||||
http_response_code(409);
|
||||
echo json_encode(['ok'=>false,'error'=>'Username ist schon vergeben']); exit;
|
||||
}
|
||||
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok'=>false,'error'=>'DB Fehler']); exit;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 953 KiB |
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
require_once "config/config.php";
|
||||
include("Data.class.php");
|
||||
|
||||
$Data = new Data($pdo);
|
||||
|
||||
if (!isset($_GET["ts"])) {
|
||||
echo json_encode(["error" => "missing timestamp"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ts = intval($_GET["ts"]);
|
||||
|
||||
// Tages-Start/Ende berechnen
|
||||
$dayStart = strtotime("00:00", $ts);
|
||||
$dayEnd = strtotime("23:59", $ts);
|
||||
|
||||
$sql = $pdo->prepare("
|
||||
SELECT id, startzeit, location
|
||||
FROM termine
|
||||
WHERE startzeit BETWEEN :s AND :e
|
||||
");
|
||||
$sql->execute([
|
||||
":s" => $dayStart,
|
||||
":e" => $dayEnd
|
||||
]);
|
||||
|
||||
$results = $sql->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode($results);
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once "config/config.php";
|
||||
include("Data.class.php");
|
||||
|
||||
$Data = new Data($pdo);
|
||||
|
||||
// Alle Termine mit Status 3 und vorhandener calendar_event_id holen
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT id, calendar_event_id
|
||||
FROM termine
|
||||
WHERE status = 3
|
||||
AND calendar_event_id IS NOT NULL
|
||||
AND calendar_event_id != ''
|
||||
");
|
||||
|
||||
$stmt->execute();
|
||||
|
||||
while ($termin = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||
|
||||
// Kalendereintrag löschen
|
||||
$Data->deleteCalendarEvent($termin['id']);
|
||||
|
||||
// Optional: calendar_event_id zurücksetzen
|
||||
$update = $pdo->prepare("
|
||||
UPDATE termine
|
||||
SET calendar_event_id = NULL
|
||||
WHERE id = ?
|
||||
");
|
||||
|
||||
$update->execute([$termin['id']]);
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"require": {
|
||||
"dompdf/dompdf": "^2.0"
|
||||
}
|
||||
}
|
||||
Generated
+304
@@ -0,0 +1,304 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "f0205c0fedf4b0bac3e0aa4c567cb1ec",
|
||||
"packages": [
|
||||
{
|
||||
"name": "dompdf/dompdf",
|
||||
"version": "v2.0.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/dompdf.git",
|
||||
"reference": "c20247574601700e1f7c8dab39310fca1964dc52"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/c20247574601700e1f7c8dab39310fca1964dc52",
|
||||
"reference": "c20247574601700e1f7c8dab39310fca1964dc52",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-mbstring": "*",
|
||||
"masterminds/html5": "^2.0",
|
||||
"phenx/php-font-lib": ">=0.5.4 <1.0.0",
|
||||
"phenx/php-svg-lib": ">=0.5.2 <1.0.0",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"ext-zip": "*",
|
||||
"mockery/mockery": "^1.3",
|
||||
"phpunit/phpunit": "^7.5 || ^8 || ^9",
|
||||
"squizlabs/php_codesniffer": "^3.5"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-gd": "Needed to process images",
|
||||
"ext-gmagick": "Improves image processing performance",
|
||||
"ext-imagick": "Improves image processing performance",
|
||||
"ext-zlib": "Needed for pdf stream compression"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dompdf\\": "src/"
|
||||
},
|
||||
"classmap": [
|
||||
"lib/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "The Dompdf Community",
|
||||
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
|
||||
}
|
||||
],
|
||||
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
|
||||
"homepage": "https://github.com/dompdf/dompdf",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/dompdf/issues",
|
||||
"source": "https://github.com/dompdf/dompdf/tree/v2.0.8"
|
||||
},
|
||||
"time": "2024-04-29T13:06:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
"version": "2.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Masterminds/html5-php.git",
|
||||
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
|
||||
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.7-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Masterminds\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Matt Butcher",
|
||||
"email": "technosophos@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Matt Farina",
|
||||
"email": "matt@mattfarina.com"
|
||||
},
|
||||
{
|
||||
"name": "Asmir Mustafic",
|
||||
"email": "goetas@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "An HTML5 parser and serializer.",
|
||||
"homepage": "http://masterminds.github.io/html5-php",
|
||||
"keywords": [
|
||||
"HTML5",
|
||||
"dom",
|
||||
"html",
|
||||
"parser",
|
||||
"querypath",
|
||||
"serializer",
|
||||
"xml"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Masterminds/html5-php/issues",
|
||||
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
|
||||
},
|
||||
"time": "2025-07-25T09:04:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phenx/php-font-lib",
|
||||
"version": "0.5.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/php-font-lib.git",
|
||||
"reference": "a1681e9793040740a405ac5b189275059e2a9863"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a1681e9793040740a405ac5b189275059e2a9863",
|
||||
"reference": "a1681e9793040740a405ac5b189275059e2a9863",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"FontLib\\": "src/FontLib"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Ménager",
|
||||
"email": "fabien.menager@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A library to read, parse, export and make subsets of different types of font files.",
|
||||
"homepage": "https://github.com/PhenX/php-font-lib",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/php-font-lib/issues",
|
||||
"source": "https://github.com/dompdf/php-font-lib/tree/0.5.6"
|
||||
},
|
||||
"time": "2024-01-29T14:45:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phenx/php-svg-lib",
|
||||
"version": "0.5.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/php-svg-lib.git",
|
||||
"reference": "46b25da81613a9cf43c83b2a8c2c1bdab27df691"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/46b25da81613a9cf43c83b2a8c2c1bdab27df691",
|
||||
"reference": "46b25da81613a9cf43c83b2a8c2c1bdab27df691",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8.0",
|
||||
"sabberworm/php-css-parser": "^8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Svg\\": "src/Svg"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-3.0-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Ménager",
|
||||
"email": "fabien.menager@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A library to read, parse and export to PDF SVG files.",
|
||||
"homepage": "https://github.com/PhenX/php-svg-lib",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/php-svg-lib/issues",
|
||||
"source": "https://github.com/dompdf/php-svg-lib/tree/0.5.4"
|
||||
},
|
||||
"time": "2024-04-08T12:52:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sabberworm/php-css-parser",
|
||||
"version": "v8.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
|
||||
"reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9",
|
||||
"reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-iconv": "*",
|
||||
"php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41",
|
||||
"rawr/cross-data-providers": "^2.0.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "for parsing UTF-8 CSS"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "9.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Sabberworm\\CSS\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Raphael Schweikert"
|
||||
},
|
||||
{
|
||||
"name": "Oliver Klee",
|
||||
"email": "github@oliverklee.de"
|
||||
},
|
||||
{
|
||||
"name": "Jake Hotson",
|
||||
"email": "jake.github@qzdesign.co.uk"
|
||||
}
|
||||
],
|
||||
"description": "Parser for CSS Files written in PHP",
|
||||
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
|
||||
"keywords": [
|
||||
"css",
|
||||
"parser",
|
||||
"stylesheet"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
|
||||
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0"
|
||||
},
|
||||
"time": "2025-07-11T13:20:48+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.3.0"
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
[server]
|
||||
port=3000
|
||||
|
||||
[obs]
|
||||
url=ws://192.168.1.20:4455
|
||||
password=yourpassword
|
||||
|
||||
[mediamtx]
|
||||
stats=http://192.168.1.59:9997/v3/paths/list
|
||||
webrtc_base=http://192.168.1.59:8889
|
||||
|
||||
[streams]
|
||||
live1=anjuma/live1
|
||||
live2=anjuma/live2
|
||||
|
||||
[thresholds]
|
||||
low=2000
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/* Database credentials. Assuming you are running MySQL
|
||||
server with default setting (user 'root' with no password) */
|
||||
define('DB_SERVER', '192.168.1.91');
|
||||
define('DB_USERNAME', 'anjuma');
|
||||
define('DB_PASSWORD', '+HhuL6kq5iC7t');
|
||||
define('DB_NAME', 'anjuma-dashboard');
|
||||
|
||||
/* Attempt to connect to MySQL database */
|
||||
try{
|
||||
$pdo = new PDO("mysql:host=" . DB_SERVER . ";dbname=" . DB_NAME, DB_USERNAME, DB_PASSWORD);
|
||||
// Set the PDO error mode to exception
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
} catch(PDOException $e){
|
||||
die("ERROR: Could not connect. " . $e->getMessage());
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
return [
|
||||
// Aussteller (euer Rechnungskopf)
|
||||
'issuer' => [
|
||||
'name' => 'ANJUMA CREW', // z.B. ANJUMA / DJ-Service Justin Bosker
|
||||
'street' => 'Musterstraße 1',
|
||||
'zip' => '31582',
|
||||
'city' => 'Nienburg',
|
||||
'country' => 'Deutschland',
|
||||
'tax_no' => '12/345/67890', // Steuernummer
|
||||
'email' => 'itzanjuma@gmail.com',
|
||||
'phone' => '+49 ',
|
||||
],
|
||||
|
||||
// Zahlung
|
||||
'payment' => [
|
||||
'iban' => 'DE00 0000 0000 0000 0000 00',
|
||||
'bic' => 'XXXXXXXXXXX',
|
||||
'bank' => 'Musterbank',
|
||||
'pay_within_days' => 14,
|
||||
],
|
||||
|
||||
// Kleinunternehmer-Hinweis (§19)
|
||||
'kleinunternehmer_text' => 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet und ausgewiesen.',
|
||||
|
||||
// Speicherort für PDFs (muss schreibbar sein!)
|
||||
// Empfehlung: außerhalb Webroot oder per .htaccess schützen
|
||||
'pdf_dir' => __DIR__ . '/../storage/invoices',
|
||||
|
||||
'branding' => [
|
||||
'logo_path' => __DIR__ . '/../assets/logo.png', // <- Dateiname anpassen!
|
||||
'accent' => '#111111',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
return [
|
||||
'host' => '192.168.1.100', // IP/Host vom Proxmox
|
||||
'node' => 'pve',
|
||||
'token_id' => 'anjuma@pam!flx4februar', // user@realm!tokenid
|
||||
'token_secret' => 'e5d73e3e-30e7-48d7-b176-2f7591e50964', // secret
|
||||
'timeout' => 15,
|
||||
'verify_tls' => false, // in prod true lassen
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
$secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
||||
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'domain' => '',
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
* Trying 192.168.1.122:5232...
|
||||
* Connected to 192.168.1.122 (192.168.1.122) port 5232 (#0)
|
||||
* Server auth using Basic with user 'admin'
|
||||
> PUT /admin/c6b9bd9c-3f58-c0dc-21b1-21aa8221ed45/termin-@anjuma.ics HTTP/1.1
|
||||
Host: 192.168.1.122:5232
|
||||
Authorization: Basic YWRtaW46VjFncmhScnZXWDVBaQ==
|
||||
Accept: */*
|
||||
Content-Type: text/calendar
|
||||
Content-Length: 270
|
||||
|
||||
* HTTP 1.0, assume close after body
|
||||
< HTTP/1.0 201 Created
|
||||
< Date: Wed, 03 Dec 2025 14:32:39 GMT
|
||||
< Server: WSGIServer/0.2 CPython/3.13.5
|
||||
< ETag: "6cfa4258cb04182271a2b63a4cad3d24041abf047edf6dd2ba317dba4c65c2f5"
|
||||
< Content-Length: 0
|
||||
<
|
||||
* Closing connection 0
|
||||
+389
@@ -0,0 +1,389 @@
|
||||
const API = "https://api.anjuma-crew.de";
|
||||
const WS_URL = "wss://api.anjuma-crew.de";
|
||||
const STREAM_URL = "https://stream.anjuma-crew.de/anjuma/";
|
||||
|
||||
const MAX_POINTS = 40;
|
||||
const SCENES = ["Starting", "Live1", "Live2", "Low", "Offline"];
|
||||
|
||||
let ws;
|
||||
let reconnectTimeout = null;
|
||||
let retryDelay = 1500;
|
||||
|
||||
let lowThreshold = 1000;
|
||||
|
||||
// ===== FIX STATE =====
|
||||
let isFixing = false;
|
||||
|
||||
// ===== OBS STATE =====
|
||||
let obsStarting = false;
|
||||
|
||||
// ===== DATA =====
|
||||
const data = [];
|
||||
|
||||
// ===== PREVIEW STATE =====
|
||||
const previewState = {
|
||||
live1: false,
|
||||
live2: false
|
||||
};
|
||||
|
||||
// ===== DOM CACHE =====
|
||||
const el = {};
|
||||
|
||||
// ===== INIT =====
|
||||
window.addEventListener("load", () => {
|
||||
cacheDOM();
|
||||
connectWS();
|
||||
});
|
||||
|
||||
// ===== CACHE DOM =====
|
||||
function cacheDOM() {
|
||||
|
||||
el.scene = document.getElementById("scene");
|
||||
el.live1 = document.getElementById("live1");
|
||||
el.live2 = document.getElementById("live2");
|
||||
el.chart = document.getElementById("chart");
|
||||
|
||||
el.obsStartBtn = document.getElementById("obsStartBtn");
|
||||
el.obsStatus = document.getElementById("obsStatus");
|
||||
|
||||
el.fixBtn = document.getElementById("fixBtn");
|
||||
}
|
||||
|
||||
// ===== WS =====
|
||||
function connectWS() {
|
||||
|
||||
ws = new WebSocket(WS_URL);
|
||||
|
||||
ws.onopen = () => {
|
||||
retryDelay = 1500;
|
||||
console.log("WS connected");
|
||||
};
|
||||
|
||||
ws.onclose = () => scheduleReconnect();
|
||||
ws.onerror = () => scheduleReconnect();
|
||||
|
||||
ws.onmessage = (msg) => {
|
||||
const packet = JSON.parse(msg.data);
|
||||
if (packet.type === "update") handle(packet.data);
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
|
||||
if (reconnectTimeout) return;
|
||||
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
reconnectTimeout = null;
|
||||
retryDelay = Math.min(retryDelay * 1.5, 10000);
|
||||
connectWS();
|
||||
}, retryDelay);
|
||||
}
|
||||
|
||||
// ===== OBS STATUS =====
|
||||
function updateOBSStatus(d) {
|
||||
|
||||
if (!el.obsStatus) return;
|
||||
|
||||
if (d.obsConnected) {
|
||||
el.obsStatus.innerText = "🟢 Connected";
|
||||
el.obsStatus.className = "text-success";
|
||||
} else {
|
||||
el.obsStatus.innerText = "🔴 Disconnected";
|
||||
el.obsStatus.className = "text-danger";
|
||||
}
|
||||
}
|
||||
|
||||
// ===== HANDLE =====
|
||||
function handle(d) {
|
||||
|
||||
evaluateStreams(d);
|
||||
updateOBSStatus(d);
|
||||
|
||||
if (d.lowThreshold) {
|
||||
lowThreshold = parseInt(d.lowThreshold);
|
||||
}
|
||||
|
||||
const v1 = d.streams?.live1?.bitrate || 0;
|
||||
const v2 = d.streams?.live2?.bitrate || 0;
|
||||
|
||||
data.push({ live1: v1, live2: v2 });
|
||||
|
||||
if (data.length > MAX_POINTS) {
|
||||
data.shift();
|
||||
}
|
||||
|
||||
updateUI(d);
|
||||
scheduleDraw();
|
||||
|
||||
updatePreview("live1", v1);
|
||||
updatePreview("live2", v2);
|
||||
|
||||
updateFixState(d);
|
||||
}
|
||||
|
||||
// ===== OBS START (WOL) =====
|
||||
function startOBS() {
|
||||
|
||||
if (obsStarting) return;
|
||||
|
||||
obsStarting = true;
|
||||
|
||||
if (el.obsStartBtn) {
|
||||
el.obsStartBtn.innerText = "Starting OBS...";
|
||||
el.obsStartBtn.disabled = true;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
reset();
|
||||
}, 20000);
|
||||
|
||||
fetch(`${API}/obs/start`, {
|
||||
method: "POST"
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
console.log("OBS WOL:", data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
reset();
|
||||
});
|
||||
|
||||
function reset() {
|
||||
clearTimeout(timeout);
|
||||
|
||||
obsStarting = false;
|
||||
|
||||
if (el.obsStartBtn) {
|
||||
el.obsStartBtn.innerText = "OBS Starten";
|
||||
el.obsStartBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== FIX UI =====
|
||||
function updateFixState(d) {
|
||||
|
||||
if (!el.fixBtn) return;
|
||||
|
||||
if (isFixing) return;
|
||||
|
||||
if (d.isFixRunning) {
|
||||
|
||||
isFixing = true;
|
||||
el.fixBtn.innerText = "FIXING...";
|
||||
el.fixBtn.disabled = true;
|
||||
|
||||
} else {
|
||||
|
||||
isFixing = false;
|
||||
el.fixBtn.innerText = "FIX STREAM";
|
||||
el.fixBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== FIX ACTION =====
|
||||
async function fixStream() {
|
||||
|
||||
if (isFixing) return;
|
||||
|
||||
isFixing = true;
|
||||
|
||||
if (el.fixBtn) {
|
||||
el.fixBtn.innerText = "FIXING...";
|
||||
el.fixBtn.disabled = true;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const res = await fetch(`${API}/fix`, {
|
||||
method: "POST"
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
console.log("FIX RESULT:", result);
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
|
||||
isFixing = false;
|
||||
|
||||
if (el.fixBtn) {
|
||||
el.fixBtn.innerText = "FIX STREAM";
|
||||
el.fixBtn.disabled = false;
|
||||
}
|
||||
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// ===== UI =====
|
||||
function updateUI(d) {
|
||||
|
||||
if (el.scene) {
|
||||
el.scene.innerText = d.currentScene || "-";
|
||||
}
|
||||
|
||||
if (el.live1) {
|
||||
el.live1.innerText = (d.streams?.live1?.bitrate || 0) + " kbps";
|
||||
}
|
||||
|
||||
if (el.live2) {
|
||||
el.live2.innerText = (d.streams?.live2?.bitrate || 0) + " kbps";
|
||||
}
|
||||
|
||||
SCENES.forEach(s => {
|
||||
|
||||
const btn = document.getElementById("scene-" + s);
|
||||
if (!btn) return;
|
||||
|
||||
btn.classList.remove("active");
|
||||
|
||||
if (d.currentScene === s) {
|
||||
btn.classList.add("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== PREVIEW =====
|
||||
function updatePreview(stream, bitrate) {
|
||||
|
||||
const frame = document.getElementById("preview-" + stream);
|
||||
const offline = document.getElementById("offline-" + stream);
|
||||
|
||||
if (!frame || !offline) return;
|
||||
|
||||
if (bitrate > 0) {
|
||||
|
||||
if (!previewState[stream]) {
|
||||
|
||||
frame.src = STREAM_URL + stream;
|
||||
frame.style.display = "block";
|
||||
offline.style.display = "none";
|
||||
|
||||
previewState[stream] = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if (previewState[stream]) {
|
||||
|
||||
frame.src = "";
|
||||
frame.style.display = "none";
|
||||
|
||||
previewState[stream] = false;
|
||||
}
|
||||
|
||||
offline.style.display = "flex";
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CHART =====
|
||||
let drawPending = false;
|
||||
|
||||
function scheduleDraw() {
|
||||
|
||||
if (drawPending) return;
|
||||
|
||||
drawPending = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
draw();
|
||||
drawPending = false;
|
||||
});
|
||||
}
|
||||
|
||||
function draw() {
|
||||
|
||||
const canvas = el.chart;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
const w = canvas.width = canvas.offsetWidth;
|
||||
const h = canvas.height = canvas.offsetHeight;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const values = data.flatMap(d => [d.live1, d.live2, lowThreshold]);
|
||||
const maxVal = Math.ceil(Math.max(...values, 100) * 1.15);
|
||||
|
||||
ctx.strokeStyle = "#e5e7eb";
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
for (let i = 0; i <= 5; i++) {
|
||||
|
||||
const y = (h / 5) * i;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(w, y);
|
||||
ctx.stroke();
|
||||
|
||||
const label = Math.round(maxVal * (1 - i / 5));
|
||||
|
||||
ctx.fillStyle = "#666";
|
||||
ctx.font = "11px Arial";
|
||||
ctx.fillText(label + " kbps", 5, y - 3);
|
||||
}
|
||||
|
||||
const lowY = h - (lowThreshold / maxVal) * h;
|
||||
|
||||
ctx.fillStyle = "rgba(239,68,68,0.08)";
|
||||
ctx.fillRect(0, lowY, w, h - lowY);
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = "#f59e0b";
|
||||
ctx.setLineDash([8, 4]);
|
||||
ctx.moveTo(0, lowY);
|
||||
ctx.lineTo(w, lowY);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
ctx.fillStyle = "#f59e0b";
|
||||
ctx.font = "bold 11px Arial";
|
||||
ctx.fillText(`LOW (${lowThreshold})`, w - 140, lowY - 6);
|
||||
|
||||
drawLine(ctx, data.map(d => d.live1), "#22c55e", w, h, maxVal);
|
||||
drawLine(ctx, data.map(d => d.live2), "#3b82f6", w, h, maxVal);
|
||||
}
|
||||
|
||||
function drawLine(ctx, arr, color, w, h, maxVal) {
|
||||
|
||||
if (arr.length < 2) return;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 3;
|
||||
|
||||
arr.forEach((v, i) => {
|
||||
|
||||
const x = (i / (arr.length - 1)) * w;
|
||||
const y = h - (v / maxVal) * h;
|
||||
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
});
|
||||
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// ===== ACTIONS =====
|
||||
function setScene(scene) {
|
||||
fetch(`${API}/scene`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ scene })
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
function startStream() {
|
||||
fetch(`${API}/stream/start`, { method: "POST" });
|
||||
}
|
||||
|
||||
function stopStream() {
|
||||
fetch(`${API}/stream/stop`, { method: "POST" });
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
require_once "config/session.php";
|
||||
|
||||
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
include('navbar.php');
|
||||
|
||||
?>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
|
||||
|
||||
|
||||
<div id="tableHolder"></div>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
refreshTable();
|
||||
});
|
||||
var text = document.getElementById("tableHolder").textContent;
|
||||
function refreshTable(){
|
||||
$.get("getDashboard.php", function (data) {
|
||||
var newtext = data;
|
||||
if(newtext != text){
|
||||
text = newtext;
|
||||
$('#tableHolder').html(data);
|
||||
console.log("Updated!");
|
||||
}
|
||||
});
|
||||
setTimeout(refreshTable, 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
@@ -0,0 +1,358 @@
|
||||
[2025-12-03 16:14:38] UPLOAD BEGIN: http://192.168.1.122:5232/admin/c6b9bd9c-3f58-c0dc-21b1-21aa8221ed45/31.ics
|
||||
[2025-12-03 16:14:38] HTTP 400
|
||||
[2025-12-03 16:14:38] ERROR:
|
||||
[2025-12-03 16:14:38] RESPONSE: Bad Request
|
||||
[2025-12-03 16:14:38] ICS:
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//ANJUMA//Kalender//DE
|
||||
BEGIN:VEVENT
|
||||
UID:anjuma-31@dashboard.anjuma-crew.de
|
||||
DTSTAMP:20251203T151438Z
|
||||
DTSTART:20251204T143600Z
|
||||
DTEND:20251204T153600Z
|
||||
SUMMARY:ANJUMA Termin #31
|
||||
LOCATION:Test 12
|
||||
DESCRIPTION:Test 123
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
[2025-12-03 16:14:38] UPLOAD END
|
||||
--------------------
|
||||
[2025-12-03 18:16:49] UPLOAD BEGIN: http://192.168.1.122:5232/admin/c6b9bd9c-3f58-c0dc-21b1-21aa8221ed45/19.ics
|
||||
[2025-12-03 18:16:49] HTTP 400
|
||||
[2025-12-03 18:16:49] ERROR:
|
||||
[2025-12-03 18:16:49] RESPONSE: Bad Request
|
||||
[2025-12-03 18:16:49] ICS:
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//ANJUMA//Kalender//DE
|
||||
BEGIN:VEVENT
|
||||
UID:anjuma-19@dashboard.anjuma-crew.de
|
||||
DTSTAMP:20251203T171649Z
|
||||
DTSTART:20251203T190000Z
|
||||
DTEND:20251204T000000Z
|
||||
SUMMARY:ANJUMA Termin #19
|
||||
LOCATION:Test
|
||||
DESCRIPTION:
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
[2025-12-03 18:16:49] UPLOAD END
|
||||
--------------------
|
||||
[2025-12-03 18:22:07] UPLOAD BEGIN: http://192.168.1.122:5232/admin/c6b9bd9c-3f58-c0dc-21b1-21aa8221ed45/32.ics
|
||||
[2025-12-03 18:22:07] HTTP 400
|
||||
[2025-12-03 18:22:07] ERROR:
|
||||
[2025-12-03 18:22:07] RESPONSE: Bad Request
|
||||
[2025-12-03 18:22:07] ICS:
|
||||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//ANJUMA//Kalender//DE
|
||||
BEGIN:VEVENT
|
||||
UID:anjuma-32@dashboard.anjuma-crew.de
|
||||
DTSTAMP:20251203T172207Z
|
||||
DTSTART:20251206T210000Z
|
||||
DTEND:20251207T050000Z
|
||||
SUMMARY:ANJUMA Termin #32
|
||||
LOCATION:Hoya
|
||||
DESCRIPTION:wrgsfgsfs
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
[2025-12-03 18:22:07] UPLOAD END
|
||||
--------------------
|
||||
[2025-12-04 23:41:18] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-60.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-04 23:42:02] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-60.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-04 22:48:00] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-1.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:00] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-2.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:01] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-4.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:02] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-34.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:02] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-35.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:03] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-1.ics | status=412 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:03] Response body:
|
||||
[2025-12-04 22:48:03] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-2.ics | status=412 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:03] Response body:
|
||||
[2025-12-04 22:48:03] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-36.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:03] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-4.ics | status=412 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:03] Response body:
|
||||
[2025-12-04 22:48:04] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-34.ics | status=412 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:04] Response body:
|
||||
[2025-12-04 22:48:04] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-37.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:04] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-35.ics | status=412 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:04] Response body:
|
||||
[2025-12-04 22:48:04] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-38.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:04] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-36.ics | status=412 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:04] Response body:
|
||||
[2025-12-04 22:48:05] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-37.ics | status=412 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:05] Response body:
|
||||
[2025-12-04 22:48:05] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-39.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:05] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-38.ics | status=412 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:05] Response body:
|
||||
[2025-12-04 22:48:06] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-39.ics | status=412 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:06] Response body:
|
||||
[2025-12-04 22:48:06] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-40.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:06] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-40.ics | status=412 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:06] Response body:
|
||||
[2025-12-04 22:48:06] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-41.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:07] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-41.ics | status=412 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:07] Response body:
|
||||
[2025-12-04 22:48:07] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-42.ics | status=412 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:07] Response body:
|
||||
[2025-12-04 22:48:07] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-42.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:07] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-43.ics | status=503 | error= | isUpdate=0
|
||||
[2025-12-04 22:48:07] Response body:
|
||||
[2025-12-04 22:48:07] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-43.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-04 23:55:40] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-61.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-05 01:19:26] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-62.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-05 01:19:52] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-62.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-05 19:23:06] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-63.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-05 19:25:30] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-63.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-05 19:31:30] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-63.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-05 19:32:10] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-63.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-05 19:35:02] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-64.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-05 22:10:43] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-34.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-06 19:20:36] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-34.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-06 19:20:49] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-35.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-07 05:26:15] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-35.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-07 05:31:32] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-65.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-07 05:32:14] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-65.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-07 05:36:20] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-65.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-10 23:38:22] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-66.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-10 23:38:33] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-10 23:39:05] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-67.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-10 23:39:09] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-67.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-10 23:39:33] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-67.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-10 23:40:20] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-10 23:40:37] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-67.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-11 01:38:59] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-69.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-11 01:55:26] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-70.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-11 01:55:30] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-70.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-11 01:56:17] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-70.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-11 01:56:28] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-71.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-11 02:05:01] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-72.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-11 03:24:11] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-73.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-14 18:27:28] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-36.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-14 18:27:51] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-37.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-15 02:13:01] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-74.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-15 15:00:54] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-2.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-17 17:56:51] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-17 17:57:48] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-17 18:18:20] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-17 18:54:33] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-17 18:55:30] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-17 19:52:22] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-75.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-17 19:52:56] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-76.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-17 19:53:21] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-76.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-17 23:51:45] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-17 23:53:38] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:02:05] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:23:44] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-83.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-18 00:25:10] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-83.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:25:36] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-83.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:25:54] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-40.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:26:09] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-38.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:26:19] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-39.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:37:05] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-40.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:37:27] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-40.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:38:21] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-40.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:38:41] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-40.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:39:46] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-40.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:40:10] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-40.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:40:26] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-40.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:48:23] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-40.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 00:49:24] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 01:34:12] PUT to https://caldav.icloud.com/25128744805/calendars/3D8A6558-A26E-49CC-8DB3-098E0D509C72/termin-84.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-18 02:23:36] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-66.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:23:53] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-38.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:23:56] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-40.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:24:14] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-39.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:24:28] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-41.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:24:34] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-42.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:24:41] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-43.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:24:51] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-67.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:25:00] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-2.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:25:03] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-4.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:25:08] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-1.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:25:17] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-2.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:25:30] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-36.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:25:38] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-37.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:26:42] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-34.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:26:48] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-35.ics | status=201 | error= | isUpdate=1
|
||||
[2025-12-18 02:27:14] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-85.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-18 02:27:28] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-85.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:28:06] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-86.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-18 02:28:15] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-86.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:28:55] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-87.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-18 02:29:05] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-87.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:31] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-1.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:33] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-2.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:34] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-4.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:36] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-34.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:37] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-35.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:38] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-36.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:40] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-37.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:41] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-38.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:43] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-39.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:44] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-40.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:45] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-41.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:47] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-42.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:48] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-43.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:50] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:51] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-67.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:53] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-85.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:54] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-86.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 02:07:56] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-87.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 03:40:28] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-67.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 03:48:25] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-88.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-18 03:50:34] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-88.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-18 22:49:26] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-89.ics | status=201 | error= | isUpdate=0
|
||||
[2025-12-19 17:15:19] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-38.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-19 17:15:28] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-40.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-21 06:52:13] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-40.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-21 06:52:21] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-38.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-21 06:52:33] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-39.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-21 06:52:45] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-41.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-21 06:52:54] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-39.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-21 06:53:38] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-2.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-21 06:54:05] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-85.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-21 06:54:11] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-86.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-21 06:54:36] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-87.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-25 17:30:41] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-85.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-26 16:01:04] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-86.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-27 06:19:47] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-42.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-28 19:51:07] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-43.ics | status=204 | error= | isUpdate=1
|
||||
[2025-12-31 18:43:25] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-87.ics | status=204 | error= | isUpdate=1
|
||||
[2026-01-04 19:15:38] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-87.ics | status=204 | error= | isUpdate=1
|
||||
[2026-01-04 19:16:43] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-90.ics | status=201 | error= | isUpdate=0
|
||||
[2026-01-04 19:17:12] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-90.ics | status=204 | error= | isUpdate=1
|
||||
[2026-01-11 20:50:59] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-90.ics | status=204 | error= | isUpdate=1
|
||||
[2026-01-11 20:51:35] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-91.ics | status=201 | error= | isUpdate=0
|
||||
[2026-01-11 20:51:42] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-91.ics | status=204 | error= | isUpdate=1
|
||||
[2026-01-31 22:53:22] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-92.ics | status=201 | error= | isUpdate=0
|
||||
[2026-01-31 22:53:39] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-92.ics | status=204 | error= | isUpdate=1
|
||||
[2026-01-31 23:01:07] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-92.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-01 03:11:58] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-92.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-01 03:24:08] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-93.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-01 03:39:37] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-93.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-03 22:12:25] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-94.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-03 22:12:51] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-94.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-06 12:42:19] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-95.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-06 12:42:29] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-95.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-15 18:21:40] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-95.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-15 18:22:51] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-95.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-15 18:23:06] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-95.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-15 18:23:47] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-67.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-15 18:24:21] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-94.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-15 18:26:25] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-96.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-15 18:26:38] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-96.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-15 18:27:31] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-97.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-15 18:27:36] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-97.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-15 18:28:15] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-98.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-15 18:28:23] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-98.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-15 19:02:51] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-99.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-15 19:03:24] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-99.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-15 19:48:58] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-100.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-15 19:49:19] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-100.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-18 10:50:33] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-89.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-21 00:05:07] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-99.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-21 00:30:11] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-99.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-21 00:30:26] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-99.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-21 18:50:49] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-89.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:32:17] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-101.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:32:28] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-101.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:33:11] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-102.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:33:18] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-102.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:34:01] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-103.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:34:12] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-103.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:34:56] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-104.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:35:00] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-104.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:37:17] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-105.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:37:21] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-105.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:39:10] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-106.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:39:15] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-106.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:40:07] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-107.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:40:11] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-107.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:42:01] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-108.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:42:16] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-108.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:43:45] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-109.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:43:48] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-109.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:44:47] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-110.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:44:52] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-110.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:45:45] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-111.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:45:48] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-111.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:46:32] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-112.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:46:36] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-112.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:46:44] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-112.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:47:33] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-113.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:47:58] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-113.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:49:09] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-114.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:49:12] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-114.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:51:19] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-115.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:51:22] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-115.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:52:24] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-116.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:52:28] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-116.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:53:10] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-117.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:53:13] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-117.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:53:42] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-118.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:53:46] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-118.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:54:22] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-119.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:54:25] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-119.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:57:08] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-120.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:57:11] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-120.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 15:57:48] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-121.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 15:57:51] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-121.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 18:59:19] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-122.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 18:59:26] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-122.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-22 19:01:17] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-123.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-22 19:01:20] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-123.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-25 10:49:40] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-124.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-25 10:50:06] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-124.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-25 10:50:46] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-124.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-25 12:56:07] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-125.ics | status=201 | error= | isUpdate=0
|
||||
[2026-02-25 12:56:18] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-125.ics | status=204 | error= | isUpdate=1
|
||||
[2026-02-26 10:02:30] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-125.ics | status=204 | error= | isUpdate=1
|
||||
[2026-03-05 09:29:36] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-124.ics | status=204 | error= | isUpdate=1
|
||||
[2026-03-05 10:06:10] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-91.ics | status=204 | error= | isUpdate=1
|
||||
[2026-03-05 10:06:20] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-91.ics | status=204 | error= | isUpdate=1
|
||||
[2026-03-08 03:44:35] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-94.ics | status=204 | error= | isUpdate=1
|
||||
[2026-03-22 11:01:41] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-100.ics | status=204 | error= | isUpdate=1
|
||||
[2026-03-28 20:09:53] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-100.ics | status=204 | error= | isUpdate=1
|
||||
[2026-03-28 20:09:59] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-100.ics | status=204 | error= | isUpdate=1
|
||||
[2026-03-29 16:33:40] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-99.ics | status=204 | error= | isUpdate=1
|
||||
[2026-04-02 23:05:42] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-126.ics | status=201 | error= | isUpdate=0
|
||||
[2026-04-02 23:06:02] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-126.ics | status=204 | error= | isUpdate=1
|
||||
[2026-04-05 03:24:33] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-96.ics | status=204 | error= | isUpdate=1
|
||||
[2026-04-05 03:24:59] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-97.ics | status=204 | error= | isUpdate=1
|
||||
[2026-04-22 19:08:03] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-89.ics | status=204 | error= | isUpdate=1
|
||||
[2026-04-26 17:19:48] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-126.ics | status=204 | error= | isUpdate=1
|
||||
[2026-04-30 23:25:39] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-127.ics | status=201 | error= | isUpdate=0
|
||||
[2026-04-30 23:26:25] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-127.ics | status=204 | error= | isUpdate=1
|
||||
[2026-05-11 13:50:29] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-101.ics | status=204 | error= | isUpdate=1
|
||||
[2026-05-15 12:26:39] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-101.ics | status=204 | error= | isUpdate=1
|
||||
[2026-05-16 22:10:37] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-98.ics | status=204 | error= | isUpdate=1
|
||||
[2026-05-16 22:11:09] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2026-05-16 22:11:50] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-126.ics | status=204 | error= | isUpdate=1
|
||||
[2026-05-27 17:23:31] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-126.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-14 13:36:47] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-14 13:37:03] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-67.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-15 13:34:15] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-67.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-15 22:44:59] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-128.ics | status=201 | error= | isUpdate=0
|
||||
[2026-06-15 22:45:42] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-128.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-15 22:56:31] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-89.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-15 22:56:35] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-89.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-15 22:56:38] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-89.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-15 23:08:20] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-89.ics | status=201 | error= | isUpdate=1
|
||||
[2026-06-15 23:08:57] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-89.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-15 23:21:38] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-89.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-20 13:44:58] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-128.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-23 21:24:03] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-128.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-23 21:24:11] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-128.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-23 23:14:54] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-66.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-25 20:58:31] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-129.ics | status=201 | error= | isUpdate=0
|
||||
[2026-06-25 20:59:03] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-129.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-25 20:59:43] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-130.ics | status=201 | error= | isUpdate=0
|
||||
[2026-06-25 21:00:20] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-130.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-25 21:04:58] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-130.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-30 18:13:48] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-102.ics | status=204 | error= | isUpdate=1
|
||||
[2026-06-30 18:13:56] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-103.ics | status=204 | error= | isUpdate=1
|
||||
[2026-07-04 13:49:54] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-122.ics | status=204 | error= | isUpdate=1
|
||||
[2026-07-04 13:50:04] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-123.ics | status=204 | error= | isUpdate=1
|
||||
[2026-07-05 01:13:44] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-122.ics | status=204 | error= | isUpdate=1
|
||||
[2026-07-05 01:13:47] PUT to https://p144-caldav.icloud.com/18998614209/calendars/ea9819c0-518a-4f23-ba11-476f639fd66e/termin-122.ics | status=204 | error= | isUpdate=1
|
||||
+506
@@ -0,0 +1,506 @@
|
||||
<?php
|
||||
require_once "config/session.php";
|
||||
|
||||
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
include('navbar.php');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ANJUMA • Finanzen</title>
|
||||
|
||||
<!-- Bootstrap 5 + DataTables via CDN (sicher, keine 404) -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/css/dataTables.bootstrap5.min.css">
|
||||
|
||||
<style>
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||
.kpi-card { min-height: 92px; }
|
||||
.dt-nowrap td { white-space: nowrap; }
|
||||
|
||||
@media (max-width: 576px) {
|
||||
#kpiIncomeHint, #kpiExpensesHint { display:none; }
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.dt-nowrap td { white-space: normal !important; }
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<h3 class="mb-0">Finanzen</h3>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button id="btnRefresh" class="btn btn-outline-secondary btn-sm">↻ Refresh</button>
|
||||
<button id="btnAddExpense" class="btn btn-primary btn-sm">+ Ausgabe</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label mb-1">Von</label>
|
||||
<input type="date" class="form-control" id="fromDate">
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label mb-1">Bis</label>
|
||||
<input type="date" class="form-control" id="toDate">
|
||||
</div>
|
||||
<div class="col-12 col-md-6 d-flex gap-2">
|
||||
<button id="btnThisMonth" class="btn btn-outline-primary">Dieser Monat</button>
|
||||
<button id="btnLastMonth" class="btn btn-outline-primary">Letzter Monat</button>
|
||||
<button id="btnThisYear" class="btn btn-outline-primary">Dieses Jahr</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card shadow-sm kpi-card">
|
||||
<div class="card-body">
|
||||
<div class="text-muted small">Einnahmen</div>
|
||||
<div class="fs-3 fw-semibold" id="kpiIncome">–</div>
|
||||
<div class="text-muted small" id="kpiIncomeHint">–</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card shadow-sm kpi-card">
|
||||
<div class="card-body">
|
||||
<div class="text-muted small">Ausgaben</div>
|
||||
<div class="fs-3 fw-semibold" id="kpiExpenses">–</div>
|
||||
<div class="text-muted small" id="kpiExpensesHint">–</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card shadow-sm kpi-card">
|
||||
<div class="card-body">
|
||||
<div class="text-muted small">Netto</div>
|
||||
<div class="fs-3 fw-semibold" id="kpiNet">–</div>
|
||||
<div class="text-muted small">Einnahmen − Ausgaben</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabIncome" type="button" role="tab">Einnahmen (Termine)</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabExpenses" type="button" role="tab">Ausgaben</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- Income -->
|
||||
<div class="tab-pane fade show active" id="tabIncome" role="tabpanel">
|
||||
<div class="card shadow-sm border-top-0 rounded-top-0">
|
||||
<div class="card-body">
|
||||
<table id="tblIncome" class="table table-striped table-hover align-middle dt-nowrap w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Zeit</th>
|
||||
<th>Location</th>
|
||||
<th>Gage</th>
|
||||
<th>Notiz</th>
|
||||
<th>ID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<div class="text-muted small mt-2" id="incomeInfo"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expenses -->
|
||||
<div class="tab-pane fade" id="tabExpenses" role="tabpanel">
|
||||
<div class="card shadow-sm border-top-0 rounded-top-0">
|
||||
<div class="card-body">
|
||||
<table id="tblExpenses" class="table table-striped table-hover align-middle dt-nowrap w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Kategorie</th>
|
||||
<th>Titel</th>
|
||||
<th>Betrag</th>
|
||||
<th>Methode</th>
|
||||
<th>Notiz</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<div class="text-muted small mt-2" id="expensesInfo"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expense Modal -->
|
||||
<div class="modal fade" id="expenseModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form class="modal-content" id="expenseForm">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="expenseModalTitle">Ausgabe</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="expId" name="id" value="">
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label mb-1">Datum</label>
|
||||
<input type="date" class="form-control" id="expDate" name="date" required>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label mb-1">Betrag (EUR)</label>
|
||||
<input type="number" class="form-control" id="expAmount" name="amount_eur" min="1" step="1" required>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label mb-1">Kategorie</label>
|
||||
<input type="text" class="form-control" id="expCategory" name="category" placeholder="equipment / fuel_travel / marketing ..." required>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label mb-1">Methode</label>
|
||||
<select class="form-select" id="expMethod" name="method">
|
||||
<option value="bank">Bank</option>
|
||||
<option value="cash">Cash</option>
|
||||
<option value="paypal">PayPal</option>
|
||||
<option value="card">Karte</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label mb-1">Titel</label>
|
||||
<input type="text" class="form-control" id="expTitle" name="title" required>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label mb-1">Notiz</label>
|
||||
<textarea class="form-control" id="expNotes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning mt-3 mb-0 small">
|
||||
Tipp: Kategorien konsistent halten (z.B. <span class="mono">equipment</span>, <span class="mono">fuel_travel</span>, <span class="mono">marketing</span>), dann sind Auswertungen später easy.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary" id="btnSaveExpense">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirm Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Ausgabe löschen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="deleteBody">Wirklich löschen?</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="button" class="btn btn-danger" id="btnDoDelete">Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 1080">
|
||||
<div id="toast" class="toast align-items-center text-bg-dark border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body" id="toastBody">...</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/datatables.net@1.13.10/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
const API_INCOME = '/api/finance_income.php';
|
||||
const API_EXP_LIST = '/api/finance_expenses_list.php';
|
||||
const API_EXP_SAVE = '/api/finance_expenses_save.php';
|
||||
const API_EXP_DEL = '/api/finance_expenses_delete.php';
|
||||
|
||||
let dtIncome, dtExpenses;
|
||||
let deleteId = null;
|
||||
|
||||
const toast = new bootstrap.Toast(document.getElementById('toast'), { delay: 3500 });
|
||||
function showToast(msg) {
|
||||
document.getElementById('toastBody').textContent = msg;
|
||||
toast.show();
|
||||
}
|
||||
|
||||
function eur(n) {
|
||||
const v = Number(n) || 0;
|
||||
return v.toLocaleString('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 });
|
||||
}
|
||||
|
||||
function fmtDateDE(iso) {
|
||||
if (!iso) return '';
|
||||
const [y, m, d] = iso.split('-');
|
||||
return `${d}.${m}.${y}`;
|
||||
}
|
||||
|
||||
|
||||
function todayISO() {
|
||||
const d = new Date();
|
||||
const m = String(d.getMonth()+1).padStart(2,'0');
|
||||
const day = String(d.getDate()).padStart(2,'0');
|
||||
return `${d.getFullYear()}-${m}-${day}`;
|
||||
}
|
||||
|
||||
function setRange(from, to) {
|
||||
document.getElementById('fromDate').value = from;
|
||||
document.getElementById('toDate').value = to;
|
||||
}
|
||||
|
||||
function isoLocal(dateObj) {
|
||||
const y = dateObj.getFullYear();
|
||||
const m = String(dateObj.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(dateObj.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
function thisMonthRange() {
|
||||
const d = new Date();
|
||||
const from = new Date(d.getFullYear(), d.getMonth(), 1);
|
||||
const to = new Date(d.getFullYear(), d.getMonth() + 1, 0);
|
||||
return [isoLocal(from), isoLocal(to)];
|
||||
}
|
||||
|
||||
function lastMonthRange() {
|
||||
const d = new Date();
|
||||
const from = new Date(d.getFullYear(), d.getMonth() - 1, 1);
|
||||
const to = new Date(d.getFullYear(), d.getMonth(), 0);
|
||||
return [isoLocal(from), isoLocal(to)];
|
||||
}
|
||||
|
||||
function thisYearRange() {
|
||||
const d = new Date();
|
||||
const from = new Date(d.getFullYear(), 0, 1);
|
||||
const to = new Date(d.getFullYear(), 11, 31);
|
||||
return [isoLocal(from), isoLocal(to)];
|
||||
}
|
||||
|
||||
|
||||
function getFilterQS() {
|
||||
const from = document.getElementById('fromDate').value;
|
||||
const to = document.getElementById('toDate').value;
|
||||
const qs = new URLSearchParams({ from, to });
|
||||
return qs.toString();
|
||||
}
|
||||
|
||||
async function fetchJSON(url) {
|
||||
const res = await fetch(url, { cache: 'no-store' });
|
||||
const json = await res.json();
|
||||
if (!json.ok) throw new Error(json.error || 'API error');
|
||||
return json;
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
try {
|
||||
const qs = getFilterQS();
|
||||
|
||||
const [income, expenses] = await Promise.all([
|
||||
fetchJSON(`${API_INCOME}?${qs}`),
|
||||
fetchJSON(`${API_EXP_LIST}?${qs}`),
|
||||
]);
|
||||
|
||||
// KPI
|
||||
const inc = Number(income.sum_eur || 0);
|
||||
const exp = Number(expenses.sum_eur || 0);
|
||||
document.getElementById('kpiIncome').textContent = eur(inc);
|
||||
document.getElementById('kpiExpenses').textContent = eur(exp);
|
||||
document.getElementById('kpiNet').textContent = eur(inc - exp);
|
||||
|
||||
document.getElementById('kpiIncomeHint').textContent = `${income.items.length} abgeschlossene Termine`;
|
||||
document.getElementById('kpiExpensesHint').textContent = `${expenses.items.length} Ausgaben`;
|
||||
|
||||
// Tables
|
||||
dtIncome.clear().rows.add(income.items || []).draw(false);
|
||||
dtExpenses.clear().rows.add(expenses.items || []).draw(false);
|
||||
|
||||
document.getElementById('incomeInfo').textContent =
|
||||
`Zeitraum ${income.from} bis ${income.to} • Summe: ${eur(inc)}`;
|
||||
|
||||
document.getElementById('expensesInfo').textContent =
|
||||
`Zeitraum ${expenses.from} bis ${expenses.to} • Summe: ${eur(exp)}`;
|
||||
|
||||
} catch (e) {
|
||||
showToast('Fehler: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function expenseActions(row) {
|
||||
const encoded = encodeURIComponent(JSON.stringify(row));
|
||||
return `
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-secondary btnEditExpense" data-row="${encoded}">Edit</button>
|
||||
<button class="btn btn-outline-danger btnDeleteExpense" data-id="${row.id}" data-title="${encodeURIComponent(row.title)}">Delete</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function openExpenseModal(row=null) {
|
||||
document.getElementById('expenseModalTitle').textContent = row ? 'Ausgabe bearbeiten' : 'Neue Ausgabe';
|
||||
document.getElementById('expId').value = row ? row.id : '';
|
||||
document.getElementById('expDate').value = row ? row.date : todayISO();
|
||||
document.getElementById('expCategory').value = row ? row.category : '';
|
||||
document.getElementById('expTitle').value = row ? row.title : '';
|
||||
document.getElementById('expAmount').value = row ? row.amount_eur : '';
|
||||
document.getElementById('expMethod').value = row ? row.method : 'bank';
|
||||
document.getElementById('expNotes').value = row ? (row.notes || '') : '';
|
||||
|
||||
new bootstrap.Modal(document.getElementById('expenseModal')).show();
|
||||
}
|
||||
|
||||
function openDeleteModal(id, title) {
|
||||
deleteId = id;
|
||||
const t = title ? decodeURIComponent(title) : '';
|
||||
document.getElementById('deleteBody').innerHTML =
|
||||
`Ausgabe <b>${t}</b> (ID <span class="mono">${id}</span>) wirklich löschen?`;
|
||||
new bootstrap.Modal(document.getElementById('deleteModal')).show();
|
||||
}
|
||||
|
||||
document.getElementById('btnRefresh').addEventListener('click', refreshAll);
|
||||
|
||||
document.getElementById('btnThisMonth').addEventListener('click', () => {
|
||||
const [f,t] = thisMonthRange(); setRange(f,t); refreshAll();
|
||||
});
|
||||
document.getElementById('btnLastMonth').addEventListener('click', () => {
|
||||
const [f,t] = lastMonthRange(); setRange(f,t); refreshAll();
|
||||
});
|
||||
document.getElementById('btnThisYear').addEventListener('click', () => {
|
||||
const [f,t] = thisYearRange(); setRange(f,t); refreshAll();
|
||||
});
|
||||
|
||||
document.getElementById('btnAddExpense').addEventListener('click', () => openExpenseModal(null));
|
||||
|
||||
document.getElementById('expenseForm').addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault();
|
||||
try {
|
||||
const fd = new FormData(ev.target);
|
||||
const res = await fetch(API_EXP_SAVE, { method: 'POST', body: fd });
|
||||
const json = await res.json();
|
||||
if (!json.ok) throw new Error(json.error || 'Save failed');
|
||||
|
||||
showToast('Gespeichert ✅');
|
||||
bootstrap.Modal.getInstance(document.getElementById('expenseModal'))?.hide();
|
||||
await refreshAll();
|
||||
} catch (e) {
|
||||
showToast('Fehler: ' + e.message);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btnDoDelete').addEventListener('click', async () => {
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('id', deleteId);
|
||||
const res = await fetch(API_EXP_DEL, { method: 'POST', body: fd });
|
||||
const json = await res.json();
|
||||
if (!json.ok) throw new Error(json.error || 'Delete failed');
|
||||
|
||||
showToast('Gelöscht 🗑️');
|
||||
bootstrap.Modal.getInstance(document.getElementById('deleteModal'))?.hide();
|
||||
deleteId = null;
|
||||
await refreshAll();
|
||||
} catch (e) {
|
||||
showToast('Fehler: ' + e.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Delegated events for table buttons
|
||||
document.addEventListener('click', (ev) => {
|
||||
const editBtn = ev.target.closest('.btnEditExpense');
|
||||
if (editBtn) {
|
||||
const row = JSON.parse(decodeURIComponent(editBtn.dataset.row));
|
||||
openExpenseModal(row);
|
||||
return;
|
||||
}
|
||||
|
||||
const delBtn = ev.target.closest('.btnDeleteExpense');
|
||||
if (delBtn) {
|
||||
openDeleteModal(delBtn.dataset.id, delBtn.dataset.title);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Init
|
||||
$(function() {
|
||||
// Default range: this month
|
||||
const [f,t] = thisMonthRange();
|
||||
setRange(f,t);
|
||||
|
||||
dtIncome = $('#tblIncome').DataTable({
|
||||
pageLength: 10,
|
||||
order: [[0,'desc']],
|
||||
columns: [
|
||||
{ data: 'date', className: 'mono', render: (d)=>fmtDateDE(d) },
|
||||
{ data: 'time', className: 'mono' },
|
||||
{ data: 'location' },
|
||||
{ data: 'gage', className: 'mono', render: (d)=>eur(d) },
|
||||
{ data: 'desc', render: (d)=>(d||'').toString().slice(0,80) },
|
||||
{ data: 'id', className: 'mono' },
|
||||
],
|
||||
columnDefs: [
|
||||
{ targets: [1,4,5], visible: window.matchMedia("(max-width: 576px)").matches ? false : true },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
dtExpenses = $('#tblExpenses').DataTable({
|
||||
pageLength: 10,
|
||||
order: [[0,'desc']],
|
||||
columns: [
|
||||
{ data: 'date', className: 'mono', render: (d)=>fmtDateDE(d) },
|
||||
{ data: 'category', className: 'mono' },
|
||||
{ data: 'title' },
|
||||
{ data: 'amount_eur', className: 'mono', render: (d)=>eur(d) },
|
||||
{ data: 'method', className: 'mono' },
|
||||
{ data: 'notes', render: (d)=>(d||'').toString().slice(0,80) },
|
||||
{ data: null, orderable:false, searchable:false, render:(row)=>expenseActions(row) },
|
||||
],
|
||||
columnDefs: [
|
||||
{ targets: [1,4,5], visible: window.matchMedia("(max-width: 576px)").matches ? false : true },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
refreshAll();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,439 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
date_default_timezone_set("Europe/Berlin");
|
||||
require_once "config/config.php";
|
||||
include("Data.class.php");
|
||||
$Data = new Data($pdo);
|
||||
|
||||
session_start();
|
||||
|
||||
// Login-Check
|
||||
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- STATISTIKEN / DATEN HOLEN ---
|
||||
|
||||
// Status-Counts
|
||||
$statusMap = [
|
||||
0 => "Offen",
|
||||
1 => "Bereit",
|
||||
2 => "Erledigt",
|
||||
3 => "Storniert"
|
||||
];
|
||||
|
||||
$statusCounts = [
|
||||
0 => 0,
|
||||
1 => 0,
|
||||
2 => 0,
|
||||
3 => 0
|
||||
];
|
||||
|
||||
$stmt = $pdo->query("SELECT status, COUNT(*) AS cnt FROM termine GROUP BY status");
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$s = (int)$row['status'];
|
||||
if (isset($statusCounts[$s])) {
|
||||
$statusCounts[$s] = (int)$row['cnt'];
|
||||
}
|
||||
}
|
||||
|
||||
// Nächste 5 Termine
|
||||
$nowTs = time();
|
||||
$nextStmt = $pdo->prepare("
|
||||
SELECT t.*, u.name AS user_name
|
||||
FROM termine t
|
||||
LEFT JOIN users u ON u.id = t.user_id
|
||||
WHERE t.startzeit >= :now
|
||||
ORDER BY t.startzeit ASC
|
||||
LIMIT 5
|
||||
");
|
||||
$nextStmt->execute([':now' => $nowTs]);
|
||||
$nextEvents = $nextStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Letzte Änderungen (Activity Log)
|
||||
$changesStmt = $pdo->query("
|
||||
SELECT c.*, u.name AS user_name
|
||||
FROM changes c
|
||||
LEFT JOIN users u ON u.id = c.user_id
|
||||
ORDER BY c.timestamp DESC
|
||||
LIMIT 5
|
||||
");
|
||||
$recentChanges = $changesStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Einnahmen dieses Monats
|
||||
$currentMonth = date('Y-m-01 00:00:00');
|
||||
$monthStartTs = strtotime($currentMonth);
|
||||
$nextMonthTs = strtotime('+1 month', $monthStartTs);
|
||||
|
||||
$gageStmt = $pdo->prepare("
|
||||
SELECT
|
||||
SUM(gage) AS sum_gage,
|
||||
COUNT(*) AS count_terms
|
||||
FROM termine
|
||||
WHERE startzeit >= :start
|
||||
AND startzeit < :end
|
||||
AND status = 2 -- nur abgeschlossene Termine zählen!
|
||||
");
|
||||
$gageStmt->execute([
|
||||
':start' => $monthStartTs,
|
||||
':end' => $nextMonthTs
|
||||
]);
|
||||
$gageData = $gageStmt->fetch(PDO::FETCH_ASSOC);
|
||||
$sumGage = (float)($gageData['sum_gage'] ?? 0);
|
||||
$countGigs = (int)($gageData['count_terms'] ?? 0);
|
||||
|
||||
|
||||
// Offene ToDos (Beispiele: Gage = 0, Endzeit = 0)
|
||||
$todoGageStmt = $pdo->query("SELECT COUNT(*) FROM termine WHERE (gage = 0 OR gage IS NULL)");
|
||||
$todoNoGage = (int)$todoGageStmt->fetchColumn();
|
||||
|
||||
$todoEndStmt = $pdo->query("SELECT COUNT(*) FROM termine WHERE (endzeit = 0 OR endzeit IS NULL)");
|
||||
$todoNoEnd = (int)$todoEndStmt->fetchColumn();
|
||||
|
||||
?>
|
||||
<style>
|
||||
/* Allgemeines Dashboard-Layout */
|
||||
.dashboard-section-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.dashboard-subtitle {
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Status-Kacheln */
|
||||
.status-card {
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
box-shadow: 0 3px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.status-card {
|
||||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||||
}
|
||||
|
||||
.status-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 5px 14px rgba(0, 0, 0, 0.25);
|
||||
cursor: pointer;
|
||||
}
|
||||
.status-label {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.status-value {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Farben für Status */
|
||||
.status-open { background: #a0a0a0; } /* blau */
|
||||
.status-ready { background: #0d6efd; color: #212529; }
|
||||
.status-done { background: #198754; }
|
||||
.status-cancel { background: #dc3545; }
|
||||
|
||||
/* Karten fürs Dashboard */
|
||||
.card-soft {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* Nächste Termine */
|
||||
.next-event-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.next-event-meta {
|
||||
font-size: 0.85rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Activity Log im Dashboard */
|
||||
.activity-entry {
|
||||
font-size: 0.9rem;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f1f3f5;
|
||||
}
|
||||
.activity-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.activity-time {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* ToDo Badges */
|
||||
.todo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 0.9rem;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.status-link {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
.status-link:hover .status-card {
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
|
||||
/* Mobile spacing */
|
||||
@media (max-width: 768px){
|
||||
.status-card {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container mt-4 mb-5">
|
||||
|
||||
<!-- Überschrift + Quick-Action -->
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center mb-4">
|
||||
<div>
|
||||
<h3 class="mb-1">Dashboard</h3>
|
||||
<div class="dashboard-subtitle">
|
||||
Überblick über kommende Termine, Status & Änderungen.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 mt-md-0">
|
||||
<a href="termin_create.php" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Neuer Termin
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STATUS-KACHELN -->
|
||||
<div class="row g-3 mb-4">
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<a href="termine.php?status=0" class="status-link">
|
||||
<div class="status-card status-open">
|
||||
<div class="status-label">Offen</div>
|
||||
<div class="status-value"><?= $statusCounts[0]; ?></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<a href="termine.php?status=1" class="status-link">
|
||||
<div class="status-card status-ready">
|
||||
<div class="status-label">Bereit</div>
|
||||
<div class="status-value"><?= $statusCounts[1]; ?></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<a href="termine.php?status=2" class="status-link">
|
||||
<div class="status-card status-done">
|
||||
<div class="status-label">Abgeschlossen</div>
|
||||
<div class="status-value"><?= $statusCounts[2]; ?></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-3">
|
||||
<a href="termine.php?status=3" class="status-link">
|
||||
<div class="status-card status-cancel">
|
||||
<div class="status-label">Storniert</div>
|
||||
<div class="status-value"><?= $statusCounts[3]; ?></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- OBERE REIHE: Nächste Termine + ToDos -->
|
||||
<div class="row g-4 mb-4">
|
||||
|
||||
<!-- Nächste Termine -->
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card card-soft">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div class="dashboard-section-title">
|
||||
<i class="bi bi-calendar-event me-1"></i> Nächste Termine
|
||||
</div>
|
||||
<a href="termine.php" class="btn btn-sm btn-outline-secondary">
|
||||
Alle Termine
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if (count($nextEvents) === 0): ?>
|
||||
<p class="text-muted mb-0">Keine kommenden Termine eingetragen.</p>
|
||||
<?php else: ?>
|
||||
<div class="list-group list-group-flush">
|
||||
<?php foreach ($nextEvents as $ev): ?>
|
||||
<?php
|
||||
$startStr = date('d.m.Y H:i', $ev['startzeit']);
|
||||
$statusLabel = $statusMap[$ev['status']] ?? $ev['status'];
|
||||
$badgeClass = match((int)$ev['status']) {
|
||||
0 => 'bg-secondary',
|
||||
1 => 'bg-primary',
|
||||
2 => 'bg-success',
|
||||
3 => 'bg-danger',
|
||||
default => 'bg-secondary'
|
||||
};
|
||||
?>
|
||||
<a href="termin_show.php?id=<?= $ev['id']; ?>"
|
||||
class="list-group-item list-group-item-action d-flex justify-content-between align-items-start">
|
||||
<div class="me-3">
|
||||
<div class="next-event-title"><?= htmlspecialchars($ev['location']); ?></div>
|
||||
<div class="next-event-meta">
|
||||
<i class="bi bi-clock"></i> <?= $startStr; ?>
|
||||
<?php if (!empty($ev['user_name'])): ?>
|
||||
· <i class="bi bi-person"></i> <?= htmlspecialchars($ev['user_name']); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge rounded-pill <?= $badgeClass; ?>">
|
||||
<?= $statusLabel; ?>
|
||||
</span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ToDos / Quick Infos -->
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card card-soft mb-3">
|
||||
<div class="card-body">
|
||||
<div class="dashboard-section-title mb-2">
|
||||
<i class="bi bi-list-check me-1"></i> Offene Punkte
|
||||
</div>
|
||||
<div class="todo-item">
|
||||
<span>Termine ohne Gage</span>
|
||||
<span class="badge bg-warning text-dark"><?= $todoNoGage; ?></span>
|
||||
</div>
|
||||
<div class="todo-item">
|
||||
<span>Termine ohne Endzeit</span>
|
||||
<span class="badge bg-warning text-dark"><?= $todoNoEnd; ?></span>
|
||||
</div>
|
||||
<!-- ggf. weitere ToDos -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-soft">
|
||||
<div class="card-body">
|
||||
<div class="dashboard-section-title mb-1">
|
||||
<i class="bi bi-cash-stack me-1"></i> Einnahmen <?= date('m.Y'); ?>
|
||||
</div>
|
||||
<p class="mb-1">
|
||||
<strong><?= number_format($sumGage, 2, ',', '.'); ?> €</strong> Gesamt
|
||||
</p>
|
||||
<p class="mb-0 text-muted">
|
||||
<?= $countGigs; ?> Termin(e) in diesem Monat.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- UNTERE REIHE: Letzte Änderungen -->
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card card-soft">
|
||||
<div class="card-body">
|
||||
|
||||
<div class="dashboard-section-title mb-2">
|
||||
<i class="bi bi-clock-history me-1"></i> Letzte Änderungen
|
||||
</div>
|
||||
|
||||
<?php if (count($recentChanges) === 0): ?>
|
||||
<p class="text-muted mb-0">Noch keine Änderungen protokolliert.</p>
|
||||
<?php else: ?>
|
||||
|
||||
<?php foreach ($recentChanges as $change): ?>
|
||||
<?php
|
||||
$timeStr = date('d.m.Y H:i', $change['timestamp']);
|
||||
$userName = $change['user_name'] ?? 'Unbekannt';
|
||||
$field = strtoupper($change['was']);
|
||||
$termId = $change['termin_id'];
|
||||
|
||||
// --- ALT UND NEU LESEN ---
|
||||
$alt = $change['alt'];
|
||||
$neu = $change['neu'];
|
||||
|
||||
// Spezielle Formatierungen
|
||||
if ($field === 'STATUS') {
|
||||
$alt = $statusMap[$alt] ?? $alt;
|
||||
$neu = $statusMap[$neu] ?? $neu;
|
||||
}
|
||||
|
||||
if (in_array($field, ['STARTZEIT','ENDZEIT','REALENDZEIT'])) {
|
||||
if ($alt > 0) $alt = date('d.m.Y H:i', $alt);
|
||||
$neu = date('d.m.Y H:i', $neu);
|
||||
}
|
||||
|
||||
if ($field === 'ANWESEND') {
|
||||
$altArr = json_decode($alt, true) ?: [];
|
||||
$neuArr = json_decode($neu, true) ?: [];
|
||||
|
||||
$alt = implode(', ', array_map(fn($id)=>$Data->getDataByID($id,"name"), $altArr));
|
||||
$neu = implode(', ', array_map(fn($id)=>$Data->getDataByID($id,"name"), $neuArr));
|
||||
}
|
||||
|
||||
// CREATE hat keinen alten Wert
|
||||
$isCreate = ($field === 'CREATE');
|
||||
$isDelete = ($field === 'DELETE');
|
||||
?>
|
||||
|
||||
<!-- KLICKBARE ÄNDERUNG -->
|
||||
<a href="termin_show.php?id=<?= $termId ?>"
|
||||
class="text-decoration-none text-dark">
|
||||
<div class="activity-entry">
|
||||
|
||||
<div class="activity-time">
|
||||
<i class="bi bi-clock"></i> <?= $timeStr ?>
|
||||
• <span class="text-primary">#<?= $termId ?></span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong><?= htmlspecialchars($userName) ?></strong>
|
||||
|
||||
<?php if ($isCreate): ?>
|
||||
|
||||
hat einen neuen <strong>TERMIN</strong> erstellt.
|
||||
|
||||
<?php elseif ($isDelete): ?>
|
||||
|
||||
hat einen <strong>TERMIN</strong> gelöscht.
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
hat <strong><?= htmlspecialchars($field) ?></strong> geändert:
|
||||
<br>
|
||||
<span class="text-danger fw-semibold"><?= htmlspecialchars($alt) ?></span>
|
||||
→
|
||||
<span class="text-success fw-semibold"><?= htmlspecialchars($neu) ?></span>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
date_default_timezone_set("Europe/Berlin");
|
||||
require_once "config/config.php";
|
||||
include("Data.class.php");
|
||||
$Data = new Data($pdo);
|
||||
|
||||
$statusFilter = (isset($_GET['status']) && $_GET['status'] !== '')
|
||||
? intval($_GET['status'])
|
||||
: null;
|
||||
|
||||
|
||||
if ($statusFilter !== null) {
|
||||
$sql = $pdo->prepare("SELECT * FROM termine WHERE status = :s ORDER BY startzeit ASC");
|
||||
$sql->execute([':s' => $statusFilter]);
|
||||
$termine = $sql->fetchAll(PDO::FETCH_ASSOC);
|
||||
} else {
|
||||
$sql = $pdo->query("SELECT * FROM termine ORDER BY status ASC, startzeit ASC");
|
||||
$termine = $sql->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
function badgeLocationType($art) {
|
||||
$class = '';
|
||||
switch (strtolower($art)) {
|
||||
case 'party': $class = 'badge-party'; break;
|
||||
case 'club': $class = 'badge-club'; break;
|
||||
case 'hochzeit': $class = 'badge-hochzeit'; break;
|
||||
}
|
||||
return "<span class='badge $class text-white'>" . htmlspecialchars($art) . "</span>";
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<!-- KORREKTER VIEWPORT -->
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
|
||||
<title>ANJUMA Dashboard</title>
|
||||
</head>
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
|
||||
.badge-party {
|
||||
background-color: #0d6efd; /* Blau */
|
||||
}
|
||||
.badge-club {
|
||||
background-color: #6f42c1; /* Lila */
|
||||
}
|
||||
.badge-hochzeit {
|
||||
background-color: #dc3545; /* Rot */
|
||||
}
|
||||
|
||||
/* Bootstrap Borders killen */
|
||||
.table td,
|
||||
.table th,
|
||||
.table tr {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* Hintergrundfarben (lassen wir wie du sie hast) */
|
||||
.table tr.status-open > td {
|
||||
background-color: #cfe2ff !important;
|
||||
}
|
||||
.table tr.status-ready > td {
|
||||
background-color: #fff3cd !important;
|
||||
}
|
||||
.table tr.status-done > td {
|
||||
background-color: #d1e7dd !important;
|
||||
}
|
||||
.table tr.status-cancel > td {
|
||||
background-color: #f8d7da !important;
|
||||
}
|
||||
|
||||
/* Linker Farbstreifen – stabil & sauber */
|
||||
tr.status-open td:first-child {
|
||||
box-shadow: inset 4px 0 0 0 #0d6efd !important;
|
||||
}
|
||||
tr.status-ready td:first-child {
|
||||
box-shadow: inset 4px 0 0 0 #ffc107 !important;
|
||||
}
|
||||
tr.status-done td:first-child {
|
||||
box-shadow: inset 4px 0 0 0 #198754 !important;
|
||||
}
|
||||
tr.status-cancel td:first-child {
|
||||
box-shadow: inset 4px 0 0 0 #dc3545 !important;
|
||||
}
|
||||
|
||||
|
||||
/* MOBILE OPTIMIERUNG */
|
||||
/* Desktop normal */
|
||||
@media (min-width: 769px) {
|
||||
.mobile-label {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile optimiert */
|
||||
@media (max-width: 768px) {
|
||||
|
||||
/* Kopfzeile ausblenden (wir ersetzen sie je Zelle) */
|
||||
thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Zeilen als Blöcke */
|
||||
table tr {
|
||||
display: block;
|
||||
margin-bottom: 18px;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
/* Tabellenzellen untereinander */
|
||||
table td {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 8px 5px !important;
|
||||
font-size: 15px;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* Label links, Wert rechts */
|
||||
table td::before {
|
||||
content: attr(data-label);
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Bearbeiten Button schön */
|
||||
td .btn {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12">
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h3 class="mb-0"><i class="bi bi-calendar-event"></i> Termine</h3>
|
||||
|
||||
<a href="termin_create.php" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Neuer Termin
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle shadow-sm">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th><i class="bi bi-hash"></i> ID</th>
|
||||
<th><i class="bi bi-clock-history"></i> Startzeit</th>
|
||||
<th><i class="bi bi-geo-alt"></i> Location</th>
|
||||
<th><i class="bi bi-person-circle"></i> Erstellt von</th>
|
||||
<th class="text-end">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<?php foreach ($termine as $row): ?>
|
||||
<tr class="<?= $Data->getTerminLabel($row['id']); ?>">
|
||||
|
||||
<td data-label="ID"><?= $row['id']; ?></td>
|
||||
|
||||
<td data-label="Startzeit">
|
||||
<?= $Data->timestampToDate($row['startzeit']); ?>
|
||||
</td>
|
||||
|
||||
<td data-label="Location"><?= $row['location'] . " " . badgeLocationType($row['locationtype']); ?></td>
|
||||
|
||||
<td data-label="Erstellt von">
|
||||
<?= $Data->getDataByID($row['user_id'], "name"); ?>
|
||||
</td>
|
||||
|
||||
<td data-label="Aktion" class="text-end">
|
||||
<a href="termin_show.php?id=<?= $row['id']; ?>"
|
||||
class="btn btn-warning btn-sm w-100">
|
||||
<i class="bi bi-pencil-square"></i> Bearbeiten
|
||||
</a>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<body>
|
||||
<!-- Bootstrap JS Bundle (mit Popper) -->
|
||||
</body>
|
||||
@@ -0,0 +1,186 @@
|
||||
const HEALTH_CONFIG = {
|
||||
warningThreshold: 60,
|
||||
dangerThreshold: 30,
|
||||
|
||||
staleWarningMs: 2500,
|
||||
staleDangerMs: 5000,
|
||||
|
||||
bitrateLow: 1000,
|
||||
bitrateGood: 1001,
|
||||
|
||||
autoSceneSwitch: false,
|
||||
sceneLow: "Low",
|
||||
sceneOffline: "Offline",
|
||||
|
||||
enableAlerts: false
|
||||
};
|
||||
|
||||
// ================= STATE =================
|
||||
let lastHealthState = {
|
||||
live1: "UNKNOWN",
|
||||
live2: "UNKNOWN"
|
||||
};
|
||||
|
||||
// ================= MAIN ENTRY =================
|
||||
function evaluateStreams(status) {
|
||||
|
||||
if (!status?.streams) return;
|
||||
|
||||
const results = {};
|
||||
|
||||
for (const key of Object.keys(status.streams)) {
|
||||
|
||||
const stream = status.streams[key];
|
||||
|
||||
const health = calculateHealth(stream);
|
||||
|
||||
results[key] = health;
|
||||
|
||||
handleStateChange(key, health, status);
|
||||
updateHealthUI(key, health);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// ================= HEALTH CALC =================
|
||||
function calculateHealth(stream) {
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const bitrate = stream.bitrate || 0;
|
||||
const staleness = now - (stream.time || now);
|
||||
|
||||
let score = 100;
|
||||
|
||||
// 🔴 STALENESS (most important)
|
||||
if (staleness > HEALTH_CONFIG.staleDangerMs) score -= 60;
|
||||
else if (staleness > HEALTH_CONFIG.staleWarningMs) score -= 30;
|
||||
|
||||
// 📉 BITRATE
|
||||
if (bitrate === 0) score -= 50;
|
||||
else if (bitrate < HEALTH_CONFIG.bitrateLow) score -= 30;
|
||||
else if (bitrate < HEALTH_CONFIG.bitrateGood) score -= 10;
|
||||
|
||||
// clamp
|
||||
score = Math.max(0, Math.min(100, score));
|
||||
|
||||
return {
|
||||
score,
|
||||
bitrate,
|
||||
staleness,
|
||||
status: getStatus(score, bitrate, staleness)
|
||||
};
|
||||
}
|
||||
|
||||
// ================= STATUS =================
|
||||
function getStatus(score, bitrate, staleness) {
|
||||
|
||||
if (bitrate === 0 && staleness > HEALTH_CONFIG.staleDangerMs) {
|
||||
return "DOWN";
|
||||
}
|
||||
|
||||
if (score < HEALTH_CONFIG.dangerThreshold) {
|
||||
return "DANGER";
|
||||
}
|
||||
|
||||
if (score < HEALTH_CONFIG.warningThreshold) {
|
||||
return "WEAK";
|
||||
}
|
||||
|
||||
return "GOOD";
|
||||
}
|
||||
|
||||
// ================= STATE HANDLER =================
|
||||
function handleStateChange(streamName, health, status) {
|
||||
|
||||
const prev = lastHealthState[streamName];
|
||||
const current = health.status;
|
||||
|
||||
if (prev === current) return;
|
||||
|
||||
lastHealthState[streamName] = current;
|
||||
|
||||
console.log(`[HEALTH] ${streamName}: ${prev} → ${current}`);
|
||||
|
||||
// 🚨 ALERTS
|
||||
if (HEALTH_CONFIG.enableAlerts) {
|
||||
sendAlert(streamName, health, status);
|
||||
}
|
||||
|
||||
// 🎛 AUTO SCENE SWITCH
|
||||
if (HEALTH_CONFIG.autoSceneSwitch) {
|
||||
handleAutoScene(streamName, health, status);
|
||||
}
|
||||
}
|
||||
|
||||
// ================= AUTO SCENE =================
|
||||
function handleAutoScene(streamName, health, status) {
|
||||
|
||||
if (streamName !== status.activeStream) return;
|
||||
|
||||
if (health.status === "DOWN") {
|
||||
setSceneSafe(HEALTH_CONFIG.sceneOffline);
|
||||
}
|
||||
|
||||
if (health.status === "DANGER") {
|
||||
setSceneSafe(HEALTH_CONFIG.sceneLow);
|
||||
}
|
||||
}
|
||||
|
||||
// ================= SAFE SCENE SWITCH =================
|
||||
function setSceneSafe(scene) {
|
||||
|
||||
fetch("https://api.anjuma-crew.de/scene", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ scene })
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
// ================= ALERT SYSTEM =================
|
||||
function sendAlert(streamName, health, status) {
|
||||
|
||||
const msg = {
|
||||
stream: streamName,
|
||||
status: health.status,
|
||||
score: health.score,
|
||||
bitrate: health.bitrate,
|
||||
staleness: health.staleness
|
||||
};
|
||||
|
||||
console.warn("[ALERT]", msg);
|
||||
|
||||
// 👉 optional webhook
|
||||
/*
|
||||
fetch("YOUR_WEBHOOK_URL", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(msg)
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
// ================= UI =================
|
||||
function updateHealthUI(streamName, health) {
|
||||
|
||||
const el = document.getElementById(`health-${streamName}`);
|
||||
if (el) {
|
||||
el.innerText = health.score;
|
||||
|
||||
el.style.color =
|
||||
health.status === "GOOD" ? "green" :
|
||||
health.status === "WEAK" ? "orange" :
|
||||
"red";
|
||||
}
|
||||
|
||||
const label = document.getElementById(`status-${streamName}`);
|
||||
if (label) {
|
||||
label.innerText = health.status;
|
||||
}
|
||||
|
||||
const staleEl = document.getElementById(`stale-${streamName}`);
|
||||
if (staleEl) {
|
||||
staleEl.innerText = Math.round(health.staleness / 1000) + "s";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
// -------------------- Secure session cookie params (before session_start) --------------------
|
||||
$secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
||||
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0, // Session-Cookie (endet mit Browser), Remember-me macht "lang"
|
||||
'path' => '/',
|
||||
'domain' => '',
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
|
||||
session_start();
|
||||
|
||||
require_once "config/config.php";
|
||||
|
||||
// -------------------- Auto-Login via remember cookie (if no session) + ROTATION --------------------
|
||||
if ((!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) && !empty($_COOKIE['remember'])) {
|
||||
try {
|
||||
// optional housekeeping
|
||||
$pdo->exec("DELETE FROM remember_tokens WHERE expires_at < NOW()");
|
||||
|
||||
$oldToken = (string)$_COOKIE['remember'];
|
||||
$oldHash = hash('sha256', $oldToken);
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT rt.id AS token_id,
|
||||
rt.user_id,
|
||||
UNIX_TIMESTAMP(rt.expires_at) AS exp,
|
||||
u.username, u.name, u.role
|
||||
FROM remember_tokens rt
|
||||
JOIN users u ON u.id = rt.user_id
|
||||
WHERE rt.token_hash = :th
|
||||
LIMIT 1
|
||||
");
|
||||
$stmt->execute([':th' => $oldHash]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($row && (int)$row['exp'] > time()) {
|
||||
// ✅ valid token -> log in
|
||||
session_regenerate_id(true);
|
||||
|
||||
$_SESSION["loggedin"] = true;
|
||||
$_SESSION["id"] = (int)$row["user_id"];
|
||||
$_SESSION["username"] = $row["username"];
|
||||
$_SESSION["name"] = $row["name"];
|
||||
$_SESSION["role"] = $row["role"];
|
||||
|
||||
// last_login auch bei Auto-Login aktualisieren
|
||||
$upd = $pdo->prepare("UPDATE users SET last_login = :ts WHERE id = :id");
|
||||
$upd->execute([
|
||||
':ts' => time(),
|
||||
':id' => (int)$row['user_id']
|
||||
]);
|
||||
|
||||
|
||||
// ✅ TOKEN ROTATION: replace old token with a fresh one & extend expiry to 7 days
|
||||
$newToken = bin2hex(random_bytes(32));
|
||||
$newHash = hash('sha256', $newToken);
|
||||
$newExp = time() + 7 * 24 * 60 * 60; // 7 days
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
// Update the existing token row (keeps 1 token per "cookie session")
|
||||
$upd = $pdo->prepare("
|
||||
UPDATE remember_tokens
|
||||
SET token_hash = :new_hash,
|
||||
expires_at = FROM_UNIXTIME(:exp)
|
||||
WHERE id = :tid
|
||||
LIMIT 1
|
||||
");
|
||||
$upd->execute([
|
||||
':new_hash' => $newHash,
|
||||
':exp' => $newExp,
|
||||
':tid' => (int)$row['token_id'],
|
||||
]);
|
||||
|
||||
$pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
// If rotation fails, fail closed-ish:
|
||||
// We'll still keep the session (user is in), but we remove cookie to avoid loops.
|
||||
setcookie('remember', '', [
|
||||
'expires' => time() - 3600,
|
||||
'path' => '/',
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
header("location: dashboard.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set new cookie
|
||||
setcookie('remember', $newToken, [
|
||||
'expires' => $newExp,
|
||||
'path' => '/',
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
|
||||
header("location: dashboard.php");
|
||||
exit;
|
||||
} else {
|
||||
// invalid/expired token -> delete cookie
|
||||
setcookie('remember', '', [
|
||||
'expires' => time() - 3600,
|
||||
'path' => '/',
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// fail closed (do nothing, user stays logged out)
|
||||
}
|
||||
}
|
||||
|
||||
// If already logged in -> go dashboard
|
||||
if (isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true) {
|
||||
header("location: dashboard.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
$username = $password = "";
|
||||
$username_err = $password_err = "";
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
|
||||
// Username prüfen
|
||||
if (empty(trim($_POST["username"] ?? ""))) {
|
||||
$username_err = "Bitte Username eingeben.";
|
||||
} else {
|
||||
$username = trim((string)$_POST["username"]);
|
||||
}
|
||||
|
||||
$password = (string)($_POST["password"] ?? "");
|
||||
|
||||
if (empty($username_err) && empty($password_err)) {
|
||||
|
||||
$sql = "
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE username = :username
|
||||
LIMIT 1
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindParam(":username", $username, PDO::PARAM_STR);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
|
||||
if ($stmt->rowCount() === 1) {
|
||||
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$hash = $user['password_hash'] ?? '';
|
||||
$first = (int)($user['first_login'] ?? 0);
|
||||
|
||||
// 1️⃣ Nur wenn wirklich First Login ODER kein Hash vorhanden
|
||||
if ($first === 1 || empty($hash)) {
|
||||
$_SESSION['set_pw_user'] = (int)$user['id'];
|
||||
header("location: set_password.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2️⃣ Passwort prüfen
|
||||
if (!password_verify($password, $hash)) {
|
||||
$password_err = "Falsches Passwort.";
|
||||
} else {
|
||||
|
||||
session_regenerate_id(true);
|
||||
|
||||
$_SESSION["loggedin"] = true;
|
||||
$_SESSION["id"] = (int)$user["id"];
|
||||
$_SESSION["username"] = $user["username"];
|
||||
$_SESSION["name"] = $user["name"];
|
||||
$_SESSION["role"] = $user["role"];
|
||||
|
||||
// Remember-me (7 Tage), nur wenn Checkbox gesetzt
|
||||
if (!empty($_POST['remember'])) {
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$tokenHash = hash('sha256', $token);
|
||||
$expires = time() + 7 * 24 * 60 * 60; // 7 days
|
||||
|
||||
// optional housekeeping
|
||||
$pdo->exec("DELETE FROM remember_tokens WHERE expires_at < NOW()");
|
||||
|
||||
$ins = $pdo->prepare("
|
||||
INSERT INTO remember_tokens (user_id, token_hash, expires_at)
|
||||
VALUES (:uid, :th, FROM_UNIXTIME(:exp))
|
||||
");
|
||||
$ins->execute([
|
||||
':uid' => (int)$user['id'],
|
||||
':th' => $tokenHash,
|
||||
':exp' => $expires
|
||||
]);
|
||||
|
||||
setcookie('remember', $token, [
|
||||
'expires' => $expires,
|
||||
'path' => '/',
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
}
|
||||
|
||||
// last_login setzen
|
||||
$upd = $pdo->prepare("UPDATE users SET last_login = :ts WHERE id = :id");
|
||||
$upd->execute([
|
||||
':ts' => time(),
|
||||
':id' => (int)$user['id']
|
||||
]);
|
||||
|
||||
header("location: dashboard.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
} else {
|
||||
$username_err = "Dieser Benutzer existiert nicht.";
|
||||
}
|
||||
|
||||
} else {
|
||||
echo "Fehler. Bitte später erneut versuchen.";
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ANJUMA Login</title>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: #ffffff;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 90%;
|
||||
max-width: 380px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 6px 25px rgba(0,0,0,0.08);
|
||||
text-align: center;
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
border: 1px solid #f1f1f1;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
width: 200px;
|
||||
max-width: 100%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-card { padding: 22px; }
|
||||
h4 { font-size: 1.2rem; }
|
||||
.btn-login { padding: 10px; font-size: 15px; }
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid #dcdcdc;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 2px rgba(13,110,253,0.15);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
background: #0d6efd;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
background: #0b5ed7;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #d9534f;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 4px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="login-card">
|
||||
|
||||
<img src="ANJUMA_LOGO.png" class="login-logo" alt="ANJUMA Logo">
|
||||
|
||||
<h4 class="mb-3">Willkommen zurück</h4>
|
||||
|
||||
<form action="" method="post">
|
||||
|
||||
<div class="text-start mb-3">
|
||||
<label class="mb-1">Username</label>
|
||||
<input type="text" name="username" class="form-control"
|
||||
value="<?php echo htmlspecialchars($username, ENT_QUOTES, 'UTF-8'); ?>">
|
||||
<?php if (!empty($username_err)) echo "<div class='error'>".htmlspecialchars($username_err, ENT_QUOTES, 'UTF-8')."</div>"; ?>
|
||||
</div>
|
||||
|
||||
<div class="text-start mb-2">
|
||||
<label class="mb-1">Passwort</label>
|
||||
<input type="password" name="password" class="form-control">
|
||||
<?php if (!empty($password_err)) echo "<div class='error'>".htmlspecialchars($password_err, ENT_QUOTES, 'UTF-8')."</div>"; ?>
|
||||
</div>
|
||||
|
||||
<div class="form-check text-start mb-4">
|
||||
<input class="form-check-input" type="checkbox" name="remember" id="remember" value="1" checked>
|
||||
<label class="form-check-label" for="remember">Angemeldet bleiben</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-login">Einloggen</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
if (isset($_SESSION["loggedin"]) && $_SESSION["loggedin"] === true) {
|
||||
header("location: dashboard.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once "config/config.php";
|
||||
|
||||
$username = $password = "";
|
||||
$username_err = $password_err = "";
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
|
||||
// Username prüfen
|
||||
if (empty(trim($_POST["username"]))) {
|
||||
$username_err = "Bitte Username eingeben.";
|
||||
} else {
|
||||
$username = trim($_POST["username"]);
|
||||
}
|
||||
|
||||
$password = $_POST["password"] ?? "";
|
||||
|
||||
|
||||
if (empty($username_err) && empty($password_err)) {
|
||||
|
||||
$sql = "
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE username = :username
|
||||
LIMIT 1
|
||||
";
|
||||
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->bindParam(":username", $username, PDO::PARAM_STR);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
|
||||
if ($stmt->rowCount() === 1) {
|
||||
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$hash = $user['password_hash'];
|
||||
$first = (int)$user['first_login'];
|
||||
|
||||
// 1️⃣ Nur wenn wirklich First Login ODER kein Hash vorhanden
|
||||
if ($first === 1 || empty($hash)) {
|
||||
$_SESSION['set_pw_user'] = (int)$user['id'];
|
||||
header("location: set_password.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// 2️⃣ Passwort prüfen
|
||||
if (!password_verify($password, $hash)) {
|
||||
$password_err = "Falsches Passwort.";
|
||||
} else {
|
||||
|
||||
session_regenerate_id(true);
|
||||
|
||||
$_SESSION["loggedin"] = true;
|
||||
$_SESSION["id"] = (int)$user["id"];
|
||||
$_SESSION["username"] = $user["username"];
|
||||
$_SESSION["name"] = $user["name"];
|
||||
$_SESSION["role"] = $user["role"];
|
||||
|
||||
// last_login setzen
|
||||
$upd = $pdo->prepare("UPDATE users SET last_login = :ts WHERE id = :id");
|
||||
$upd->execute([
|
||||
':ts' => time(),
|
||||
':id' => (int)$user['id']
|
||||
]);
|
||||
|
||||
header("location: dashboard.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
} else {
|
||||
$username_err = "Dieser Benutzer existiert nicht.";
|
||||
}
|
||||
|
||||
} else {
|
||||
echo "Fehler. Bitte später erneut versuchen.";
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ANJUMA Login</title>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: #ffffff;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 90%;
|
||||
max-width: 380px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 6px 25px rgba(0,0,0,0.08);
|
||||
text-align: center;
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
border: 1px solid #f1f1f1;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
width: 200px;
|
||||
max-width: 100%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-card {
|
||||
padding: 22px;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.btn-login {
|
||||
padding: 10px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.form-control {
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid #dcdcdc;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 2px rgba(13,110,253,0.15);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
background: #0d6efd;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
background: #0b5ed7;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #d9534f;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 4px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="login-card">
|
||||
|
||||
<img src="ANJUMA_LOGO.png" class="login-logo" alt="ANJUMA Logo">
|
||||
|
||||
<h4 class="mb-3">Willkommen zurück</h4>
|
||||
|
||||
<form action="" method="post">
|
||||
|
||||
<div class="text-start mb-3">
|
||||
<label class="mb-1">Username</label>
|
||||
<input type="text" name="username" class="form-control"
|
||||
value="<?php echo $username; ?>">
|
||||
<?php if (!empty($username_err)) echo "<div class='error'>$username_err</div>"; ?>
|
||||
</div>
|
||||
|
||||
<div class="text-start mb-4">
|
||||
<label class="mb-1">Passwort</label>
|
||||
<input type="password" name="password" class="form-control">
|
||||
<?php if (!empty($password_err)) echo "<div class='error'>$password_err</div>"; ?>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-login">
|
||||
Einloggen
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
require_once "config/session.php";
|
||||
|
||||
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
include('navbar.php');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ANJUMA • Infrastructure</title>
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
|
||||
<!-- DataTables (Bootstrap 5) CSS -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/css/dataTables.bootstrap5.min.css">
|
||||
|
||||
<!-- jQuery (muss vor DataTables) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
|
||||
<!-- DataTables -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/datatables.net@1.13.10/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/js/dataTables.bootstrap5.min.js"></script>
|
||||
|
||||
<!-- Bootstrap 5 JS (bundle enthält Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<style>
|
||||
.badge-status { text-transform: uppercase; letter-spacing: .04em; }
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||
.dt-nowrap td { white-space: nowrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<h3 class="mb-0">Infrastructure</h3>
|
||||
<div class="text-muted">Proxmox Node: <span class="mono">pve</span> • ANJUMA-Range: <span class="mono">100–299</span></div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button id="btnRefresh" class="btn btn-outline-secondary btn-sm">↻ Refresh</button>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="autoRefresh" checked>
|
||||
<label class="form-check-label" for="autoRefresh">Auto-Refresh</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<table id="tblInfra" class="table table-striped table-hover align-middle dt-nowrap w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>VMID</th>
|
||||
<th>Typ</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>CPU</th>
|
||||
<th>RAM</th>
|
||||
<th>Uptime</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><!-- via JS --></tbody>
|
||||
</table>
|
||||
<div id="lastUpdate" class="text-muted small mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Modal -->
|
||||
<div class="modal fade" id="confirmModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="confirmTitle">Aktion bestätigen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="confirmBody"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDo">Ausführen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 1080">
|
||||
<div id="toast" class="toast align-items-center text-bg-dark border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body" id="toastBody">...</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JS -->
|
||||
<script src="/assets/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script src="/assets/datatables/jquery-3.7.1.min.js"></script>
|
||||
<script src="/assets/datatables/jquery.dataTables.min.js"></script>
|
||||
<script src="/assets/datatables/dataTables.bootstrap5.min.js"></script>
|
||||
|
||||
<script>
|
||||
const API_LIST = '/api/pve_list.php';
|
||||
const API_ACTION = '/api/pve_action.php';
|
||||
|
||||
let dt;
|
||||
let refreshTimer = null;
|
||||
|
||||
const toastEl = document.getElementById('toast');
|
||||
const toast = new bootstrap.Toast(toastEl, { delay: 3500 });
|
||||
|
||||
function showToast(msg) {
|
||||
document.getElementById('toastBody').textContent = msg;
|
||||
toast.show();
|
||||
}
|
||||
|
||||
function fmtType(t) {
|
||||
return t === 'lxc'
|
||||
? '<span class="badge text-bg-primary">LXC</span>'
|
||||
: '<span class="badge text-bg-info">VM</span>';
|
||||
}
|
||||
|
||||
function fmtStatus(s) {
|
||||
const st = (s || 'unknown').toLowerCase();
|
||||
if (st === 'running') return '<span class="badge text-bg-success badge-status">RUNNING</span>';
|
||||
if (st === 'stopped') return '<span class="badge text-bg-secondary badge-status">STOPPED</span>';
|
||||
return '<span class="badge text-bg-warning badge-status">UNKNOWN</span>';
|
||||
}
|
||||
|
||||
function fmtCpu(cpu) {
|
||||
// Proxmox liefert cpu oft als 0.x => grob in %
|
||||
const p = Math.round((Number(cpu) || 0) * 100);
|
||||
return `${p}%`;
|
||||
}
|
||||
|
||||
function fmtRam(mem, maxmem) {
|
||||
const m = Number(mem) || 0;
|
||||
const mm = Number(maxmem) || 0;
|
||||
if (!mm) return '-';
|
||||
const used = (m / 1024 / 1024 / 1024);
|
||||
const total = (mm / 1024 / 1024 / 1024);
|
||||
return `${used.toFixed(2)} / ${total.toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
function fmtUptime(sec) {
|
||||
sec = Number(sec) || 0;
|
||||
if (sec <= 0) return '-';
|
||||
const d = Math.floor(sec / 86400); sec %= 86400;
|
||||
const h = Math.floor(sec / 3600); sec %= 3600;
|
||||
const m = Math.floor(sec / 60);
|
||||
if (d > 0) return `${d}d ${h}h`;
|
||||
if (h > 0) return `${h}h ${m}m`;
|
||||
return `${m}m`;
|
||||
}
|
||||
|
||||
function actionsHtml(row) {
|
||||
const vmid = row.vmid;
|
||||
const status = (row.status || '').toLowerCase();
|
||||
const disabledStart = status === 'running' ? 'disabled' : '';
|
||||
const disabledStop = status === 'stopped' ? 'disabled' : '';
|
||||
const disabledReboot = status === 'stopped' ? 'disabled' : '';
|
||||
|
||||
return `
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-success act" data-action="start" data-vmid="${vmid}" ${disabledStart}>Start</button>
|
||||
<button class="btn btn-outline-warning act" data-action="stop" data-vmid="${vmid}" ${disabledStop}>Stop</button>
|
||||
<button class="btn btn-outline-danger act" data-action="reboot" data-vmid="${vmid}" ${disabledReboot}>Reboot</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
const res = await fetch(API_LIST, { cache: 'no-store' });
|
||||
const json = await res.json();
|
||||
if (!json.ok) throw new Error(json.error || 'List failed');
|
||||
return json.items || [];
|
||||
}
|
||||
|
||||
async function refreshTable() {
|
||||
try {
|
||||
const items = await loadList();
|
||||
|
||||
// DataTables: bestehende Rows ersetzen
|
||||
dt.clear();
|
||||
dt.rows.add(items);
|
||||
dt.draw(false);
|
||||
|
||||
document.getElementById('lastUpdate').textContent =
|
||||
'Letztes Update: ' + new Date().toLocaleTimeString();
|
||||
|
||||
} catch (e) {
|
||||
showToast('Fehler beim Laden: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function openConfirm(vmid, action, name) {
|
||||
const title = `${action.toUpperCase()} • ${vmid}`;
|
||||
const body = `
|
||||
<div>Aktion <b>${action}</b> für <b>${name || 'VM/CT'}</b> (VMID <span class="mono">${vmid}</span>) ausführen?</div>
|
||||
<div class="text-muted small mt-2">Tipp: Reboot ist “soft”, Stop ist “hart”.</div>
|
||||
`;
|
||||
|
||||
document.getElementById('confirmTitle').textContent = title;
|
||||
document.getElementById('confirmBody').innerHTML = body;
|
||||
|
||||
const btn = document.getElementById('confirmDo');
|
||||
btn.className = 'btn ' + (action === 'stop' ? 'btn-warning' : action === 'start' ? 'btn-success' : 'btn-danger');
|
||||
btn.textContent = 'Ja, ausführen';
|
||||
btn.onclick = () => doAction(vmid, action);
|
||||
|
||||
new bootstrap.Modal(document.getElementById('confirmModal')).show();
|
||||
}
|
||||
|
||||
async function doAction(vmid, action) {
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('vmid', vmid);
|
||||
fd.append('action', action);
|
||||
|
||||
const res = await fetch(API_ACTION, { method: 'POST', body: fd });
|
||||
const json = await res.json();
|
||||
if (!json.ok) throw new Error(json.error || 'Action failed');
|
||||
|
||||
showToast(`OK: ${action} für ${vmid}` + (json.upid ? ` (UPID)` : ''));
|
||||
|
||||
// nach kurzer Zeit refreshen
|
||||
setTimeout(refreshTable, 1200);
|
||||
|
||||
// modal schließen
|
||||
bootstrap.Modal.getInstance(document.getElementById('confirmModal'))?.hide();
|
||||
} catch (e) {
|
||||
showToast('Fehler: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', (ev) => {
|
||||
const btn = ev.target.closest('.act');
|
||||
if (!btn) return;
|
||||
|
||||
const vmid = btn.dataset.vmid;
|
||||
const action = btn.dataset.action;
|
||||
|
||||
// Name aus Tabelle holen
|
||||
const row = dt.row($(btn).closest('tr')).data();
|
||||
openConfirm(vmid, action, row?.name);
|
||||
});
|
||||
|
||||
document.getElementById('btnRefresh').addEventListener('click', refreshTable);
|
||||
|
||||
function startAutoRefresh() {
|
||||
stopAutoRefresh();
|
||||
refreshTimer = setInterval(() => {
|
||||
if (document.getElementById('autoRefresh').checked) refreshTable();
|
||||
}, 5000);
|
||||
}
|
||||
function stopAutoRefresh() {
|
||||
if (refreshTimer) clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
|
||||
document.getElementById('autoRefresh').addEventListener('change', (e) => {
|
||||
if (e.target.checked) startAutoRefresh();
|
||||
else stopAutoRefresh();
|
||||
});
|
||||
|
||||
// Init
|
||||
$(async function() {
|
||||
dt = $('#tblInfra').DataTable({
|
||||
pageLength: 25,
|
||||
order: [[0, 'asc']],
|
||||
columns: [
|
||||
{ data: 'vmid', className: 'mono' },
|
||||
{ data: 'type', render: (d) => fmtType(d) },
|
||||
{ data: 'name' },
|
||||
{ data: 'status', render: (d) => fmtStatus(d) },
|
||||
{ data: 'cpu', render: (d) => fmtCpu(d) },
|
||||
{ data: null, render: (row) => fmtRam(row.mem, row.maxmem) },
|
||||
{ data: 'uptime', render: (d) => fmtUptime(d) },
|
||||
{ data: null, orderable: false, searchable: false, render: (row) => actionsHtml(row) },
|
||||
],
|
||||
});
|
||||
|
||||
await refreshTable();
|
||||
startAutoRefresh();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
function nextInvoiceNo(PDO $pdo, int $year): string {
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
// row lock (InnoDB)
|
||||
$stmt = $pdo->prepare("SELECT last_number FROM invoice_counters WHERE year = :y FOR UPDATE");
|
||||
$stmt->execute([':y' => $year]);
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
$last = 0;
|
||||
$pdo->prepare("INSERT INTO invoice_counters (year, last_number) VALUES (:y, 0)")
|
||||
->execute([':y' => $year]);
|
||||
} else {
|
||||
$last = (int)$row['last_number'];
|
||||
}
|
||||
|
||||
$next = $last + 1;
|
||||
$pdo->prepare("UPDATE invoice_counters SET last_number = :n WHERE year = :y")
|
||||
->execute([':n' => $next, ':y' => $year]);
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
return sprintf("RE-%d-%04d", $year, $next);
|
||||
} catch (Throwable $e) {
|
||||
$pdo->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
final class PveApi {
|
||||
private string $host;
|
||||
private string $node;
|
||||
private string $tokenId;
|
||||
private string $tokenSecret;
|
||||
private int $timeout;
|
||||
private bool $verifyTls;
|
||||
|
||||
public function __construct(array $cfg) {
|
||||
$this->host = $cfg['host'];
|
||||
$this->node = $cfg['node'];
|
||||
$this->tokenId = $cfg['token_id'];
|
||||
$this->tokenSecret = $cfg['token_secret'];
|
||||
$this->timeout = (int)($cfg['timeout'] ?? 15);
|
||||
$this->verifyTls = (bool)($cfg['verify_tls'] ?? true);
|
||||
}
|
||||
|
||||
public function node(): string { return $this->node; }
|
||||
|
||||
/** VMID 100-199 => lxc, 200-299 => qemu */
|
||||
public function typeForVmid(int $vmid): string {
|
||||
if ($vmid >= 100 && $vmid <= 199) return 'lxc';
|
||||
if ($vmid >= 200 && $vmid <= 299) return 'qemu';
|
||||
throw new InvalidArgumentException("VMID {$vmid} liegt nicht im ANJUMA Bereich (100-299).");
|
||||
}
|
||||
|
||||
public function statusCurrent(int $vmid): array {
|
||||
$type = $this->typeForVmid($vmid);
|
||||
return $this->request('GET', "/nodes/{$this->node}/{$type}/{$vmid}/status/current")['data'] ?? [];
|
||||
}
|
||||
|
||||
/** action: start|stop|reboot|reset (reset nur qemu sinnvoll) */
|
||||
public function powerAction(int $vmid, string $action): string {
|
||||
$type = $this->typeForVmid($vmid);
|
||||
|
||||
$allowed = ['start','stop','reboot','reset'];
|
||||
if (!in_array($action, $allowed, true)) {
|
||||
throw new InvalidArgumentException("Ungültige action: {$action}");
|
||||
}
|
||||
if ($type === 'lxc' && $action === 'reset') {
|
||||
throw new InvalidArgumentException("reset gibt's praktisch nur für qemu/VMs.");
|
||||
}
|
||||
|
||||
$res = $this->request('POST', "/nodes/{$this->node}/{$type}/{$vmid}/status/{$action}");
|
||||
// Proxmox liefert meist UPID als string in data
|
||||
return (string)($res['data'] ?? '');
|
||||
}
|
||||
|
||||
private function request(string $method, string $path, array $data = null): array {
|
||||
$url = "https://{$this->host}:8006/api2/json{$path}";
|
||||
|
||||
$ch = curl_init($url);
|
||||
$headers = [
|
||||
"Authorization: PVEAPIToken={$this->tokenId}={$this->tokenSecret}",
|
||||
];
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CUSTOMREQUEST => strtoupper($method),
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_TIMEOUT => $this->timeout,
|
||||
CURLOPT_SSL_VERIFYPEER => $this->verifyTls,
|
||||
CURLOPT_SSL_VERIFYHOST => $this->verifyTls ? 2 : 0,
|
||||
]);
|
||||
|
||||
if ($data !== null) {
|
||||
$headers[] = "Content-Type: application/x-www-form-urlencoded";
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||
}
|
||||
|
||||
$raw = curl_exec($ch);
|
||||
if ($raw === false) {
|
||||
throw new RuntimeException("cURL error: " . curl_error($ch));
|
||||
}
|
||||
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$json = json_decode($raw, true);
|
||||
if ($code < 200 || $code >= 300) {
|
||||
$msg = is_array($json) ? json_encode($json) : $raw;
|
||||
throw new RuntimeException("PVE HTTP {$code}: {$msg}");
|
||||
}
|
||||
return is_array($json) ? $json : [];
|
||||
}
|
||||
|
||||
public function listLxc(): array {
|
||||
return $this->request('GET', "/nodes/{$this->node}/lxc")['data'] ?? [];
|
||||
}
|
||||
|
||||
public function listQemu(): array {
|
||||
return $this->request('GET', "/nodes/{$this->node}/qemu")['data'] ?? [];
|
||||
}
|
||||
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
require_once "config/config.php";
|
||||
|
||||
$secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
||||
|
||||
session_start();
|
||||
|
||||
// -------------------- Remember-Token löschen --------------------
|
||||
if (!empty($_COOKIE['remember'])) {
|
||||
|
||||
$token = (string)$_COOKIE['remember'];
|
||||
$tokenHash = hash('sha256', $token);
|
||||
|
||||
try {
|
||||
$del = $pdo->prepare("DELETE FROM remember_tokens WHERE token_hash = :th");
|
||||
$del->execute([':th' => $tokenHash]);
|
||||
} catch (Throwable $e) {
|
||||
// fail silently
|
||||
}
|
||||
|
||||
// Remember-Cookie löschen
|
||||
setcookie('remember', '', [
|
||||
'expires' => time() - 3600,
|
||||
'path' => '/',
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
}
|
||||
|
||||
// -------------------- Session löschen --------------------
|
||||
$_SESSION = array();
|
||||
|
||||
if (ini_get("session.use_cookies")) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(
|
||||
session_name(),
|
||||
'',
|
||||
time() - 42000,
|
||||
$params["path"],
|
||||
$params["domain"],
|
||||
$params["secure"],
|
||||
$params["httponly"]
|
||||
);
|
||||
}
|
||||
|
||||
session_destroy();
|
||||
|
||||
// Weiterleiten zum Login
|
||||
header("location: index.php");
|
||||
exit;
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
|
||||
<title>ANJUMA Dashboard</title>
|
||||
</head>
|
||||
|
||||
<?php
|
||||
$userName = $_SESSION["name"] ?? "Benutzer";
|
||||
$initial = strtoupper(substr($userName, 0, 1));
|
||||
?>
|
||||
<!-- NAVBAR -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark anjuma-nav shadow-sm fixed-top">
|
||||
<div class="container-fluid">
|
||||
|
||||
<!-- Brand -->
|
||||
<a class="navbar-brand fw-bold" href="dashboard.php">
|
||||
<i class="bi bi-lightning-charge-fill text-warning me-1"></i> ANJUMA
|
||||
</a>
|
||||
|
||||
<!-- Hamburger -->
|
||||
<button class="navbar-toggler border-0" type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<!-- NAVIGATION -->
|
||||
<div class="collapse navbar-collapse mt-2 mt-lg-0" id="navbarNav">
|
||||
|
||||
<!-- Linke Seite -->
|
||||
<ul class="navbar-nav me-auto">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-flex align-items-center" href="dashboard.php">
|
||||
<i class="bi bi-speedometer2 me-2"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-flex align-items-center" href="termine.php">
|
||||
<i class="bi bi-calendar-event me-2"></i> Termine
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-flex align-items-center" href="finance.php">
|
||||
<i class="bi bi-bank me-2"></i> Finanzen
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-flex align-items-center" href="rechnungen.php">
|
||||
<i class="bi bi-receipt me-2"></i> Rechnungen
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-flex align-items-center" href="stream.php">
|
||||
<i class="bi bi-cast me-2"></i> Stream
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
<?php if ($_SESSION['role'] === 'admin'): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-flex align-items-center" href="infrastructure.php">
|
||||
<i class="bi bi-server me-2"></i> Infrastructure
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($_SESSION['role'] === 'admin'): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-flex align-items-center" href="users.php">
|
||||
<i class="bi bi-people me-2"></i> Users
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($_SESSION['role'] === 'admin'): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-flex align-items-center" href="settings_calendar.php">
|
||||
<i class="bi bi-wrench-adjustable me-2"></i> Settings
|
||||
</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
</ul>
|
||||
|
||||
<!-- RECHTS: USER DROPDOWN (einmalig, korrekt platziert) -->
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item dropdown">
|
||||
|
||||
<!-- Dropdown Trigger -->
|
||||
<a class="nav-link dropdown-toggle d-flex align-items-center user-dropdown-trigger"
|
||||
href="#"
|
||||
id="userDropdown"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
|
||||
<div class="user-avatar me-2"><?= $initial; ?></div>
|
||||
<span><?= htmlspecialchars($userName); ?></span>
|
||||
</a>
|
||||
|
||||
<!-- Dropdown Menü -->
|
||||
<ul class="dropdown-menu dropdown-menu-end user-dropdown shadow-lg" aria-labelledby="userDropdown">
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item logout-item" href="logout.php">
|
||||
<i class="bi bi-box-arrow-right me-2"></i> Logout
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
<style>
|
||||
/* NAVBAR – ANJUMA STYLE */
|
||||
.anjuma-nav {
|
||||
background: rgba(30, 30, 30, 0.85) !important;
|
||||
backdrop-filter: blur(6px);
|
||||
border-bottom: 2px solid rgba(255, 193, 7, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* Avatar */
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #ffc107, #ffdd57);
|
||||
color: #212529;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 8px rgba(255,193,7,0.4);
|
||||
transition: 0.3s ease;
|
||||
}
|
||||
|
||||
.user-avatar:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 12px rgba(255,193,7,0.7);
|
||||
}
|
||||
|
||||
/* Dropdown Trigger */
|
||||
.user-dropdown-trigger:hover {
|
||||
color: #ffc107 !important;
|
||||
}
|
||||
|
||||
/* Dropdown */
|
||||
.user-dropdown {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-dropdown .dropdown-item {
|
||||
padding: 10px 16px;
|
||||
transition: 0.2s ease;
|
||||
}
|
||||
|
||||
.user-dropdown .dropdown-item:hover {
|
||||
background: rgba(255,193,7,0.15);
|
||||
color: #ffc107 !important;
|
||||
}
|
||||
|
||||
/* Logout rot */
|
||||
.logout-item {
|
||||
color: #dc3545 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.logout-item:hover {
|
||||
background: rgba(220,53,69,0.15) !important;
|
||||
color: #dc3545 !important;
|
||||
}
|
||||
|
||||
/* Abstand wegen fixed-top */
|
||||
body {
|
||||
padding-top: 70px;
|
||||
}
|
||||
</style>
|
||||
<!-- Bootstrap JS Bundle für Burger & Dropdown -->
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
require_once "config/config.php";
|
||||
|
||||
date_default_timezone_set("Europe/Berlin");
|
||||
|
||||
$now = time();
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
(
|
||||
SELECT *, 1 AS is_live
|
||||
FROM termine
|
||||
WHERE status = 1
|
||||
AND startzeit <= UNIX_TIMESTAMP()
|
||||
AND endzeit >= UNIX_TIMESTAMP()
|
||||
ORDER BY startzeit DESC
|
||||
LIMIT 1
|
||||
)
|
||||
UNION ALL
|
||||
(
|
||||
SELECT *, 0 AS is_live
|
||||
FROM termine
|
||||
WHERE status = 1
|
||||
AND startzeit > UNIX_TIMESTAMP()
|
||||
ORDER BY startzeit ASC
|
||||
LIMIT 1
|
||||
)
|
||||
LIMIT 1
|
||||
");
|
||||
|
||||
$stmt->execute();
|
||||
$event = $stmt->fetch();
|
||||
|
||||
$isLive = false;
|
||||
|
||||
if ($event) {
|
||||
$now = time();
|
||||
|
||||
if ($event['startzeit'] <= $now && $event['endzeit'] >= $now) {
|
||||
$isLive = true;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ANJUMA Overlay</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
left: 60px;
|
||||
right: 60px;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* LEFT */
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 0 14px rgba(0,245,255,0.6));
|
||||
}
|
||||
|
||||
/* RIGHT */
|
||||
.right {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* TEXT */
|
||||
.event {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 0 10px rgba(0,245,255,0.35);
|
||||
}
|
||||
|
||||
.location {
|
||||
font-size: 14px;
|
||||
color: rgba(255,255,255,0.85);
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 14px;
|
||||
color: #00ff66;
|
||||
text-shadow: 0 0 8px rgba(0,255,102,0.6);
|
||||
}
|
||||
|
||||
.live {
|
||||
color: #ff2b2b;
|
||||
text-shadow: 0 0 10px rgba(255,0,0,0.6);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.next {
|
||||
color: #00f5ff;
|
||||
text-shadow: 0 0 10px rgba(0,245,255,0.4);
|
||||
font-weight: 900;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="overlay">
|
||||
|
||||
<!-- LEFT -->
|
||||
<div class="left">
|
||||
<img src="assets/logo.png" class="logo">
|
||||
</div>
|
||||
|
||||
<!-- RIGHT -->
|
||||
<div class="right">
|
||||
|
||||
<?php if ($event): ?>
|
||||
|
||||
<div class="event <?= $isLive ? 'live' : '' ?>">
|
||||
<?= $isLive ? '🔴 LIVE' : '' ?>
|
||||
</div>
|
||||
|
||||
<div class="location">
|
||||
📍 <?= htmlspecialchars($event['location']) ?>
|
||||
</div>
|
||||
|
||||
<div class="date">
|
||||
🕒 <?= date("d.m.Y H:i", $event['startzeit']) ?>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<div class="event">NO EVENTS</div>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script>
|
||||
setInterval(() => {
|
||||
location.reload();
|
||||
}, 10000);
|
||||
</script>
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
require_once "config/session.php";
|
||||
|
||||
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
include('navbar.php');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ANJUMA • Rechnungen</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/css/dataTables.bootstrap5.min.css">
|
||||
<style>
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||
@media (max-width: 768px){ .d-md-table-cell{display:none!important;} }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<h3 class="mb-0">Rechnungen</h3>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button id="btnRefresh" class="btn btn-outline-secondary btn-sm">↻ Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label mb-1">Von</label>
|
||||
<input type="date" class="form-control" id="fromDate">
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label mb-1">Bis</label>
|
||||
<input type="date" class="form-control" id="toDate">
|
||||
</div>
|
||||
<div class="col-12 col-md-6 d-flex gap-2">
|
||||
<button id="btnThisMonth" class="btn btn-outline-primary">Dieser Monat</button>
|
||||
<button id="btnThisYear" class="btn btn-outline-primary">Dieses Jahr</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="tblInv" class="table table-striped table-hover align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nr</th>
|
||||
<th>Datum</th>
|
||||
<th class="d-md-table-cell">Leistung</th>
|
||||
<th>Kunde</th>
|
||||
<th>Status</th>
|
||||
<th>Summe</th>
|
||||
<th>PDF</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="text-muted small mt-2" id="info"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/datatables.net@1.13.10/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
const API_LIST = '/api/invoice_list.php';
|
||||
const PDF_URL = '/api/invoice_pdf.php?id=';
|
||||
|
||||
let dt;
|
||||
|
||||
function isoLocal(d){ const y=d.getFullYear(), m=String(d.getMonth()+1).padStart(2,'0'), day=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${day}`; }
|
||||
function thisMonthRange(){ const d=new Date(); return [isoLocal(new Date(d.getFullYear(), d.getMonth(), 1)), isoLocal(new Date(d.getFullYear(), d.getMonth()+1, 0))]; }
|
||||
function thisYearRange(){ const d=new Date(); return [isoLocal(new Date(d.getFullYear(), 0, 1)), isoLocal(new Date(d.getFullYear(), 11, 31))]; }
|
||||
function fmtDateDE(iso){ const [y,m,d]=String(iso||'').split('-'); return (y && m && d) ? `${d}.${m}.${y}` : ''; }
|
||||
function eur(n){ return (Number(n)||0).toLocaleString('de-DE',{style:'currency',currency:'EUR',maximumFractionDigits:0}); }
|
||||
|
||||
function statusBadge(s){
|
||||
s = String(s||'draft');
|
||||
if (s==='paid') return '<span class="badge text-bg-success">PAID</span>';
|
||||
if (s==='sent') return '<span class="badge text-bg-primary">SENT</span>';
|
||||
if (s==='canceled') return '<span class="badge text-bg-danger">CANCELED</span>';
|
||||
return '<span class="badge text-bg-secondary">DRAFT</span>';
|
||||
}
|
||||
|
||||
function pdfBtn(row){
|
||||
return `<a class="btn btn-outline-secondary btn-sm" href="${PDF_URL}${row.id}" target="_blank">PDF</a>`;
|
||||
}
|
||||
|
||||
async function fetchJSON(url){
|
||||
const res = await fetch(url, { cache:'no-store' });
|
||||
const text = await res.text();
|
||||
const json = JSON.parse(text);
|
||||
if (!json.ok) throw new Error(json.error || 'API error');
|
||||
return json;
|
||||
}
|
||||
|
||||
async function refresh(){
|
||||
const qs = new URLSearchParams({ from: $('#fromDate').val(), to: $('#toDate').val() }).toString();
|
||||
const json = await fetchJSON(`${API_LIST}?${qs}`);
|
||||
const items = json.items || [];
|
||||
dt.clear().rows.add(items).draw(false);
|
||||
$('#info').text(`Treffer: ${items.length} • Zeitraum: ${json.from} bis ${json.to}`);
|
||||
}
|
||||
|
||||
$(function(){
|
||||
const [f,t] = thisMonthRange();
|
||||
$('#fromDate').val(f); $('#toDate').val(t);
|
||||
|
||||
dt = $('#tblInv').DataTable({
|
||||
pageLength: 25,
|
||||
order: [[1,'desc']],
|
||||
columns: [
|
||||
{ data:'invoice_no', className:'mono' },
|
||||
{ data:'invoice_date', className:'mono', render:(d)=>fmtDateDE(d) },
|
||||
{ data:'service_date', className:'mono d-md-table-cell', render:(d)=>fmtDateDE(d) },
|
||||
{ data:'customer_name' },
|
||||
{ data:'status', render:(d)=>statusBadge(d) },
|
||||
{ data:'amount_net_eur', className:'mono', render:(d)=>eur(d) },
|
||||
{ data:null, orderable:false, searchable:false, render:(row)=>pdfBtn(row) },
|
||||
]
|
||||
});
|
||||
|
||||
$('#btnRefresh').on('click', refresh);
|
||||
$('#fromDate,#toDate').on('change', refresh);
|
||||
|
||||
$('#btnThisMonth').on('click', () => {
|
||||
const [a,b]=thisMonthRange(); $('#fromDate').val(a); $('#toDate').val(b); refresh();
|
||||
});
|
||||
$('#btnThisYear').on('click', () => {
|
||||
const [a,b]=thisYearRange(); $('#fromDate').val(a); $('#toDate').val(b); refresh();
|
||||
});
|
||||
|
||||
refresh();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
if (!isset($_SESSION['set_pw_user'])) {
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once "config/config.php";
|
||||
|
||||
$pw1 = $pw2 = "";
|
||||
$pw_err = "";
|
||||
|
||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
|
||||
$pw1 = $_POST['password1'] ?? '';
|
||||
$pw2 = $_POST['password2'] ?? '';
|
||||
|
||||
if (strlen($pw1) < 5) {
|
||||
$pw_err = "Passwort muss mindestens 5 Zeichen lang sein.";
|
||||
} elseif ($pw1 !== $pw2) {
|
||||
$pw_err = "Passwörter stimmen nicht überein.";
|
||||
} else {
|
||||
|
||||
$hash = password_hash($pw1, PASSWORD_DEFAULT);
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE users
|
||||
SET password_hash = :hash,
|
||||
first_login = 0
|
||||
WHERE id = :id
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
':hash' => $hash,
|
||||
':id' => $_SESSION['set_pw_user']
|
||||
]);
|
||||
|
||||
unset($_SESSION['set_pw_user']);
|
||||
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Passwort setzen – ANJUMA</title>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: #ffffff;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'Segoe UI', sans-serif;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 90%;
|
||||
max-width: 380px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 6px 25px rgba(0,0,0,0.08);
|
||||
text-align: center;
|
||||
animation: fadeIn 0.4s ease-out;
|
||||
border: 1px solid #f1f1f1;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
width: 200px;
|
||||
max-width: 100%;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid #dcdcdc;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: #0d6efd;
|
||||
box-shadow: 0 0 0 2px rgba(13,110,253,0.15);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
background: #0d6efd;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
background: #0b5ed7;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #d9534f;
|
||||
font-size: 0.85rem;
|
||||
margin-top: 6px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="login-card">
|
||||
|
||||
<img src="ANJUMA_LOGO.png" class="login-logo" alt="ANJUMA Logo">
|
||||
|
||||
<h4 class="mb-2">Passwort festlegen</h4>
|
||||
<p class="text-muted mb-4" style="font-size: 0.9rem;">
|
||||
Bitte setze dein persönliches Passwort für das ANJUMA Dashboard.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
|
||||
<div class="text-start mb-3">
|
||||
<label class="mb-1">Neues Passwort</label>
|
||||
<input type="password" name="password1" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="text-start mb-4">
|
||||
<label class="mb-1">Passwort wiederholen</label>
|
||||
<input type="password" name="password2" class="form-control" required>
|
||||
<?php if (!empty($pw_err)) echo "<div class='error'>$pw_err</div>"; ?>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-login">
|
||||
Passwort speichern
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
require_once "config/config.php"; // PDO $pdo
|
||||
include("Data.class.php");
|
||||
$Data = new Data($pdo);
|
||||
|
||||
require_once "config/session.php";
|
||||
|
||||
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
// Kein Admin?
|
||||
if ($_SESSION['role'] !== 'admin') {
|
||||
http_response_code(403);
|
||||
echo "<h1>Zugriff verweigert</h1>";
|
||||
echo "<p>Diese Seite ist nur für Admins.</p>";
|
||||
exit;
|
||||
}
|
||||
|
||||
// speichern
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$title = trim($_POST['title_template']);
|
||||
$desc = trim($_POST['description_template']);
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE settings_calendar
|
||||
SET title_template = :t, description_template = :d
|
||||
WHERE id = 1
|
||||
");
|
||||
$stmt->execute([
|
||||
':t' => $title,
|
||||
':d' => $desc
|
||||
]);
|
||||
|
||||
$success = true;
|
||||
}
|
||||
|
||||
// laden
|
||||
$stmt = $pdo->query("SELECT * FROM settings_calendar WHERE id = 1");
|
||||
$settings = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
include('navbar.php');
|
||||
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Kalender Einstellungen – ANJUMA</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container py-4">
|
||||
|
||||
<h2 class="mb-4">📅 Kalender-Template Einstellungen</h2>
|
||||
|
||||
<?php if (!empty($success)): ?>
|
||||
<div class="alert alert-success">
|
||||
Einstellungen gespeichert ✅
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post">
|
||||
|
||||
<!-- TITEL -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Kalender-Titel Template</label>
|
||||
<input
|
||||
type="text"
|
||||
name="title_template"
|
||||
class="form-control"
|
||||
value="<?= htmlspecialchars($settings['title_template']) ?>"
|
||||
>
|
||||
<div class="form-text">
|
||||
Beispiel: <code>ANJUMA – {{locationtype}} @ {{location}}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BESCHREIBUNG -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Kalender-Beschreibung Template</label>
|
||||
<textarea
|
||||
name="description_template"
|
||||
rows="8"
|
||||
class="form-control"
|
||||
><?= htmlspecialchars($settings['description_template']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<!-- HINWEISE -->
|
||||
<div class="alert alert-secondary">
|
||||
<strong>Verfügbare Platzhalter:</strong><br>
|
||||
<code>{{status}}</code>,
|
||||
<code>{{localtiontype}}</code>,
|
||||
<code>{{location}}</code>,
|
||||
<code>{{startzeit}}</code>,
|
||||
<code>{{endzeit}}</code>,
|
||||
<code>{{gage}}</code>,
|
||||
<code>{{desc}}</code>,
|
||||
<code>{{creator}}</code>,
|
||||
<code>{{last_editor}}</code>,
|
||||
<code>{{last_edited}}</code>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary">
|
||||
💾 Speichern
|
||||
</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
+191
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
require_once "config/session.php";
|
||||
|
||||
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
include('navbar.php');
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ANJUMA Control Dashboard</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
|
||||
<style>
|
||||
body {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.scene-btn {
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #ddd;
|
||||
cursor: pointer;
|
||||
transition: 0.2s;
|
||||
background: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.scene-btn.active {
|
||||
background: #0d6efd;
|
||||
color: white;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.scene-btn:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
#chart {
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.preview-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: #000;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-box iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.offline {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
background: #111;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container py-3">
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- ================= LEFT CONTROL PANEL ================= -->
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card p-3">
|
||||
|
||||
<h5>🎛 OBS Control</h5>
|
||||
|
||||
<div class="mb-2 small-text">
|
||||
Scene: <b id="scene">-</b>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 small-text">
|
||||
Live1: <span id="live1">0</span><br>
|
||||
Live2: <span id="live2">0</span>
|
||||
</div>
|
||||
|
||||
<!-- STREAM CONTROL -->
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button class="btn btn-success btn-sm" onclick="startStream()">Start</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="stopStream()">Stop</button>
|
||||
|
||||
<button id="fixBtn" class="btn btn-warning btn-sm" onclick="fixStream()">
|
||||
FIX STREAM
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- SYSTEM CONTROL -->
|
||||
<div class="d-flex gap-2 mt-3 align-items-center">
|
||||
|
||||
<button id="obsStartBtn" class="btn btn-dark btn-sm" onclick="startOBS()">
|
||||
OBS Starten
|
||||
</button>
|
||||
|
||||
<span id="obsStatus" class="small-text ms-2">
|
||||
🔴 Disconnected
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= SCENES ================= -->
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card p-3">
|
||||
|
||||
<h5>🎬 Scenes</h5>
|
||||
|
||||
<div class="row g-2">
|
||||
|
||||
<div class="col-6 col-md-3"><div id="scene-Starting" class="scene-btn" onclick="setScene('Starting')">Starting</div></div>
|
||||
<div class="col-6 col-md-3"><div id="scene-Live1" class="scene-btn" onclick="setScene('Live1')">Live1</div></div>
|
||||
<div class="col-6 col-md-3"><div id="scene-Live2" class="scene-btn" onclick="setScene('Live2')">Live2</div></div>
|
||||
<div class="col-6 col-md-3"><div id="scene-Low" class="scene-btn" onclick="setScene('Low')">Low</div></div>
|
||||
<div class="col-6 col-md-3"><div id="scene-Offline" class="scene-btn" onclick="setScene('Offline')">Offline</div></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= CHART ================= -->
|
||||
<div class="col-12">
|
||||
<div class="card p-3">
|
||||
<h5>📊 Bitrate Live</h5>
|
||||
<canvas id="chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= PREVIEWS ================= -->
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card p-3">
|
||||
<h5>📺 Live 1 Preview</h5>
|
||||
<div class="preview-box">
|
||||
<div id="offline-live1" class="offline">OFFLINE</div>
|
||||
<iframe id="preview-live1"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card p-3">
|
||||
<h5>📺 Live 2 Preview</h5>
|
||||
<div class="preview-box">
|
||||
<div id="offline-live2" class="offline">OFFLINE</div>
|
||||
<iframe id="preview-live2"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= SCRIPTS ================= -->
|
||||
<script src="/dashboard.js"></script>
|
||||
<script src="/health.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once "config/config.php";
|
||||
require_once "Data.class.php";
|
||||
|
||||
$Data = new Data($pdo);
|
||||
|
||||
// Alle Termine holen (nur die mit bestehender CalDAV-UID!)
|
||||
$sql = $pdo->query("
|
||||
SELECT *
|
||||
FROM termine
|
||||
ORDER BY id ASC
|
||||
");
|
||||
|
||||
$termine = $sql->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo "<h2>Starte Update-Sync aller Termine zu iCloud…</h2>";
|
||||
echo "<pre>";
|
||||
|
||||
$counter = 0;
|
||||
|
||||
foreach ($termine as $t) {
|
||||
|
||||
$counter++;
|
||||
|
||||
$id = (int)$t['id'];
|
||||
|
||||
echo "→ Update Termin #$id ... ";
|
||||
|
||||
$ok = $Data->updateCalendarEvent($id);
|
||||
|
||||
if ($ok) {
|
||||
echo "OK\n";
|
||||
} else {
|
||||
echo "FEHLER!\n";
|
||||
}
|
||||
|
||||
// 🔒 iCloud Rate-Limit Schutz
|
||||
usleep(800000); // 0.8 Sekunden
|
||||
}
|
||||
|
||||
echo "\n---\n";
|
||||
echo "Geupdatete Termine: $counter\n";
|
||||
echo "</pre>";
|
||||
|
||||
echo "<h3>Update-Sync abgeschlossen!</h3>";
|
||||
?>
|
||||
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
date_default_timezone_set("Europe/Berlin");
|
||||
// Header / Navbar / Zugriffsschutz falls nötig
|
||||
require_once "config/config.php";
|
||||
require_once "Data.class.php";
|
||||
$Data = new Data($pdo);
|
||||
|
||||
require_once "config/session.php";
|
||||
|
||||
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isset($_POST['submit'])) {
|
||||
|
||||
// Required fields
|
||||
if (
|
||||
!empty($_POST['inputStartzeit_ts']) &&
|
||||
!empty($_POST['inputLocation']) &&
|
||||
!empty($_POST['inputArt']) // <-- NEU
|
||||
) {
|
||||
|
||||
// Endzeit optional → 0 wenn leer
|
||||
$endzeit = !empty($_POST['inputEndzeit_ts']) ? $_POST['inputEndzeit_ts'] : 0;
|
||||
|
||||
// Gage muss angegeben werden – kann aber 0 sein
|
||||
$gage = $_POST['inputGage'];
|
||||
if ($gage === '' || $gage === null) {
|
||||
$gage = 0;
|
||||
}
|
||||
$gage = (int)$gage;
|
||||
|
||||
// Veranstaltungsart absichern
|
||||
$art = $_POST['inputArt'] ?? '';
|
||||
$validArts = ['Party', 'Club', 'Hochzeit'];
|
||||
if (!in_array($art, $validArts, true)) {
|
||||
$art = 'Party'; // Fallback (oder Fehler werfen)
|
||||
}
|
||||
|
||||
$terminid = $Data->createTermin(
|
||||
$_POST['inputStartzeit_ts'],
|
||||
$endzeit,
|
||||
$_POST['inputLocation'],
|
||||
$_SESSION['id'],
|
||||
$gage,
|
||||
$art // <-- NEU
|
||||
);
|
||||
|
||||
// Changelog für "TERMIN ERSTELLT"
|
||||
$Data->createChanges($terminid, $_SESSION["id"], "create", "", "");
|
||||
|
||||
// Weiterleitung
|
||||
header("Location: termin_show.php?id=" . $terminid);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
include('navbar.php');
|
||||
|
||||
?>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
<div class="container mt-4">
|
||||
|
||||
<h3 class="mb-4"><i class="bi bi-plus-circle"></i> Neuen Termin erstellen</h3>
|
||||
|
||||
<form method="POST" action="termin_create.php" id="createTerminForm">
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- Startzeit -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Startzeit</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-clock-fill"></i></span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
class="form-control"
|
||||
id="inputStartzeit"
|
||||
name="inputStartzeit"
|
||||
required
|
||||
>
|
||||
<input type="hidden" name="inputStartzeit_ts" id="inputStartzeit_ts">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Endzeit -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Endzeit</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-clock-fill"></i></span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
class="form-control"
|
||||
id="inputEndzeit"
|
||||
name="inputEndzeit"
|
||||
>
|
||||
<input type="hidden" name="inputEndzeit_ts" id="inputEndzeit_ts">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preis pro angefangene Stunde
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Preis / angefangene Stunde</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-cash"></i></span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="inputPreisProStunde"
|
||||
placeholder="z. B. 50"
|
||||
>
|
||||
</div>
|
||||
</div>-->
|
||||
|
||||
<!-- Location -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Location</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-geo-alt"></i></span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="inputLocation"
|
||||
placeholder="Veranstaltungsort"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Veranstaltungsart -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Veranstaltungsart</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-calendar-event"></i></span>
|
||||
<select
|
||||
class="form-select"
|
||||
name="inputArt"
|
||||
required
|
||||
>
|
||||
<option value="" selected disabled>Bitte auswählen</option>
|
||||
<option value="Party">Party (eigene Technik, kompletter Aufbau)</option>
|
||||
<option value="Club">Club (Technik vor Ort, nur DJ-Setup)</option>
|
||||
<option value="Hochzeit">Hochzeit (hoher Orga-Aufwand, eigenes Equipment)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gage -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Gage</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-cash-stack"></i></span>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
name="inputGage"
|
||||
placeholder="z. B. 350"
|
||||
min="0"
|
||||
step="1"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-end mt-4">
|
||||
<button type="submit" name="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-circle"></i> Termin speichern
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- MODAL: Termin existiert -->
|
||||
<div class="modal fade" id="terminConflictModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title">Achtung – Terminüberschneidung!</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>Für dieses Datum existiert bereits mindestens ein Termin:</p>
|
||||
<div id="terminConflictList" class="border rounded p-2 bg-light"></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal">Okay</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
const startInput = document.getElementById("inputStartzeit");
|
||||
const endInput = document.getElementById("inputEndzeit");
|
||||
|
||||
let conflictShownForDate = null; // speichert das Datum, für das das Modal schon gezeigt wurde
|
||||
|
||||
startInput.addEventListener("change", async () => {
|
||||
|
||||
if (!startInput.value) return;
|
||||
|
||||
const selectedDate = startInput.value; // YYYY-MM-DDTHH:MM
|
||||
|
||||
// ---------- 1) TERMIN-KONFLIKT MODAL NUR EINMAL PRO DATUM ----------
|
||||
|
||||
const dateOnly = selectedDate.split("T")[0]; // 2025-12-08
|
||||
|
||||
if (conflictShownForDate !== dateOnly) {
|
||||
|
||||
const ts = Math.floor(new Date(selectedDate).getTime() / 1000);
|
||||
const res = await fetch(`check_date.php?ts=${ts}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
|
||||
let html = "";
|
||||
data.forEach(t => {
|
||||
html += `
|
||||
<div class="p-2 mb-2 bg-white rounded border">
|
||||
<strong>Termin #${t.id}</strong><br>
|
||||
Start: ${new Date(t.startzeit * 1000).toLocaleString()}<br>
|
||||
Location: ${t.location}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
document.getElementById("terminConflictList").innerHTML = html;
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById("terminConflictModal"));
|
||||
modal.show();
|
||||
|
||||
// merken → für dieses Datum NICHT erneut anzeigen
|
||||
conflictShownForDate = dateOnly;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 2) ENDZEIT BEI JEDER STARTZEIT-ÄNDERUNG +8h ----------
|
||||
|
||||
if (startInput.value) {
|
||||
const startDate = new Date(startInput.value);
|
||||
|
||||
startDate.setHours(startDate.getHours() + 8);
|
||||
|
||||
const iso = startDate.toISOString().slice(0, 16);
|
||||
endInput.value = iso;
|
||||
|
||||
const hidden = document.getElementById("inputEndzeit_ts");
|
||||
if (hidden) hidden.value = Math.floor(startDate.getTime() / 1000);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Timestamp-Konvertierung wie bei termin_show
|
||||
function toTimestampLocal(datetime) {
|
||||
return datetime ? Math.floor(new Date(datetime).getTime() / 1000) : "";
|
||||
}
|
||||
|
||||
document.getElementById("createTerminForm").addEventListener("submit", function() {
|
||||
document.getElementById("inputStartzeit_ts").value = toTimestampLocal(document.getElementById("inputStartzeit").value);
|
||||
document.getElementById("inputEndzeit_ts").value = toTimestampLocal(document.getElementById("inputEndzeit").value);
|
||||
document.getElementById("inputRealEndzeit_ts").value = toTimestampLocal(document.getElementById("inputRealEndzeit").value);
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once "config/config.php";
|
||||
include("Data.class.php");
|
||||
$Data = new Data($pdo);
|
||||
require_once "config/session.php";
|
||||
|
||||
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
http_response_code(403);
|
||||
exit("Keine Berechtigung");
|
||||
}
|
||||
|
||||
if (!isset($_POST["id"])) {
|
||||
die("Kein Termin angegeben.");
|
||||
}
|
||||
|
||||
$id = intval($_POST["id"]);
|
||||
|
||||
$Data->deleteCalendarEvent($id);
|
||||
|
||||
// --------------------------------------------------
|
||||
// 2) Changelog löschen
|
||||
// --------------------------------------------------
|
||||
$pdo->prepare("DELETE FROM changes WHERE termin_id = :id")
|
||||
->execute([':id' => $id]);
|
||||
|
||||
//$Data->createChanges($id, $_SESSION["id"], "delete", "", "");
|
||||
|
||||
// --------------------------------------------------
|
||||
// 3) Termin löschen
|
||||
// --------------------------------------------------
|
||||
$pdo->prepare("DELETE FROM termine WHERE id = :id")
|
||||
->execute([':id' => $id]);
|
||||
|
||||
|
||||
// --------------------------------------------------
|
||||
// Weiterleitung
|
||||
// --------------------------------------------------
|
||||
header("Location: termine.php");
|
||||
exit;
|
||||
+891
@@ -0,0 +1,891 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
date_default_timezone_set("Europe/Berlin");
|
||||
require_once "config/config.php";
|
||||
include("Data.class.php");
|
||||
$Data = new Data($pdo);
|
||||
|
||||
/* ===============================
|
||||
KALENDER HASH – Hybrid Phase 1
|
||||
=============================== */
|
||||
function buildCalendarHash(array $termin): string {
|
||||
$data = [
|
||||
'start' => (int)$termin['startzeit'],
|
||||
'end' => (int)$termin['endzeit'],
|
||||
'title' => trim((string)$termin['location']),
|
||||
'desc' => trim((string)$termin['beschreibung']),
|
||||
];
|
||||
|
||||
return hash('sha256', json_encode($data));
|
||||
}
|
||||
|
||||
|
||||
$statusMap = [
|
||||
"0" => "Offen",
|
||||
"1" => "Bereit",
|
||||
"2" => "Erledigt",
|
||||
"3" => "Storniert"
|
||||
];
|
||||
|
||||
require_once "config/session.php";
|
||||
|
||||
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
if(isset($_GET["id"])){
|
||||
$id = $_GET["id"];
|
||||
}
|
||||
if(isset($_POST["id"])){
|
||||
$id = $_POST["id"];
|
||||
}
|
||||
|
||||
/* ===============================
|
||||
TERMIN LADEN (für Kalenderstatus)
|
||||
=============================== */
|
||||
$stmt = $pdo->prepare("SELECT * FROM termine WHERE id = :id");
|
||||
$stmt->execute([':id' => $id]);
|
||||
$termin = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// ===============================
|
||||
// RECHNUNG ZU DIESEM TERMIN?
|
||||
// ===============================
|
||||
$invoice = null;
|
||||
try {
|
||||
$stInv = $pdo->prepare("SELECT id, invoice_no FROM invoices WHERE termine_id = :tid ORDER BY id DESC LIMIT 1");
|
||||
$stInv->execute([':tid' => $id]);
|
||||
$invoice = $stInv->fetch(PDO::FETCH_ASSOC) ?: null;
|
||||
} catch (Throwable $e) {
|
||||
// Falls Tabellen noch nicht existieren oder so: einfach ignorieren
|
||||
$invoice = null;
|
||||
}
|
||||
|
||||
|
||||
/* ===============================
|
||||
KALENDER STATUS BERECHNEN
|
||||
=============================== */
|
||||
$currentHash = buildCalendarHash($termin);
|
||||
|
||||
if (empty($termin['calendar_event_id'])) {
|
||||
$calendarStatus = 'missing'; // 🔴
|
||||
} elseif ($termin['calendar_hash'] !== $currentHash) {
|
||||
$calendarStatus = 'outdated'; // 🟡
|
||||
} else {
|
||||
$calendarStatus = 'ok'; // 🟢
|
||||
}
|
||||
|
||||
|
||||
|
||||
$per_page = 5; // Anzahl der Changelog-Einträge pro Seite
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
if ($page < 1) $page = 1;
|
||||
|
||||
$offset = ($page - 1) * $per_page;
|
||||
|
||||
// Gesamtanzahl Einträge
|
||||
$count_sql = $pdo->prepare("SELECT COUNT(*) FROM changes WHERE termin_id = :id");
|
||||
$count_sql->execute([':id' => $id]);
|
||||
$total_entries = $count_sql->fetchColumn();
|
||||
$total_pages = ceil($total_entries / $per_page);
|
||||
|
||||
// Daten holen
|
||||
$sql = $pdo->prepare("
|
||||
SELECT * FROM changes
|
||||
WHERE termin_id = :id
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
");
|
||||
|
||||
$sql->bindValue(':id', $id, PDO::PARAM_INT);
|
||||
$sql->bindValue(':limit', $per_page, PDO::PARAM_INT);
|
||||
$sql->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||
$sql->execute();
|
||||
|
||||
$changes = $sql->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
|
||||
if(isset($_POST['save'])){
|
||||
|
||||
if(isset($_POST['inputStartzeit_ts']) && !empty($_POST['inputStartzeit_ts'])){
|
||||
$startzeit = $_POST['inputStartzeit_ts'];
|
||||
if($Data->getTerminDataById($id, "startzeit") != $startzeit){
|
||||
$Data->createChanges($id, $_SESSION["id"], "startzeit", $Data->getTerminDataById($id, "startzeit"), $startzeit);
|
||||
$Data->changeTermin($id, "startzeit", $startzeit);
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($_POST['inputEndzeit_ts']) && !empty($_POST['inputEndzeit_ts'])){
|
||||
$endzeit = $_POST['inputEndzeit_ts'];
|
||||
if($Data->getTerminDataById($id, "endzeit") != $endzeit){
|
||||
$Data->createChanges($id, $_SESSION["id"], "endzeit", $Data->getTerminDataById($id, "endzeit"), $endzeit);
|
||||
$Data->changeTermin($id, "endzeit", $endzeit);
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($_POST['inputLocation'])){
|
||||
$location = $_POST['inputLocation'];
|
||||
if($Data->getTerminDataById($id, "location") != $location){
|
||||
$Data->createChanges($id, $_SESSION["id"], "location", $Data->getTerminDataById($id, "location"), $location);
|
||||
$Data->changeTermin($id, "location", $location);
|
||||
}
|
||||
}
|
||||
|
||||
if(isset($_POST['inputGage'])){
|
||||
$gage = $_POST['inputGage'];
|
||||
if($Data->getTerminDataById($id, "gage") != $gage){
|
||||
$Data->createChanges($id, $_SESSION["id"], "gage", $Data->getTerminDataById($id, "gage"), $gage);
|
||||
$Data->changeTermin($id, "gage", $gage);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['users'])) {
|
||||
// Neue Daten
|
||||
$userIDs_new = array_map('intval', $_POST['users']);
|
||||
$json_new = json_encode($userIDs_new);
|
||||
|
||||
// Alte Daten holen und in Array umwandeln
|
||||
$old_json = $Data->getTerminDataById($id, "anwesend");
|
||||
$userIDs_old = json_decode($old_json, true);
|
||||
|
||||
// Falls alt NULL oder kein Array
|
||||
if (!is_array($userIDs_old)) {
|
||||
$userIDs_old = [];
|
||||
}
|
||||
|
||||
// Arrays vergleichen – unabhängig von Reihenfolge
|
||||
if (array_diff($userIDs_old, $userIDs_new) || array_diff($userIDs_new, $userIDs_old)) {
|
||||
|
||||
// Changelog schreiben
|
||||
$Data->createChanges(
|
||||
$id,
|
||||
$_SESSION["id"],
|
||||
"anwesend",
|
||||
json_encode($userIDs_old),
|
||||
$json_new
|
||||
);
|
||||
|
||||
// Speichern
|
||||
$Data->changeTermin($id, "anwesend", $json_new);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['inputBeschreibung'])) {
|
||||
$beschreibung = trim($_POST['inputBeschreibung']);
|
||||
if ($Data->getTerminDataById($id, "beschreibung") != $beschreibung) {
|
||||
$Data->createChanges($id, $_SESSION["id"], "beschreibung", "", "");
|
||||
$Data->changeTermin($id, "beschreibung", $beschreibung);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(isset($_POST['status'])){
|
||||
$status = $_POST['status'];
|
||||
if($Data->getTerminDataById($id, "status") != $status){
|
||||
$Data->createChanges($id, $_SESSION["id"], "status", $Data->getTerminDataById($id, "status"), $status);
|
||||
$Data->changeTermin($id, "status", $status);
|
||||
if($status == "3"){
|
||||
$Data->deleteCalendarEvent($id);
|
||||
header("location: termin_show.php?id=" . $id);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$Data->updateCalendarEvent($id);
|
||||
|
||||
header("location: termin_show.php?id=" . $id);
|
||||
exit;
|
||||
}
|
||||
|
||||
include('navbar.php');
|
||||
|
||||
?>
|
||||
<style>
|
||||
/* ---------- CHANGES TABLE / TIMELINE OPTIMIERUNG ---------- */
|
||||
|
||||
.select2-container--classic .select2-selection--multiple {
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
border-color: #ced4da;
|
||||
}
|
||||
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice {
|
||||
background: #0d6efd;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: white;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
|
||||
.changelog-table {
|
||||
border-collapse: separate !important;
|
||||
border-spacing: 0 12px !important; /* Abstand zwischen den Reihen */
|
||||
}
|
||||
|
||||
.changelog-row {
|
||||
background: #ffffff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.changelog-date {
|
||||
font-weight: 600;
|
||||
color: #444;
|
||||
white-space: nowrap;
|
||||
padding-top: 12px !important;
|
||||
}
|
||||
|
||||
.change-entry {
|
||||
padding: 14px 18px !important;
|
||||
border-left: 4px solid #0d6efd;
|
||||
background: #f8f9fa !important;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.change-entry strong {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.change-old {
|
||||
color: #dc3545 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.change-new {
|
||||
color: #198754 !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Mobile Version */
|
||||
@media (max-width: 768px) {
|
||||
.changelog-table {
|
||||
border-spacing: 0 10px !important;
|
||||
}
|
||||
|
||||
.change-entry {
|
||||
font-size: 16px;
|
||||
padding: 16px 16px !important;
|
||||
}
|
||||
|
||||
.changelog-date {
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* MOBILE: Datum/Uhrzeit schmaler machen */
|
||||
@media (max-width: 768px) {
|
||||
|
||||
/* Datum/Uhrzeit-Spalte schmaler */
|
||||
.changelog-date {
|
||||
width: 95px !important; /* vorher ~150–180px */
|
||||
font-size: 13px !important; /* leicht verkleinern */
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Änderungstext bekommt mehr Platz */
|
||||
.change-entry {
|
||||
font-size: 15px !important;
|
||||
padding: 14px 14px !important;
|
||||
}
|
||||
|
||||
/* Tabelle anpassen */
|
||||
.changelog-table th:first-child {
|
||||
width: 95px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
|
||||
<title>ANJUMA Dashboard</title>
|
||||
</head>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-10">
|
||||
|
||||
<form action="termin_show.php" method="post">
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Startzeit (nicht änderbar) -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Startzeit</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-clock-fill"></i></span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
class="form-control"
|
||||
id="inputStartzeit"
|
||||
value="<?php echo date('Y-m-d\TH:i', $Data->getTerminDataById($id, 'startzeit')); ?>"
|
||||
>
|
||||
<input type="hidden" name="inputStartzeit_ts" id="inputStartzeit_ts">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Endzeit -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Endzeit</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-clock-fill"></i></span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
class="form-control"
|
||||
id="inputEndzeit"
|
||||
value="<?php echo date('Y-m-d\TH:i', $Data->getTerminDataById($id, 'endzeit')); ?>"
|
||||
>
|
||||
<input type="hidden" name="inputEndzeit_ts" id="inputEndzeit_ts">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Location</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-geo-alt"></i></span>
|
||||
<input type="text" class="form-control" name="inputLocation"
|
||||
value="<?php echo $Data->getTerminDataById($id, 'location') ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gage -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Gage</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-cash-stack"></i></span>
|
||||
<input type="text" class="form-control" name="inputGage"
|
||||
value="<?php echo $Data->getTerminDataById($id, 'gage') ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Beschreibung -->
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-bold">Beschreibung</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-card-text"></i></span>
|
||||
<textarea
|
||||
class="form-control"
|
||||
name="inputBeschreibung"
|
||||
id="beschreibungField"
|
||||
rows="1"
|
||||
style="overflow:hidden; resize:none;"
|
||||
><?php
|
||||
echo htmlspecialchars($Data->getTerminDataById($id, 'beschreibung'));
|
||||
?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Beteiligte Personen -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Anwesend</label>
|
||||
<select class="form-select select2-users" name="users[]" multiple>
|
||||
<?php
|
||||
$userList = $pdo->query("SELECT id, name FROM users ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$selectedUsers = json_decode($Data->getTerminDataById($id, "anwesend"), true) ?: [];
|
||||
|
||||
foreach ($userList as $u) {
|
||||
$sel = in_array($u['id'], $selectedUsers) ? "selected" : "";
|
||||
echo "<option value='{$u['id']}' $sel>{$u['name']}</option>";
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="btn-group my-4" role="group">
|
||||
<label class="btn btn-primary">
|
||||
<input type="radio" name="status" value="0" <?php if($Data->getTerminDataById($id, "status") == 0){ echo "checked"; } ?>> Offen
|
||||
</label>
|
||||
|
||||
<label class="btn btn-warning">
|
||||
<input type="radio" name="status" value="1" <?php if($Data->getTerminDataById($id, "status") == 1){ echo "checked"; } ?>> Bereit
|
||||
</label>
|
||||
|
||||
<label class="btn btn-success">
|
||||
<input type="radio" name="status" value="2" <?php if($Data->getTerminDataById($id, "status") == 2){ echo "checked"; } ?>> Abgeschlossen
|
||||
</label>
|
||||
|
||||
<label class="btn btn-danger">
|
||||
<input type="radio" name="status" value="3" <?php if($Data->getTerminDataById($id, "status") == 3){ echo "checked"; } ?>> Storniert
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="id" value="<?php echo $id ?>">
|
||||
|
||||
|
||||
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
|
||||
<button type="button" class="btn btn-danger ms-3"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal">
|
||||
<i class="bi bi-trash"></i> Termin löschen
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<button type="submit" name="save" class="btn btn-primary">
|
||||
<i class="bi bi-save"></i> Speichern
|
||||
</button>
|
||||
|
||||
<?php
|
||||
$terminStatus = (int)$Data->getTerminDataById($id, "status");
|
||||
?>
|
||||
|
||||
<?php if ($terminStatus === 2): ?>
|
||||
<?php if ($invoice): ?>
|
||||
<a class="btn btn-outline-success ms-3"
|
||||
href="/api/invoice_pdf.php?id=<?= (int)$invoice['id'] ?>"
|
||||
target="_blank">
|
||||
<i class="bi bi-receipt"></i> PDF öffnen (<?= htmlspecialchars($invoice['invoice_no']) ?>)
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-success ms-3" id="btnInvoiceOpen">
|
||||
<i class="bi bi-receipt"></i> Rechnung erstellen
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
|
||||
<!-- ===============================
|
||||
KALENDER STATUS (Hybrid Phase 1)
|
||||
=============================== -->
|
||||
<div class="mb-3">
|
||||
|
||||
<?php if ($calendarStatus === 'ok'): ?>
|
||||
<span class="badge bg-success">
|
||||
📅 Kalender aktuell
|
||||
<?php if (!empty($termin['calendar_synced_at'])): ?>
|
||||
<small>(<?= date('d.m.Y H:i', $termin['calendar_synced_at']) ?>)</small>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
|
||||
<?php elseif ($calendarStatus === 'outdated'): ?>
|
||||
<span class="badge bg-warning text-dark">
|
||||
📅 Kalender nicht aktuell
|
||||
</span>
|
||||
|
||||
<?php else: ?>
|
||||
<span class="badge bg-danger">
|
||||
📅 Kein Kalender-Eintrag
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if ($calendarStatus === 'outdated'): ?>
|
||||
<div class="alert alert-warning py-2">
|
||||
⚠️ Der Termin wurde geändert, der Kalendereintrag ist nicht mehr aktuell.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Changelog -->
|
||||
<div class="container mt-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-10 col-lg-10">
|
||||
|
||||
<table class="table changelog-table">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th style="width: 180px;">Datum/Uhrzeit</th>
|
||||
<th>Änderung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
foreach ($changes as $row) {
|
||||
|
||||
$datum = $Data->timestampToDate($row['timestamp']);
|
||||
$name = $Data->getDataByID($row['user_id'], "name");
|
||||
$feld = strtoupper($row['was']);
|
||||
$alt = $row['alt'];
|
||||
$neu = $row['neu'];
|
||||
|
||||
if ($row['was'] === 'anwesend'){
|
||||
$alt = json_decode($row["alt"], true) ?: [];
|
||||
$neu = json_decode($row["neu"], true) ?: [];
|
||||
|
||||
$alt = array_map(fn($uid) => $Data->getDataByID($uid, "name"), $alt);
|
||||
$neu = array_map(fn($uid) => $Data->getDataByID($uid, "name"), $neu);
|
||||
}
|
||||
|
||||
if ($row['was'] === 'status') {
|
||||
$alt = $statusMap[$alt] ?? $alt;
|
||||
$neu = $statusMap[$neu] ?? $neu;
|
||||
}
|
||||
|
||||
if (in_array($row['was'], ['startzeit','endzeit','realendzeit'])) {
|
||||
if ($alt > 0) $alt = date('d.m.Y H:i', $alt);
|
||||
$neu = date('d.m.Y H:i', $neu);
|
||||
}
|
||||
|
||||
echo "<tr class='changelog-row'>
|
||||
<td class='changelog-date'>$datum</td>
|
||||
<td>
|
||||
<div class='change-entry'>";
|
||||
|
||||
if ($row['was'] === 'create') {
|
||||
echo "<strong>$name</strong> hat diesen <strong>Termin</strong> erstellt!";
|
||||
}
|
||||
elseif ($row['was'] === 'anwesend') {
|
||||
echo "<strong>$name</strong> hat <strong>$feld</strong> geändert von
|
||||
<span class='change-old'>".implode(', ', $alt)."</span>
|
||||
zu <span class='change-new'>".implode(', ', $neu)."</span>.";
|
||||
}
|
||||
elseif ($row['was'] === 'startzeit' && $row['alt'] == 0) {
|
||||
echo "<strong>$name</strong> hat <strong>STARTZEIT</strong> geändert zu
|
||||
<span class='change-new'>$neu</span>.";
|
||||
}
|
||||
elseif ($row['was'] === 'beschreibung') {
|
||||
echo "<strong>$name</strong> hat die <strong>Beschreibung</strong> geändert!";
|
||||
}else{
|
||||
echo "<strong>$name</strong> hat <strong>$feld</strong> geändert von
|
||||
<span class='change-old'>$alt</span>
|
||||
zu <span class='change-new'>$neu</span>.";
|
||||
}
|
||||
|
||||
echo " </div>
|
||||
</td>
|
||||
</tr>";
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Pagination -->
|
||||
<nav>
|
||||
<ul class="pagination justify-content-center">
|
||||
|
||||
<!-- Prev Button -->
|
||||
<li class="page-item <?php if ($page <= 1) echo 'disabled'; ?>">
|
||||
<a class="page-link" href="?id=<?php echo $id; ?>&page=<?php echo $page - 1; ?>">
|
||||
« Zurück
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Seitenzahlen -->
|
||||
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
|
||||
<li class="page-item <?php if ($i == $page) echo 'active'; ?>">
|
||||
<a class="page-link" href="?id=<?php echo $id; ?>&page=<?php echo $i; ?>">
|
||||
<?php echo $i; ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
|
||||
<!-- Next Button -->
|
||||
<li class="page-item <?php if ($page >= $total_pages) echo 'disabled'; ?>">
|
||||
<a class="page-link" href="?id=<?php echo $id; ?>&page=<?php echo $page + 1; ?>">
|
||||
Weiter »
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Invoice Modal -->
|
||||
<div class="modal fade" id="invoiceModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form class="modal-content" id="invoiceForm">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Rechnung erstellen</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-1">Kunde / Firma *</label>
|
||||
<input type="text" class="form-control" id="invCustomerName" required placeholder="z.B. Lug Lounge GmbH">
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-1">Straße & Nr.</label>
|
||||
<input type="text" class="form-control" id="invStreet" placeholder="Musterstraße 1">
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-5">
|
||||
<label class="form-label mb-1">PLZ</label>
|
||||
<input type="text" class="form-control" id="invZip" placeholder="00000">
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<label class="form-label mb-1">Ort</label>
|
||||
<input type="text" class="form-control" id="invCity" placeholder="Nienburg">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<label class="form-label mb-1">Land</label>
|
||||
<input type="text" class="form-control" id="invCountry" value="DE">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-success" id="btnCreateInvoice">Erstellen & PDF</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title">Termin löschen?</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>Bist du sicher, dass du diesen Termin löschen möchtest?</p>
|
||||
<ul>
|
||||
<li>Der Termin wird vollständig entfernt</li>
|
||||
<li>Alle Änderungen (Changelog) werden gelöscht</li>
|
||||
<li>Kalendereintrag wird entfernt</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
|
||||
<form action="termin_delete.php" method="POST">
|
||||
<input type="hidden" name="id" value="<?= $id ?>">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="bi bi-trash"></i> Ja, löschen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- MODAL: Termin existiert -->
|
||||
<div class="modal fade" id="terminConflictModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title">Achtung – Terminüberschneidung!</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>Für dieses Datum existiert bereits mindestens ein Termin:</p>
|
||||
<div id="terminConflictList" class="border rounded p-2 bg-light"></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" data-bs-dismiss="modal">Okay</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
const startInput = document.getElementById("inputStartzeit");
|
||||
|
||||
startInput.addEventListener("change", async () => {
|
||||
if (!startInput.value) return;
|
||||
|
||||
// ISO → Timestamp
|
||||
const ts = Math.floor(new Date(startInput.value).getTime() / 1000);
|
||||
|
||||
const res = await fetch(`check_date.php?ts=${ts}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
|
||||
let html = "";
|
||||
data.forEach(t => {
|
||||
html += `
|
||||
<div class="p-2 mb-2 bg-white rounded border">
|
||||
<strong>Termin #${t.id}</strong><br>
|
||||
Start: ${new Date(t.startzeit * 1000).toLocaleString()}<br>
|
||||
Location: ${t.location}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
document.getElementById("terminConflictList").innerHTML = html;
|
||||
|
||||
// Modal anzeigen
|
||||
const myModal = new bootstrap.Modal(document.getElementById("terminConflictModal"));
|
||||
myModal.show();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
|
||||
// Select2 aktivieren
|
||||
$('.select2-users').select2({
|
||||
placeholder: "Anwesende auswählen…",
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
theme: "classic"
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
function convertToTimestamp(idInput, idHidden) {
|
||||
const input = document.getElementById(idInput);
|
||||
const hidden = document.getElementById(idHidden);
|
||||
|
||||
if (input && hidden && input.value) {
|
||||
const ts = Math.floor(new Date(input.value).getTime() / 1000);
|
||||
hidden.value = ts;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const startzeit = document.getElementById("inputStartzeit");
|
||||
const endzeit = document.getElementById("inputEndzeit");
|
||||
const real = document.getElementById("inputRealEndzeit");
|
||||
|
||||
if (endzeit) {
|
||||
endzeit.addEventListener("change", () => {
|
||||
convertToTimestamp("inputEndzeit", "inputEndzeit_ts");
|
||||
});
|
||||
}
|
||||
|
||||
if (real) {
|
||||
real.addEventListener("change", () => {
|
||||
convertToTimestamp("inputRealEndzeit", "inputRealEndzeit_ts");
|
||||
});
|
||||
}
|
||||
|
||||
if (startzeit) {
|
||||
startzeit.addEventListener("change", () => {
|
||||
convertToTimestamp("inputStartzeit", "inputStartzeit_ts");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function autoResizeTextarea(el) {
|
||||
el.style.height = 'auto';
|
||||
el.style.height = (el.scrollHeight) + 'px';
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const textarea = document.getElementById("beschreibungField");
|
||||
|
||||
if (textarea) {
|
||||
// Beim Laden automatisch anpassen
|
||||
autoResizeTextarea(textarea);
|
||||
|
||||
// Beim Eingeben automatisch anpassen
|
||||
textarea.addEventListener('input', function() {
|
||||
autoResizeTextarea(textarea);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// === Rechnung erstellen (Termin Show) ===
|
||||
const API_INVOICE_CREATE = '/api/invoice_create_from_termin.php';
|
||||
const API_INVOICE_PDF = '/api/invoice_pdf.php?id=';
|
||||
const terminIdForInvoice = <?= (int)$id ?>;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const btnOpen = document.getElementById("btnInvoiceOpen");
|
||||
if (!btnOpen) return;
|
||||
|
||||
const invoiceModalEl = document.getElementById('invoiceModal');
|
||||
const invoiceModal = new bootstrap.Modal(invoiceModalEl);
|
||||
|
||||
btnOpen.addEventListener('click', () => {
|
||||
// reset
|
||||
document.getElementById('invCustomerName').value = '';
|
||||
document.getElementById('invStreet').value = '';
|
||||
document.getElementById('invZip').value = '';
|
||||
document.getElementById('invCity').value = '';
|
||||
document.getElementById('invCountry').value = 'DE';
|
||||
|
||||
invoiceModal.show();
|
||||
});
|
||||
|
||||
document.getElementById('invoiceForm').addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const cname = document.getElementById('invCustomerName').value.trim();
|
||||
if (!cname) { alert('Bitte Kunde/Firma eintragen'); return; }
|
||||
|
||||
const btn = document.getElementById('btnCreateInvoice');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Erstelle…';
|
||||
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('termin_id', terminIdForInvoice);
|
||||
fd.append('customer_name', cname);
|
||||
fd.append('customer_street', document.getElementById('invStreet').value.trim());
|
||||
fd.append('customer_zip', document.getElementById('invZip').value.trim());
|
||||
fd.append('customer_city', document.getElementById('invCity').value.trim());
|
||||
fd.append('customer_country', document.getElementById('invCountry').value.trim() || 'DE');
|
||||
|
||||
const res = await fetch(API_INVOICE_CREATE, { method: 'POST', body: fd });
|
||||
const json = await res.json();
|
||||
|
||||
if (!json.ok) throw new Error(json.error || 'Rechnung konnte nicht erstellt werden');
|
||||
|
||||
invoiceModal.hide();
|
||||
|
||||
// PDF öffnen
|
||||
window.open(API_INVOICE_PDF + json.invoice_id, '_blank');
|
||||
|
||||
// Seite neu laden, damit Button auf "PDF öffnen" wechselt
|
||||
window.location.reload();
|
||||
|
||||
} catch (e) {
|
||||
alert('Fehler: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Erstellen & PDF';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
+435
@@ -0,0 +1,435 @@
|
||||
<?php
|
||||
require_once "config/session.php";
|
||||
|
||||
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
include('navbar.php');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ANJUMA • Termine</title>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/css/dataTables.bootstrap5.min.css">
|
||||
|
||||
<style>
|
||||
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||
.kpi-card { min-height: 92px; }
|
||||
.badge-status { text-transform: uppercase; letter-spacing: .04em; }
|
||||
.table-responsive { overflow-x: auto; }
|
||||
@media (max-width: 576px) {
|
||||
.hide-xs { display:none !important; }
|
||||
.kpi-hint { display:none !important; }
|
||||
}
|
||||
.mobile-card .meta { font-size: .9rem; }
|
||||
.mobile-card .title { font-weight: 600; }
|
||||
.mobile-card .sub { color: #6c757d; }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<h3 class="mb-0">Termine</h3>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button id="btnRefresh" class="btn btn-outline-secondary btn-sm">↻ Refresh</button>
|
||||
<button id="btnAdd" class="btn btn-primary btn-sm">+ Termin</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="card shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label mb-1">Von</label>
|
||||
<input type="date" class="form-control" id="fromDate">
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label mb-1">Bis</label>
|
||||
<input type="date" class="form-control" id="toDate">
|
||||
</div>
|
||||
<div class="col-12 col-md-3">
|
||||
<label class="form-label mb-1">Status</label>
|
||||
<select class="form-select" id="statusFilter">
|
||||
<option value="">Alle</option>
|
||||
<option value="0">Offen</option>
|
||||
<option value="1">Bereit</option>
|
||||
<option value="2">Abgeschlossen</option>
|
||||
<option value="3">Storniert</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-12 col-md-3 d-flex gap-2">
|
||||
<button id="btnThisMonth" class="btn btn-outline-primary w-100">Dieser Monat</button>
|
||||
<button id="btnThisYear" class="btn btn-outline-primary w-100">Dieses Jahr</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card shadow-sm kpi-card">
|
||||
<div class="card-body">
|
||||
<div class="text-muted small">Heute</div>
|
||||
<div class="fs-3 fw-semibold" id="kpiToday">–</div>
|
||||
<div class="text-muted small kpi-hint" id="kpiTodayHint">–</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card shadow-sm kpi-card">
|
||||
<div class="card-body">
|
||||
<div class="text-muted small">Diese Woche</div>
|
||||
<div class="fs-3 fw-semibold" id="kpiWeek">–</div>
|
||||
<div class="text-muted small kpi-hint" id="kpiWeekHint">–</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card shadow-sm kpi-card">
|
||||
<div class="card-body">
|
||||
<div class="text-muted small">Dieser Monat</div>
|
||||
<div class="fs-3 fw-semibold" id="kpiMonth">–</div>
|
||||
<div class="text-muted small kpi-hint" id="kpiMonthHint">–</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<!-- Mobile Cards (nur xs/sm) -->
|
||||
<div id="mobileCards" class="d-block d-md-none"></div>
|
||||
|
||||
<!-- Desktop Table (ab md) -->
|
||||
<div class="table-responsive d-none d-md-block">
|
||||
<table id="tblTermine" class="table table-striped table-hover align-middle w-100">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th class="hide-xs">Zeit</th>
|
||||
<th>Location</th>
|
||||
<th>Status</th>
|
||||
<th>Gage</th>
|
||||
<th class="d-none d-md-table-cell">Beschreibung</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="text-muted small mt-2" id="tableInfo"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 1080">
|
||||
<div id="toast" class="toast align-items-center text-bg-dark border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body" id="toastBody">...</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/datatables.net@1.13.10/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.10/js/dataTables.bootstrap5.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
const API_LIST = '/api/termine_list.php';
|
||||
|
||||
let dt;
|
||||
const toast = new bootstrap.Toast(document.getElementById('toast'), { delay: 3500 });
|
||||
function showToast(msg){ document.getElementById('toastBody').textContent = msg; toast.show(); }
|
||||
|
||||
function escapeHtml(s){
|
||||
return String(s ?? '')
|
||||
.replaceAll('&','&').replaceAll('<','<')
|
||||
.replaceAll('>','>').replaceAll('"','"')
|
||||
.replaceAll("'","'");
|
||||
}
|
||||
|
||||
function renderMobileCards(items){
|
||||
const el = document.getElementById('mobileCards');
|
||||
if (!el) return;
|
||||
|
||||
if (!items.length) {
|
||||
el.innerHTML = `<div class="text-muted">Keine Termine im Zeitraum.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
el.innerHTML = items.map(r => {
|
||||
const date = fmtDateDEFromTs(r.startzeit);
|
||||
const time = `${fmtTimeFromTs(r.startzeit)}–${fmtTimeFromTs(r.endzeit)}`;
|
||||
const loc = `${(r.locationtype||'').trim()} ${(r.location||'').trim()}`.trim();
|
||||
const desc = (r.beschreibung || '').toString().trim();
|
||||
const descShort = desc.length > 90 ? desc.slice(0, 90) + '…' : desc;
|
||||
|
||||
return `
|
||||
<div class="card shadow-sm mb-2 mobile-card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start gap-2">
|
||||
<div class="title">${escapeHtml(loc || '—')}</div>
|
||||
<div class="text-end">
|
||||
${statusBadge(r.status)}
|
||||
<div class="mono mt-1">${eur(r.gage || 0)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="meta sub mt-1">
|
||||
<span class="mono">${date}</span> · <span class="mono">${time}</span>
|
||||
</div>
|
||||
|
||||
${desc ? `<div class="mt-2">${escapeHtml(descShort)}</div>` : ''}
|
||||
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button class="btn btn-outline-primary btn-sm actEdit" data-id="${r.id}">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function isoLocal(d){
|
||||
const y=d.getFullYear(), m=String(d.getMonth()+1).padStart(2,'0'), day=String(d.getDate()).padStart(2,'0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
function thisMonthRange(){
|
||||
const d=new Date();
|
||||
return [isoLocal(new Date(d.getFullYear(), d.getMonth(), 1)), isoLocal(new Date(d.getFullYear(), d.getMonth()+1, 0))];
|
||||
}
|
||||
function thisYearRange(){
|
||||
const d=new Date();
|
||||
return [isoLocal(new Date(d.getFullYear(), 0, 1)), isoLocal(new Date(d.getFullYear(), 11, 31))];
|
||||
}
|
||||
|
||||
function fmtDateDEFromTs(ts){
|
||||
const d=new Date(Number(ts)*1000);
|
||||
const dd=String(d.getDate()).padStart(2,'0');
|
||||
const mm=String(d.getMonth()+1).padStart(2,'0');
|
||||
const yy=d.getFullYear();
|
||||
return `${dd}.${mm}.${yy}`;
|
||||
}
|
||||
function fmtTimeFromTs(ts){
|
||||
const d=new Date(Number(ts)*1000);
|
||||
const hh=String(d.getHours()).padStart(2,'0');
|
||||
const mm=String(d.getMinutes()).padStart(2,'0');
|
||||
return `${hh}:${mm}`;
|
||||
}
|
||||
function eur(n){
|
||||
const v = Number(n)||0;
|
||||
return v.toLocaleString('de-DE', { style:'currency', currency:'EUR', maximumFractionDigits:0 });
|
||||
}
|
||||
|
||||
function statusBadge(s){
|
||||
s = String(s ?? '').trim();
|
||||
if (s === '2') return '<span class="badge text-bg-success badge-status">ABGESCHLOSSEN</span>';
|
||||
if (s === '0') return '<span class="badge text-bg-secondary badge-status">OFFEN</span>';
|
||||
if (s === '1') return '<span class="badge text-bg-primary badge-status">BEREIT</span>';
|
||||
if (s === '3') return '<span class="badge text-bg-danger badge-status">STORNIERT</span>';
|
||||
return '<span class="badge text-bg-warning badge-status">UNBEKANNT</span>';
|
||||
}
|
||||
|
||||
function actionsHtml(row){
|
||||
return `
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-primary actEdit" data-id="${row.id}">Edit</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function getQS(){
|
||||
const from = document.getElementById('fromDate').value;
|
||||
const to = document.getElementById('toDate').value;
|
||||
const status = document.getElementById('statusFilter').value;
|
||||
const qs = new URLSearchParams({ from, to });
|
||||
if (status !== '') qs.set('status', status);
|
||||
return qs.toString();
|
||||
}
|
||||
|
||||
async function fetchJSON(url){
|
||||
const res = await fetch(url, { cache:'no-store' });
|
||||
const text = await res.text();
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
if (!json.ok) throw new Error(json.error || 'API error');
|
||||
return json;
|
||||
} catch(e){
|
||||
throw new Error(`API liefert kein JSON (${res.status}). ${text.slice(0,200)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Sort: Zukunft zuerst (nächster Termin oben), dann Vergangenheit
|
||||
function sortNextFirst(items){
|
||||
const nowTs = Math.floor(Date.now() / 1000);
|
||||
|
||||
return items.slice().sort((a, b) => {
|
||||
const as = Number(a.startzeit) || 0;
|
||||
const bs = Number(b.startzeit) || 0;
|
||||
|
||||
const aFuture = as >= nowTs;
|
||||
const bFuture = bs >= nowTs;
|
||||
|
||||
if (aFuture && !bFuture) return -1;
|
||||
if (!aFuture && bFuture) return 1;
|
||||
|
||||
// Beide Zukunft -> ASC (nächster zuerst)
|
||||
if (aFuture && bFuture) return as - bs;
|
||||
|
||||
// Beide Vergangenheit -> DESC (letzter vergangener oben)
|
||||
return bs - as;
|
||||
});
|
||||
}
|
||||
|
||||
async function refresh(){
|
||||
try{
|
||||
const qs = getQS();
|
||||
const json = await fetchJSON(`${API_LIST}?${qs}`);
|
||||
|
||||
// ✅ sortierte items nutzen (für Mobile & Desktop identisch)
|
||||
const items = sortNextFirst(json.items || []);
|
||||
|
||||
const isMobile = window.matchMedia("(max-width: 767.98px)").matches;
|
||||
|
||||
// Desktop: DataTable füllen
|
||||
if (!isMobile) {
|
||||
dt.clear().rows.add(items).draw(false);
|
||||
}
|
||||
|
||||
// Mobile: Cards füllen
|
||||
if (isMobile) {
|
||||
renderMobileCards(items);
|
||||
}
|
||||
|
||||
document.getElementById('tableInfo').textContent =
|
||||
`Treffer: ${items.length} • Zeitraum: ${json.from} bis ${json.to}`;
|
||||
|
||||
// KPI (clientseitig)
|
||||
const now = new Date();
|
||||
const todayKey = isoLocal(now);
|
||||
|
||||
// week start (Mon)
|
||||
const day = (now.getDay()+6)%7; // 0=Mon
|
||||
const weekStart = new Date(now);
|
||||
weekStart.setDate(now.getDate()-day);
|
||||
weekStart.setHours(0,0,0,0);
|
||||
|
||||
const [mFrom, mTo] = thisMonthRange();
|
||||
const mFromMs = new Date(mFrom+'T00:00:00').getTime();
|
||||
const mToMs = new Date(mTo+'T23:59:59').getTime();
|
||||
|
||||
let todayCount=0;
|
||||
let weekCount=0;
|
||||
let monthCount = 0;
|
||||
|
||||
for(const r of items){
|
||||
const startMs = Number(r.startzeit)*1000;
|
||||
const dIso = isoLocal(new Date(startMs));
|
||||
|
||||
if (dIso === todayKey) todayCount++;
|
||||
if (startMs >= weekStart.getTime()) weekCount++;
|
||||
|
||||
if (startMs >= mFromMs && startMs <= mToMs) monthCount++;
|
||||
}
|
||||
|
||||
document.getElementById('kpiToday').textContent = todayCount;
|
||||
document.getElementById('kpiTodayHint').textContent = 'Termine heute';
|
||||
document.getElementById('kpiWeek').textContent = weekCount;
|
||||
document.getElementById('kpiWeekHint').textContent = 'Termine seit Montag';
|
||||
document.getElementById('kpiMonth').textContent = monthCount;
|
||||
document.getElementById('kpiMonthHint').textContent = 'Termine diesen Monat';
|
||||
|
||||
} catch(e){
|
||||
showToast('Fehler: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Init
|
||||
$(function(){
|
||||
const [f,t] = thisMonthRange();
|
||||
document.getElementById('fromDate').value = f;
|
||||
document.getElementById('toDate').value = t;
|
||||
|
||||
dt = $('#tblTermine').DataTable({
|
||||
pageLength: 25,
|
||||
|
||||
// ✅ wichtig: keine Default-Sortierung, wir sortieren im JS für "nächster Termin oben"
|
||||
order: [],
|
||||
|
||||
columns: [
|
||||
{
|
||||
data: 'startzeit',
|
||||
className:'mono',
|
||||
render: (d, type) => {
|
||||
const ts = parseInt(d, 10) || 0;
|
||||
if (type === 'sort' || type === 'type') return ts;
|
||||
return fmtDateDEFromTs(ts);
|
||||
}
|
||||
},
|
||||
{ data: null, className:'mono hide-xs', render: (row)=> `${fmtTimeFromTs(row.startzeit)}–${fmtTimeFromTs(row.endzeit)}` },
|
||||
{ data: null, render: (row)=> `${(row.locationtype||'').trim()} ${(row.location||'').trim()}`.trim() },
|
||||
{ data: 'status', render: (d)=>statusBadge(d) },
|
||||
{ data: 'gage', className:'mono', render: (d)=> eur(d) },
|
||||
{ data: 'beschreibung', className:'d-none d-md-table-cell', render: (d)=> (d||'').toString().slice(0,80) },
|
||||
{ data: null, orderable:false, searchable:false, render: (row)=>actionsHtml(row) },
|
||||
],
|
||||
});
|
||||
|
||||
['fromDate','toDate','statusFilter'].forEach(id => {
|
||||
document.getElementById(id).addEventListener('change', refresh);
|
||||
});
|
||||
|
||||
document.getElementById('btnThisMonth').addEventListener('click', () => {
|
||||
const [a,b] = thisMonthRange();
|
||||
document.getElementById('fromDate').value = a;
|
||||
document.getElementById('toDate').value = b;
|
||||
refresh();
|
||||
});
|
||||
|
||||
document.getElementById('btnThisYear').addEventListener('click', () => {
|
||||
const [a,b] = thisYearRange();
|
||||
document.getElementById('fromDate').value = a;
|
||||
document.getElementById('toDate').value = b;
|
||||
refresh();
|
||||
});
|
||||
|
||||
document.getElementById('btnRefresh').addEventListener('click', refresh);
|
||||
|
||||
refresh();
|
||||
});
|
||||
|
||||
// Hooks für View/Edit
|
||||
document.addEventListener('click', (ev) => {
|
||||
const e = ev.target.closest('.actEdit');
|
||||
if (e) {
|
||||
const id = e.dataset.id;
|
||||
window.location = `/termin_show.php?id=${id}`;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btnAdd').addEventListener('click', () => {
|
||||
window.location = `/termin_create.php`;
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
<?php
|
||||
// Initialize the session
|
||||
session_start();
|
||||
|
||||
// Check if the user is logged in, if not then redirect him to login page
|
||||
if(!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true){
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
include('navbar.php');
|
||||
|
||||
?>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="//cdn.datatables.net/1.10.12/css/jquery.dataTables.css">
|
||||
<script type="text/javascript" charset="utf8" src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
|
||||
<div id="tableHolder"></div>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function(){
|
||||
refreshTable();
|
||||
});
|
||||
var text = document.getElementById("tableHolder").textContent;
|
||||
|
||||
|
||||
var statusFilter = "<?= $statusFilter !== null ? $statusFilter : '' ?>";
|
||||
|
||||
function refreshTable(){
|
||||
let url = "getTermine.php";
|
||||
if (statusFilter !== "") {
|
||||
url += "?status=" + statusFilter;
|
||||
}
|
||||
|
||||
$.get(url, function(data) {
|
||||
if (data !== text) {
|
||||
text = data;
|
||||
$('#tableHolder').html(data);
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(refreshTable, 1000);
|
||||
}
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
@@ -0,0 +1,460 @@
|
||||
<?php
|
||||
ini_set('display_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once "config/session.php";
|
||||
require_once "config/config.php";
|
||||
|
||||
if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
|
||||
header("location: index.php");
|
||||
exit;
|
||||
}
|
||||
if(($_SESSION["role"] ?? "user") !== "admin"){
|
||||
header("location: dashboard.php"); exit;
|
||||
}
|
||||
|
||||
// CSRF Token (für öffentliche Seite: MUSS)
|
||||
if (empty($_SESSION['csrf'])) {
|
||||
$_SESSION['csrf'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
include('navbar.php');
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Users • ANJUMA Dashboard</title>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.8/css/dataTables.bootstrap5.min.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container my-4">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
|
||||
<div>
|
||||
<h3 class="mb-0">Users</h3>
|
||||
<div class="text-muted small">Benutzer verwalten (Admin-only)</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" id="btnNewUser">
|
||||
<i class="bi bi-person-plus"></i> Neuer User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card-body">
|
||||
|
||||
<!-- Mobile Cards -->
|
||||
<div id="mobileCards" class="d-md-none"></div>
|
||||
|
||||
<!-- Desktop Table -->
|
||||
<div class="table-responsive d-none d-md-block">
|
||||
<table id="tblUsers" class="table table-hover align-middle w-100 mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th>First Login</th>
|
||||
<th>Last Login</th>
|
||||
<th style="width:260px;">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1080;">
|
||||
<div id="appToast" class="toast align-items-center text-bg-dark border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body" id="toastBody">…</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Modal -->
|
||||
<div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<form class="modal-content" id="userForm">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="userModalTitle">User</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="u_id" value="">
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-1">Name *</label>
|
||||
<input class="form-control" id="u_name" required maxlength="32" placeholder="z.B. Justin">
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-1">Username *</label>
|
||||
<input class="form-control" id="u_username" required maxlength="32" placeholder="z.B. justin">
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-1">Role</label>
|
||||
<select class="form-select" id="u_role">
|
||||
<option value="user">user</option>
|
||||
<option value="admin">admin</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label mb-1">Passwort (optional)</label>
|
||||
<input class="form-control" id="u_password" type="password" placeholder="leer lassen = First Login setzt User selbst">
|
||||
<div class="text-muted small mt-1">
|
||||
Wenn leer: <b>first_login=1</b> und User setzt sein Passwort beim ersten Login.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-secondary small mb-0">
|
||||
Tipp: Passwort resetten geht auch über den Button in der Liste.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-success" id="btnSaveUser">
|
||||
<i class="bi bi-check2"></i> Speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Delete Modal -->
|
||||
<div class="modal fade" id="confirmDeleteModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-danger text-white">
|
||||
<h5 class="modal-title">User löschen?</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div>Willst du diesen User wirklich löschen?</div>
|
||||
<div class="text-muted small mt-2" id="delInfo"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button class="btn btn-danger" id="btnConfirmDelete"><i class="bi bi-trash"></i> Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/datatables.net@1.13.8/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.13.8/js/dataTables.bootstrap5.min.js"></script>
|
||||
|
||||
<script>
|
||||
const API_LIST = '/api/users_list.php';
|
||||
const API_SAVE = '/api/users_save.php';
|
||||
const API_DEL = '/api/users_delete.php';
|
||||
const API_RESET = '/api/users_reset_pw.php';
|
||||
const API_LOGOUTALL = '/api/users_logout_all.php';
|
||||
|
||||
const CSRF = <?= json_encode($_SESSION['csrf']) ?>;
|
||||
const meId = <?= (int)($_SESSION['id'] ?? 0) ?>;
|
||||
|
||||
const userModal = new bootstrap.Modal(document.getElementById('userModal'));
|
||||
const delModal = new bootstrap.Modal(document.getElementById('confirmDeleteModal'));
|
||||
|
||||
const toastEl = document.getElementById('appToast');
|
||||
const toastBody = document.getElementById('toastBody');
|
||||
const toast = new bootstrap.Toast(toastEl, { delay: 2600 });
|
||||
|
||||
function showToast(msg){
|
||||
toastBody.textContent = msg;
|
||||
toast.show();
|
||||
}
|
||||
|
||||
let dt = null;
|
||||
let deleteId = null;
|
||||
let deleteLabel = '';
|
||||
let cacheUsers = []; // ✅ kein reload bei edit click
|
||||
|
||||
function fmtTs(ts){
|
||||
ts = parseInt(ts || 0, 10);
|
||||
if (!ts) return '—';
|
||||
const d = new Date(ts * 1000);
|
||||
const pad = n => String(n).padStart(2,'0');
|
||||
return `${pad(d.getDate())}.${pad(d.getMonth()+1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
function roleBadge(role){
|
||||
if (role === 'admin') return '<span class="badge bg-dark">admin</span>';
|
||||
return '<span class="badge bg-secondary">user</span>';
|
||||
}
|
||||
|
||||
function yesNoBadge(v){
|
||||
return String(v) === '1'
|
||||
? '<span class="badge bg-warning text-dark">yes</span>'
|
||||
: '<span class="badge bg-success">no</span>';
|
||||
}
|
||||
|
||||
function actionsHtml(u){
|
||||
const disableSelf = (parseInt(u.id,10) === meId);
|
||||
|
||||
return `
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<button class="btn btn-outline-primary actEdit" data-id="${u.id}" title="Bearbeiten">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-warning actReset" data-id="${u.id}" ${disableSelf ? 'disabled title="Nicht für dich selbst"' : 'title="Passwort resetten"'}>
|
||||
<i class="bi bi-key"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-secondary actLogoutAll" data-id="${u.id}" ${disableSelf ? 'disabled title="Nicht für dich selbst"' : 'title="Alle Geräte abmelden"'}>
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-danger actDel" data-id="${u.id}" data-label="${u.name} (${u.username})"
|
||||
${disableSelf ? 'disabled title="Du kannst dich nicht selbst löschen"' : 'title="Löschen"'}>
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function apiPost(url, formData){
|
||||
formData.append('csrf', CSRF);
|
||||
const res = await fetch(url, { method:'POST', body: formData });
|
||||
const json = await res.json().catch(() => ({}));
|
||||
if (!json || json.ok !== true) throw new Error(json?.error || 'Request failed');
|
||||
return json;
|
||||
}
|
||||
|
||||
async function loadUsers(){
|
||||
const res = await fetch(API_LIST);
|
||||
const json = await res.json();
|
||||
if (!json.ok) throw new Error(json.error || 'Load failed');
|
||||
return json.items || [];
|
||||
}
|
||||
|
||||
function renderMobileCards(items){
|
||||
const wrap = document.getElementById('mobileCards');
|
||||
if (!wrap) return;
|
||||
|
||||
if (!items.length){
|
||||
wrap.innerHTML = `<div class="text-muted">Keine User gefunden.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
wrap.innerHTML = items.map(u => `
|
||||
<div class="card mb-2">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="fw-bold">${u.name}</div>
|
||||
<div class="text-muted small">@${u.username}</div>
|
||||
</div>
|
||||
<div>${roleBadge(u.role)}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 small">
|
||||
<div>First Login: ${yesNoBadge(u.first_login)}</div>
|
||||
<div>Last Login: <span class="text-muted">${fmtTs(u.last_login)}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
${actionsHtml(u)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function initDataTable(items){
|
||||
if (dt) {
|
||||
dt.clear().rows.add(items).draw();
|
||||
return;
|
||||
}
|
||||
|
||||
dt = $('#tblUsers').DataTable({
|
||||
data: items,
|
||||
pageLength: 10,
|
||||
responsive: false,
|
||||
dom: '<"row align-items-center mb-2"<"col-md-6"l><"col-md-6 text-md-end"f>>rt<"row align-items-center mt-2"<"col-md-6"i><"col-md-6 text-md-end"p>>',
|
||||
columns: [
|
||||
{ data: 'name' },
|
||||
{ data: 'username', render: (d) => '@' + d },
|
||||
{ data: 'role', render: (d) => roleBadge(d) },
|
||||
{ data: 'first_login', render: (d) => yesNoBadge(d) },
|
||||
{ data: 'last_login', render: (d) => `<span class="text-muted">${fmtTs(d)}</span>` },
|
||||
{ data: null, orderable: false, render: (row) => actionsHtml(row) },
|
||||
],
|
||||
language: {
|
||||
search: "Suche:",
|
||||
lengthMenu: "_MENU_ pro Seite",
|
||||
info: "_START_ bis _END_ von _TOTAL_",
|
||||
paginate: { previous: "Zurück", next: "Weiter" },
|
||||
emptyTable: "Keine Daten"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function refresh(){
|
||||
cacheUsers = await loadUsers(); // ✅ einmal holen, dann lokal nutzen
|
||||
renderMobileCards(cacheUsers);
|
||||
initDataTable(cacheUsers);
|
||||
}
|
||||
|
||||
function openNew(){
|
||||
document.getElementById('userModalTitle').textContent = 'Neuer User';
|
||||
document.getElementById('u_id').value = '';
|
||||
document.getElementById('u_name').value = '';
|
||||
document.getElementById('u_username').value = '';
|
||||
document.getElementById('u_role').value = 'user';
|
||||
document.getElementById('u_password').value = '';
|
||||
userModal.show();
|
||||
}
|
||||
|
||||
function openEdit(u){
|
||||
document.getElementById('userModalTitle').textContent = 'User bearbeiten';
|
||||
document.getElementById('u_id').value = u.id;
|
||||
document.getElementById('u_name').value = u.name;
|
||||
document.getElementById('u_username').value = u.username;
|
||||
document.getElementById('u_role').value = u.role;
|
||||
document.getElementById('u_password').value = '';
|
||||
userModal.show();
|
||||
}
|
||||
|
||||
document.getElementById('btnNewUser').addEventListener('click', openNew);
|
||||
|
||||
document.getElementById('userForm').addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
const id = document.getElementById('u_id').value.trim();
|
||||
const name = document.getElementById('u_name').value.trim();
|
||||
const username = document.getElementById('u_username').value.trim();
|
||||
const role = document.getElementById('u_role').value;
|
||||
const password = document.getElementById('u_password').value;
|
||||
|
||||
const fd = new FormData();
|
||||
if (id) fd.append('id', id);
|
||||
fd.append('name', name);
|
||||
fd.append('username', username);
|
||||
fd.append('role', role);
|
||||
if (password) fd.append('password', password);
|
||||
|
||||
const btn = document.getElementById('btnSaveUser');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
await apiPost(API_SAVE, fd);
|
||||
userModal.hide();
|
||||
showToast('User gespeichert ✅');
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
showToast('Fehler: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', async (ev) => {
|
||||
const editBtn = ev.target.closest('.actEdit');
|
||||
if (editBtn){
|
||||
const id = editBtn.dataset.id;
|
||||
const u = cacheUsers.find(x => String(x.id) === String(id));
|
||||
if (u) openEdit(u);
|
||||
return;
|
||||
}
|
||||
|
||||
const delBtn = ev.target.closest('.actDel');
|
||||
if (delBtn){
|
||||
deleteId = delBtn.dataset.id;
|
||||
deleteLabel = delBtn.dataset.label || '';
|
||||
document.getElementById('delInfo').textContent = deleteLabel;
|
||||
delModal.show();
|
||||
return;
|
||||
}
|
||||
|
||||
const resetBtn = ev.target.closest('.actReset');
|
||||
if (resetBtn){
|
||||
const id = resetBtn.dataset.id;
|
||||
if (!confirm('Passwort zurücksetzen?\nUser muss beim nächsten Login ein neues Passwort setzen.\n(Optional: Wir loggen ihn auch auf allen Geräten aus)')) return;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('id', id);
|
||||
|
||||
try {
|
||||
await apiPost(API_RESET, fd);
|
||||
showToast('Passwort-Reset gesetzt 🔑');
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
showToast('Fehler: ' + e.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const loBtn = ev.target.closest('.actLogoutAll');
|
||||
if (loBtn){
|
||||
const id = loBtn.dataset.id;
|
||||
if (!confirm('Alle Geräte abmelden?\n(Das löscht alle Remember-Me Tokens dieses Users)')) return;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('id', id);
|
||||
|
||||
try {
|
||||
await apiPost(API_LOGOUTALL, fd);
|
||||
showToast('Alle Geräte abgemeldet ✅');
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
showToast('Fehler: ' + e.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btnConfirmDelete').addEventListener('click', async () => {
|
||||
if (!deleteId) return;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('id', deleteId);
|
||||
|
||||
try {
|
||||
await apiPost(API_DEL, fd);
|
||||
delModal.hide();
|
||||
deleteId = null;
|
||||
showToast('User gelöscht 🗑️');
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
showToast('Fehler: ' + e.message);
|
||||
}
|
||||
});
|
||||
|
||||
refresh().catch(e => showToast('Fehler: ' + e.message));
|
||||
</script>
|
||||
|
||||
<style>
|
||||
div.dataTables_wrapper .dataTables_length select,
|
||||
div.dataTables_wrapper .dataTables_filter input {
|
||||
border-radius: .5rem;
|
||||
border: 1px solid #dee2e6;
|
||||
padding: .375rem .5rem;
|
||||
}
|
||||
div.dataTables_wrapper .dataTables_filter input { margin-left: .5rem; }
|
||||
div.dataTables_wrapper .dataTables_length label,
|
||||
div.dataTables_wrapper .dataTables_filter label {
|
||||
font-size: .9rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
table.dataTable thead th { border-bottom: 1px solid #e9ecef !important; }
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
$err,
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInitddcff9e2cf8efd3607e533551f8363dc::getLoader();
|
||||
Vendored
+585
@@ -0,0 +1,585 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
/**
|
||||
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||
*
|
||||
* $loader = new \Composer\Autoload\ClassLoader();
|
||||
*
|
||||
* // register classes with namespaces
|
||||
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||
* $loader->add('Symfony', __DIR__.'/framework');
|
||||
*
|
||||
* // activate the autoloader
|
||||
* $loader->register();
|
||||
*
|
||||
* // to enable searching the include path (eg. for PEAR packages)
|
||||
* $loader->setUseIncludePath(true);
|
||||
*
|
||||
* In this example, if you try to use a class in the Symfony\Component
|
||||
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||
* the autoloader will first look for the class under the component/
|
||||
* directory, and it will then fallback to the framework/ directory if not
|
||||
* found before giving up.
|
||||
*
|
||||
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @see https://www.php-fig.org/psr/psr-0/
|
||||
* @see https://www.php-fig.org/psr/psr-4/
|
||||
*/
|
||||
class ClassLoader
|
||||
{
|
||||
/** @var \Closure(string):void */
|
||||
private static $includeFile;
|
||||
|
||||
/** @var ?string */
|
||||
private $vendorDir;
|
||||
|
||||
// PSR-4
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array<string, int>>
|
||||
*/
|
||||
private $prefixLengthsPsr4 = array();
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array<int, string>>
|
||||
*/
|
||||
private $prefixDirsPsr4 = array();
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, string>
|
||||
*/
|
||||
private $fallbackDirsPsr4 = array();
|
||||
|
||||
// PSR-0
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array<string, string[]>>
|
||||
*/
|
||||
private $prefixesPsr0 = array();
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, string>
|
||||
*/
|
||||
private $fallbackDirsPsr0 = array();
|
||||
|
||||
/** @var bool */
|
||||
private $useIncludePath = false;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
* @psalm-var array<string, string>
|
||||
*/
|
||||
private $classMap = array();
|
||||
|
||||
/** @var bool */
|
||||
private $classMapAuthoritative = false;
|
||||
|
||||
/**
|
||||
* @var bool[]
|
||||
* @psalm-var array<string, bool>
|
||||
*/
|
||||
private $missingClasses = array();
|
||||
|
||||
/** @var ?string */
|
||||
private $apcuPrefix;
|
||||
|
||||
/**
|
||||
* @var self[]
|
||||
*/
|
||||
private static $registeredLoaders = array();
|
||||
|
||||
/**
|
||||
* @param ?string $vendorDir
|
||||
*/
|
||||
public function __construct($vendorDir = null)
|
||||
{
|
||||
$this->vendorDir = $vendorDir;
|
||||
self::initializeIncludeClosure();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPrefixes()
|
||||
{
|
||||
if (!empty($this->prefixesPsr0)) {
|
||||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return array<string, array<int, string>>
|
||||
*/
|
||||
public function getPrefixesPsr4()
|
||||
{
|
||||
return $this->prefixDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return array<string, string>
|
||||
*/
|
||||
public function getFallbackDirs()
|
||||
{
|
||||
return $this->fallbackDirsPsr0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return array<string, string>
|
||||
*/
|
||||
public function getFallbackDirsPsr4()
|
||||
{
|
||||
return $this->fallbackDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[] Array of classname => path
|
||||
* @psalm-return array<string, string>
|
||||
*/
|
||||
public function getClassMap()
|
||||
{
|
||||
return $this->classMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $classMap Class to filename map
|
||||
* @psalm-param array<string, string> $classMap
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addClassMap(array $classMap)
|
||||
{
|
||||
if ($this->classMap) {
|
||||
$this->classMap = array_merge($this->classMap, $classMap);
|
||||
} else {
|
||||
$this->classMap = $classMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix, either
|
||||
* appending or prepending to the ones previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param string[]|string $paths The PSR-0 root directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add($prefix, $paths, $prepend = false)
|
||||
{
|
||||
if (!$prefix) {
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
(array) $paths,
|
||||
$this->fallbackDirsPsr0
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$this->fallbackDirsPsr0,
|
||||
(array) $paths
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$first = $prefix[0];
|
||||
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($prepend) {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
(array) $paths,
|
||||
$this->prefixesPsr0[$first][$prefix]
|
||||
);
|
||||
} else {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$this->prefixesPsr0[$first][$prefix],
|
||||
(array) $paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace, either
|
||||
* appending or prepending to the ones previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param string[]|string $paths The PSR-4 base directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addPsr4($prefix, $paths, $prepend = false)
|
||||
{
|
||||
if (!$prefix) {
|
||||
// Register directories for the root namespace.
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
(array) $paths,
|
||||
$this->fallbackDirsPsr4
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$this->fallbackDirsPsr4,
|
||||
(array) $paths
|
||||
);
|
||||
}
|
||||
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||
// Register directories for a new namespace.
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
} elseif ($prepend) {
|
||||
// Prepend directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
(array) $paths,
|
||||
$this->prefixDirsPsr4[$prefix]
|
||||
);
|
||||
} else {
|
||||
// Append directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$this->prefixDirsPsr4[$prefix],
|
||||
(array) $paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix,
|
||||
* replacing any others previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param string[]|string $paths The PSR-0 base directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr0 = (array) $paths;
|
||||
} else {
|
||||
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace,
|
||||
* replacing any others previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param string[]|string $paths The PSR-4 base directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPsr4($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr4 = (array) $paths;
|
||||
} else {
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on searching the include path for class files.
|
||||
*
|
||||
* @param bool $useIncludePath
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUseIncludePath($useIncludePath)
|
||||
{
|
||||
$this->useIncludePath = $useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to check if the autoloader uses the include path to check
|
||||
* for classes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getUseIncludePath()
|
||||
{
|
||||
return $this->useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off searching the prefix and fallback directories for classes
|
||||
* that have not been registered with the class map.
|
||||
*
|
||||
* @param bool $classMapAuthoritative
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||
{
|
||||
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should class lookup fail if not found in the current class map?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isClassMapAuthoritative()
|
||||
{
|
||||
return $this->classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||
*
|
||||
* @param string|null $apcuPrefix
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setApcuPrefix($apcuPrefix)
|
||||
{
|
||||
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getApcuPrefix()
|
||||
{
|
||||
return $this->apcuPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers this instance as an autoloader.
|
||||
*
|
||||
* @param bool $prepend Whether to prepend the autoloader or not
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register($prepend = false)
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||
|
||||
if (null === $this->vendorDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($prepend) {
|
||||
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||
} else {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this instance as an autoloader.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unregister()
|
||||
{
|
||||
spl_autoload_unregister(array($this, 'loadClass'));
|
||||
|
||||
if (null !== $this->vendorDir) {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given class or interface.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
* @return true|null True if loaded, null otherwise
|
||||
*/
|
||||
public function loadClass($class)
|
||||
{
|
||||
if ($file = $this->findFile($class)) {
|
||||
$includeFile = self::$includeFile;
|
||||
$includeFile($file);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the file where the class is defined.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
*
|
||||
* @return string|false The path if found, false otherwise
|
||||
*/
|
||||
public function findFile($class)
|
||||
{
|
||||
// class map lookup
|
||||
if (isset($this->classMap[$class])) {
|
||||
return $this->classMap[$class];
|
||||
}
|
||||
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||
return false;
|
||||
}
|
||||
if (null !== $this->apcuPrefix) {
|
||||
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||
if ($hit) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
$file = $this->findFileWithExtension($class, '.php');
|
||||
|
||||
// Search for Hack files if we are running on HHVM
|
||||
if (false === $file && defined('HHVM_VERSION')) {
|
||||
$file = $this->findFileWithExtension($class, '.hh');
|
||||
}
|
||||
|
||||
if (null !== $this->apcuPrefix) {
|
||||
apcu_add($this->apcuPrefix.$class, $file);
|
||||
}
|
||||
|
||||
if (false === $file) {
|
||||
// Remember that this class does not exist.
|
||||
$this->missingClasses[$class] = true;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently registered loaders indexed by their corresponding vendor directories.
|
||||
*
|
||||
* @return self[]
|
||||
*/
|
||||
public static function getRegisteredLoaders()
|
||||
{
|
||||
return self::$registeredLoaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $ext
|
||||
* @return string|false
|
||||
*/
|
||||
private function findFileWithExtension($class, $ext)
|
||||
{
|
||||
// PSR-4 lookup
|
||||
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||
|
||||
$first = $class[0];
|
||||
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||
$subPath = $class;
|
||||
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||
$subPath = substr($subPath, 0, $lastPos);
|
||||
$search = $subPath . '\\';
|
||||
if (isset($this->prefixDirsPsr4[$search])) {
|
||||
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||
if (file_exists($file = $dir . $pathEnd)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-4 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 lookup
|
||||
if (false !== $pos = strrpos($class, '\\')) {
|
||||
// namespaced class name
|
||||
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||
} else {
|
||||
// PEAR-like class name
|
||||
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||
}
|
||||
|
||||
if (isset($this->prefixesPsr0[$first])) {
|
||||
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||
if (0 === strpos($class, $prefix)) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 include paths.
|
||||
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private static function initializeIncludeClosure()
|
||||
{
|
||||
if (self::$includeFile !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope isolated include.
|
||||
*
|
||||
* Prevents access to $this/self from included files.
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
self::$includeFile = \Closure::bind(static function($file) {
|
||||
include $file;
|
||||
}, null, null);
|
||||
}
|
||||
}
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
/**
|
||||
* This class is copied in every Composer installed project and available to all
|
||||
*
|
||||
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||
*
|
||||
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class InstalledVersions
|
||||
{
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private static $canGetVendors;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static $installedByVendor = array();
|
||||
|
||||
/**
|
||||
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||
*
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackages()
|
||||
{
|
||||
$packages = array();
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
$packages[] = array_keys($installed['versions']);
|
||||
}
|
||||
|
||||
if (1 === \count($packages)) {
|
||||
return $packages[0];
|
||||
}
|
||||
|
||||
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all package names with a specific type e.g. 'library'
|
||||
*
|
||||
* @param string $type
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackagesByType($type)
|
||||
{
|
||||
$packagesByType = array();
|
||||
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
foreach ($installed['versions'] as $name => $package) {
|
||||
if (isset($package['type']) && $package['type'] === $type) {
|
||||
$packagesByType[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $packagesByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package is installed
|
||||
*
|
||||
* This also returns true if the package name is provided or replaced by another package
|
||||
*
|
||||
* @param string $packageName
|
||||
* @param bool $includeDevRequirements
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (isset($installed['versions'][$packageName])) {
|
||||
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package satisfies a version constraint
|
||||
*
|
||||
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||
*
|
||||
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||
*
|
||||
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||
* @param string $packageName
|
||||
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||
* @return bool
|
||||
*/
|
||||
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||
{
|
||||
$constraint = $parser->parseConstraints((string) $constraint);
|
||||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||
|
||||
return $provided->matches($constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||
*
|
||||
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||
* whether a given version of a package is installed, and not just whether it exists
|
||||
*
|
||||
* @param string $packageName
|
||||
* @return string Version constraint usable with composer/semver
|
||||
*/
|
||||
public static function getVersionRanges($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ranges = array();
|
||||
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||
}
|
||||
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||
}
|
||||
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||
}
|
||||
|
||||
return implode(' || ', $ranges);
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getPrettyVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||
*/
|
||||
public static function getReference($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['reference'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||
*/
|
||||
public static function getInstallPath($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||
*/
|
||||
public static function getRootPackage()
|
||||
{
|
||||
$installed = self::getInstalled();
|
||||
|
||||
return $installed[0]['root'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw installed.php data for custom implementations
|
||||
*
|
||||
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||
* @return array[]
|
||||
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
||||
*/
|
||||
public static function getRawData()
|
||||
{
|
||||
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = include __DIR__ . '/installed.php';
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
return self::$installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||
*
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
public static function getAllRawData()
|
||||
{
|
||||
return self::getInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets you reload the static array from another file
|
||||
*
|
||||
* This is only useful for complex integrations in which a project needs to use
|
||||
* this class but then also needs to execute another project's autoloader in process,
|
||||
* and wants to ensure both projects have access to their version of installed.php.
|
||||
*
|
||||
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||
* the data it needs from this class, then call reload() with
|
||||
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||
* the project in which it runs can then also use this class safely, without
|
||||
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||
*
|
||||
* @param array[] $data A vendor/composer/installed.php data set
|
||||
* @return void
|
||||
*
|
||||
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||
*/
|
||||
public static function reload($data)
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static function getInstalled()
|
||||
{
|
||||
if (null === self::$canGetVendors) {
|
||||
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||
}
|
||||
|
||||
$installed = array();
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
$installed[] = self::$installedByVendor[$vendorDir] = $required;
|
||||
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
|
||||
self::$installed = $installed[count($installed) - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require __DIR__ . '/installed.php';
|
||||
self::$installed = $required;
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$installed !== array()) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
// autoload_classmap.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
'Dompdf\\Cpdf' => $vendorDir . '/dompdf/dompdf/lib/Cpdf.php',
|
||||
);
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
);
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Svg\\' => array($vendorDir . '/phenx/php-svg-lib/src/Svg'),
|
||||
'Sabberworm\\CSS\\' => array($vendorDir . '/sabberworm/php-css-parser/src'),
|
||||
'Masterminds\\' => array($vendorDir . '/masterminds/html5/src'),
|
||||
'FontLib\\' => array($vendorDir . '/phenx/php-font-lib/src/FontLib'),
|
||||
'Dompdf\\' => array($vendorDir . '/dompdf/dompdf/src'),
|
||||
);
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInitddcff9e2cf8efd3607e533551f8363dc
|
||||
{
|
||||
private static $loader;
|
||||
|
||||
public static function loadClassLoader($class)
|
||||
{
|
||||
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||
require __DIR__ . '/ClassLoader.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Composer\Autoload\ClassLoader
|
||||
*/
|
||||
public static function getLoader()
|
||||
{
|
||||
if (null !== self::$loader) {
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
require __DIR__ . '/platform_check.php';
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInitddcff9e2cf8efd3607e533551f8363dc', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInitddcff9e2cf8efd3607e533551f8363dc', 'loadClassLoader'));
|
||||
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInitddcff9e2cf8efd3607e533551f8363dc::getInitializer($loader));
|
||||
|
||||
$loader->register(true);
|
||||
|
||||
return $loader;
|
||||
}
|
||||
}
|
||||
Vendored
+66
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
// autoload_static.php @generated by Composer
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
class ComposerStaticInitddcff9e2cf8efd3607e533551f8363dc
|
||||
{
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
'S' =>
|
||||
array (
|
||||
'Svg\\' => 4,
|
||||
'Sabberworm\\CSS\\' => 15,
|
||||
),
|
||||
'M' =>
|
||||
array (
|
||||
'Masterminds\\' => 12,
|
||||
),
|
||||
'F' =>
|
||||
array (
|
||||
'FontLib\\' => 8,
|
||||
),
|
||||
'D' =>
|
||||
array (
|
||||
'Dompdf\\' => 7,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'Svg\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/phenx/php-svg-lib/src/Svg',
|
||||
),
|
||||
'Sabberworm\\CSS\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/sabberworm/php-css-parser/src',
|
||||
),
|
||||
'Masterminds\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/masterminds/html5/src',
|
||||
),
|
||||
'FontLib\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/phenx/php-font-lib/src/FontLib',
|
||||
),
|
||||
'Dompdf\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/dompdf/dompdf/src',
|
||||
),
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
'Dompdf\\Cpdf' => __DIR__ . '/..' . '/dompdf/dompdf/lib/Cpdf.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
{
|
||||
return \Closure::bind(function () use ($loader) {
|
||||
$loader->prefixLengthsPsr4 = ComposerStaticInitddcff9e2cf8efd3607e533551f8363dc::$prefixLengthsPsr4;
|
||||
$loader->prefixDirsPsr4 = ComposerStaticInitddcff9e2cf8efd3607e533551f8363dc::$prefixDirsPsr4;
|
||||
$loader->classMap = ComposerStaticInitddcff9e2cf8efd3607e533551f8363dc::$classMap;
|
||||
|
||||
}, null, ClassLoader::class);
|
||||
}
|
||||
}
|
||||
Vendored
+306
@@ -0,0 +1,306 @@
|
||||
{
|
||||
"packages": [
|
||||
{
|
||||
"name": "dompdf/dompdf",
|
||||
"version": "v2.0.8",
|
||||
"version_normalized": "2.0.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/dompdf.git",
|
||||
"reference": "c20247574601700e1f7c8dab39310fca1964dc52"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/dompdf/zipball/c20247574601700e1f7c8dab39310fca1964dc52",
|
||||
"reference": "c20247574601700e1f7c8dab39310fca1964dc52",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-mbstring": "*",
|
||||
"masterminds/html5": "^2.0",
|
||||
"phenx/php-font-lib": ">=0.5.4 <1.0.0",
|
||||
"phenx/php-svg-lib": ">=0.5.2 <1.0.0",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"ext-zip": "*",
|
||||
"mockery/mockery": "^1.3",
|
||||
"phpunit/phpunit": "^7.5 || ^8 || ^9",
|
||||
"squizlabs/php_codesniffer": "^3.5"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-gd": "Needed to process images",
|
||||
"ext-gmagick": "Improves image processing performance",
|
||||
"ext-imagick": "Improves image processing performance",
|
||||
"ext-zlib": "Needed for pdf stream compression"
|
||||
},
|
||||
"time": "2024-04-29T13:06:17+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dompdf\\": "src/"
|
||||
},
|
||||
"classmap": [
|
||||
"lib/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "The Dompdf Community",
|
||||
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
|
||||
}
|
||||
],
|
||||
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
|
||||
"homepage": "https://github.com/dompdf/dompdf",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/dompdf/issues",
|
||||
"source": "https://github.com/dompdf/dompdf/tree/v2.0.8"
|
||||
},
|
||||
"install-path": "../dompdf/dompdf"
|
||||
},
|
||||
{
|
||||
"name": "masterminds/html5",
|
||||
"version": "2.10.0",
|
||||
"version_normalized": "2.10.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Masterminds/html5-php.git",
|
||||
"reference": "fcf91eb64359852f00d921887b219479b4f21251"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
|
||||
"reference": "fcf91eb64359852f00d921887b219479b4f21251",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
|
||||
},
|
||||
"time": "2025-07-25T09:04:22+00:00",
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.7-dev"
|
||||
}
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Masterminds\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Matt Butcher",
|
||||
"email": "technosophos@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Matt Farina",
|
||||
"email": "matt@mattfarina.com"
|
||||
},
|
||||
{
|
||||
"name": "Asmir Mustafic",
|
||||
"email": "goetas@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "An HTML5 parser and serializer.",
|
||||
"homepage": "http://masterminds.github.io/html5-php",
|
||||
"keywords": [
|
||||
"HTML5",
|
||||
"dom",
|
||||
"html",
|
||||
"parser",
|
||||
"querypath",
|
||||
"serializer",
|
||||
"xml"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Masterminds/html5-php/issues",
|
||||
"source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
|
||||
},
|
||||
"install-path": "../masterminds/html5"
|
||||
},
|
||||
{
|
||||
"name": "phenx/php-font-lib",
|
||||
"version": "0.5.6",
|
||||
"version_normalized": "0.5.6.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/php-font-lib.git",
|
||||
"reference": "a1681e9793040740a405ac5b189275059e2a9863"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a1681e9793040740a405ac5b189275059e2a9863",
|
||||
"reference": "a1681e9793040740a405ac5b189275059e2a9863",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6"
|
||||
},
|
||||
"time": "2024-01-29T14:45:26+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"FontLib\\": "src/FontLib"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Ménager",
|
||||
"email": "fabien.menager@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A library to read, parse, export and make subsets of different types of font files.",
|
||||
"homepage": "https://github.com/PhenX/php-font-lib",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/php-font-lib/issues",
|
||||
"source": "https://github.com/dompdf/php-font-lib/tree/0.5.6"
|
||||
},
|
||||
"install-path": "../phenx/php-font-lib"
|
||||
},
|
||||
{
|
||||
"name": "phenx/php-svg-lib",
|
||||
"version": "0.5.4",
|
||||
"version_normalized": "0.5.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dompdf/php-svg-lib.git",
|
||||
"reference": "46b25da81613a9cf43c83b2a8c2c1bdab27df691"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/46b25da81613a9cf43c83b2a8c2c1bdab27df691",
|
||||
"reference": "46b25da81613a9cf43c83b2a8c2c1bdab27df691",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-mbstring": "*",
|
||||
"php": "^7.1 || ^8.0",
|
||||
"sabberworm/php-css-parser": "^8.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5"
|
||||
},
|
||||
"time": "2024-04-08T12:52:34+00:00",
|
||||
"type": "library",
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Svg\\": "src/Svg"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-3.0-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Ménager",
|
||||
"email": "fabien.menager@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "A library to read, parse and export to PDF SVG files.",
|
||||
"homepage": "https://github.com/PhenX/php-svg-lib",
|
||||
"support": {
|
||||
"issues": "https://github.com/dompdf/php-svg-lib/issues",
|
||||
"source": "https://github.com/dompdf/php-svg-lib/tree/0.5.4"
|
||||
},
|
||||
"install-path": "../phenx/php-svg-lib"
|
||||
},
|
||||
{
|
||||
"name": "sabberworm/php-css-parser",
|
||||
"version": "v8.9.0",
|
||||
"version_normalized": "8.9.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
|
||||
"reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9",
|
||||
"reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-iconv": "*",
|
||||
"php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41",
|
||||
"rawr/cross-data-providers": "^2.0.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-mbstring": "for parsing UTF-8 CSS"
|
||||
},
|
||||
"time": "2025-07-11T13:20:48+00:00",
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "9.0.x-dev"
|
||||
}
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Sabberworm\\CSS\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Raphael Schweikert"
|
||||
},
|
||||
{
|
||||
"name": "Oliver Klee",
|
||||
"email": "github@oliverklee.de"
|
||||
},
|
||||
{
|
||||
"name": "Jake Hotson",
|
||||
"email": "jake.github@qzdesign.co.uk"
|
||||
}
|
||||
],
|
||||
"description": "Parser for CSS Files written in PHP",
|
||||
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
|
||||
"keywords": [
|
||||
"css",
|
||||
"parser",
|
||||
"stylesheet"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
|
||||
"source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0"
|
||||
},
|
||||
"install-path": "../sabberworm/php-css-parser"
|
||||
}
|
||||
],
|
||||
"dev": true,
|
||||
"dev-package-names": []
|
||||
}
|
||||
Vendored
+68
@@ -0,0 +1,68 @@
|
||||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => '__root__',
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev' => true,
|
||||
),
|
||||
'versions' => array(
|
||||
'__root__' => array(
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'dompdf/dompdf' => array(
|
||||
'pretty_version' => 'v2.0.8',
|
||||
'version' => '2.0.8.0',
|
||||
'reference' => 'c20247574601700e1f7c8dab39310fca1964dc52',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../dompdf/dompdf',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'masterminds/html5' => array(
|
||||
'pretty_version' => '2.10.0',
|
||||
'version' => '2.10.0.0',
|
||||
'reference' => 'fcf91eb64359852f00d921887b219479b4f21251',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../masterminds/html5',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'phenx/php-font-lib' => array(
|
||||
'pretty_version' => '0.5.6',
|
||||
'version' => '0.5.6.0',
|
||||
'reference' => 'a1681e9793040740a405ac5b189275059e2a9863',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phenx/php-font-lib',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'phenx/php-svg-lib' => array(
|
||||
'pretty_version' => '0.5.4',
|
||||
'version' => '0.5.4.0',
|
||||
'reference' => '46b25da81613a9cf43c83b2a8c2c1bdab27df691',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phenx/php-svg-lib',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'sabberworm/php-css-parser' => array(
|
||||
'pretty_version' => 'v8.9.0',
|
||||
'version' => '8.9.0.0',
|
||||
'reference' => 'd8e916507b88e389e26d4ab03c904a082aa66bb9',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../sabberworm/php-css-parser',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
);
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
// platform_check.php @generated by Composer
|
||||
|
||||
$issues = array();
|
||||
|
||||
if (!(PHP_VERSION_ID >= 70100)) {
|
||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.1.0". You are running ' . PHP_VERSION . '.';
|
||||
}
|
||||
|
||||
if ($issues) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
|
||||
} elseif (!headers_sent()) {
|
||||
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||
}
|
||||
}
|
||||
trigger_error(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues),
|
||||
E_USER_ERROR
|
||||
);
|
||||
}
|
||||
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
Dompdf was designed and developed by Benj Carson.
|
||||
|
||||
### Current Team
|
||||
|
||||
* **Brian Sweeney** (maintainer)
|
||||
* **Till Berger**
|
||||
|
||||
### Alumni
|
||||
|
||||
* **Benj Carson** (creator)
|
||||
* **Fabien Ménager**
|
||||
* **Simon Berger**
|
||||
* **Orion Richardson**
|
||||
|
||||
### Contributors
|
||||
* **Gabriel Bull**
|
||||
* **Barry vd. Heuvel**
|
||||
* **Ryan H. Masten**
|
||||
* **Helmut Tischer**
|
||||
* [and many more...](https://github.com/dompdf/dompdf/graphs/contributors)
|
||||
|
||||
### Thanks
|
||||
|
||||
Dompdf would not have been possible without strong community support.
|
||||
Vendored
+456
@@ -0,0 +1,456 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
Vendored
+232
@@ -0,0 +1,232 @@
|
||||
Dompdf
|
||||
======
|
||||
|
||||
[](https://github.com/dompdf/dompdf/actions/workflows/test.yml)
|
||||
[](https://packagist.org/packages/dompdf/dompdf)
|
||||
[](https://packagist.org/packages/dompdf/dompdf)
|
||||
[](https://packagist.org/packages/dompdf/dompdf)
|
||||
|
||||
**Dompdf is an HTML to PDF converter**
|
||||
|
||||
At its heart, dompdf is (mostly) a [CSS 2.1](http://www.w3.org/TR/CSS2/) compliant
|
||||
HTML layout and rendering engine written in PHP. It is a style-driven renderer:
|
||||
it will download and read external stylesheets, inline style tags, and the style
|
||||
attributes of individual HTML elements. It also supports most presentational
|
||||
HTML attributes.
|
||||
|
||||
*This document applies to the latest stable code which may not reflect the current
|
||||
release. For released code please
|
||||
[navigate to the appropriate tag](https://github.com/dompdf/dompdf/tags).*
|
||||
|
||||
----
|
||||
|
||||
**Check out the [demo](http://eclecticgeek.com/dompdf/debug.php) and ask any
|
||||
question on [StackOverflow](https://stackoverflow.com/questions/tagged/dompdf) or
|
||||
in [Discussions](https://github.com/dompdf/dompdf/discussions).**
|
||||
|
||||
Follow us on [](http://www.twitter.com/dompdf).
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* Handles most CSS 2.1 and a few CSS3 properties, including @import, @media &
|
||||
@page rules
|
||||
* Supports most presentational HTML 4.0 attributes
|
||||
* Supports external stylesheets, either local or through http/ftp (via
|
||||
fopen-wrappers)
|
||||
* Supports complex tables, including row & column spans, separate & collapsed
|
||||
border models, individual cell styling
|
||||
* Image support (gif, png (8, 24 and 32 bit with alpha channel), bmp & jpeg)
|
||||
* No dependencies on external PDF libraries, thanks to the R&OS PDF class
|
||||
* Inline PHP support
|
||||
* Basic SVG support (see "Limitations" below)
|
||||
|
||||
## Requirements
|
||||
|
||||
* PHP version 7.1 or higher
|
||||
* DOM extension
|
||||
* MBString extension
|
||||
* php-font-lib
|
||||
* php-svg-lib
|
||||
|
||||
Note that some required dependencies may have further dependencies
|
||||
(notably php-svg-lib requires sabberworm/php-css-parser).
|
||||
|
||||
### Recommendations
|
||||
|
||||
* OPcache (OPcache, XCache, APC, etc.): improves performance
|
||||
* GD (for image processing)
|
||||
* IMagick or GMagick extension: improves image processing performance
|
||||
|
||||
Visit the wiki for more information:
|
||||
https://github.com/dompdf/dompdf/wiki/Requirements
|
||||
|
||||
## About Fonts & Character Encoding
|
||||
|
||||
PDF documents internally support the following fonts: Helvetica, Times-Roman,
|
||||
Courier, Zapf-Dingbats, & Symbol. These fonts only support Windows ANSI
|
||||
encoding. In order for a PDF to display characters that are not available in
|
||||
Windows ANSI, you must supply an external font. Dompdf will embed any referenced
|
||||
font in the PDF so long as it has been pre-loaded or is accessible to dompdf and
|
||||
reference in CSS @font-face rules. See the
|
||||
[font overview](https://github.com/dompdf/dompdf/wiki/About-Fonts-and-Character-Encoding)
|
||||
for more information on how to use fonts.
|
||||
|
||||
The [DejaVu TrueType fonts](https://dejavu-fonts.github.io/) have been pre-installed
|
||||
to give dompdf decent Unicode character coverage by default. To use the DejaVu
|
||||
fonts reference the font in your stylesheet, e.g. `body { font-family: DejaVu
|
||||
Sans; }` (for DejaVu Sans). The following DejaVu 2.34 fonts are available:
|
||||
DejaVu Sans, DejaVu Serif, and DejaVu Sans Mono.
|
||||
|
||||
## Easy Installation
|
||||
|
||||
### Install with composer
|
||||
|
||||
To install with [Composer](https://getcomposer.org/), simply require the
|
||||
latest version of this package.
|
||||
|
||||
```bash
|
||||
composer require dompdf/dompdf
|
||||
```
|
||||
|
||||
Make sure that the autoload file from Composer is loaded.
|
||||
|
||||
```php
|
||||
// somewhere early in your project's loading, require the Composer autoloader
|
||||
// see: http://getcomposer.org/doc/00-intro.md
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
```
|
||||
|
||||
### Download and install
|
||||
|
||||
Download a packaged archive of dompdf and extract it into the
|
||||
directory where dompdf will reside
|
||||
|
||||
* You can download stable copies of dompdf from
|
||||
https://github.com/dompdf/dompdf/releases
|
||||
* Or download a nightly (the latest, unreleased code) from
|
||||
http://eclecticgeek.com/dompdf
|
||||
|
||||
Use the packaged release autoloader to load dompdf, libraries,
|
||||
and helper functions in your PHP:
|
||||
|
||||
```php
|
||||
// include autoloader
|
||||
require_once 'dompdf/autoload.inc.php';
|
||||
```
|
||||
|
||||
Note: packaged releases are named according using semantic
|
||||
versioning (_dompdf_MAJOR-MINOR-PATCH.zip_). So the 1.0.0
|
||||
release would be dompdf_1-0-0.zip. This is the only download
|
||||
that includes the autoloader for Dompdf and all its dependencies.
|
||||
|
||||
### Install with git
|
||||
|
||||
From the command line, switch to the directory where dompdf will
|
||||
reside and run the following commands:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/dompdf/dompdf.git
|
||||
cd dompdf/lib
|
||||
|
||||
git clone https://github.com/PhenX/php-font-lib.git php-font-lib
|
||||
cd php-font-lib
|
||||
git checkout 0.5.1
|
||||
cd ..
|
||||
|
||||
git clone https://github.com/PhenX/php-svg-lib.git php-svg-lib
|
||||
cd php-svg-lib
|
||||
git checkout v0.3.2
|
||||
cd ..
|
||||
|
||||
git clone https://github.com/sabberworm/PHP-CSS-Parser.git php-css-parser
|
||||
cd php-css-parser
|
||||
git checkout 8.1.0
|
||||
```
|
||||
|
||||
Require dompdf and it's dependencies in your PHP.
|
||||
For details see the [autoloader in the utils project](https://github.com/dompdf/utils/blob/master/autoload.inc.php).
|
||||
|
||||
## Quick Start
|
||||
|
||||
Just pass your HTML in to dompdf and stream the output:
|
||||
|
||||
```php
|
||||
// reference the Dompdf namespace
|
||||
use Dompdf\Dompdf;
|
||||
|
||||
// instantiate and use the dompdf class
|
||||
$dompdf = new Dompdf();
|
||||
$dompdf->loadHtml('hello world');
|
||||
|
||||
// (Optional) Setup the paper size and orientation
|
||||
$dompdf->setPaper('A4', 'landscape');
|
||||
|
||||
// Render the HTML as PDF
|
||||
$dompdf->render();
|
||||
|
||||
// Output the generated PDF to Browser
|
||||
$dompdf->stream();
|
||||
```
|
||||
|
||||
### Setting Options
|
||||
|
||||
Set options during dompdf instantiation:
|
||||
|
||||
```php
|
||||
use Dompdf\Dompdf;
|
||||
use Dompdf\Options;
|
||||
|
||||
$options = new Options();
|
||||
$options->set('defaultFont', 'Courier');
|
||||
$dompdf = new Dompdf($options);
|
||||
```
|
||||
|
||||
or at run time
|
||||
|
||||
```php
|
||||
use Dompdf\Dompdf;
|
||||
|
||||
$dompdf = new Dompdf();
|
||||
$options = $dompdf->getOptions();
|
||||
$options->setDefaultFont('Courier');
|
||||
$dompdf->setOptions($options);
|
||||
```
|
||||
|
||||
See [Dompdf\Options](src/Options.php) for a list of available options.
|
||||
|
||||
### Resource Reference Requirements
|
||||
|
||||
In order to protect potentially sensitive information Dompdf imposes
|
||||
restrictions on files referenced from the local file system or the web.
|
||||
|
||||
Files accessed through web-based protocols have the following requirements:
|
||||
* The Dompdf option "isRemoteEnabled" must be set to "true"
|
||||
* PHP must either have the curl extension enabled or the
|
||||
allow_url_fopen setting set to true
|
||||
|
||||
Files accessed through the local file system have the following requirement:
|
||||
* The file must fall within the path(s) specified for the Dompdf "chroot" option
|
||||
|
||||
## Limitations (Known Issues)
|
||||
|
||||
* Table cells are not pageable, meaning a table row must fit on a single page.
|
||||
* Elements are rendered on the active page when they are parsed.
|
||||
* Embedding "raw" SVG's (`<svg><path...></svg>`) isn't working yet, you need to
|
||||
either link to an external SVG file, or use a DataURI like this:
|
||||
```php
|
||||
$html = '<img src="data:image/svg+xml;base64,' . base64_encode($svg) . '" ...>';
|
||||
```
|
||||
Watch https://github.com/dompdf/dompdf/issues/320 for progress
|
||||
* Does not support CSS flexbox.
|
||||
* Does not support CSS Grid.
|
||||
---
|
||||
|
||||
[](http://goo.gl/DSvWf)
|
||||
|
||||
*If you find this project useful, please consider making a donation.
|
||||
Any funds donated will be used to help further development on this project.)*
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
2.0.8
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "dompdf/dompdf",
|
||||
"type": "library",
|
||||
"description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
|
||||
"homepage": "https://github.com/dompdf/dompdf",
|
||||
"license": "LGPL-2.1",
|
||||
"authors": [
|
||||
{
|
||||
"name": "The Dompdf Community",
|
||||
"homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dompdf\\": "src/"
|
||||
},
|
||||
"classmap": [
|
||||
"lib/"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Dompdf\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1 || ^8.0",
|
||||
"ext-dom": "*",
|
||||
"ext-mbstring": "*",
|
||||
"masterminds/html5": "^2.0",
|
||||
"phenx/php-font-lib": ">=0.5.4 <1.0.0",
|
||||
"phenx/php-svg-lib": ">=0.5.2 <1.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-json": "*",
|
||||
"ext-zip": "*",
|
||||
"phpunit/phpunit": "^7.5 || ^8 || ^9",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"mockery/mockery": "^1.3"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-gd": "Needed to process images",
|
||||
"ext-imagick": "Improves image processing performance",
|
||||
"ext-gmagick": "Improves image processing performance",
|
||||
"ext-zlib": "Needed for pdf stream compression"
|
||||
}
|
||||
}
|
||||
Vendored
+6501
File diff suppressed because it is too large
Load Diff
+344
@@ -0,0 +1,344 @@
|
||||
StartFontMetrics 4.1
|
||||
Comment Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
Comment Creation Date: Mon Jun 23 16:28:00 0:00:00
|
||||
Comment UniqueID 43048
|
||||
Comment VMusage 41139 52164
|
||||
FontName Courier-Bold
|
||||
FullName Courier Bold
|
||||
FamilyName Courier
|
||||
Weight Bold
|
||||
ItalicAngle 0
|
||||
IsFixedPitch true
|
||||
CharacterSet ExtendedRoman
|
||||
FontBBox -113 -250 749 801
|
||||
UnderlinePosition -100
|
||||
UnderlineThickness 50
|
||||
Version 003.000
|
||||
Notice Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
EncodingScheme WinAnsiEncoding
|
||||
CapHeight 562
|
||||
XHeight 439
|
||||
Ascender 629
|
||||
Descender -157
|
||||
StdHW 84
|
||||
StdVW 106
|
||||
StartCharMetrics 317
|
||||
C 32 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 160 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 33 ; WX 600 ; N exclam ; B 202 -15 398 572 ;
|
||||
C 34 ; WX 600 ; N quotedbl ; B 135 277 465 562 ;
|
||||
C 35 ; WX 600 ; N numbersign ; B 56 -45 544 651 ;
|
||||
C 36 ; WX 600 ; N dollar ; B 82 -126 519 666 ;
|
||||
C 37 ; WX 600 ; N percent ; B 5 -15 595 616 ;
|
||||
C 38 ; WX 600 ; N ampersand ; B 36 -15 546 543 ;
|
||||
C 146 ; WX 600 ; N quoteright ; B 171 277 423 562 ;
|
||||
C 40 ; WX 600 ; N parenleft ; B 219 -102 461 616 ;
|
||||
C 41 ; WX 600 ; N parenright ; B 139 -102 381 616 ;
|
||||
C 42 ; WX 600 ; N asterisk ; B 91 219 509 601 ;
|
||||
C 43 ; WX 600 ; N plus ; B 71 39 529 478 ;
|
||||
C 44 ; WX 600 ; N comma ; B 123 -111 393 174 ;
|
||||
C 45 ; WX 600 ; N hyphen ; B 100 203 500 313 ;
|
||||
C 173 ; WX 600 ; N hyphen ; B 100 203 500 313 ;
|
||||
C 46 ; WX 600 ; N period ; B 192 -15 408 171 ;
|
||||
C 47 ; WX 600 ; N slash ; B 98 -77 502 626 ;
|
||||
C 48 ; WX 600 ; N zero ; B 87 -15 513 616 ;
|
||||
C 49 ; WX 600 ; N one ; B 81 0 539 616 ;
|
||||
C 50 ; WX 600 ; N two ; B 61 0 499 616 ;
|
||||
C 51 ; WX 600 ; N three ; B 63 -15 501 616 ;
|
||||
C 52 ; WX 600 ; N four ; B 53 0 507 616 ;
|
||||
C 53 ; WX 600 ; N five ; B 70 -15 521 601 ;
|
||||
C 54 ; WX 600 ; N six ; B 90 -15 521 616 ;
|
||||
C 55 ; WX 600 ; N seven ; B 55 0 494 601 ;
|
||||
C 56 ; WX 600 ; N eight ; B 83 -15 517 616 ;
|
||||
C 57 ; WX 600 ; N nine ; B 79 -15 510 616 ;
|
||||
C 58 ; WX 600 ; N colon ; B 191 -15 407 425 ;
|
||||
C 59 ; WX 600 ; N semicolon ; B 123 -111 408 425 ;
|
||||
C 60 ; WX 600 ; N less ; B 66 15 523 501 ;
|
||||
C 61 ; WX 600 ; N equal ; B 71 118 529 398 ;
|
||||
C 62 ; WX 600 ; N greater ; B 77 15 534 501 ;
|
||||
C 63 ; WX 600 ; N question ; B 98 -14 501 580 ;
|
||||
C 64 ; WX 600 ; N at ; B 16 -15 584 616 ;
|
||||
C 65 ; WX 600 ; N A ; B -9 0 609 562 ;
|
||||
C 66 ; WX 600 ; N B ; B 30 0 573 562 ;
|
||||
C 67 ; WX 600 ; N C ; B 22 -18 560 580 ;
|
||||
C 68 ; WX 600 ; N D ; B 30 0 594 562 ;
|
||||
C 69 ; WX 600 ; N E ; B 25 0 560 562 ;
|
||||
C 70 ; WX 600 ; N F ; B 39 0 570 562 ;
|
||||
C 71 ; WX 600 ; N G ; B 22 -18 594 580 ;
|
||||
C 72 ; WX 600 ; N H ; B 20 0 580 562 ;
|
||||
C 73 ; WX 600 ; N I ; B 77 0 523 562 ;
|
||||
C 74 ; WX 600 ; N J ; B 37 -18 601 562 ;
|
||||
C 75 ; WX 600 ; N K ; B 21 0 599 562 ;
|
||||
C 76 ; WX 600 ; N L ; B 39 0 578 562 ;
|
||||
C 77 ; WX 600 ; N M ; B -2 0 602 562 ;
|
||||
C 78 ; WX 600 ; N N ; B 8 -12 610 562 ;
|
||||
C 79 ; WX 600 ; N O ; B 22 -18 578 580 ;
|
||||
C 80 ; WX 600 ; N P ; B 48 0 559 562 ;
|
||||
C 81 ; WX 600 ; N Q ; B 32 -138 578 580 ;
|
||||
C 82 ; WX 600 ; N R ; B 24 0 599 562 ;
|
||||
C 83 ; WX 600 ; N S ; B 47 -22 553 582 ;
|
||||
C 84 ; WX 600 ; N T ; B 21 0 579 562 ;
|
||||
C 85 ; WX 600 ; N U ; B 4 -18 596 562 ;
|
||||
C 86 ; WX 600 ; N V ; B -13 0 613 562 ;
|
||||
C 87 ; WX 600 ; N W ; B -18 0 618 562 ;
|
||||
C 88 ; WX 600 ; N X ; B 12 0 588 562 ;
|
||||
C 89 ; WX 600 ; N Y ; B 12 0 589 562 ;
|
||||
C 90 ; WX 600 ; N Z ; B 62 0 539 562 ;
|
||||
C 91 ; WX 600 ; N bracketleft ; B 245 -102 475 616 ;
|
||||
C 92 ; WX 600 ; N backslash ; B 99 -77 503 626 ;
|
||||
C 93 ; WX 600 ; N bracketright ; B 125 -102 355 616 ;
|
||||
C 94 ; WX 600 ; N asciicircum ; B 108 250 492 616 ;
|
||||
C 95 ; WX 600 ; N underscore ; B 0 -125 600 -75 ;
|
||||
C 145 ; WX 600 ; N quoteleft ; B 178 277 428 562 ;
|
||||
C 97 ; WX 600 ; N a ; B 35 -15 570 454 ;
|
||||
C 98 ; WX 600 ; N b ; B 0 -15 584 626 ;
|
||||
C 99 ; WX 600 ; N c ; B 40 -15 545 459 ;
|
||||
C 100 ; WX 600 ; N d ; B 20 -15 591 626 ;
|
||||
C 101 ; WX 600 ; N e ; B 40 -15 563 454 ;
|
||||
C 102 ; WX 600 ; N f ; B 83 0 547 626 ; L i fi ; L l fl ;
|
||||
C 103 ; WX 600 ; N g ; B 30 -146 580 454 ;
|
||||
C 104 ; WX 600 ; N h ; B 5 0 592 626 ;
|
||||
C 105 ; WX 600 ; N i ; B 77 0 523 658 ;
|
||||
C 106 ; WX 600 ; N j ; B 63 -146 440 658 ;
|
||||
C 107 ; WX 600 ; N k ; B 20 0 585 626 ;
|
||||
C 108 ; WX 600 ; N l ; B 77 0 523 626 ;
|
||||
C 109 ; WX 600 ; N m ; B -22 0 626 454 ;
|
||||
C 110 ; WX 600 ; N n ; B 18 0 592 454 ;
|
||||
C 111 ; WX 600 ; N o ; B 30 -15 570 454 ;
|
||||
C 112 ; WX 600 ; N p ; B -1 -142 570 454 ;
|
||||
C 113 ; WX 600 ; N q ; B 20 -142 591 454 ;
|
||||
C 114 ; WX 600 ; N r ; B 47 0 580 454 ;
|
||||
C 115 ; WX 600 ; N s ; B 68 -17 535 459 ;
|
||||
C 116 ; WX 600 ; N t ; B 47 -15 532 562 ;
|
||||
C 117 ; WX 600 ; N u ; B -1 -15 569 439 ;
|
||||
C 118 ; WX 600 ; N v ; B -1 0 601 439 ;
|
||||
C 119 ; WX 600 ; N w ; B -18 0 618 439 ;
|
||||
C 120 ; WX 600 ; N x ; B 6 0 594 439 ;
|
||||
C 121 ; WX 600 ; N y ; B -4 -142 601 439 ;
|
||||
C 122 ; WX 600 ; N z ; B 81 0 520 439 ;
|
||||
C 123 ; WX 600 ; N braceleft ; B 160 -102 464 616 ;
|
||||
C 124 ; WX 600 ; N bar ; B 255 -250 345 750 ;
|
||||
C 125 ; WX 600 ; N braceright ; B 136 -102 440 616 ;
|
||||
C 126 ; WX 600 ; N asciitilde ; B 71 153 530 356 ;
|
||||
C 161 ; WX 600 ; N exclamdown ; B 202 -146 398 449 ;
|
||||
C 162 ; WX 600 ; N cent ; B 66 -49 518 614 ;
|
||||
C 163 ; WX 600 ; N sterling ; B 72 -28 558 611 ;
|
||||
C -1 ; WX 600 ; N fraction ; B 25 -60 576 661 ;
|
||||
C 165 ; WX 600 ; N yen ; B 10 0 590 562 ;
|
||||
C 131 ; WX 600 ; N florin ; B -30 -131 572 616 ;
|
||||
C 167 ; WX 600 ; N section ; B 83 -70 517 580 ;
|
||||
C 164 ; WX 600 ; N currency ; B 54 49 546 517 ;
|
||||
C 39 ; WX 600 ; N quotesingle ; B 227 277 373 562 ;
|
||||
C 147 ; WX 600 ; N quotedblleft ; B 71 277 535 562 ;
|
||||
C 171 ; WX 600 ; N guillemotleft ; B 8 70 553 446 ;
|
||||
C 139 ; WX 600 ; N guilsinglleft ; B 141 70 459 446 ;
|
||||
C 155 ; WX 600 ; N guilsinglright ; B 141 70 459 446 ;
|
||||
C -1 ; WX 600 ; N fi ; B 12 0 593 626 ;
|
||||
C -1 ; WX 600 ; N fl ; B 12 0 593 626 ;
|
||||
C 150 ; WX 600 ; N endash ; B 65 203 535 313 ;
|
||||
C 134 ; WX 600 ; N dagger ; B 106 -70 494 580 ;
|
||||
C 135 ; WX 600 ; N daggerdbl ; B 106 -70 494 580 ;
|
||||
C 183 ; WX 600 ; N periodcentered ; B 196 165 404 351 ;
|
||||
C 182 ; WX 600 ; N paragraph ; B 6 -70 576 580 ;
|
||||
C 149 ; WX 600 ; N bullet ; B 140 132 460 430 ;
|
||||
C 130 ; WX 600 ; N quotesinglbase ; B 175 -142 427 143 ;
|
||||
C 132 ; WX 600 ; N quotedblbase ; B 65 -142 529 143 ;
|
||||
C 148 ; WX 600 ; N quotedblright ; B 61 277 525 562 ;
|
||||
C 187 ; WX 600 ; N guillemotright ; B 47 70 592 446 ;
|
||||
C 133 ; WX 600 ; N ellipsis ; B 26 -15 574 116 ;
|
||||
C 137 ; WX 600 ; N perthousand ; B -113 -15 713 616 ;
|
||||
C 191 ; WX 600 ; N questiondown ; B 99 -146 502 449 ;
|
||||
C 96 ; WX 600 ; N grave ; B 132 508 395 661 ;
|
||||
C 180 ; WX 600 ; N acute ; B 205 508 468 661 ;
|
||||
C 136 ; WX 600 ; N circumflex ; B 103 483 497 657 ;
|
||||
C 152 ; WX 600 ; N tilde ; B 89 493 512 636 ;
|
||||
C 175 ; WX 600 ; N macron ; B 88 505 512 585 ;
|
||||
C -1 ; WX 600 ; N breve ; B 83 468 517 631 ;
|
||||
C -1 ; WX 600 ; N dotaccent ; B 230 498 370 638 ;
|
||||
C 168 ; WX 600 ; N dieresis ; B 128 498 472 638 ;
|
||||
C -1 ; WX 600 ; N ring ; B 198 481 402 678 ;
|
||||
C 184 ; WX 600 ; N cedilla ; B 205 -206 387 0 ;
|
||||
C -1 ; WX 600 ; N hungarumlaut ; B 68 488 588 661 ;
|
||||
C -1 ; WX 600 ; N ogonek ; B 169 -199 400 0 ;
|
||||
C -1 ; WX 600 ; N caron ; B 103 493 497 667 ;
|
||||
C 151 ; WX 600 ; N emdash ; B -10 203 610 313 ;
|
||||
C 198 ; WX 600 ; N AE ; B -29 0 602 562 ;
|
||||
C 170 ; WX 600 ; N ordfeminine ; B 147 196 453 580 ;
|
||||
C -1 ; WX 600 ; N Lslash ; B 39 0 578 562 ;
|
||||
C 216 ; WX 600 ; N Oslash ; B 22 -22 578 584 ;
|
||||
C 140 ; WX 600 ; N OE ; B -25 0 595 562 ;
|
||||
C 186 ; WX 600 ; N ordmasculine ; B 147 196 453 580 ;
|
||||
C 230 ; WX 600 ; N ae ; B -4 -15 601 454 ;
|
||||
C -1 ; WX 600 ; N dotlessi ; B 77 0 523 439 ;
|
||||
C -1 ; WX 600 ; N lslash ; B 77 0 523 626 ;
|
||||
C 248 ; WX 600 ; N oslash ; B 30 -24 570 463 ;
|
||||
C 156 ; WX 600 ; N oe ; B -18 -15 611 454 ;
|
||||
C 223 ; WX 600 ; N germandbls ; B 22 -15 596 626 ;
|
||||
C 207 ; WX 600 ; N Idieresis ; B 77 0 523 761 ;
|
||||
C 233 ; WX 600 ; N eacute ; B 40 -15 563 661 ;
|
||||
C -1 ; WX 600 ; N abreve ; B 35 -15 570 661 ;
|
||||
C -1 ; WX 600 ; N uhungarumlaut ; B -1 -15 628 661 ;
|
||||
C -1 ; WX 600 ; N ecaron ; B 40 -15 563 667 ;
|
||||
C 159 ; WX 600 ; N Ydieresis ; B 12 0 589 761 ;
|
||||
C 247 ; WX 600 ; N divide ; B 71 16 529 500 ;
|
||||
C 221 ; WX 600 ; N Yacute ; B 12 0 589 784 ;
|
||||
C 194 ; WX 600 ; N Acircumflex ; B -9 0 609 780 ;
|
||||
C 225 ; WX 600 ; N aacute ; B 35 -15 570 661 ;
|
||||
C 219 ; WX 600 ; N Ucircumflex ; B 4 -18 596 780 ;
|
||||
C 253 ; WX 600 ; N yacute ; B -4 -142 601 661 ;
|
||||
C -1 ; WX 600 ; N scommaaccent ; B 68 -250 535 459 ;
|
||||
C 234 ; WX 600 ; N ecircumflex ; B 40 -15 563 657 ;
|
||||
C -1 ; WX 600 ; N Uring ; B 4 -18 596 801 ;
|
||||
C 220 ; WX 600 ; N Udieresis ; B 4 -18 596 761 ;
|
||||
C -1 ; WX 600 ; N aogonek ; B 35 -199 586 454 ;
|
||||
C 218 ; WX 600 ; N Uacute ; B 4 -18 596 784 ;
|
||||
C -1 ; WX 600 ; N uogonek ; B -1 -199 585 439 ;
|
||||
C 203 ; WX 600 ; N Edieresis ; B 25 0 560 761 ;
|
||||
C -1 ; WX 600 ; N Dcroat ; B 30 0 594 562 ;
|
||||
C -1 ; WX 600 ; N commaaccent ; B 205 -250 397 -57 ;
|
||||
C 169 ; WX 600 ; N copyright ; B 0 -18 600 580 ;
|
||||
C -1 ; WX 600 ; N Emacron ; B 25 0 560 708 ;
|
||||
C -1 ; WX 600 ; N ccaron ; B 40 -15 545 667 ;
|
||||
C 229 ; WX 600 ; N aring ; B 35 -15 570 678 ;
|
||||
C -1 ; WX 600 ; N Ncommaaccent ; B 8 -250 610 562 ;
|
||||
C -1 ; WX 600 ; N lacute ; B 77 0 523 801 ;
|
||||
C 224 ; WX 600 ; N agrave ; B 35 -15 570 661 ;
|
||||
C -1 ; WX 600 ; N Tcommaaccent ; B 21 -250 579 562 ;
|
||||
C -1 ; WX 600 ; N Cacute ; B 22 -18 560 784 ;
|
||||
C 227 ; WX 600 ; N atilde ; B 35 -15 570 636 ;
|
||||
C -1 ; WX 600 ; N Edotaccent ; B 25 0 560 761 ;
|
||||
C 154 ; WX 600 ; N scaron ; B 68 -17 535 667 ;
|
||||
C -1 ; WX 600 ; N scedilla ; B 68 -206 535 459 ;
|
||||
C 237 ; WX 600 ; N iacute ; B 77 0 523 661 ;
|
||||
C -1 ; WX 600 ; N lozenge ; B 66 0 534 740 ;
|
||||
C -1 ; WX 600 ; N Rcaron ; B 24 0 599 790 ;
|
||||
C -1 ; WX 600 ; N Gcommaaccent ; B 22 -250 594 580 ;
|
||||
C 251 ; WX 600 ; N ucircumflex ; B -1 -15 569 657 ;
|
||||
C 226 ; WX 600 ; N acircumflex ; B 35 -15 570 657 ;
|
||||
C -1 ; WX 600 ; N Amacron ; B -9 0 609 708 ;
|
||||
C -1 ; WX 600 ; N rcaron ; B 47 0 580 667 ;
|
||||
C 231 ; WX 600 ; N ccedilla ; B 40 -206 545 459 ;
|
||||
C -1 ; WX 600 ; N Zdotaccent ; B 62 0 539 761 ;
|
||||
C 222 ; WX 600 ; N Thorn ; B 48 0 557 562 ;
|
||||
C -1 ; WX 600 ; N Omacron ; B 22 -18 578 708 ;
|
||||
C -1 ; WX 600 ; N Racute ; B 24 0 599 784 ;
|
||||
C -1 ; WX 600 ; N Sacute ; B 47 -22 553 784 ;
|
||||
C -1 ; WX 600 ; N dcaron ; B 20 -15 727 626 ;
|
||||
C -1 ; WX 600 ; N Umacron ; B 4 -18 596 708 ;
|
||||
C -1 ; WX 600 ; N uring ; B -1 -15 569 678 ;
|
||||
C 179 ; WX 600 ; N threesuperior ; B 138 222 433 616 ;
|
||||
C 210 ; WX 600 ; N Ograve ; B 22 -18 578 784 ;
|
||||
C 192 ; WX 600 ; N Agrave ; B -9 0 609 784 ;
|
||||
C -1 ; WX 600 ; N Abreve ; B -9 0 609 784 ;
|
||||
C 215 ; WX 600 ; N multiply ; B 81 39 520 478 ;
|
||||
C 250 ; WX 600 ; N uacute ; B -1 -15 569 661 ;
|
||||
C -1 ; WX 600 ; N Tcaron ; B 21 0 579 790 ;
|
||||
C -1 ; WX 600 ; N partialdiff ; B 63 -38 537 728 ;
|
||||
C 255 ; WX 600 ; N ydieresis ; B -4 -142 601 638 ;
|
||||
C -1 ; WX 600 ; N Nacute ; B 8 -12 610 784 ;
|
||||
C 238 ; WX 600 ; N icircumflex ; B 73 0 523 657 ;
|
||||
C 202 ; WX 600 ; N Ecircumflex ; B 25 0 560 780 ;
|
||||
C 228 ; WX 600 ; N adieresis ; B 35 -15 570 638 ;
|
||||
C 235 ; WX 600 ; N edieresis ; B 40 -15 563 638 ;
|
||||
C -1 ; WX 600 ; N cacute ; B 40 -15 545 661 ;
|
||||
C -1 ; WX 600 ; N nacute ; B 18 0 592 661 ;
|
||||
C -1 ; WX 600 ; N umacron ; B -1 -15 569 585 ;
|
||||
C -1 ; WX 600 ; N Ncaron ; B 8 -12 610 790 ;
|
||||
C 205 ; WX 600 ; N Iacute ; B 77 0 523 784 ;
|
||||
C 177 ; WX 600 ; N plusminus ; B 71 24 529 515 ;
|
||||
C 166 ; WX 600 ; N brokenbar ; B 255 -175 345 675 ;
|
||||
C 174 ; WX 600 ; N registered ; B 0 -18 600 580 ;
|
||||
C -1 ; WX 600 ; N Gbreve ; B 22 -18 594 784 ;
|
||||
C -1 ; WX 600 ; N Idotaccent ; B 77 0 523 761 ;
|
||||
C -1 ; WX 600 ; N summation ; B 15 -10 586 706 ;
|
||||
C 200 ; WX 600 ; N Egrave ; B 25 0 560 784 ;
|
||||
C -1 ; WX 600 ; N racute ; B 47 0 580 661 ;
|
||||
C -1 ; WX 600 ; N omacron ; B 30 -15 570 585 ;
|
||||
C -1 ; WX 600 ; N Zacute ; B 62 0 539 784 ;
|
||||
C 142 ; WX 600 ; N Zcaron ; B 62 0 539 790 ;
|
||||
C -1 ; WX 600 ; N greaterequal ; B 26 0 523 696 ;
|
||||
C 208 ; WX 600 ; N Eth ; B 30 0 594 562 ;
|
||||
C 199 ; WX 600 ; N Ccedilla ; B 22 -206 560 580 ;
|
||||
C -1 ; WX 600 ; N lcommaaccent ; B 77 -250 523 626 ;
|
||||
C -1 ; WX 600 ; N tcaron ; B 47 -15 532 703 ;
|
||||
C -1 ; WX 600 ; N eogonek ; B 40 -199 563 454 ;
|
||||
C -1 ; WX 600 ; N Uogonek ; B 4 -199 596 562 ;
|
||||
C 193 ; WX 600 ; N Aacute ; B -9 0 609 784 ;
|
||||
C 196 ; WX 600 ; N Adieresis ; B -9 0 609 761 ;
|
||||
C 232 ; WX 600 ; N egrave ; B 40 -15 563 661 ;
|
||||
C -1 ; WX 600 ; N zacute ; B 81 0 520 661 ;
|
||||
C -1 ; WX 600 ; N iogonek ; B 77 -199 523 658 ;
|
||||
C 211 ; WX 600 ; N Oacute ; B 22 -18 578 784 ;
|
||||
C 243 ; WX 600 ; N oacute ; B 30 -15 570 661 ;
|
||||
C -1 ; WX 600 ; N amacron ; B 35 -15 570 585 ;
|
||||
C -1 ; WX 600 ; N sacute ; B 68 -17 535 661 ;
|
||||
C 239 ; WX 600 ; N idieresis ; B 77 0 523 618 ;
|
||||
C 212 ; WX 600 ; N Ocircumflex ; B 22 -18 578 780 ;
|
||||
C 217 ; WX 600 ; N Ugrave ; B 4 -18 596 784 ;
|
||||
C -1 ; WX 600 ; N Delta ; B 6 0 594 688 ;
|
||||
C 254 ; WX 600 ; N thorn ; B -14 -142 570 626 ;
|
||||
C 178 ; WX 600 ; N twosuperior ; B 143 230 436 616 ;
|
||||
C 214 ; WX 600 ; N Odieresis ; B 22 -18 578 761 ;
|
||||
C 181 ; WX 600 ; N mu ; B -1 -142 569 439 ;
|
||||
C 236 ; WX 600 ; N igrave ; B 77 0 523 661 ;
|
||||
C -1 ; WX 600 ; N ohungarumlaut ; B 30 -15 668 661 ;
|
||||
C -1 ; WX 600 ; N Eogonek ; B 25 -199 576 562 ;
|
||||
C -1 ; WX 600 ; N dcroat ; B 20 -15 591 626 ;
|
||||
C 190 ; WX 600 ; N threequarters ; B -47 -60 648 661 ;
|
||||
C -1 ; WX 600 ; N Scedilla ; B 47 -206 553 582 ;
|
||||
C -1 ; WX 600 ; N lcaron ; B 77 0 597 626 ;
|
||||
C -1 ; WX 600 ; N Kcommaaccent ; B 21 -250 599 562 ;
|
||||
C -1 ; WX 600 ; N Lacute ; B 39 0 578 784 ;
|
||||
C 153 ; WX 600 ; N trademark ; B -9 230 749 562 ;
|
||||
C -1 ; WX 600 ; N edotaccent ; B 40 -15 563 638 ;
|
||||
C 204 ; WX 600 ; N Igrave ; B 77 0 523 784 ;
|
||||
C -1 ; WX 600 ; N Imacron ; B 77 0 523 708 ;
|
||||
C -1 ; WX 600 ; N Lcaron ; B 39 0 637 562 ;
|
||||
C 189 ; WX 600 ; N onehalf ; B -47 -60 648 661 ;
|
||||
C -1 ; WX 600 ; N lessequal ; B 26 0 523 696 ;
|
||||
C 244 ; WX 600 ; N ocircumflex ; B 30 -15 570 657 ;
|
||||
C 241 ; WX 600 ; N ntilde ; B 18 0 592 636 ;
|
||||
C -1 ; WX 600 ; N Uhungarumlaut ; B 4 -18 638 784 ;
|
||||
C 201 ; WX 600 ; N Eacute ; B 25 0 560 784 ;
|
||||
C -1 ; WX 600 ; N emacron ; B 40 -15 563 585 ;
|
||||
C -1 ; WX 600 ; N gbreve ; B 30 -146 580 661 ;
|
||||
C 188 ; WX 600 ; N onequarter ; B -56 -60 656 661 ;
|
||||
C 138 ; WX 600 ; N Scaron ; B 47 -22 553 790 ;
|
||||
C -1 ; WX 600 ; N Scommaaccent ; B 47 -250 553 582 ;
|
||||
C -1 ; WX 600 ; N Ohungarumlaut ; B 22 -18 628 784 ;
|
||||
C 176 ; WX 600 ; N degree ; B 86 243 474 616 ;
|
||||
C 242 ; WX 600 ; N ograve ; B 30 -15 570 661 ;
|
||||
C -1 ; WX 600 ; N Ccaron ; B 22 -18 560 790 ;
|
||||
C 249 ; WX 600 ; N ugrave ; B -1 -15 569 661 ;
|
||||
C -1 ; WX 600 ; N radical ; B -19 -104 473 778 ;
|
||||
C -1 ; WX 600 ; N Dcaron ; B 30 0 594 790 ;
|
||||
C -1 ; WX 600 ; N rcommaaccent ; B 47 -250 580 454 ;
|
||||
C 209 ; WX 600 ; N Ntilde ; B 8 -12 610 759 ;
|
||||
C 245 ; WX 600 ; N otilde ; B 30 -15 570 636 ;
|
||||
C -1 ; WX 600 ; N Rcommaaccent ; B 24 -250 599 562 ;
|
||||
C -1 ; WX 600 ; N Lcommaaccent ; B 39 -250 578 562 ;
|
||||
C 195 ; WX 600 ; N Atilde ; B -9 0 609 759 ;
|
||||
C -1 ; WX 600 ; N Aogonek ; B -9 -199 625 562 ;
|
||||
C 197 ; WX 600 ; N Aring ; B -9 0 609 801 ;
|
||||
C 213 ; WX 600 ; N Otilde ; B 22 -18 578 759 ;
|
||||
C -1 ; WX 600 ; N zdotaccent ; B 81 0 520 638 ;
|
||||
C -1 ; WX 600 ; N Ecaron ; B 25 0 560 790 ;
|
||||
C -1 ; WX 600 ; N Iogonek ; B 77 -199 523 562 ;
|
||||
C -1 ; WX 600 ; N kcommaaccent ; B 20 -250 585 626 ;
|
||||
C -1 ; WX 600 ; N minus ; B 71 203 529 313 ;
|
||||
C 206 ; WX 600 ; N Icircumflex ; B 77 0 523 780 ;
|
||||
C -1 ; WX 600 ; N ncaron ; B 18 0 592 667 ;
|
||||
C -1 ; WX 600 ; N tcommaaccent ; B 47 -250 532 562 ;
|
||||
C 172 ; WX 600 ; N logicalnot ; B 71 103 529 413 ;
|
||||
C 246 ; WX 600 ; N odieresis ; B 30 -15 570 638 ;
|
||||
C 252 ; WX 600 ; N udieresis ; B -1 -15 569 638 ;
|
||||
C -1 ; WX 600 ; N notequal ; B 12 -47 537 563 ;
|
||||
C -1 ; WX 600 ; N gcommaaccent ; B 30 -146 580 714 ;
|
||||
C 240 ; WX 600 ; N eth ; B 58 -27 543 626 ;
|
||||
C 158 ; WX 600 ; N zcaron ; B 81 0 520 667 ;
|
||||
C -1 ; WX 600 ; N ncommaaccent ; B 18 -250 592 454 ;
|
||||
C 185 ; WX 600 ; N onesuperior ; B 153 230 447 616 ;
|
||||
C -1 ; WX 600 ; N imacron ; B 77 0 523 585 ;
|
||||
C 128 ; WX 600 ; N Euro ; B 0 0 0 0 ;
|
||||
EndCharMetrics
|
||||
EndFontMetrics
|
||||
@@ -0,0 +1,344 @@
|
||||
StartFontMetrics 4.1
|
||||
Comment Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
Comment Creation Date: Mon Jun 23 16:28:46 0:00:00
|
||||
Comment UniqueID 43049
|
||||
Comment VMusage 17529 79244
|
||||
FontName Courier-BoldOblique
|
||||
FullName Courier Bold Oblique
|
||||
FamilyName Courier
|
||||
Weight Bold
|
||||
ItalicAngle -12
|
||||
IsFixedPitch true
|
||||
CharacterSet ExtendedRoman
|
||||
FontBBox -57 -250 869 801
|
||||
UnderlinePosition -100
|
||||
UnderlineThickness 50
|
||||
Version 3
|
||||
Notice Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
EncodingScheme WinAnsiEncoding
|
||||
CapHeight 562
|
||||
XHeight 439
|
||||
Ascender 629
|
||||
Descender -157
|
||||
StdHW 84
|
||||
StdVW 106
|
||||
StartCharMetrics 317
|
||||
C 32 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 160 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 33 ; WX 600 ; N exclam ; B 215 -15 495 572 ;
|
||||
C 34 ; WX 600 ; N quotedbl ; B 211 277 585 562 ;
|
||||
C 35 ; WX 600 ; N numbersign ; B 88 -45 641 651 ;
|
||||
C 36 ; WX 600 ; N dollar ; B 87 -126 630 666 ;
|
||||
C 37 ; WX 600 ; N percent ; B 101 -15 625 616 ;
|
||||
C 38 ; WX 600 ; N ampersand ; B 61 -15 595 543 ;
|
||||
C 146 ; WX 600 ; N quoteright ; B 229 277 543 562 ;
|
||||
C 40 ; WX 600 ; N parenleft ; B 265 -102 592 616 ;
|
||||
C 41 ; WX 600 ; N parenright ; B 117 -102 444 616 ;
|
||||
C 42 ; WX 600 ; N asterisk ; B 179 219 598 601 ;
|
||||
C 43 ; WX 600 ; N plus ; B 114 39 596 478 ;
|
||||
C 44 ; WX 600 ; N comma ; B 99 -111 430 174 ;
|
||||
C 45 ; WX 600 ; N hyphen ; B 143 203 567 313 ;
|
||||
C 173 ; WX 600 ; N hyphen ; B 143 203 567 313 ;
|
||||
C 46 ; WX 600 ; N period ; B 206 -15 427 171 ;
|
||||
C 47 ; WX 600 ; N slash ; B 90 -77 626 626 ;
|
||||
C 48 ; WX 600 ; N zero ; B 135 -15 593 616 ;
|
||||
C 49 ; WX 600 ; N one ; B 93 0 562 616 ;
|
||||
C 50 ; WX 600 ; N two ; B 61 0 594 616 ;
|
||||
C 51 ; WX 600 ; N three ; B 71 -15 571 616 ;
|
||||
C 52 ; WX 600 ; N four ; B 81 0 559 616 ;
|
||||
C 53 ; WX 600 ; N five ; B 77 -15 621 601 ;
|
||||
C 54 ; WX 600 ; N six ; B 135 -15 652 616 ;
|
||||
C 55 ; WX 600 ; N seven ; B 147 0 622 601 ;
|
||||
C 56 ; WX 600 ; N eight ; B 115 -15 604 616 ;
|
||||
C 57 ; WX 600 ; N nine ; B 75 -15 592 616 ;
|
||||
C 58 ; WX 600 ; N colon ; B 205 -15 480 425 ;
|
||||
C 59 ; WX 600 ; N semicolon ; B 99 -111 481 425 ;
|
||||
C 60 ; WX 600 ; N less ; B 120 15 613 501 ;
|
||||
C 61 ; WX 600 ; N equal ; B 96 118 614 398 ;
|
||||
C 62 ; WX 600 ; N greater ; B 97 15 589 501 ;
|
||||
C 63 ; WX 600 ; N question ; B 183 -14 592 580 ;
|
||||
C 64 ; WX 600 ; N at ; B 65 -15 642 616 ;
|
||||
C 65 ; WX 600 ; N A ; B -9 0 632 562 ;
|
||||
C 66 ; WX 600 ; N B ; B 30 0 630 562 ;
|
||||
C 67 ; WX 600 ; N C ; B 74 -18 675 580 ;
|
||||
C 68 ; WX 600 ; N D ; B 30 0 664 562 ;
|
||||
C 69 ; WX 600 ; N E ; B 25 0 670 562 ;
|
||||
C 70 ; WX 600 ; N F ; B 39 0 684 562 ;
|
||||
C 71 ; WX 600 ; N G ; B 74 -18 675 580 ;
|
||||
C 72 ; WX 600 ; N H ; B 20 0 700 562 ;
|
||||
C 73 ; WX 600 ; N I ; B 77 0 643 562 ;
|
||||
C 74 ; WX 600 ; N J ; B 58 -18 721 562 ;
|
||||
C 75 ; WX 600 ; N K ; B 21 0 692 562 ;
|
||||
C 76 ; WX 600 ; N L ; B 39 0 636 562 ;
|
||||
C 77 ; WX 600 ; N M ; B -2 0 722 562 ;
|
||||
C 78 ; WX 600 ; N N ; B 8 -12 730 562 ;
|
||||
C 79 ; WX 600 ; N O ; B 74 -18 645 580 ;
|
||||
C 80 ; WX 600 ; N P ; B 48 0 643 562 ;
|
||||
C 81 ; WX 600 ; N Q ; B 83 -138 636 580 ;
|
||||
C 82 ; WX 600 ; N R ; B 24 0 617 562 ;
|
||||
C 83 ; WX 600 ; N S ; B 54 -22 673 582 ;
|
||||
C 84 ; WX 600 ; N T ; B 86 0 679 562 ;
|
||||
C 85 ; WX 600 ; N U ; B 101 -18 716 562 ;
|
||||
C 86 ; WX 600 ; N V ; B 84 0 733 562 ;
|
||||
C 87 ; WX 600 ; N W ; B 79 0 738 562 ;
|
||||
C 88 ; WX 600 ; N X ; B 12 0 690 562 ;
|
||||
C 89 ; WX 600 ; N Y ; B 109 0 709 562 ;
|
||||
C 90 ; WX 600 ; N Z ; B 62 0 637 562 ;
|
||||
C 91 ; WX 600 ; N bracketleft ; B 223 -102 606 616 ;
|
||||
C 92 ; WX 600 ; N backslash ; B 222 -77 496 626 ;
|
||||
C 93 ; WX 600 ; N bracketright ; B 103 -102 486 616 ;
|
||||
C 94 ; WX 600 ; N asciicircum ; B 171 250 556 616 ;
|
||||
C 95 ; WX 600 ; N underscore ; B -27 -125 585 -75 ;
|
||||
C 145 ; WX 600 ; N quoteleft ; B 297 277 487 562 ;
|
||||
C 97 ; WX 600 ; N a ; B 61 -15 593 454 ;
|
||||
C 98 ; WX 600 ; N b ; B 13 -15 636 626 ;
|
||||
C 99 ; WX 600 ; N c ; B 81 -15 631 459 ;
|
||||
C 100 ; WX 600 ; N d ; B 60 -15 645 626 ;
|
||||
C 101 ; WX 600 ; N e ; B 81 -15 605 454 ;
|
||||
C 102 ; WX 600 ; N f ; B 83 0 677 626 ; L i fi ; L l fl ;
|
||||
C 103 ; WX 600 ; N g ; B 40 -146 674 454 ;
|
||||
C 104 ; WX 600 ; N h ; B 18 0 615 626 ;
|
||||
C 105 ; WX 600 ; N i ; B 77 0 546 658 ;
|
||||
C 106 ; WX 600 ; N j ; B 36 -146 580 658 ;
|
||||
C 107 ; WX 600 ; N k ; B 33 0 643 626 ;
|
||||
C 108 ; WX 600 ; N l ; B 77 0 546 626 ;
|
||||
C 109 ; WX 600 ; N m ; B -22 0 649 454 ;
|
||||
C 110 ; WX 600 ; N n ; B 18 0 615 454 ;
|
||||
C 111 ; WX 600 ; N o ; B 71 -15 622 454 ;
|
||||
C 112 ; WX 600 ; N p ; B -32 -142 622 454 ;
|
||||
C 113 ; WX 600 ; N q ; B 60 -142 685 454 ;
|
||||
C 114 ; WX 600 ; N r ; B 47 0 655 454 ;
|
||||
C 115 ; WX 600 ; N s ; B 66 -17 608 459 ;
|
||||
C 116 ; WX 600 ; N t ; B 118 -15 567 562 ;
|
||||
C 117 ; WX 600 ; N u ; B 70 -15 592 439 ;
|
||||
C 118 ; WX 600 ; N v ; B 70 0 695 439 ;
|
||||
C 119 ; WX 600 ; N w ; B 53 0 712 439 ;
|
||||
C 120 ; WX 600 ; N x ; B 6 0 671 439 ;
|
||||
C 121 ; WX 600 ; N y ; B -21 -142 695 439 ;
|
||||
C 122 ; WX 600 ; N z ; B 81 0 614 439 ;
|
||||
C 123 ; WX 600 ; N braceleft ; B 203 -102 595 616 ;
|
||||
C 124 ; WX 600 ; N bar ; B 201 -250 505 750 ;
|
||||
C 125 ; WX 600 ; N braceright ; B 114 -102 506 616 ;
|
||||
C 126 ; WX 600 ; N asciitilde ; B 120 153 590 356 ;
|
||||
C 161 ; WX 600 ; N exclamdown ; B 196 -146 477 449 ;
|
||||
C 162 ; WX 600 ; N cent ; B 121 -49 605 614 ;
|
||||
C 163 ; WX 600 ; N sterling ; B 106 -28 650 611 ;
|
||||
C -1 ; WX 600 ; N fraction ; B 22 -60 708 661 ;
|
||||
C 165 ; WX 600 ; N yen ; B 98 0 710 562 ;
|
||||
C 131 ; WX 600 ; N florin ; B -57 -131 702 616 ;
|
||||
C 167 ; WX 600 ; N section ; B 74 -70 620 580 ;
|
||||
C 164 ; WX 600 ; N currency ; B 77 49 644 517 ;
|
||||
C 39 ; WX 600 ; N quotesingle ; B 303 277 493 562 ;
|
||||
C 147 ; WX 600 ; N quotedblleft ; B 190 277 594 562 ;
|
||||
C 171 ; WX 600 ; N guillemotleft ; B 62 70 639 446 ;
|
||||
C 139 ; WX 600 ; N guilsinglleft ; B 195 70 545 446 ;
|
||||
C 155 ; WX 600 ; N guilsinglright ; B 165 70 514 446 ;
|
||||
C -1 ; WX 600 ; N fi ; B 12 0 644 626 ;
|
||||
C -1 ; WX 600 ; N fl ; B 12 0 644 626 ;
|
||||
C 150 ; WX 600 ; N endash ; B 108 203 602 313 ;
|
||||
C 134 ; WX 600 ; N dagger ; B 175 -70 586 580 ;
|
||||
C 135 ; WX 600 ; N daggerdbl ; B 121 -70 587 580 ;
|
||||
C 183 ; WX 600 ; N periodcentered ; B 248 165 461 351 ;
|
||||
C 182 ; WX 600 ; N paragraph ; B 61 -70 700 580 ;
|
||||
C 149 ; WX 600 ; N bullet ; B 196 132 523 430 ;
|
||||
C 130 ; WX 600 ; N quotesinglbase ; B 144 -142 458 143 ;
|
||||
C 132 ; WX 600 ; N quotedblbase ; B 34 -142 560 143 ;
|
||||
C 148 ; WX 600 ; N quotedblright ; B 119 277 645 562 ;
|
||||
C 187 ; WX 600 ; N guillemotright ; B 71 70 647 446 ;
|
||||
C 133 ; WX 600 ; N ellipsis ; B 35 -15 587 116 ;
|
||||
C 137 ; WX 600 ; N perthousand ; B -45 -15 743 616 ;
|
||||
C 191 ; WX 600 ; N questiondown ; B 100 -146 509 449 ;
|
||||
C 96 ; WX 600 ; N grave ; B 272 508 503 661 ;
|
||||
C 180 ; WX 600 ; N acute ; B 312 508 609 661 ;
|
||||
C 136 ; WX 600 ; N circumflex ; B 212 483 607 657 ;
|
||||
C 152 ; WX 600 ; N tilde ; B 199 493 643 636 ;
|
||||
C 175 ; WX 600 ; N macron ; B 195 505 637 585 ;
|
||||
C -1 ; WX 600 ; N breve ; B 217 468 652 631 ;
|
||||
C -1 ; WX 600 ; N dotaccent ; B 348 498 493 638 ;
|
||||
C 168 ; WX 600 ; N dieresis ; B 246 498 595 638 ;
|
||||
C -1 ; WX 600 ; N ring ; B 319 481 528 678 ;
|
||||
C 184 ; WX 600 ; N cedilla ; B 168 -206 368 0 ;
|
||||
C -1 ; WX 600 ; N hungarumlaut ; B 171 488 729 661 ;
|
||||
C -1 ; WX 600 ; N ogonek ; B 143 -199 367 0 ;
|
||||
C -1 ; WX 600 ; N caron ; B 238 493 633 667 ;
|
||||
C 151 ; WX 600 ; N emdash ; B 33 203 677 313 ;
|
||||
C 198 ; WX 600 ; N AE ; B -29 0 708 562 ;
|
||||
C 170 ; WX 600 ; N ordfeminine ; B 188 196 526 580 ;
|
||||
C -1 ; WX 600 ; N Lslash ; B 39 0 636 562 ;
|
||||
C 216 ; WX 600 ; N Oslash ; B 48 -22 673 584 ;
|
||||
C 140 ; WX 600 ; N OE ; B 26 0 701 562 ;
|
||||
C 186 ; WX 600 ; N ordmasculine ; B 188 196 543 580 ;
|
||||
C 230 ; WX 600 ; N ae ; B 21 -15 652 454 ;
|
||||
C -1 ; WX 600 ; N dotlessi ; B 77 0 546 439 ;
|
||||
C -1 ; WX 600 ; N lslash ; B 77 0 587 626 ;
|
||||
C 248 ; WX 600 ; N oslash ; B 54 -24 638 463 ;
|
||||
C 156 ; WX 600 ; N oe ; B 18 -15 662 454 ;
|
||||
C 223 ; WX 600 ; N germandbls ; B 22 -15 629 626 ;
|
||||
C 207 ; WX 600 ; N Idieresis ; B 77 0 643 761 ;
|
||||
C 233 ; WX 600 ; N eacute ; B 81 -15 609 661 ;
|
||||
C -1 ; WX 600 ; N abreve ; B 61 -15 658 661 ;
|
||||
C -1 ; WX 600 ; N uhungarumlaut ; B 70 -15 769 661 ;
|
||||
C -1 ; WX 600 ; N ecaron ; B 81 -15 633 667 ;
|
||||
C 159 ; WX 600 ; N Ydieresis ; B 109 0 709 761 ;
|
||||
C 247 ; WX 600 ; N divide ; B 114 16 596 500 ;
|
||||
C 221 ; WX 600 ; N Yacute ; B 109 0 709 784 ;
|
||||
C 194 ; WX 600 ; N Acircumflex ; B -9 0 632 780 ;
|
||||
C 225 ; WX 600 ; N aacute ; B 61 -15 609 661 ;
|
||||
C 219 ; WX 600 ; N Ucircumflex ; B 101 -18 716 780 ;
|
||||
C 253 ; WX 600 ; N yacute ; B -21 -142 695 661 ;
|
||||
C -1 ; WX 600 ; N scommaaccent ; B 66 -250 608 459 ;
|
||||
C 234 ; WX 600 ; N ecircumflex ; B 81 -15 607 657 ;
|
||||
C -1 ; WX 600 ; N Uring ; B 101 -18 716 801 ;
|
||||
C 220 ; WX 600 ; N Udieresis ; B 101 -18 716 761 ;
|
||||
C -1 ; WX 600 ; N aogonek ; B 61 -199 593 454 ;
|
||||
C 218 ; WX 600 ; N Uacute ; B 101 -18 716 784 ;
|
||||
C -1 ; WX 600 ; N uogonek ; B 70 -199 592 439 ;
|
||||
C 203 ; WX 600 ; N Edieresis ; B 25 0 670 761 ;
|
||||
C -1 ; WX 600 ; N Dcroat ; B 30 0 664 562 ;
|
||||
C -1 ; WX 600 ; N commaaccent ; B 151 -250 385 -57 ;
|
||||
C 169 ; WX 600 ; N copyright ; B 53 -18 667 580 ;
|
||||
C -1 ; WX 600 ; N Emacron ; B 25 0 670 708 ;
|
||||
C -1 ; WX 600 ; N ccaron ; B 81 -15 633 667 ;
|
||||
C 229 ; WX 600 ; N aring ; B 61 -15 593 678 ;
|
||||
C -1 ; WX 600 ; N Ncommaaccent ; B 8 -250 730 562 ;
|
||||
C -1 ; WX 600 ; N lacute ; B 77 0 639 801 ;
|
||||
C 224 ; WX 600 ; N agrave ; B 61 -15 593 661 ;
|
||||
C -1 ; WX 600 ; N Tcommaaccent ; B 86 -250 679 562 ;
|
||||
C -1 ; WX 600 ; N Cacute ; B 74 -18 675 784 ;
|
||||
C 227 ; WX 600 ; N atilde ; B 61 -15 643 636 ;
|
||||
C -1 ; WX 600 ; N Edotaccent ; B 25 0 670 761 ;
|
||||
C 154 ; WX 600 ; N scaron ; B 66 -17 633 667 ;
|
||||
C -1 ; WX 600 ; N scedilla ; B 66 -206 608 459 ;
|
||||
C 237 ; WX 600 ; N iacute ; B 77 0 609 661 ;
|
||||
C -1 ; WX 600 ; N lozenge ; B 145 0 614 740 ;
|
||||
C -1 ; WX 600 ; N Rcaron ; B 24 0 659 790 ;
|
||||
C -1 ; WX 600 ; N Gcommaaccent ; B 74 -250 675 580 ;
|
||||
C 251 ; WX 600 ; N ucircumflex ; B 70 -15 597 657 ;
|
||||
C 226 ; WX 600 ; N acircumflex ; B 61 -15 607 657 ;
|
||||
C -1 ; WX 600 ; N Amacron ; B -9 0 633 708 ;
|
||||
C -1 ; WX 600 ; N rcaron ; B 47 0 655 667 ;
|
||||
C 231 ; WX 600 ; N ccedilla ; B 81 -206 631 459 ;
|
||||
C -1 ; WX 600 ; N Zdotaccent ; B 62 0 637 761 ;
|
||||
C 222 ; WX 600 ; N Thorn ; B 48 0 620 562 ;
|
||||
C -1 ; WX 600 ; N Omacron ; B 74 -18 663 708 ;
|
||||
C -1 ; WX 600 ; N Racute ; B 24 0 665 784 ;
|
||||
C -1 ; WX 600 ; N Sacute ; B 54 -22 673 784 ;
|
||||
C -1 ; WX 600 ; N dcaron ; B 60 -15 861 626 ;
|
||||
C -1 ; WX 600 ; N Umacron ; B 101 -18 716 708 ;
|
||||
C -1 ; WX 600 ; N uring ; B 70 -15 592 678 ;
|
||||
C 179 ; WX 600 ; N threesuperior ; B 193 222 526 616 ;
|
||||
C 210 ; WX 600 ; N Ograve ; B 74 -18 645 784 ;
|
||||
C 192 ; WX 600 ; N Agrave ; B -9 0 632 784 ;
|
||||
C -1 ; WX 600 ; N Abreve ; B -9 0 684 784 ;
|
||||
C 215 ; WX 600 ; N multiply ; B 104 39 606 478 ;
|
||||
C 250 ; WX 600 ; N uacute ; B 70 -15 599 661 ;
|
||||
C -1 ; WX 600 ; N Tcaron ; B 86 0 679 790 ;
|
||||
C -1 ; WX 600 ; N partialdiff ; B 91 -38 627 728 ;
|
||||
C 255 ; WX 600 ; N ydieresis ; B -21 -142 695 638 ;
|
||||
C -1 ; WX 600 ; N Nacute ; B 8 -12 730 784 ;
|
||||
C 238 ; WX 600 ; N icircumflex ; B 77 0 577 657 ;
|
||||
C 202 ; WX 600 ; N Ecircumflex ; B 25 0 670 780 ;
|
||||
C 228 ; WX 600 ; N adieresis ; B 61 -15 595 638 ;
|
||||
C 235 ; WX 600 ; N edieresis ; B 81 -15 605 638 ;
|
||||
C -1 ; WX 600 ; N cacute ; B 81 -15 649 661 ;
|
||||
C -1 ; WX 600 ; N nacute ; B 18 0 639 661 ;
|
||||
C -1 ; WX 600 ; N umacron ; B 70 -15 637 585 ;
|
||||
C -1 ; WX 600 ; N Ncaron ; B 8 -12 730 790 ;
|
||||
C 205 ; WX 600 ; N Iacute ; B 77 0 643 784 ;
|
||||
C 177 ; WX 600 ; N plusminus ; B 76 24 614 515 ;
|
||||
C 166 ; WX 600 ; N brokenbar ; B 217 -175 489 675 ;
|
||||
C 174 ; WX 600 ; N registered ; B 53 -18 667 580 ;
|
||||
C -1 ; WX 600 ; N Gbreve ; B 74 -18 684 784 ;
|
||||
C -1 ; WX 600 ; N Idotaccent ; B 77 0 643 761 ;
|
||||
C -1 ; WX 600 ; N summation ; B 15 -10 672 706 ;
|
||||
C 200 ; WX 600 ; N Egrave ; B 25 0 670 784 ;
|
||||
C -1 ; WX 600 ; N racute ; B 47 0 655 661 ;
|
||||
C -1 ; WX 600 ; N omacron ; B 71 -15 637 585 ;
|
||||
C -1 ; WX 600 ; N Zacute ; B 62 0 665 784 ;
|
||||
C 142 ; WX 600 ; N Zcaron ; B 62 0 659 790 ;
|
||||
C -1 ; WX 600 ; N greaterequal ; B 26 0 627 696 ;
|
||||
C 208 ; WX 600 ; N Eth ; B 30 0 664 562 ;
|
||||
C 199 ; WX 600 ; N Ccedilla ; B 74 -206 675 580 ;
|
||||
C -1 ; WX 600 ; N lcommaaccent ; B 77 -250 546 626 ;
|
||||
C -1 ; WX 600 ; N tcaron ; B 118 -15 627 703 ;
|
||||
C -1 ; WX 600 ; N eogonek ; B 81 -199 605 454 ;
|
||||
C -1 ; WX 600 ; N Uogonek ; B 101 -199 716 562 ;
|
||||
C 193 ; WX 600 ; N Aacute ; B -9 0 655 784 ;
|
||||
C 196 ; WX 600 ; N Adieresis ; B -9 0 632 761 ;
|
||||
C 232 ; WX 600 ; N egrave ; B 81 -15 605 661 ;
|
||||
C -1 ; WX 600 ; N zacute ; B 81 0 614 661 ;
|
||||
C -1 ; WX 600 ; N iogonek ; B 77 -199 546 658 ;
|
||||
C 211 ; WX 600 ; N Oacute ; B 74 -18 645 784 ;
|
||||
C 243 ; WX 600 ; N oacute ; B 71 -15 649 661 ;
|
||||
C -1 ; WX 600 ; N amacron ; B 61 -15 637 585 ;
|
||||
C -1 ; WX 600 ; N sacute ; B 66 -17 609 661 ;
|
||||
C 239 ; WX 600 ; N idieresis ; B 77 0 561 618 ;
|
||||
C 212 ; WX 600 ; N Ocircumflex ; B 74 -18 645 780 ;
|
||||
C 217 ; WX 600 ; N Ugrave ; B 101 -18 716 784 ;
|
||||
C -1 ; WX 600 ; N Delta ; B 6 0 594 688 ;
|
||||
C 254 ; WX 600 ; N thorn ; B -32 -142 622 626 ;
|
||||
C 178 ; WX 600 ; N twosuperior ; B 191 230 542 616 ;
|
||||
C 214 ; WX 600 ; N Odieresis ; B 74 -18 645 761 ;
|
||||
C 181 ; WX 600 ; N mu ; B 49 -142 592 439 ;
|
||||
C 236 ; WX 600 ; N igrave ; B 77 0 546 661 ;
|
||||
C -1 ; WX 600 ; N ohungarumlaut ; B 71 -15 809 661 ;
|
||||
C -1 ; WX 600 ; N Eogonek ; B 25 -199 670 562 ;
|
||||
C -1 ; WX 600 ; N dcroat ; B 60 -15 712 626 ;
|
||||
C 190 ; WX 600 ; N threequarters ; B 8 -60 699 661 ;
|
||||
C -1 ; WX 600 ; N Scedilla ; B 54 -206 673 582 ;
|
||||
C -1 ; WX 600 ; N lcaron ; B 77 0 731 626 ;
|
||||
C -1 ; WX 600 ; N Kcommaaccent ; B 21 -250 692 562 ;
|
||||
C -1 ; WX 600 ; N Lacute ; B 39 0 636 784 ;
|
||||
C 153 ; WX 600 ; N trademark ; B 86 230 869 562 ;
|
||||
C -1 ; WX 600 ; N edotaccent ; B 81 -15 605 638 ;
|
||||
C 204 ; WX 600 ; N Igrave ; B 77 0 643 784 ;
|
||||
C -1 ; WX 600 ; N Imacron ; B 77 0 663 708 ;
|
||||
C -1 ; WX 600 ; N Lcaron ; B 39 0 757 562 ;
|
||||
C 189 ; WX 600 ; N onehalf ; B 22 -60 716 661 ;
|
||||
C -1 ; WX 600 ; N lessequal ; B 26 0 671 696 ;
|
||||
C 244 ; WX 600 ; N ocircumflex ; B 71 -15 622 657 ;
|
||||
C 241 ; WX 600 ; N ntilde ; B 18 0 643 636 ;
|
||||
C -1 ; WX 600 ; N Uhungarumlaut ; B 101 -18 805 784 ;
|
||||
C 201 ; WX 600 ; N Eacute ; B 25 0 670 784 ;
|
||||
C -1 ; WX 600 ; N emacron ; B 81 -15 637 585 ;
|
||||
C -1 ; WX 600 ; N gbreve ; B 40 -146 674 661 ;
|
||||
C 188 ; WX 600 ; N onequarter ; B 13 -60 707 661 ;
|
||||
C 138 ; WX 600 ; N Scaron ; B 54 -22 689 790 ;
|
||||
C -1 ; WX 600 ; N Scommaaccent ; B 54 -250 673 582 ;
|
||||
C -1 ; WX 600 ; N Ohungarumlaut ; B 74 -18 795 784 ;
|
||||
C 176 ; WX 600 ; N degree ; B 173 243 570 616 ;
|
||||
C 242 ; WX 600 ; N ograve ; B 71 -15 622 661 ;
|
||||
C -1 ; WX 600 ; N Ccaron ; B 74 -18 689 790 ;
|
||||
C 249 ; WX 600 ; N ugrave ; B 70 -15 592 661 ;
|
||||
C -1 ; WX 600 ; N radical ; B 67 -104 635 778 ;
|
||||
C -1 ; WX 600 ; N Dcaron ; B 30 0 664 790 ;
|
||||
C -1 ; WX 600 ; N rcommaaccent ; B 47 -250 655 454 ;
|
||||
C 209 ; WX 600 ; N Ntilde ; B 8 -12 730 759 ;
|
||||
C 245 ; WX 600 ; N otilde ; B 71 -15 643 636 ;
|
||||
C -1 ; WX 600 ; N Rcommaaccent ; B 24 -250 617 562 ;
|
||||
C -1 ; WX 600 ; N Lcommaaccent ; B 39 -250 636 562 ;
|
||||
C 195 ; WX 600 ; N Atilde ; B -9 0 669 759 ;
|
||||
C -1 ; WX 600 ; N Aogonek ; B -9 -199 632 562 ;
|
||||
C 197 ; WX 600 ; N Aring ; B -9 0 632 801 ;
|
||||
C 213 ; WX 600 ; N Otilde ; B 74 -18 669 759 ;
|
||||
C -1 ; WX 600 ; N zdotaccent ; B 81 0 614 638 ;
|
||||
C -1 ; WX 600 ; N Ecaron ; B 25 0 670 790 ;
|
||||
C -1 ; WX 600 ; N Iogonek ; B 77 -199 643 562 ;
|
||||
C -1 ; WX 600 ; N kcommaaccent ; B 33 -250 643 626 ;
|
||||
C -1 ; WX 600 ; N minus ; B 114 203 596 313 ;
|
||||
C 206 ; WX 600 ; N Icircumflex ; B 77 0 643 780 ;
|
||||
C -1 ; WX 600 ; N ncaron ; B 18 0 633 667 ;
|
||||
C -1 ; WX 600 ; N tcommaaccent ; B 118 -250 567 562 ;
|
||||
C 172 ; WX 600 ; N logicalnot ; B 135 103 617 413 ;
|
||||
C 246 ; WX 600 ; N odieresis ; B 71 -15 622 638 ;
|
||||
C 252 ; WX 600 ; N udieresis ; B 70 -15 595 638 ;
|
||||
C -1 ; WX 600 ; N notequal ; B 30 -47 626 563 ;
|
||||
C -1 ; WX 600 ; N gcommaaccent ; B 40 -146 674 714 ;
|
||||
C 240 ; WX 600 ; N eth ; B 93 -27 661 626 ;
|
||||
C 158 ; WX 600 ; N zcaron ; B 81 0 643 667 ;
|
||||
C -1 ; WX 600 ; N ncommaaccent ; B 18 -250 615 454 ;
|
||||
C 185 ; WX 600 ; N onesuperior ; B 212 230 514 616 ;
|
||||
C -1 ; WX 600 ; N imacron ; B 77 0 575 585 ;
|
||||
C 128 ; WX 600 ; N Euro ; B 0 0 0 0 ;
|
||||
EndCharMetrics
|
||||
EndFontMetrics
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
StartFontMetrics 4.1
|
||||
Comment Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
Comment Creation Date: Thu May 0:00:00 17:37:52 1997
|
||||
Comment UniqueID 43051
|
||||
Comment VMusage 16248 75829
|
||||
FontName Courier-Oblique
|
||||
FullName Courier Oblique
|
||||
FamilyName Courier
|
||||
Weight Medium
|
||||
ItalicAngle -12
|
||||
IsFixedPitch true
|
||||
CharacterSet ExtendedRoman
|
||||
FontBBox -27 -250 849 805
|
||||
UnderlinePosition -100
|
||||
UnderlineThickness 50
|
||||
Version 003.000
|
||||
Notice Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
EncodingScheme WinAnsiEncoding
|
||||
CapHeight 562
|
||||
XHeight 426
|
||||
Ascender 629
|
||||
Descender -157
|
||||
StdHW 51
|
||||
StdVW 51
|
||||
StartCharMetrics 317
|
||||
C 32 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 160 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 33 ; WX 600 ; N exclam ; B 243 -15 464 572 ;
|
||||
C 34 ; WX 600 ; N quotedbl ; B 273 328 532 562 ;
|
||||
C 35 ; WX 600 ; N numbersign ; B 133 -32 596 639 ;
|
||||
C 36 ; WX 600 ; N dollar ; B 108 -126 596 662 ;
|
||||
C 37 ; WX 600 ; N percent ; B 134 -15 599 622 ;
|
||||
C 38 ; WX 600 ; N ampersand ; B 87 -15 580 543 ;
|
||||
C 146 ; WX 600 ; N quoteright ; B 283 328 495 562 ;
|
||||
C 40 ; WX 600 ; N parenleft ; B 313 -108 572 622 ;
|
||||
C 41 ; WX 600 ; N parenright ; B 137 -108 396 622 ;
|
||||
C 42 ; WX 600 ; N asterisk ; B 212 257 580 607 ;
|
||||
C 43 ; WX 600 ; N plus ; B 129 44 580 470 ;
|
||||
C 44 ; WX 600 ; N comma ; B 157 -112 370 122 ;
|
||||
C 45 ; WX 600 ; N hyphen ; B 152 231 558 285 ;
|
||||
C 173 ; WX 600 ; N hyphen ; B 152 231 558 285 ;
|
||||
C 46 ; WX 600 ; N period ; B 238 -15 382 109 ;
|
||||
C 47 ; WX 600 ; N slash ; B 112 -80 604 629 ;
|
||||
C 48 ; WX 600 ; N zero ; B 154 -15 575 622 ;
|
||||
C 49 ; WX 600 ; N one ; B 98 0 515 622 ;
|
||||
C 50 ; WX 600 ; N two ; B 70 0 568 622 ;
|
||||
C 51 ; WX 600 ; N three ; B 82 -15 538 622 ;
|
||||
C 52 ; WX 600 ; N four ; B 108 0 541 622 ;
|
||||
C 53 ; WX 600 ; N five ; B 99 -15 589 607 ;
|
||||
C 54 ; WX 600 ; N six ; B 155 -15 629 622 ;
|
||||
C 55 ; WX 600 ; N seven ; B 182 0 612 607 ;
|
||||
C 56 ; WX 600 ; N eight ; B 132 -15 588 622 ;
|
||||
C 57 ; WX 600 ; N nine ; B 93 -15 574 622 ;
|
||||
C 58 ; WX 600 ; N colon ; B 238 -15 441 385 ;
|
||||
C 59 ; WX 600 ; N semicolon ; B 157 -112 441 385 ;
|
||||
C 60 ; WX 600 ; N less ; B 96 42 610 472 ;
|
||||
C 61 ; WX 600 ; N equal ; B 109 138 600 376 ;
|
||||
C 62 ; WX 600 ; N greater ; B 85 42 599 472 ;
|
||||
C 63 ; WX 600 ; N question ; B 222 -15 583 572 ;
|
||||
C 64 ; WX 600 ; N at ; B 127 -15 582 622 ;
|
||||
C 65 ; WX 600 ; N A ; B 3 0 607 562 ;
|
||||
C 66 ; WX 600 ; N B ; B 43 0 616 562 ;
|
||||
C 67 ; WX 600 ; N C ; B 93 -18 655 580 ;
|
||||
C 68 ; WX 600 ; N D ; B 43 0 645 562 ;
|
||||
C 69 ; WX 600 ; N E ; B 53 0 660 562 ;
|
||||
C 70 ; WX 600 ; N F ; B 53 0 660 562 ;
|
||||
C 71 ; WX 600 ; N G ; B 83 -18 645 580 ;
|
||||
C 72 ; WX 600 ; N H ; B 32 0 687 562 ;
|
||||
C 73 ; WX 600 ; N I ; B 96 0 623 562 ;
|
||||
C 74 ; WX 600 ; N J ; B 52 -18 685 562 ;
|
||||
C 75 ; WX 600 ; N K ; B 38 0 671 562 ;
|
||||
C 76 ; WX 600 ; N L ; B 47 0 607 562 ;
|
||||
C 77 ; WX 600 ; N M ; B 4 0 715 562 ;
|
||||
C 78 ; WX 600 ; N N ; B 7 -13 712 562 ;
|
||||
C 79 ; WX 600 ; N O ; B 94 -18 625 580 ;
|
||||
C 80 ; WX 600 ; N P ; B 79 0 644 562 ;
|
||||
C 81 ; WX 600 ; N Q ; B 95 -138 625 580 ;
|
||||
C 82 ; WX 600 ; N R ; B 38 0 598 562 ;
|
||||
C 83 ; WX 600 ; N S ; B 76 -20 650 580 ;
|
||||
C 84 ; WX 600 ; N T ; B 108 0 665 562 ;
|
||||
C 85 ; WX 600 ; N U ; B 125 -18 702 562 ;
|
||||
C 86 ; WX 600 ; N V ; B 105 -13 723 562 ;
|
||||
C 87 ; WX 600 ; N W ; B 106 -13 722 562 ;
|
||||
C 88 ; WX 600 ; N X ; B 23 0 675 562 ;
|
||||
C 89 ; WX 600 ; N Y ; B 133 0 695 562 ;
|
||||
C 90 ; WX 600 ; N Z ; B 86 0 610 562 ;
|
||||
C 91 ; WX 600 ; N bracketleft ; B 246 -108 574 622 ;
|
||||
C 92 ; WX 600 ; N backslash ; B 249 -80 468 629 ;
|
||||
C 93 ; WX 600 ; N bracketright ; B 135 -108 463 622 ;
|
||||
C 94 ; WX 600 ; N asciicircum ; B 175 354 587 622 ;
|
||||
C 95 ; WX 600 ; N underscore ; B -27 -125 584 -75 ;
|
||||
C 145 ; WX 600 ; N quoteleft ; B 343 328 457 562 ;
|
||||
C 97 ; WX 600 ; N a ; B 76 -15 569 441 ;
|
||||
C 98 ; WX 600 ; N b ; B 29 -15 625 629 ;
|
||||
C 99 ; WX 600 ; N c ; B 106 -15 608 441 ;
|
||||
C 100 ; WX 600 ; N d ; B 85 -15 640 629 ;
|
||||
C 101 ; WX 600 ; N e ; B 106 -15 598 441 ;
|
||||
C 102 ; WX 600 ; N f ; B 114 0 662 629 ; L i fi ; L l fl ;
|
||||
C 103 ; WX 600 ; N g ; B 61 -157 657 441 ;
|
||||
C 104 ; WX 600 ; N h ; B 33 0 592 629 ;
|
||||
C 105 ; WX 600 ; N i ; B 95 0 515 657 ;
|
||||
C 106 ; WX 600 ; N j ; B 52 -157 550 657 ;
|
||||
C 107 ; WX 600 ; N k ; B 58 0 633 629 ;
|
||||
C 108 ; WX 600 ; N l ; B 95 0 515 629 ;
|
||||
C 109 ; WX 600 ; N m ; B -5 0 615 441 ;
|
||||
C 110 ; WX 600 ; N n ; B 26 0 585 441 ;
|
||||
C 111 ; WX 600 ; N o ; B 102 -15 588 441 ;
|
||||
C 112 ; WX 600 ; N p ; B -24 -157 605 441 ;
|
||||
C 113 ; WX 600 ; N q ; B 85 -157 682 441 ;
|
||||
C 114 ; WX 600 ; N r ; B 60 0 636 441 ;
|
||||
C 115 ; WX 600 ; N s ; B 78 -15 584 441 ;
|
||||
C 116 ; WX 600 ; N t ; B 167 -15 561 561 ;
|
||||
C 117 ; WX 600 ; N u ; B 101 -15 572 426 ;
|
||||
C 118 ; WX 600 ; N v ; B 90 -10 681 426 ;
|
||||
C 119 ; WX 600 ; N w ; B 76 -10 695 426 ;
|
||||
C 120 ; WX 600 ; N x ; B 20 0 655 426 ;
|
||||
C 121 ; WX 600 ; N y ; B -4 -157 683 426 ;
|
||||
C 122 ; WX 600 ; N z ; B 99 0 593 426 ;
|
||||
C 123 ; WX 600 ; N braceleft ; B 233 -108 569 622 ;
|
||||
C 124 ; WX 600 ; N bar ; B 222 -250 485 750 ;
|
||||
C 125 ; WX 600 ; N braceright ; B 140 -108 477 622 ;
|
||||
C 126 ; WX 600 ; N asciitilde ; B 116 197 600 320 ;
|
||||
C 161 ; WX 600 ; N exclamdown ; B 225 -157 445 430 ;
|
||||
C 162 ; WX 600 ; N cent ; B 151 -49 588 614 ;
|
||||
C 163 ; WX 600 ; N sterling ; B 124 -21 621 611 ;
|
||||
C -1 ; WX 600 ; N fraction ; B 84 -57 646 665 ;
|
||||
C 165 ; WX 600 ; N yen ; B 120 0 693 562 ;
|
||||
C 131 ; WX 600 ; N florin ; B -26 -143 671 622 ;
|
||||
C 167 ; WX 600 ; N section ; B 104 -78 590 580 ;
|
||||
C 164 ; WX 600 ; N currency ; B 94 58 628 506 ;
|
||||
C 39 ; WX 600 ; N quotesingle ; B 345 328 460 562 ;
|
||||
C 147 ; WX 600 ; N quotedblleft ; B 262 328 541 562 ;
|
||||
C 171 ; WX 600 ; N guillemotleft ; B 92 70 652 446 ;
|
||||
C 139 ; WX 600 ; N guilsinglleft ; B 204 70 540 446 ;
|
||||
C 155 ; WX 600 ; N guilsinglright ; B 170 70 506 446 ;
|
||||
C -1 ; WX 600 ; N fi ; B 3 0 619 629 ;
|
||||
C -1 ; WX 600 ; N fl ; B 3 0 619 629 ;
|
||||
C 150 ; WX 600 ; N endash ; B 124 231 586 285 ;
|
||||
C 134 ; WX 600 ; N dagger ; B 217 -78 546 580 ;
|
||||
C 135 ; WX 600 ; N daggerdbl ; B 163 -78 546 580 ;
|
||||
C 183 ; WX 600 ; N periodcentered ; B 275 189 434 327 ;
|
||||
C 182 ; WX 600 ; N paragraph ; B 100 -78 630 562 ;
|
||||
C 149 ; WX 600 ; N bullet ; B 224 130 485 383 ;
|
||||
C 130 ; WX 600 ; N quotesinglbase ; B 185 -134 397 100 ;
|
||||
C 132 ; WX 600 ; N quotedblbase ; B 115 -134 478 100 ;
|
||||
C 148 ; WX 600 ; N quotedblright ; B 213 328 576 562 ;
|
||||
C 187 ; WX 600 ; N guillemotright ; B 58 70 618 446 ;
|
||||
C 133 ; WX 600 ; N ellipsis ; B 46 -15 575 111 ;
|
||||
C 137 ; WX 600 ; N perthousand ; B 59 -15 627 622 ;
|
||||
C 191 ; WX 600 ; N questiondown ; B 105 -157 466 430 ;
|
||||
C 96 ; WX 600 ; N grave ; B 294 497 484 672 ;
|
||||
C 180 ; WX 600 ; N acute ; B 348 497 612 672 ;
|
||||
C 136 ; WX 600 ; N circumflex ; B 229 477 581 654 ;
|
||||
C 152 ; WX 600 ; N tilde ; B 212 489 629 606 ;
|
||||
C 175 ; WX 600 ; N macron ; B 232 525 600 565 ;
|
||||
C -1 ; WX 600 ; N breve ; B 279 501 576 609 ;
|
||||
C -1 ; WX 600 ; N dotaccent ; B 373 537 478 640 ;
|
||||
C 168 ; WX 600 ; N dieresis ; B 272 537 579 640 ;
|
||||
C -1 ; WX 600 ; N ring ; B 332 463 500 627 ;
|
||||
C 184 ; WX 600 ; N cedilla ; B 197 -151 344 10 ;
|
||||
C -1 ; WX 600 ; N hungarumlaut ; B 239 497 683 672 ;
|
||||
C -1 ; WX 600 ; N ogonek ; B 189 -172 377 4 ;
|
||||
C -1 ; WX 600 ; N caron ; B 262 492 614 669 ;
|
||||
C 151 ; WX 600 ; N emdash ; B 49 231 661 285 ;
|
||||
C 198 ; WX 600 ; N AE ; B 3 0 655 562 ;
|
||||
C 170 ; WX 600 ; N ordfeminine ; B 209 249 512 580 ;
|
||||
C -1 ; WX 600 ; N Lslash ; B 47 0 607 562 ;
|
||||
C 216 ; WX 600 ; N Oslash ; B 94 -80 625 629 ;
|
||||
C 140 ; WX 600 ; N OE ; B 59 0 672 562 ;
|
||||
C 186 ; WX 600 ; N ordmasculine ; B 210 249 535 580 ;
|
||||
C 230 ; WX 600 ; N ae ; B 41 -15 626 441 ;
|
||||
C -1 ; WX 600 ; N dotlessi ; B 95 0 515 426 ;
|
||||
C -1 ; WX 600 ; N lslash ; B 95 0 587 629 ;
|
||||
C 248 ; WX 600 ; N oslash ; B 102 -80 588 506 ;
|
||||
C 156 ; WX 600 ; N oe ; B 54 -15 615 441 ;
|
||||
C 223 ; WX 600 ; N germandbls ; B 48 -15 617 629 ;
|
||||
C 207 ; WX 600 ; N Idieresis ; B 96 0 623 753 ;
|
||||
C 233 ; WX 600 ; N eacute ; B 106 -15 612 672 ;
|
||||
C -1 ; WX 600 ; N abreve ; B 76 -15 576 609 ;
|
||||
C -1 ; WX 600 ; N uhungarumlaut ; B 101 -15 723 672 ;
|
||||
C -1 ; WX 600 ; N ecaron ; B 106 -15 614 669 ;
|
||||
C 159 ; WX 600 ; N Ydieresis ; B 133 0 695 753 ;
|
||||
C 247 ; WX 600 ; N divide ; B 136 48 573 467 ;
|
||||
C 221 ; WX 600 ; N Yacute ; B 133 0 695 805 ;
|
||||
C 194 ; WX 600 ; N Acircumflex ; B 3 0 607 787 ;
|
||||
C 225 ; WX 600 ; N aacute ; B 76 -15 612 672 ;
|
||||
C 219 ; WX 600 ; N Ucircumflex ; B 125 -18 702 787 ;
|
||||
C 253 ; WX 600 ; N yacute ; B -4 -157 683 672 ;
|
||||
C -1 ; WX 600 ; N scommaaccent ; B 78 -250 584 441 ;
|
||||
C 234 ; WX 600 ; N ecircumflex ; B 106 -15 598 654 ;
|
||||
C -1 ; WX 600 ; N Uring ; B 125 -18 702 760 ;
|
||||
C 220 ; WX 600 ; N Udieresis ; B 125 -18 702 753 ;
|
||||
C -1 ; WX 600 ; N aogonek ; B 76 -172 569 441 ;
|
||||
C 218 ; WX 600 ; N Uacute ; B 125 -18 702 805 ;
|
||||
C -1 ; WX 600 ; N uogonek ; B 101 -172 572 426 ;
|
||||
C 203 ; WX 600 ; N Edieresis ; B 53 0 660 753 ;
|
||||
C -1 ; WX 600 ; N Dcroat ; B 43 0 645 562 ;
|
||||
C -1 ; WX 600 ; N commaaccent ; B 145 -250 323 -58 ;
|
||||
C 169 ; WX 600 ; N copyright ; B 53 -18 667 580 ;
|
||||
C -1 ; WX 600 ; N Emacron ; B 53 0 660 698 ;
|
||||
C -1 ; WX 600 ; N ccaron ; B 106 -15 614 669 ;
|
||||
C 229 ; WX 600 ; N aring ; B 76 -15 569 627 ;
|
||||
C -1 ; WX 600 ; N Ncommaaccent ; B 7 -250 712 562 ;
|
||||
C -1 ; WX 600 ; N lacute ; B 95 0 640 805 ;
|
||||
C 224 ; WX 600 ; N agrave ; B 76 -15 569 672 ;
|
||||
C -1 ; WX 600 ; N Tcommaaccent ; B 108 -250 665 562 ;
|
||||
C -1 ; WX 600 ; N Cacute ; B 93 -18 655 805 ;
|
||||
C 227 ; WX 600 ; N atilde ; B 76 -15 629 606 ;
|
||||
C -1 ; WX 600 ; N Edotaccent ; B 53 0 660 753 ;
|
||||
C 154 ; WX 600 ; N scaron ; B 78 -15 614 669 ;
|
||||
C -1 ; WX 600 ; N scedilla ; B 78 -151 584 441 ;
|
||||
C 237 ; WX 600 ; N iacute ; B 95 0 612 672 ;
|
||||
C -1 ; WX 600 ; N lozenge ; B 94 0 519 706 ;
|
||||
C -1 ; WX 600 ; N Rcaron ; B 38 0 642 802 ;
|
||||
C -1 ; WX 600 ; N Gcommaaccent ; B 83 -250 645 580 ;
|
||||
C 251 ; WX 600 ; N ucircumflex ; B 101 -15 572 654 ;
|
||||
C 226 ; WX 600 ; N acircumflex ; B 76 -15 581 654 ;
|
||||
C -1 ; WX 600 ; N Amacron ; B 3 0 607 698 ;
|
||||
C -1 ; WX 600 ; N rcaron ; B 60 0 636 669 ;
|
||||
C 231 ; WX 600 ; N ccedilla ; B 106 -151 614 441 ;
|
||||
C -1 ; WX 600 ; N Zdotaccent ; B 86 0 610 753 ;
|
||||
C 222 ; WX 600 ; N Thorn ; B 79 0 606 562 ;
|
||||
C -1 ; WX 600 ; N Omacron ; B 94 -18 628 698 ;
|
||||
C -1 ; WX 600 ; N Racute ; B 38 0 670 805 ;
|
||||
C -1 ; WX 600 ; N Sacute ; B 76 -20 650 805 ;
|
||||
C -1 ; WX 600 ; N dcaron ; B 85 -15 849 629 ;
|
||||
C -1 ; WX 600 ; N Umacron ; B 125 -18 702 698 ;
|
||||
C -1 ; WX 600 ; N uring ; B 101 -15 572 627 ;
|
||||
C 179 ; WX 600 ; N threesuperior ; B 213 240 501 622 ;
|
||||
C 210 ; WX 600 ; N Ograve ; B 94 -18 625 805 ;
|
||||
C 192 ; WX 600 ; N Agrave ; B 3 0 607 805 ;
|
||||
C -1 ; WX 600 ; N Abreve ; B 3 0 607 732 ;
|
||||
C 215 ; WX 600 ; N multiply ; B 103 43 607 470 ;
|
||||
C 250 ; WX 600 ; N uacute ; B 101 -15 602 672 ;
|
||||
C -1 ; WX 600 ; N Tcaron ; B 108 0 665 802 ;
|
||||
C -1 ; WX 600 ; N partialdiff ; B 45 -38 546 710 ;
|
||||
C 255 ; WX 600 ; N ydieresis ; B -4 -157 683 620 ;
|
||||
C -1 ; WX 600 ; N Nacute ; B 7 -13 712 805 ;
|
||||
C 238 ; WX 600 ; N icircumflex ; B 95 0 551 654 ;
|
||||
C 202 ; WX 600 ; N Ecircumflex ; B 53 0 660 787 ;
|
||||
C 228 ; WX 600 ; N adieresis ; B 76 -15 575 620 ;
|
||||
C 235 ; WX 600 ; N edieresis ; B 106 -15 598 620 ;
|
||||
C -1 ; WX 600 ; N cacute ; B 106 -15 612 672 ;
|
||||
C -1 ; WX 600 ; N nacute ; B 26 0 602 672 ;
|
||||
C -1 ; WX 600 ; N umacron ; B 101 -15 600 565 ;
|
||||
C -1 ; WX 600 ; N Ncaron ; B 7 -13 712 802 ;
|
||||
C 205 ; WX 600 ; N Iacute ; B 96 0 640 805 ;
|
||||
C 177 ; WX 600 ; N plusminus ; B 96 44 594 558 ;
|
||||
C 166 ; WX 600 ; N brokenbar ; B 238 -175 469 675 ;
|
||||
C 174 ; WX 600 ; N registered ; B 53 -18 667 580 ;
|
||||
C -1 ; WX 600 ; N Gbreve ; B 83 -18 645 732 ;
|
||||
C -1 ; WX 600 ; N Idotaccent ; B 96 0 623 753 ;
|
||||
C -1 ; WX 600 ; N summation ; B 15 -10 670 706 ;
|
||||
C 200 ; WX 600 ; N Egrave ; B 53 0 660 805 ;
|
||||
C -1 ; WX 600 ; N racute ; B 60 0 636 672 ;
|
||||
C -1 ; WX 600 ; N omacron ; B 102 -15 600 565 ;
|
||||
C -1 ; WX 600 ; N Zacute ; B 86 0 670 805 ;
|
||||
C 142 ; WX 600 ; N Zcaron ; B 86 0 642 802 ;
|
||||
C -1 ; WX 600 ; N greaterequal ; B 98 0 594 710 ;
|
||||
C 208 ; WX 600 ; N Eth ; B 43 0 645 562 ;
|
||||
C 199 ; WX 600 ; N Ccedilla ; B 93 -151 658 580 ;
|
||||
C -1 ; WX 600 ; N lcommaaccent ; B 95 -250 515 629 ;
|
||||
C -1 ; WX 600 ; N tcaron ; B 167 -15 587 717 ;
|
||||
C -1 ; WX 600 ; N eogonek ; B 106 -172 598 441 ;
|
||||
C -1 ; WX 600 ; N Uogonek ; B 124 -172 702 562 ;
|
||||
C 193 ; WX 600 ; N Aacute ; B 3 0 660 805 ;
|
||||
C 196 ; WX 600 ; N Adieresis ; B 3 0 607 753 ;
|
||||
C 232 ; WX 600 ; N egrave ; B 106 -15 598 672 ;
|
||||
C -1 ; WX 600 ; N zacute ; B 99 0 612 672 ;
|
||||
C -1 ; WX 600 ; N iogonek ; B 95 -172 515 657 ;
|
||||
C 211 ; WX 600 ; N Oacute ; B 94 -18 640 805 ;
|
||||
C 243 ; WX 600 ; N oacute ; B 102 -15 612 672 ;
|
||||
C -1 ; WX 600 ; N amacron ; B 76 -15 600 565 ;
|
||||
C -1 ; WX 600 ; N sacute ; B 78 -15 612 672 ;
|
||||
C 239 ; WX 600 ; N idieresis ; B 95 0 545 620 ;
|
||||
C 212 ; WX 600 ; N Ocircumflex ; B 94 -18 625 787 ;
|
||||
C 217 ; WX 600 ; N Ugrave ; B 125 -18 702 805 ;
|
||||
C -1 ; WX 600 ; N Delta ; B 6 0 598 688 ;
|
||||
C 254 ; WX 600 ; N thorn ; B -24 -157 605 629 ;
|
||||
C 178 ; WX 600 ; N twosuperior ; B 230 249 535 622 ;
|
||||
C 214 ; WX 600 ; N Odieresis ; B 94 -18 625 753 ;
|
||||
C 181 ; WX 600 ; N mu ; B 72 -157 572 426 ;
|
||||
C 236 ; WX 600 ; N igrave ; B 95 0 515 672 ;
|
||||
C -1 ; WX 600 ; N ohungarumlaut ; B 102 -15 723 672 ;
|
||||
C -1 ; WX 600 ; N Eogonek ; B 53 -172 660 562 ;
|
||||
C -1 ; WX 600 ; N dcroat ; B 85 -15 704 629 ;
|
||||
C 190 ; WX 600 ; N threequarters ; B 73 -56 659 666 ;
|
||||
C -1 ; WX 600 ; N Scedilla ; B 76 -151 650 580 ;
|
||||
C -1 ; WX 600 ; N lcaron ; B 95 0 667 629 ;
|
||||
C -1 ; WX 600 ; N Kcommaaccent ; B 38 -250 671 562 ;
|
||||
C -1 ; WX 600 ; N Lacute ; B 47 0 607 805 ;
|
||||
C 153 ; WX 600 ; N trademark ; B 75 263 742 562 ;
|
||||
C -1 ; WX 600 ; N edotaccent ; B 106 -15 598 620 ;
|
||||
C 204 ; WX 600 ; N Igrave ; B 96 0 623 805 ;
|
||||
C -1 ; WX 600 ; N Imacron ; B 96 0 628 698 ;
|
||||
C -1 ; WX 600 ; N Lcaron ; B 47 0 632 562 ;
|
||||
C 189 ; WX 600 ; N onehalf ; B 65 -57 669 665 ;
|
||||
C -1 ; WX 600 ; N lessequal ; B 98 0 645 710 ;
|
||||
C 244 ; WX 600 ; N ocircumflex ; B 102 -15 588 654 ;
|
||||
C 241 ; WX 600 ; N ntilde ; B 26 0 629 606 ;
|
||||
C -1 ; WX 600 ; N Uhungarumlaut ; B 125 -18 761 805 ;
|
||||
C 201 ; WX 600 ; N Eacute ; B 53 0 670 805 ;
|
||||
C -1 ; WX 600 ; N emacron ; B 106 -15 600 565 ;
|
||||
C -1 ; WX 600 ; N gbreve ; B 61 -157 657 609 ;
|
||||
C 188 ; WX 600 ; N onequarter ; B 65 -57 674 665 ;
|
||||
C 138 ; WX 600 ; N Scaron ; B 76 -20 672 802 ;
|
||||
C -1 ; WX 600 ; N Scommaaccent ; B 76 -250 650 580 ;
|
||||
C -1 ; WX 600 ; N Ohungarumlaut ; B 94 -18 751 805 ;
|
||||
C 176 ; WX 600 ; N degree ; B 214 269 576 622 ;
|
||||
C 242 ; WX 600 ; N ograve ; B 102 -15 588 672 ;
|
||||
C -1 ; WX 600 ; N Ccaron ; B 93 -18 672 802 ;
|
||||
C 249 ; WX 600 ; N ugrave ; B 101 -15 572 672 ;
|
||||
C -1 ; WX 600 ; N radical ; B 85 -15 765 792 ;
|
||||
C -1 ; WX 600 ; N Dcaron ; B 43 0 645 802 ;
|
||||
C -1 ; WX 600 ; N rcommaaccent ; B 60 -250 636 441 ;
|
||||
C 209 ; WX 600 ; N Ntilde ; B 7 -13 712 729 ;
|
||||
C 245 ; WX 600 ; N otilde ; B 102 -15 629 606 ;
|
||||
C -1 ; WX 600 ; N Rcommaaccent ; B 38 -250 598 562 ;
|
||||
C -1 ; WX 600 ; N Lcommaaccent ; B 47 -250 607 562 ;
|
||||
C 195 ; WX 600 ; N Atilde ; B 3 0 655 729 ;
|
||||
C -1 ; WX 600 ; N Aogonek ; B 3 -172 607 562 ;
|
||||
C 197 ; WX 600 ; N Aring ; B 3 0 607 750 ;
|
||||
C 213 ; WX 600 ; N Otilde ; B 94 -18 655 729 ;
|
||||
C -1 ; WX 600 ; N zdotaccent ; B 99 0 593 620 ;
|
||||
C -1 ; WX 600 ; N Ecaron ; B 53 0 660 802 ;
|
||||
C -1 ; WX 600 ; N Iogonek ; B 96 -172 623 562 ;
|
||||
C -1 ; WX 600 ; N kcommaaccent ; B 58 -250 633 629 ;
|
||||
C -1 ; WX 600 ; N minus ; B 129 232 580 283 ;
|
||||
C 206 ; WX 600 ; N Icircumflex ; B 96 0 623 787 ;
|
||||
C -1 ; WX 600 ; N ncaron ; B 26 0 614 669 ;
|
||||
C -1 ; WX 600 ; N tcommaaccent ; B 165 -250 561 561 ;
|
||||
C 172 ; WX 600 ; N logicalnot ; B 155 108 591 369 ;
|
||||
C 246 ; WX 600 ; N odieresis ; B 102 -15 588 620 ;
|
||||
C 252 ; WX 600 ; N udieresis ; B 101 -15 575 620 ;
|
||||
C -1 ; WX 600 ; N notequal ; B 43 -16 621 529 ;
|
||||
C -1 ; WX 600 ; N gcommaaccent ; B 61 -157 657 708 ;
|
||||
C 240 ; WX 600 ; N eth ; B 102 -15 639 629 ;
|
||||
C 158 ; WX 600 ; N zcaron ; B 99 0 624 669 ;
|
||||
C -1 ; WX 600 ; N ncommaaccent ; B 26 -250 585 441 ;
|
||||
C 185 ; WX 600 ; N onesuperior ; B 231 249 491 622 ;
|
||||
C -1 ; WX 600 ; N imacron ; B 95 0 543 565 ;
|
||||
C 128 ; WX 600 ; N Euro ; B 0 0 0 0 ;
|
||||
EndCharMetrics
|
||||
EndFontMetrics
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
StartFontMetrics 4.1
|
||||
Comment Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
Comment Creation Date: Thu May 1 17:27:09 1997
|
||||
Comment UniqueID 43050
|
||||
Comment VMusage 39754 50779
|
||||
FontName Courier
|
||||
FullName Courier
|
||||
FamilyName Courier
|
||||
Weight Medium
|
||||
ItalicAngle 0
|
||||
IsFixedPitch true
|
||||
CharacterSet ExtendedRoman
|
||||
FontBBox -23 -250 715 805
|
||||
UnderlinePosition -100
|
||||
UnderlineThickness 50
|
||||
Version 003.000
|
||||
Notice Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved.
|
||||
EncodingScheme WinAnsiEncoding
|
||||
CapHeight 562
|
||||
XHeight 426
|
||||
Ascender 629
|
||||
Descender -157
|
||||
StdHW 51
|
||||
StdVW 51
|
||||
StartCharMetrics 317
|
||||
C 32 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 160 ; WX 600 ; N space ; B 0 0 0 0 ;
|
||||
C 33 ; WX 600 ; N exclam ; B 236 -15 364 572 ;
|
||||
C 34 ; WX 600 ; N quotedbl ; B 187 328 413 562 ;
|
||||
C 35 ; WX 600 ; N numbersign ; B 93 -32 507 639 ;
|
||||
C 36 ; WX 600 ; N dollar ; B 105 -126 496 662 ;
|
||||
C 37 ; WX 600 ; N percent ; B 81 -15 518 622 ;
|
||||
C 38 ; WX 600 ; N ampersand ; B 63 -15 538 543 ;
|
||||
C 146 ; WX 600 ; N quoteright ; B 213 328 376 562 ;
|
||||
C 40 ; WX 600 ; N parenleft ; B 269 -108 440 622 ;
|
||||
C 41 ; WX 600 ; N parenright ; B 160 -108 331 622 ;
|
||||
C 42 ; WX 600 ; N asterisk ; B 116 257 484 607 ;
|
||||
C 43 ; WX 600 ; N plus ; B 80 44 520 470 ;
|
||||
C 44 ; WX 600 ; N comma ; B 181 -112 344 122 ;
|
||||
C 45 ; WX 600 ; N hyphen ; B 103 231 497 285 ;
|
||||
C 173 ; WX 600 ; N hyphen ; B 103 231 497 285 ;
|
||||
C 46 ; WX 600 ; N period ; B 229 -15 371 109 ;
|
||||
C 47 ; WX 600 ; N slash ; B 125 -80 475 629 ;
|
||||
C 48 ; WX 600 ; N zero ; B 106 -15 494 622 ;
|
||||
C 49 ; WX 600 ; N one ; B 96 0 505 622 ;
|
||||
C 50 ; WX 600 ; N two ; B 70 0 471 622 ;
|
||||
C 51 ; WX 600 ; N three ; B 75 -15 466 622 ;
|
||||
C 52 ; WX 600 ; N four ; B 78 0 500 622 ;
|
||||
C 53 ; WX 600 ; N five ; B 92 -15 497 607 ;
|
||||
C 54 ; WX 600 ; N six ; B 111 -15 497 622 ;
|
||||
C 55 ; WX 600 ; N seven ; B 82 0 483 607 ;
|
||||
C 56 ; WX 600 ; N eight ; B 102 -15 498 622 ;
|
||||
C 57 ; WX 600 ; N nine ; B 96 -15 489 622 ;
|
||||
C 58 ; WX 600 ; N colon ; B 229 -15 371 385 ;
|
||||
C 59 ; WX 600 ; N semicolon ; B 181 -112 371 385 ;
|
||||
C 60 ; WX 600 ; N less ; B 41 42 519 472 ;
|
||||
C 61 ; WX 600 ; N equal ; B 80 138 520 376 ;
|
||||
C 62 ; WX 600 ; N greater ; B 66 42 544 472 ;
|
||||
C 63 ; WX 600 ; N question ; B 129 -15 492 572 ;
|
||||
C 64 ; WX 600 ; N at ; B 77 -15 533 622 ;
|
||||
C 65 ; WX 600 ; N A ; B 3 0 597 562 ;
|
||||
C 66 ; WX 600 ; N B ; B 43 0 559 562 ;
|
||||
C 67 ; WX 600 ; N C ; B 41 -18 540 580 ;
|
||||
C 68 ; WX 600 ; N D ; B 43 0 574 562 ;
|
||||
C 69 ; WX 600 ; N E ; B 53 0 550 562 ;
|
||||
C 70 ; WX 600 ; N F ; B 53 0 545 562 ;
|
||||
C 71 ; WX 600 ; N G ; B 31 -18 575 580 ;
|
||||
C 72 ; WX 600 ; N H ; B 32 0 568 562 ;
|
||||
C 73 ; WX 600 ; N I ; B 96 0 504 562 ;
|
||||
C 74 ; WX 600 ; N J ; B 34 -18 566 562 ;
|
||||
C 75 ; WX 600 ; N K ; B 38 0 582 562 ;
|
||||
C 76 ; WX 600 ; N L ; B 47 0 554 562 ;
|
||||
C 77 ; WX 600 ; N M ; B 4 0 596 562 ;
|
||||
C 78 ; WX 600 ; N N ; B 7 -13 593 562 ;
|
||||
C 79 ; WX 600 ; N O ; B 43 -18 557 580 ;
|
||||
C 80 ; WX 600 ; N P ; B 79 0 558 562 ;
|
||||
C 81 ; WX 600 ; N Q ; B 43 -138 557 580 ;
|
||||
C 82 ; WX 600 ; N R ; B 38 0 588 562 ;
|
||||
C 83 ; WX 600 ; N S ; B 72 -20 529 580 ;
|
||||
C 84 ; WX 600 ; N T ; B 38 0 563 562 ;
|
||||
C 85 ; WX 600 ; N U ; B 17 -18 583 562 ;
|
||||
C 86 ; WX 600 ; N V ; B -4 -13 604 562 ;
|
||||
C 87 ; WX 600 ; N W ; B -3 -13 603 562 ;
|
||||
C 88 ; WX 600 ; N X ; B 23 0 577 562 ;
|
||||
C 89 ; WX 600 ; N Y ; B 24 0 576 562 ;
|
||||
C 90 ; WX 600 ; N Z ; B 86 0 514 562 ;
|
||||
C 91 ; WX 600 ; N bracketleft ; B 269 -108 442 622 ;
|
||||
C 92 ; WX 600 ; N backslash ; B 118 -80 482 629 ;
|
||||
C 93 ; WX 600 ; N bracketright ; B 158 -108 331 622 ;
|
||||
C 94 ; WX 600 ; N asciicircum ; B 94 354 506 622 ;
|
||||
C 95 ; WX 600 ; N underscore ; B 0 -125 600 -75 ;
|
||||
C 145 ; WX 600 ; N quoteleft ; B 224 328 387 562 ;
|
||||
C 97 ; WX 600 ; N a ; B 53 -15 559 441 ;
|
||||
C 98 ; WX 600 ; N b ; B 14 -15 575 629 ;
|
||||
C 99 ; WX 600 ; N c ; B 66 -15 529 441 ;
|
||||
C 100 ; WX 600 ; N d ; B 45 -15 591 629 ;
|
||||
C 101 ; WX 600 ; N e ; B 66 -15 548 441 ;
|
||||
C 102 ; WX 600 ; N f ; B 114 0 531 629 ; L i fi ; L l fl ;
|
||||
C 103 ; WX 600 ; N g ; B 45 -157 566 441 ;
|
||||
C 104 ; WX 600 ; N h ; B 18 0 582 629 ;
|
||||
C 105 ; WX 600 ; N i ; B 95 0 505 657 ;
|
||||
C 106 ; WX 600 ; N j ; B 82 -157 410 657 ;
|
||||
C 107 ; WX 600 ; N k ; B 43 0 580 629 ;
|
||||
C 108 ; WX 600 ; N l ; B 95 0 505 629 ;
|
||||
C 109 ; WX 600 ; N m ; B -5 0 605 441 ;
|
||||
C 110 ; WX 600 ; N n ; B 26 0 575 441 ;
|
||||
C 111 ; WX 600 ; N o ; B 62 -15 538 441 ;
|
||||
C 112 ; WX 600 ; N p ; B 9 -157 555 441 ;
|
||||
C 113 ; WX 600 ; N q ; B 45 -157 591 441 ;
|
||||
C 114 ; WX 600 ; N r ; B 60 0 559 441 ;
|
||||
C 115 ; WX 600 ; N s ; B 80 -15 513 441 ;
|
||||
C 116 ; WX 600 ; N t ; B 87 -15 530 561 ;
|
||||
C 117 ; WX 600 ; N u ; B 21 -15 562 426 ;
|
||||
C 118 ; WX 600 ; N v ; B 10 -10 590 426 ;
|
||||
C 119 ; WX 600 ; N w ; B -4 -10 604 426 ;
|
||||
C 120 ; WX 600 ; N x ; B 20 0 580 426 ;
|
||||
C 121 ; WX 600 ; N y ; B 7 -157 592 426 ;
|
||||
C 122 ; WX 600 ; N z ; B 99 0 502 426 ;
|
||||
C 123 ; WX 600 ; N braceleft ; B 182 -108 437 622 ;
|
||||
C 124 ; WX 600 ; N bar ; B 275 -250 326 750 ;
|
||||
C 125 ; WX 600 ; N braceright ; B 163 -108 418 622 ;
|
||||
C 126 ; WX 600 ; N asciitilde ; B 63 197 540 320 ;
|
||||
C 161 ; WX 600 ; N exclamdown ; B 236 -157 364 430 ;
|
||||
C 162 ; WX 600 ; N cent ; B 96 -49 500 614 ;
|
||||
C 163 ; WX 600 ; N sterling ; B 84 -21 521 611 ;
|
||||
C -1 ; WX 600 ; N fraction ; B 92 -57 509 665 ;
|
||||
C 165 ; WX 600 ; N yen ; B 26 0 574 562 ;
|
||||
C 131 ; WX 600 ; N florin ; B 4 -143 539 622 ;
|
||||
C 167 ; WX 600 ; N section ; B 113 -78 488 580 ;
|
||||
C 164 ; WX 600 ; N currency ; B 73 58 527 506 ;
|
||||
C 39 ; WX 600 ; N quotesingle ; B 259 328 341 562 ;
|
||||
C 147 ; WX 600 ; N quotedblleft ; B 143 328 471 562 ;
|
||||
C 171 ; WX 600 ; N guillemotleft ; B 37 70 563 446 ;
|
||||
C 139 ; WX 600 ; N guilsinglleft ; B 149 70 451 446 ;
|
||||
C 155 ; WX 600 ; N guilsinglright ; B 149 70 451 446 ;
|
||||
C -1 ; WX 600 ; N fi ; B 3 0 597 629 ;
|
||||
C -1 ; WX 600 ; N fl ; B 3 0 597 629 ;
|
||||
C 150 ; WX 600 ; N endash ; B 75 231 525 285 ;
|
||||
C 134 ; WX 600 ; N dagger ; B 141 -78 459 580 ;
|
||||
C 135 ; WX 600 ; N daggerdbl ; B 141 -78 459 580 ;
|
||||
C 183 ; WX 600 ; N periodcentered ; B 222 189 378 327 ;
|
||||
C 182 ; WX 600 ; N paragraph ; B 50 -78 511 562 ;
|
||||
C 149 ; WX 600 ; N bullet ; B 172 130 428 383 ;
|
||||
C 130 ; WX 600 ; N quotesinglbase ; B 213 -134 376 100 ;
|
||||
C 132 ; WX 600 ; N quotedblbase ; B 143 -134 457 100 ;
|
||||
C 148 ; WX 600 ; N quotedblright ; B 143 328 457 562 ;
|
||||
C 187 ; WX 600 ; N guillemotright ; B 37 70 563 446 ;
|
||||
C 133 ; WX 600 ; N ellipsis ; B 37 -15 563 111 ;
|
||||
C 137 ; WX 600 ; N perthousand ; B 3 -15 600 622 ;
|
||||
C 191 ; WX 600 ; N questiondown ; B 108 -157 471 430 ;
|
||||
C 96 ; WX 600 ; N grave ; B 151 497 378 672 ;
|
||||
C 180 ; WX 600 ; N acute ; B 242 497 469 672 ;
|
||||
C 136 ; WX 600 ; N circumflex ; B 124 477 476 654 ;
|
||||
C 152 ; WX 600 ; N tilde ; B 105 489 503 606 ;
|
||||
C 175 ; WX 600 ; N macron ; B 120 525 480 565 ;
|
||||
C -1 ; WX 600 ; N breve ; B 153 501 447 609 ;
|
||||
C -1 ; WX 600 ; N dotaccent ; B 249 537 352 640 ;
|
||||
C 168 ; WX 600 ; N dieresis ; B 148 537 453 640 ;
|
||||
C -1 ; WX 600 ; N ring ; B 218 463 382 627 ;
|
||||
C 184 ; WX 600 ; N cedilla ; B 224 -151 362 10 ;
|
||||
C -1 ; WX 600 ; N hungarumlaut ; B 133 497 540 672 ;
|
||||
C -1 ; WX 600 ; N ogonek ; B 211 -172 407 4 ;
|
||||
C -1 ; WX 600 ; N caron ; B 124 492 476 669 ;
|
||||
C 151 ; WX 600 ; N emdash ; B 0 231 600 285 ;
|
||||
C 198 ; WX 600 ; N AE ; B 3 0 550 562 ;
|
||||
C 170 ; WX 600 ; N ordfeminine ; B 156 249 442 580 ;
|
||||
C -1 ; WX 600 ; N Lslash ; B 47 0 554 562 ;
|
||||
C 216 ; WX 600 ; N Oslash ; B 43 -80 557 629 ;
|
||||
C 140 ; WX 600 ; N OE ; B 7 0 567 562 ;
|
||||
C 186 ; WX 600 ; N ordmasculine ; B 157 249 443 580 ;
|
||||
C 230 ; WX 600 ; N ae ; B 19 -15 570 441 ;
|
||||
C -1 ; WX 600 ; N dotlessi ; B 95 0 505 426 ;
|
||||
C -1 ; WX 600 ; N lslash ; B 95 0 505 629 ;
|
||||
C 248 ; WX 600 ; N oslash ; B 62 -80 538 506 ;
|
||||
C 156 ; WX 600 ; N oe ; B 19 -15 559 441 ;
|
||||
C 223 ; WX 600 ; N germandbls ; B 48 -15 588 629 ;
|
||||
C 207 ; WX 600 ; N Idieresis ; B 96 0 504 753 ;
|
||||
C 233 ; WX 600 ; N eacute ; B 66 -15 548 672 ;
|
||||
C -1 ; WX 600 ; N abreve ; B 53 -15 559 609 ;
|
||||
C -1 ; WX 600 ; N uhungarumlaut ; B 21 -15 580 672 ;
|
||||
C -1 ; WX 600 ; N ecaron ; B 66 -15 548 669 ;
|
||||
C 159 ; WX 600 ; N Ydieresis ; B 24 0 576 753 ;
|
||||
C 247 ; WX 600 ; N divide ; B 87 48 513 467 ;
|
||||
C 221 ; WX 600 ; N Yacute ; B 24 0 576 805 ;
|
||||
C 194 ; WX 600 ; N Acircumflex ; B 3 0 597 787 ;
|
||||
C 225 ; WX 600 ; N aacute ; B 53 -15 559 672 ;
|
||||
C 219 ; WX 600 ; N Ucircumflex ; B 17 -18 583 787 ;
|
||||
C 253 ; WX 600 ; N yacute ; B 7 -157 592 672 ;
|
||||
C -1 ; WX 600 ; N scommaaccent ; B 80 -250 513 441 ;
|
||||
C 234 ; WX 600 ; N ecircumflex ; B 66 -15 548 654 ;
|
||||
C -1 ; WX 600 ; N Uring ; B 17 -18 583 760 ;
|
||||
C 220 ; WX 600 ; N Udieresis ; B 17 -18 583 753 ;
|
||||
C -1 ; WX 600 ; N aogonek ; B 53 -172 587 441 ;
|
||||
C 218 ; WX 600 ; N Uacute ; B 17 -18 583 805 ;
|
||||
C -1 ; WX 600 ; N uogonek ; B 21 -172 590 426 ;
|
||||
C 203 ; WX 600 ; N Edieresis ; B 53 0 550 753 ;
|
||||
C -1 ; WX 600 ; N Dcroat ; B 30 0 574 562 ;
|
||||
C -1 ; WX 600 ; N commaaccent ; B 198 -250 335 -58 ;
|
||||
C 169 ; WX 600 ; N copyright ; B 0 -18 600 580 ;
|
||||
C -1 ; WX 600 ; N Emacron ; B 53 0 550 698 ;
|
||||
C -1 ; WX 600 ; N ccaron ; B 66 -15 529 669 ;
|
||||
C 229 ; WX 600 ; N aring ; B 53 -15 559 627 ;
|
||||
C -1 ; WX 600 ; N Ncommaaccent ; B 7 -250 593 562 ;
|
||||
C -1 ; WX 600 ; N lacute ; B 95 0 505 805 ;
|
||||
C 224 ; WX 600 ; N agrave ; B 53 -15 559 672 ;
|
||||
C -1 ; WX 600 ; N Tcommaaccent ; B 38 -250 563 562 ;
|
||||
C -1 ; WX 600 ; N Cacute ; B 41 -18 540 805 ;
|
||||
C 227 ; WX 600 ; N atilde ; B 53 -15 559 606 ;
|
||||
C -1 ; WX 600 ; N Edotaccent ; B 53 0 550 753 ;
|
||||
C 154 ; WX 600 ; N scaron ; B 80 -15 513 669 ;
|
||||
C -1 ; WX 600 ; N scedilla ; B 80 -151 513 441 ;
|
||||
C 237 ; WX 600 ; N iacute ; B 95 0 505 672 ;
|
||||
C -1 ; WX 600 ; N lozenge ; B 18 0 443 706 ;
|
||||
C -1 ; WX 600 ; N Rcaron ; B 38 0 588 802 ;
|
||||
C -1 ; WX 600 ; N Gcommaaccent ; B 31 -250 575 580 ;
|
||||
C 251 ; WX 600 ; N ucircumflex ; B 21 -15 562 654 ;
|
||||
C 226 ; WX 600 ; N acircumflex ; B 53 -15 559 654 ;
|
||||
C -1 ; WX 600 ; N Amacron ; B 3 0 597 698 ;
|
||||
C -1 ; WX 600 ; N rcaron ; B 60 0 559 669 ;
|
||||
C 231 ; WX 600 ; N ccedilla ; B 66 -151 529 441 ;
|
||||
C -1 ; WX 600 ; N Zdotaccent ; B 86 0 514 753 ;
|
||||
C 222 ; WX 600 ; N Thorn ; B 79 0 538 562 ;
|
||||
C -1 ; WX 600 ; N Omacron ; B 43 -18 557 698 ;
|
||||
C -1 ; WX 600 ; N Racute ; B 38 0 588 805 ;
|
||||
C -1 ; WX 600 ; N Sacute ; B 72 -20 529 805 ;
|
||||
C -1 ; WX 600 ; N dcaron ; B 45 -15 715 629 ;
|
||||
C -1 ; WX 600 ; N Umacron ; B 17 -18 583 698 ;
|
||||
C -1 ; WX 600 ; N uring ; B 21 -15 562 627 ;
|
||||
C 179 ; WX 600 ; N threesuperior ; B 155 240 406 622 ;
|
||||
C 210 ; WX 600 ; N Ograve ; B 43 -18 557 805 ;
|
||||
C 192 ; WX 600 ; N Agrave ; B 3 0 597 805 ;
|
||||
C -1 ; WX 600 ; N Abreve ; B 3 0 597 732 ;
|
||||
C 215 ; WX 600 ; N multiply ; B 87 43 515 470 ;
|
||||
C 250 ; WX 600 ; N uacute ; B 21 -15 562 672 ;
|
||||
C -1 ; WX 600 ; N Tcaron ; B 38 0 563 802 ;
|
||||
C -1 ; WX 600 ; N partialdiff ; B 17 -38 459 710 ;
|
||||
C 255 ; WX 600 ; N ydieresis ; B 7 -157 592 620 ;
|
||||
C -1 ; WX 600 ; N Nacute ; B 7 -13 593 805 ;
|
||||
C 238 ; WX 600 ; N icircumflex ; B 94 0 505 654 ;
|
||||
C 202 ; WX 600 ; N Ecircumflex ; B 53 0 550 787 ;
|
||||
C 228 ; WX 600 ; N adieresis ; B 53 -15 559 620 ;
|
||||
C 235 ; WX 600 ; N edieresis ; B 66 -15 548 620 ;
|
||||
C -1 ; WX 600 ; N cacute ; B 66 -15 529 672 ;
|
||||
C -1 ; WX 600 ; N nacute ; B 26 0 575 672 ;
|
||||
C -1 ; WX 600 ; N umacron ; B 21 -15 562 565 ;
|
||||
C -1 ; WX 600 ; N Ncaron ; B 7 -13 593 802 ;
|
||||
C 205 ; WX 600 ; N Iacute ; B 96 0 504 805 ;
|
||||
C 177 ; WX 600 ; N plusminus ; B 87 44 513 558 ;
|
||||
C 166 ; WX 600 ; N brokenbar ; B 275 -175 326 675 ;
|
||||
C 174 ; WX 600 ; N registered ; B 0 -18 600 580 ;
|
||||
C -1 ; WX 600 ; N Gbreve ; B 31 -18 575 732 ;
|
||||
C -1 ; WX 600 ; N Idotaccent ; B 96 0 504 753 ;
|
||||
C -1 ; WX 600 ; N summation ; B 15 -10 585 706 ;
|
||||
C 200 ; WX 600 ; N Egrave ; B 53 0 550 805 ;
|
||||
C -1 ; WX 600 ; N racute ; B 60 0 559 672 ;
|
||||
C -1 ; WX 600 ; N omacron ; B 62 -15 538 565 ;
|
||||
C -1 ; WX 600 ; N Zacute ; B 86 0 514 805 ;
|
||||
C 142 ; WX 600 ; N Zcaron ; B 86 0 514 802 ;
|
||||
C -1 ; WX 600 ; N greaterequal ; B 98 0 502 710 ;
|
||||
C 208 ; WX 600 ; N Eth ; B 30 0 574 562 ;
|
||||
C 199 ; WX 600 ; N Ccedilla ; B 41 -151 540 580 ;
|
||||
C -1 ; WX 600 ; N lcommaaccent ; B 95 -250 505 629 ;
|
||||
C -1 ; WX 600 ; N tcaron ; B 87 -15 530 717 ;
|
||||
C -1 ; WX 600 ; N eogonek ; B 66 -172 548 441 ;
|
||||
C -1 ; WX 600 ; N Uogonek ; B 17 -172 583 562 ;
|
||||
C 193 ; WX 600 ; N Aacute ; B 3 0 597 805 ;
|
||||
C 196 ; WX 600 ; N Adieresis ; B 3 0 597 753 ;
|
||||
C 232 ; WX 600 ; N egrave ; B 66 -15 548 672 ;
|
||||
C -1 ; WX 600 ; N zacute ; B 99 0 502 672 ;
|
||||
C -1 ; WX 600 ; N iogonek ; B 95 -172 505 657 ;
|
||||
C 211 ; WX 600 ; N Oacute ; B 43 -18 557 805 ;
|
||||
C 243 ; WX 600 ; N oacute ; B 62 -15 538 672 ;
|
||||
C -1 ; WX 600 ; N amacron ; B 53 -15 559 565 ;
|
||||
C -1 ; WX 600 ; N sacute ; B 80 -15 513 672 ;
|
||||
C 239 ; WX 600 ; N idieresis ; B 95 0 505 620 ;
|
||||
C 212 ; WX 600 ; N Ocircumflex ; B 43 -18 557 787 ;
|
||||
C 217 ; WX 600 ; N Ugrave ; B 17 -18 583 805 ;
|
||||
C -1 ; WX 600 ; N Delta ; B 6 0 598 688 ;
|
||||
C 254 ; WX 600 ; N thorn ; B -6 -157 555 629 ;
|
||||
C 178 ; WX 600 ; N twosuperior ; B 177 249 424 622 ;
|
||||
C 214 ; WX 600 ; N Odieresis ; B 43 -18 557 753 ;
|
||||
C 181 ; WX 600 ; N mu ; B 21 -157 562 426 ;
|
||||
C 236 ; WX 600 ; N igrave ; B 95 0 505 672 ;
|
||||
C -1 ; WX 600 ; N ohungarumlaut ; B 62 -15 580 672 ;
|
||||
C -1 ; WX 600 ; N Eogonek ; B 53 -172 561 562 ;
|
||||
C -1 ; WX 600 ; N dcroat ; B 45 -15 591 629 ;
|
||||
C 190 ; WX 600 ; N threequarters ; B 8 -56 593 666 ;
|
||||
C -1 ; WX 600 ; N Scedilla ; B 72 -151 529 580 ;
|
||||
C -1 ; WX 600 ; N lcaron ; B 95 0 533 629 ;
|
||||
C -1 ; WX 600 ; N Kcommaaccent ; B 38 -250 582 562 ;
|
||||
C -1 ; WX 600 ; N Lacute ; B 47 0 554 805 ;
|
||||
C 153 ; WX 600 ; N trademark ; B -23 263 623 562 ;
|
||||
C -1 ; WX 600 ; N edotaccent ; B 66 -15 548 620 ;
|
||||
C 204 ; WX 600 ; N Igrave ; B 96 0 504 805 ;
|
||||
C -1 ; WX 600 ; N Imacron ; B 96 0 504 698 ;
|
||||
C -1 ; WX 600 ; N Lcaron ; B 47 0 554 562 ;
|
||||
C 189 ; WX 600 ; N onehalf ; B 0 -57 611 665 ;
|
||||
C -1 ; WX 600 ; N lessequal ; B 98 0 502 710 ;
|
||||
C 244 ; WX 600 ; N ocircumflex ; B 62 -15 538 654 ;
|
||||
C 241 ; WX 600 ; N ntilde ; B 26 0 575 606 ;
|
||||
C -1 ; WX 600 ; N Uhungarumlaut ; B 17 -18 590 805 ;
|
||||
C 201 ; WX 600 ; N Eacute ; B 53 0 550 805 ;
|
||||
C -1 ; WX 600 ; N emacron ; B 66 -15 548 565 ;
|
||||
C -1 ; WX 600 ; N gbreve ; B 45 -157 566 609 ;
|
||||
C 188 ; WX 600 ; N onequarter ; B 0 -57 600 665 ;
|
||||
C 138 ; WX 600 ; N Scaron ; B 72 -20 529 802 ;
|
||||
C -1 ; WX 600 ; N Scommaaccent ; B 72 -250 529 580 ;
|
||||
C -1 ; WX 600 ; N Ohungarumlaut ; B 43 -18 580 805 ;
|
||||
C 176 ; WX 600 ; N degree ; B 123 269 477 622 ;
|
||||
C 242 ; WX 600 ; N ograve ; B 62 -15 538 672 ;
|
||||
C -1 ; WX 600 ; N Ccaron ; B 41 -18 540 802 ;
|
||||
C 249 ; WX 600 ; N ugrave ; B 21 -15 562 672 ;
|
||||
C -1 ; WX 600 ; N radical ; B 3 -15 597 792 ;
|
||||
C -1 ; WX 600 ; N Dcaron ; B 43 0 574 802 ;
|
||||
C -1 ; WX 600 ; N rcommaaccent ; B 60 -250 559 441 ;
|
||||
C 209 ; WX 600 ; N Ntilde ; B 7 -13 593 729 ;
|
||||
C 245 ; WX 600 ; N otilde ; B 62 -15 538 606 ;
|
||||
C -1 ; WX 600 ; N Rcommaaccent ; B 38 -250 588 562 ;
|
||||
C -1 ; WX 600 ; N Lcommaaccent ; B 47 -250 554 562 ;
|
||||
C 195 ; WX 600 ; N Atilde ; B 3 0 597 729 ;
|
||||
C -1 ; WX 600 ; N Aogonek ; B 3 -172 608 562 ;
|
||||
C 197 ; WX 600 ; N Aring ; B 3 0 597 750 ;
|
||||
C 213 ; WX 600 ; N Otilde ; B 43 -18 557 729 ;
|
||||
C -1 ; WX 600 ; N zdotaccent ; B 99 0 502 620 ;
|
||||
C -1 ; WX 600 ; N Ecaron ; B 53 0 550 802 ;
|
||||
C -1 ; WX 600 ; N Iogonek ; B 96 -172 504 562 ;
|
||||
C -1 ; WX 600 ; N kcommaaccent ; B 43 -250 580 629 ;
|
||||
C -1 ; WX 600 ; N minus ; B 80 232 520 283 ;
|
||||
C 206 ; WX 600 ; N Icircumflex ; B 96 0 504 787 ;
|
||||
C -1 ; WX 600 ; N ncaron ; B 26 0 575 669 ;
|
||||
C -1 ; WX 600 ; N tcommaaccent ; B 87 -250 530 561 ;
|
||||
C 172 ; WX 600 ; N logicalnot ; B 87 108 513 369 ;
|
||||
C 246 ; WX 600 ; N odieresis ; B 62 -15 538 620 ;
|
||||
C 252 ; WX 600 ; N udieresis ; B 21 -15 562 620 ;
|
||||
C -1 ; WX 600 ; N notequal ; B 15 -16 540 529 ;
|
||||
C -1 ; WX 600 ; N gcommaaccent ; B 45 -157 566 708 ;
|
||||
C 240 ; WX 600 ; N eth ; B 62 -15 538 629 ;
|
||||
C 158 ; WX 600 ; N zcaron ; B 99 0 502 669 ;
|
||||
C -1 ; WX 600 ; N ncommaaccent ; B 26 -250 575 441 ;
|
||||
C 185 ; WX 600 ; N onesuperior ; B 172 249 428 622 ;
|
||||
C -1 ; WX 600 ; N imacron ; B 95 0 505 565 ;
|
||||
C 128 ; WX 600 ; N Euro ; B 0 0 0 0 ;
|
||||
EndCharMetrics
|
||||
EndFontMetrics
|
||||
Binary file not shown.
+6067
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+5268
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
+6661
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3285
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+3284
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+4013
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user