AstraOS-Panel/AstraOS/Frontend/HTML/notifications.html
elias a6ae67c26b
All checks were successful
Deploy AstraOS Panel / deploy (push) Successful in 12s
AstraOS Ver. 1.44
Benachrichtigungen gefixt,
Overrider der Discord Verifikation Funktion
2026-08-18 18:57:50 +02:00

294 lines
17 KiB
HTML

{% extends "sidebar.html" %}
{% block title %}AstraOS — Benachrichtigungen{% endblock %}
{% block content %}
<script id="notifications-data" type="application/json">{{ notifications | tojson | safe }}</script>
<div x-data="{
showSendModal: false,
sending: false,
form: { title: '', message: '', type: 'info' },
notifications: [],
init() {
this.notifications = JSON.parse(document.getElementById('notifications-data').textContent);
},
async sendNotification() {
if (!this.form.title || !this.form.message) return;
this.sending = true;
try {
const resp = await fetch('/api/notifications/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': document.querySelector('meta[name=csrf-token]').content },
body: JSON.stringify(this.form)
});
const data = await resp.json();
if (data.success) {
this.showSendModal = false;
this.form = { title: '', message: '', type: 'info' };
window.location.reload();
} else {
alert('Fehler: ' + (data.error || 'Unbekannter Fehler'));
}
} catch(e) { alert('Netzwerkfehler'); }
this.sending = false;
},
async markRead(id, index) {
await fetch('/api/notifications/' + id + '/read', {
method: 'POST',
headers: { 'X-CSRFToken': document.querySelector('meta[name=csrf-token]').content }
});
this.notifications[index].is_read = true;
this.updateBadge();
},
async takeAction(id, action, index) {
const resp = await fetch('/api/notifications/' + id + '/action', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': document.querySelector('meta[name=csrf-token]').content },
body: JSON.stringify({ action })
});
const data = await resp.json();
if (data.success) {
this.notifications[index].action_status = action;
this.notifications[index].is_read = true;
this.updateBadge();
}
},
async deleteNotif(id, index) {
if (!confirm('Diese Benachrichtigung wirklich loschen?')) return;
await fetch('/api/notifications/' + id + '/delete', {
method: 'POST',
headers: { 'X-CSRFToken': document.querySelector('meta[name=csrf-token]').content }
});
this.notifications.splice(index, 1);
},
updateBadge() {
const unread = this.notifications.filter(n => !n.is_read).length;
const badge = document.getElementById('notif-badge');
if (badge) { badge.textContent = unread; badge.style.display = unread > 0 ? 'flex' : 'none'; }
},
typeLabel(t) {
return { info: 'Info', warning: 'Warnung', action: 'Aktion', reactivation_request: 'Reaktivierungsanfrage' }[t] || t;
},
actionLabel(s) {
return { pending: 'Ausstehend', approved: 'Genehmigt', rejected: 'Abgelehnt' }[s] || s;
},
formatDate(d) {
if (!d) return 'N/A';
const dt = new Date(d['$date'] !== undefined ? d['$date'] : d);
return isNaN(dt.getTime()) ? String(d) : dt.toLocaleString('de-DE');
}
}" class="space-y-8">
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Header -->
<header class="flex justify-between items-center">
<div>
<h2 class="text-4xl font-bold tracking-wide {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">
Benachrichtigungen
</h2>
<p class="text-gray-400 mt-1 text-sm">
<span x-text="notifications.filter(n => !n.is_read).length"></span> ungelesen &middot;
<span x-text="notifications.length"></span> gesamt
</p>
</div>
<button @click="showSendModal = true"
class="font-bold py-2 px-5 rounded-lg transition text-white flex items-center gap-2
{% if maintenance_active %}bg-yellow-600 hover:bg-yellow-700{% else %}bg-purple-600 hover:bg-purple-700{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/>
</svg>
Benachrichtigung senden
</button>
</header>
<!-- Notification List -->
<div class="space-y-4">
<template x-if="notifications.length === 0">
<div class="text-center py-20 text-gray-500 bg-black/30 rounded-xl border {% if maintenance_active %}border-yellow-500/20{% else %}border-purple-500/20{% endif %}">
<svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 mx-auto mb-4 opacity-30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
</svg>
<p class="text-lg">Keine Benachrichtigungen vorhanden.</p>
</div>
</template>
<template x-for="(notif, index) in notifications" :key="notif._id">
<div class="relative rounded-xl p-5 backdrop-blur-lg transition-all duration-300 border"
:class="notif.is_read
? 'bg-black/20 border-gray-700/40'
: '{% if maintenance_active %}bg-yellow-500/5 border-yellow-500/40 shadow-yellow-500/10{% else %}bg-purple-500/5 border-purple-500/40 shadow-purple-500/10{% endif %} shadow-lg'">
<!-- Unread dot -->
<div x-show="!notif.is_read"
class="absolute top-4 right-4 w-2.5 h-2.5 rounded-full {% if maintenance_active %}bg-yellow-400{% else %}bg-purple-400{% endif %} animate-pulse">
</div>
<div class="flex items-start gap-4">
<!-- Type Icon -->
<div class="flex-shrink-0 w-11 h-11 rounded-full flex items-center justify-center text-xl"
:class="{
'bg-blue-500/20': notif.type === 'info',
'bg-orange-500/20': notif.type === 'warning',
'bg-red-500/20': notif.type === 'action',
'bg-yellow-500/20': notif.type === 'reactivation_request'
}">
<span x-text="{ info: '&#128226;', warning: '&#9888;&#65039;', action: '&#128276;', reactivation_request: '&#128275;' }[notif.type] || '&#128226;'"></span>
</div>
<div class="flex-1 min-w-0">
<!-- Title row -->
<div class="flex items-center gap-3 flex-wrap mb-1">
<h3 class="font-semibold text-white text-base" x-text="notif.title"></h3>
<span class="px-2 py-0.5 rounded-full text-xs font-medium"
:class="{
'bg-blue-500/20 text-blue-300': notif.type === 'info',
'bg-orange-500/20 text-orange-300': notif.type === 'warning',
'bg-red-500/20 text-red-300': notif.type === 'action',
'bg-yellow-500/20 text-yellow-300': notif.type === 'reactivation_request'
}"
x-text="typeLabel(notif.type)">
</span>
<template x-if="notif.action_status">
<span class="px-2 py-0.5 rounded-full text-xs font-medium"
:class="{
'bg-yellow-500/20 text-yellow-300': notif.action_status === 'pending',
'bg-green-500/20 text-green-300': notif.action_status === 'approved',
'bg-red-500/20 text-red-300': notif.action_status === 'rejected'
}"
x-text="actionLabel(notif.action_status)">
</span>
</template>
</div>
<!-- Message -->
<p class="text-gray-300 text-sm whitespace-pre-wrap mb-3" x-text="notif.message"></p>
<!-- Meta -->
<div class="flex items-center gap-4 text-xs text-gray-500 flex-wrap">
<span>Von: <span class="text-gray-400" x-text="notif.created_by"></span></span>
<span>Am: <span class="text-gray-400" x-text="formatDate(notif.created_at)"></span></span>
<template x-if="notif.action_taken_by">
<span>Bearbeitet von: <span class="text-gray-400" x-text="notif.action_taken_by"></span></span>
</template>
</div>
<!-- Action Buttons -->
<div class="flex items-center gap-2 mt-3 flex-wrap">
<!-- Approve/Reject for pending action notifications -->
<template x-if="notif.requires_action && notif.action_status === 'pending'">
<div class="flex gap-2">
<button @click.stop="takeAction(notif._id, 'approved', index)"
class="px-3 py-1.5 bg-green-600 hover:bg-green-500 text-white text-xs font-semibold rounded-lg transition">
Genehmigen
</button>
<button @click.stop="takeAction(notif._id, 'rejected', index)"
class="px-3 py-1.5 bg-red-600 hover:bg-red-500 text-white text-xs font-semibold rounded-lg transition">
Ablehnen
</button>
</div>
</template>
<!-- Mark as read -->
<template x-if="!notif.is_read">
<button @click.stop="markRead(notif._id, index)"
class="px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-gray-300 text-xs rounded-lg transition">
Als gelesen markieren
</button>
</template>
<!-- Delete -->
<button @click.stop="deleteNotif(notif._id, index)"
class="px-3 py-1.5 bg-red-900/40 hover:bg-red-800/60 text-red-400 text-xs rounded-lg transition ml-auto">
Loschen
</button>
</div>
</div>
</div>
</div>
</template>
</div>
<!-- Send Notification Modal -->
<div x-show="showSendModal" @keydown.escape.window="showSendModal = false"
class="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50"
style="display: none;">
<div @click.away="showSendModal = false"
class="bg-gray-900 rounded-xl shadow-2xl p-8 w-full max-w-lg
{% if maintenance_active %}border border-yellow-500/50{% else %}border border-purple-500/50{% endif %}">
<h3 class="text-2xl font-bold mb-6 {% if maintenance_active %}text-yellow-300{% else %}text-purple-300{% endif %}">
Benachrichtigung senden
</h3>
<div class="space-y-4">
<!-- Type -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Typ</label>
<div class="flex gap-2 flex-wrap">
<button type="button" @click="form.type = 'info'"
:class="form.type === 'info' ? '{% if maintenance_active %}bg-yellow-600 border-yellow-500{% else %}bg-purple-600 border-purple-500{% endif %} text-white' : 'bg-gray-800 text-gray-300 border-gray-700 hover:border-gray-500'"
class="px-3 py-1.5 rounded-lg border text-xs font-medium transition">
Info
</button>
<button type="button" @click="form.type = 'warning'"
:class="form.type === 'warning' ? 'bg-orange-600 border-orange-500 text-white' : 'bg-gray-800 text-gray-300 border-gray-700 hover:border-gray-500'"
class="px-3 py-1.5 rounded-lg border text-xs font-medium transition">
Warnung
</button>
<button type="button" @click="form.type = 'action'"
:class="form.type === 'action' ? 'bg-red-600 border-red-500 text-white' : 'bg-gray-800 text-gray-300 border-gray-700 hover:border-gray-500'"
class="px-3 py-1.5 rounded-lg border text-xs font-medium transition">
Aktion (mit Approve/Reject-Buttons)
</button>
</div>
<p x-show="form.type === 'action'" class="text-xs text-yellow-400 mt-1">
Aktions-Benachrichtigungen enthalten interaktive Buttons im Discord und im Panel.
</p>
</div>
<!-- Title -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">Titel</label>
<input type="text" x-model="form.title" placeholder="Kurzer, pragnanter Titel..."
class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white text-sm
focus:outline-none focus:ring-2 {% if maintenance_active %}focus:ring-yellow-500{% else %}focus:ring-purple-500{% endif %}">
</div>
<!-- Message -->
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">Nachricht</label>
<textarea x-model="form.message" rows="4" placeholder="Detaillierte Nachricht..."
class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white text-sm resize-none
focus:outline-none focus:ring-2 {% if maintenance_active %}focus:ring-yellow-500{% else %}focus:ring-purple-500{% endif %}"></textarea>
</div>
<!-- Discord hint -->
<div class="bg-indigo-950/50 border border-indigo-500/30 rounded-lg p-3 text-xs text-indigo-300">
Der AstraOS Helper Discord Bot wird dem Owner automatisch eine DM senden.
<span x-show="form.type === 'action'"> Mit interaktiven Genehmigen/Ablehnen-Buttons!</span>
</div>
<!-- Actions -->
<div class="flex justify-end gap-3 pt-2">
<button @click="showSendModal = false; form = { title: '', message: '', type: 'info' }"
class="px-4 py-2 rounded-lg text-gray-300 hover:bg-gray-700 transition text-sm">
Abbrechen
</button>
<button @click="sendNotification()" :disabled="sending || !form.title || !form.message"
class="px-5 py-2 font-semibold rounded-lg transition text-white text-sm flex items-center gap-2 disabled:opacity-50
{% if maintenance_active %}bg-yellow-600 hover:bg-yellow-700{% else %}bg-purple-600 hover:bg-purple-700{% endif %}">
<svg x-show="sending" class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span x-text="sending ? 'Wird gesendet...' : 'Senden'"></span>
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}