feat: add ntfy and telegram notification workflows
This commit is contained in:
51
server.js
51
server.js
@@ -9,6 +9,8 @@ const { readConfig, writeConfig } = require('./services/configStore');
|
|||||||
const foodsharingClient = require('./services/foodsharingClient');
|
const foodsharingClient = require('./services/foodsharingClient');
|
||||||
const { scheduleConfig } = require('./services/pickupScheduler');
|
const { scheduleConfig } = require('./services/pickupScheduler');
|
||||||
const adminConfig = require('./services/adminConfig');
|
const adminConfig = require('./services/adminConfig');
|
||||||
|
const { readNotificationSettings, writeNotificationSettings } = require('./services/userSettingsStore');
|
||||||
|
const notificationService = require('./services/notificationService');
|
||||||
|
|
||||||
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
|
const ONE_YEAR_MS = 365 * 24 * 60 * 60 * 1000;
|
||||||
const adminEmail = (process.env.ADMIN_EMAIL || '').toLowerCase();
|
const adminEmail = (process.env.ADMIN_EMAIL || '').toLowerCase();
|
||||||
@@ -388,6 +390,55 @@ app.post('/api/config', requireAuth, (req, res) => {
|
|||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/notifications/settings', requireAuth, (req, res) => {
|
||||||
|
const userSettings = readNotificationSettings(req.session.profile.id);
|
||||||
|
const adminSettings = adminConfig.readSettings();
|
||||||
|
res.json({
|
||||||
|
settings: userSettings.notifications,
|
||||||
|
capabilities: {
|
||||||
|
ntfy: {
|
||||||
|
enabled: !!(
|
||||||
|
adminSettings.notifications?.ntfy?.enabled && adminSettings.notifications?.ntfy?.serverUrl
|
||||||
|
),
|
||||||
|
serverUrl: adminSettings.notifications?.ntfy?.serverUrl || '',
|
||||||
|
topicPrefix: adminSettings.notifications?.ntfy?.topicPrefix || ''
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
enabled: !!(
|
||||||
|
adminSettings.notifications?.telegram?.enabled && adminSettings.notifications?.telegram?.botToken
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/notifications/settings', requireAuth, (req, res) => {
|
||||||
|
const payload = {
|
||||||
|
notifications: {
|
||||||
|
ntfy: {
|
||||||
|
enabled: !!req.body?.notifications?.ntfy?.enabled,
|
||||||
|
topic: req.body?.notifications?.ntfy?.topic || '',
|
||||||
|
serverUrl: req.body?.notifications?.ntfy?.serverUrl || ''
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
enabled: !!req.body?.notifications?.telegram?.enabled,
|
||||||
|
chatId: req.body?.notifications?.telegram?.chatId || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const updated = writeNotificationSettings(req.session.profile.id, payload);
|
||||||
|
res.json(updated.notifications);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/notifications/test', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
await notificationService.sendTestNotification(req.session.profile.id, req.body?.channel);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).json({ error: error.message || 'Testbenachrichtigung fehlgeschlagen' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/api/stores', requireAuth, async (req, res) => {
|
app.get('/api/stores', requireAuth, async (req, res) => {
|
||||||
res.json(req.session.storesCache?.data || []);
|
res.json(req.session.storesCache?.data || []);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,7 +16,20 @@ const DEFAULT_SETTINGS = {
|
|||||||
storeId: '51450',
|
storeId: '51450',
|
||||||
description: 'TVS'
|
description: 'TVS'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
notifications: {
|
||||||
|
ntfy: {
|
||||||
|
enabled: false,
|
||||||
|
serverUrl: 'https://ntfy.sh',
|
||||||
|
topicPrefix: '',
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
enabled: false,
|
||||||
|
botToken: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function ensureDir() {
|
function ensureDir() {
|
||||||
@@ -45,6 +58,33 @@ function sanitizeIgnoredSlots(slots = []) {
|
|||||||
.filter((slot) => slot.storeId);
|
.filter((slot) => slot.storeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeString(value) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return String(value).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeNotifications(input = {}) {
|
||||||
|
const defaults = DEFAULT_SETTINGS.notifications;
|
||||||
|
return {
|
||||||
|
ntfy: {
|
||||||
|
enabled: !!(input?.ntfy?.enabled),
|
||||||
|
serverUrl: sanitizeString(input?.ntfy?.serverUrl || defaults.ntfy.serverUrl),
|
||||||
|
topicPrefix: sanitizeString(input?.ntfy?.topicPrefix || defaults.ntfy.topicPrefix),
|
||||||
|
username: sanitizeString(input?.ntfy?.username || defaults.ntfy.username),
|
||||||
|
password: sanitizeString(input?.ntfy?.password || defaults.ntfy.password)
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
enabled: !!(input?.telegram?.enabled),
|
||||||
|
botToken: sanitizeString(input?.telegram?.botToken || defaults.telegram.botToken)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function readSettings() {
|
function readSettings() {
|
||||||
ensureDir();
|
ensureDir();
|
||||||
if (!fs.existsSync(SETTINGS_FILE)) {
|
if (!fs.existsSync(SETTINGS_FILE)) {
|
||||||
@@ -65,7 +105,8 @@ function readSettings() {
|
|||||||
parsed.storePickupCheckDelayMs,
|
parsed.storePickupCheckDelayMs,
|
||||||
DEFAULT_SETTINGS.storePickupCheckDelayMs
|
DEFAULT_SETTINGS.storePickupCheckDelayMs
|
||||||
),
|
),
|
||||||
ignoredSlots: sanitizeIgnoredSlots(parsed.ignoredSlots)
|
ignoredSlots: sanitizeIgnoredSlots(parsed.ignoredSlots),
|
||||||
|
notifications: sanitizeNotifications(parsed.notifications)
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Konnte Admin-Einstellungen nicht lesen:', error.message);
|
console.error('Konnte Admin-Einstellungen nicht lesen:', error.message);
|
||||||
@@ -88,7 +129,11 @@ function writeSettings(patch = {}) {
|
|||||||
ignoredSlots:
|
ignoredSlots:
|
||||||
patch.ignoredSlots !== undefined
|
patch.ignoredSlots !== undefined
|
||||||
? sanitizeIgnoredSlots(patch.ignoredSlots)
|
? sanitizeIgnoredSlots(patch.ignoredSlots)
|
||||||
: current.ignoredSlots
|
: current.ignoredSlots,
|
||||||
|
notifications:
|
||||||
|
patch.notifications !== undefined
|
||||||
|
? sanitizeNotifications(patch.notifications)
|
||||||
|
: current.notifications
|
||||||
};
|
};
|
||||||
|
|
||||||
ensureDir();
|
ensureDir();
|
||||||
|
|||||||
128
services/notificationService.js
Normal file
128
services/notificationService.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const adminConfig = require('./adminConfig');
|
||||||
|
const { readNotificationSettings } = require('./userSettingsStore');
|
||||||
|
|
||||||
|
function formatDateLabel(dateInput) {
|
||||||
|
try {
|
||||||
|
const date = new Date(dateInput);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return 'unbekanntes Datum';
|
||||||
|
}
|
||||||
|
return date.toLocaleString('de-DE', {
|
||||||
|
weekday: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
} catch (_error) {
|
||||||
|
return 'unbekanntes Datum';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendNtfyNotification(adminNtfy, userNtfy, payload) {
|
||||||
|
if (!adminNtfy?.enabled || !userNtfy?.enabled || !userNtfy.topic) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const server = (userNtfy.serverUrl || adminNtfy.serverUrl || 'https://ntfy.sh').replace(/\/+$/, '');
|
||||||
|
const topicPrefix = adminNtfy.topicPrefix ? `${adminNtfy.topicPrefix.replace(/\/+$/, '')}/` : '';
|
||||||
|
const topic = `${topicPrefix}${userNtfy.topic.replace(/^\//, '')}`;
|
||||||
|
const url = `${server}/${topic}`;
|
||||||
|
const headers = {
|
||||||
|
Title: payload.title,
|
||||||
|
Priority: payload.priority || 'default'
|
||||||
|
};
|
||||||
|
if (adminNtfy.username && adminNtfy.password) {
|
||||||
|
const credentials = Buffer.from(`${adminNtfy.username}:${adminNtfy.password}`).toString('base64');
|
||||||
|
headers.Authorization = `Basic ${credentials}`;
|
||||||
|
}
|
||||||
|
await axios.post(url, payload.message, { headers, timeout: 15000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendTelegramNotification(adminTelegram, userTelegram, payload) {
|
||||||
|
if (!adminTelegram?.enabled || !adminTelegram.botToken || !userTelegram?.enabled || !userTelegram.chatId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const endpoint = `https://api.telegram.org/bot${adminTelegram.botToken}/sendMessage`;
|
||||||
|
await axios.post(
|
||||||
|
endpoint,
|
||||||
|
{
|
||||||
|
chat_id: userTelegram.chatId,
|
||||||
|
text: `${payload.title}\n${payload.message}`,
|
||||||
|
disable_web_page_preview: true
|
||||||
|
},
|
||||||
|
{ timeout: 15000 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function notifyChannels(profileId, template) {
|
||||||
|
const adminSettings = adminConfig.readSettings();
|
||||||
|
const userSettings = readNotificationSettings(profileId);
|
||||||
|
try {
|
||||||
|
await Promise.allSettled([
|
||||||
|
sendNtfyNotification(adminSettings.notifications?.ntfy, userSettings.notifications?.ntfy, template),
|
||||||
|
sendTelegramNotification(adminSettings.notifications?.telegram, userSettings.notifications?.telegram, template)
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[NOTIFICATIONS] Versand fehlgeschlagen:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendSlotNotification({ profileId, storeName, pickupDate, onlyNotify, booked }) {
|
||||||
|
const dateLabel = formatDateLabel(pickupDate);
|
||||||
|
const title = onlyNotify
|
||||||
|
? `Slot verfügbar bei ${storeName}`
|
||||||
|
: booked
|
||||||
|
? `Slot gebucht bei ${storeName}`
|
||||||
|
: `Slot gefunden bei ${storeName}`;
|
||||||
|
const message = onlyNotify
|
||||||
|
? `Es wurde ein freier Slot am ${dateLabel} entdeckt.`
|
||||||
|
: booked
|
||||||
|
? `Der Slot am ${dateLabel} wurde erfolgreich gebucht.`
|
||||||
|
: `Es wurde ein Slot am ${dateLabel} gefunden.`;
|
||||||
|
|
||||||
|
await notifyChannels(profileId, {
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
priority: booked ? 'high' : 'default'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendTestNotification(profileId, channel) {
|
||||||
|
const title = 'Pickup Benachrichtigung (Test)';
|
||||||
|
const message = 'Das ist eine Testnachricht. Bei Fragen wende dich bitte an den Admin.';
|
||||||
|
const adminSettings = adminConfig.readSettings();
|
||||||
|
const userSettings = readNotificationSettings(profileId);
|
||||||
|
const tasks = [];
|
||||||
|
|
||||||
|
if (!channel || channel === 'ntfy') {
|
||||||
|
tasks.push(
|
||||||
|
sendNtfyNotification(adminSettings.notifications?.ntfy, userSettings.notifications?.ntfy, {
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
priority: 'default'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!channel || channel === 'telegram') {
|
||||||
|
tasks.push(
|
||||||
|
sendTelegramNotification(adminSettings.notifications?.telegram, userSettings.notifications?.telegram, {
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
priority: 'default'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tasks.length === 0) {
|
||||||
|
throw new Error('Kein unterstützter Kanal oder Kanal deaktiviert.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sendSlotNotification,
|
||||||
|
sendTestNotification
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ const cron = require('node-cron');
|
|||||||
const foodsharingClient = require('./foodsharingClient');
|
const foodsharingClient = require('./foodsharingClient');
|
||||||
const sessionStore = require('./sessionStore');
|
const sessionStore = require('./sessionStore');
|
||||||
const { DEFAULT_SETTINGS } = require('./adminConfig');
|
const { DEFAULT_SETTINGS } = require('./adminConfig');
|
||||||
|
const notificationService = require('./notificationService');
|
||||||
|
|
||||||
const weekdayMap = {
|
const weekdayMap = {
|
||||||
Montag: 'Monday',
|
Montag: 'Monday',
|
||||||
@@ -37,7 +38,20 @@ function resolveSettings(settings) {
|
|||||||
initialDelayMaxSeconds: Number.isFinite(settings.initialDelayMaxSeconds)
|
initialDelayMaxSeconds: Number.isFinite(settings.initialDelayMaxSeconds)
|
||||||
? settings.initialDelayMaxSeconds
|
? settings.initialDelayMaxSeconds
|
||||||
: DEFAULT_SETTINGS.initialDelayMaxSeconds,
|
: DEFAULT_SETTINGS.initialDelayMaxSeconds,
|
||||||
ignoredSlots: Array.isArray(settings.ignoredSlots) ? settings.ignoredSlots : DEFAULT_SETTINGS.ignoredSlots
|
ignoredSlots: Array.isArray(settings.ignoredSlots) ? settings.ignoredSlots : DEFAULT_SETTINGS.ignoredSlots,
|
||||||
|
notifications: {
|
||||||
|
ntfy: {
|
||||||
|
enabled: !!settings.notifications?.ntfy?.enabled,
|
||||||
|
serverUrl: settings.notifications?.ntfy?.serverUrl || DEFAULT_SETTINGS.notifications.ntfy.serverUrl,
|
||||||
|
topicPrefix: settings.notifications?.ntfy?.topicPrefix || DEFAULT_SETTINGS.notifications.ntfy.topicPrefix,
|
||||||
|
username: settings.notifications?.ntfy?.username || DEFAULT_SETTINGS.notifications.ntfy.username,
|
||||||
|
password: settings.notifications?.ntfy?.password || DEFAULT_SETTINGS.notifications.ntfy.password
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
enabled: !!settings.notifications?.telegram?.enabled,
|
||||||
|
botToken: settings.notifications?.telegram?.botToken || DEFAULT_SETTINGS.notifications.telegram.botToken
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +161,13 @@ async function processBooking(session, entry, pickup) {
|
|||||||
|
|
||||||
if (entry.onlyNotify) {
|
if (entry.onlyNotify) {
|
||||||
console.log(`[INFO] Slot gefunden (nur Hinweis) für ${storeName} am ${readableDate}`);
|
console.log(`[INFO] Slot gefunden (nur Hinweis) für ${storeName} am ${readableDate}`);
|
||||||
|
await notificationService.sendSlotNotification({
|
||||||
|
profileId: session.profile.id,
|
||||||
|
storeName,
|
||||||
|
pickupDate: pickup.date,
|
||||||
|
onlyNotify: true,
|
||||||
|
booked: false
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +180,13 @@ async function processBooking(session, entry, pickup) {
|
|||||||
}
|
}
|
||||||
await foodsharingClient.bookSlot(entry.id, utcDate, session.profile.id, session);
|
await foodsharingClient.bookSlot(entry.id, utcDate, session.profile.id, session);
|
||||||
console.log(`[SUCCESS] Slot gebucht für ${storeName} am ${readableDate}`);
|
console.log(`[SUCCESS] Slot gebucht für ${storeName} am ${readableDate}`);
|
||||||
|
await notificationService.sendSlotNotification({
|
||||||
|
profileId: session.profile.id,
|
||||||
|
storeName,
|
||||||
|
pickupDate: pickup.date,
|
||||||
|
onlyNotify: false,
|
||||||
|
booked: true
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[ERROR] Buchung für ${storeName} am ${readableDate} fehlgeschlagen:`, error.message);
|
console.error(`[ERROR] Buchung für ${storeName} am ${readableDate} fehlgeschlagen:`, error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
108
services/userSettingsStore.js
Normal file
108
services/userSettingsStore.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const SETTINGS_DIR = path.join(__dirname, '..', 'config');
|
||||||
|
|
||||||
|
const DEFAULT_USER_SETTINGS = {
|
||||||
|
notifications: {
|
||||||
|
ntfy: {
|
||||||
|
enabled: false,
|
||||||
|
topic: '',
|
||||||
|
serverUrl: ''
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
enabled: false,
|
||||||
|
chatId: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function ensureDir() {
|
||||||
|
if (!fs.existsSync(SETTINGS_DIR)) {
|
||||||
|
fs.mkdirSync(SETTINGS_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSettingsPath(profileId = 'shared') {
|
||||||
|
return path.join(SETTINGS_DIR, `${profileId}-notifications.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeBoolean(value) {
|
||||||
|
return !!value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeString(value) {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateSettingsFile(profileId) {
|
||||||
|
ensureDir();
|
||||||
|
const filePath = getSettingsPath(profileId);
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(DEFAULT_USER_SETTINGS, null, 2));
|
||||||
|
}
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNotificationSettings(profileId) {
|
||||||
|
const filePath = hydrateSettingsFile(profileId);
|
||||||
|
try {
|
||||||
|
const raw = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return {
|
||||||
|
notifications: {
|
||||||
|
ntfy: {
|
||||||
|
enabled: sanitizeBoolean(parsed?.notifications?.ntfy?.enabled),
|
||||||
|
topic: sanitizeString(parsed?.notifications?.ntfy?.topic),
|
||||||
|
serverUrl: sanitizeString(parsed?.notifications?.ntfy?.serverUrl)
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
enabled: sanitizeBoolean(parsed?.notifications?.telegram?.enabled),
|
||||||
|
chatId: sanitizeString(parsed?.notifications?.telegram?.chatId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[NOTIFICATIONS] Konnte Einstellungen für ${profileId} nicht lesen:`, error.message);
|
||||||
|
return JSON.parse(JSON.stringify(DEFAULT_USER_SETTINGS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeNotificationSettings(profileId, patch = {}) {
|
||||||
|
const current = readNotificationSettings(profileId);
|
||||||
|
const next = {
|
||||||
|
notifications: {
|
||||||
|
ntfy: {
|
||||||
|
enabled: sanitizeBoolean(patch?.notifications?.ntfy?.enabled ?? current.notifications.ntfy.enabled),
|
||||||
|
topic: sanitizeString(patch?.notifications?.ntfy?.topic ?? current.notifications.ntfy.topic),
|
||||||
|
serverUrl: sanitizeString(
|
||||||
|
patch?.notifications?.ntfy?.serverUrl ?? current.notifications.ntfy.serverUrl
|
||||||
|
)
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
enabled: sanitizeBoolean(
|
||||||
|
patch?.notifications?.telegram?.enabled ?? current.notifications.telegram.enabled
|
||||||
|
),
|
||||||
|
chatId: sanitizeString(
|
||||||
|
patch?.notifications?.telegram?.chatId ?? current.notifications.telegram.chatId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filePath = hydrateSettingsFile(profileId);
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(next, null, 2));
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DEFAULT_USER_SETTINGS,
|
||||||
|
readNotificationSettings,
|
||||||
|
writeNotificationSettings
|
||||||
|
};
|
||||||
458
src/App.js
458
src/App.js
@@ -57,6 +57,16 @@ const buildSelectionRange = (start, end, minDate) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const defaultNotificationSettings = {
|
||||||
|
ntfy: { enabled: false, topic: '', serverUrl: '' },
|
||||||
|
telegram: { enabled: false, chatId: '' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultNotificationCapabilities = {
|
||||||
|
ntfy: { enabled: false, serverUrl: '', topicPrefix: '' },
|
||||||
|
telegram: { enabled: false }
|
||||||
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [session, setSession] = useState(null);
|
const [session, setSession] = useState(null);
|
||||||
const [credentials, setCredentials] = useState({ email: '', password: '' });
|
const [credentials, setCredentials] = useState({ email: '', password: '' });
|
||||||
@@ -83,6 +93,13 @@ function App() {
|
|||||||
const [dirtyDialogSaving, setDirtyDialogSaving] = useState(false);
|
const [dirtyDialogSaving, setDirtyDialogSaving] = useState(false);
|
||||||
const [confirmDialog, setConfirmDialog] = useState({ open: false, resolve: null });
|
const [confirmDialog, setConfirmDialog] = useState({ open: false, resolve: null });
|
||||||
const [activeRangePicker, setActiveRangePicker] = useState(null);
|
const [activeRangePicker, setActiveRangePicker] = useState(null);
|
||||||
|
const [notificationSettings, setNotificationSettings] = useState(defaultNotificationSettings);
|
||||||
|
const [notificationCapabilities, setNotificationCapabilities] = useState(defaultNotificationCapabilities);
|
||||||
|
const [notificationDirty, setNotificationDirty] = useState(false);
|
||||||
|
const [notificationLoading, setNotificationLoading] = useState(false);
|
||||||
|
const [notificationSaving, setNotificationSaving] = useState(false);
|
||||||
|
const [notificationMessage, setNotificationMessage] = useState('');
|
||||||
|
const [notificationError, setNotificationError] = useState('');
|
||||||
const minSelectableDate = useMemo(() => startOfDay(new Date()), []);
|
const minSelectableDate = useMemo(() => startOfDay(new Date()), []);
|
||||||
|
|
||||||
const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'];
|
const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'];
|
||||||
@@ -192,7 +209,20 @@ function App() {
|
|||||||
storeId: slot?.storeId ? String(slot.storeId) : '',
|
storeId: slot?.storeId ? String(slot.storeId) : '',
|
||||||
description: slot?.description || ''
|
description: slot?.description || ''
|
||||||
}))
|
}))
|
||||||
: []
|
: [],
|
||||||
|
notifications: {
|
||||||
|
ntfy: {
|
||||||
|
enabled: !!raw.notifications?.ntfy?.enabled,
|
||||||
|
serverUrl: raw.notifications?.ntfy?.serverUrl || 'https://ntfy.sh',
|
||||||
|
topicPrefix: raw.notifications?.ntfy?.topicPrefix || '',
|
||||||
|
username: raw.notifications?.ntfy?.username || '',
|
||||||
|
password: raw.notifications?.ntfy?.password || ''
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
enabled: !!raw.notifications?.telegram?.enabled,
|
||||||
|
botToken: raw.notifications?.telegram?.botToken || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -206,6 +236,13 @@ function App() {
|
|||||||
setAdminSettingsLoading(false);
|
setAdminSettingsLoading(false);
|
||||||
setAvailableCollapsed(true);
|
setAvailableCollapsed(true);
|
||||||
setInitializing(false);
|
setInitializing(false);
|
||||||
|
setNotificationSettings(defaultNotificationSettings);
|
||||||
|
setNotificationCapabilities(defaultNotificationCapabilities);
|
||||||
|
setNotificationDirty(false);
|
||||||
|
setNotificationError('');
|
||||||
|
setNotificationMessage('');
|
||||||
|
setNotificationLoading(false);
|
||||||
|
setNotificationSaving(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleUnauthorized = useCallback(() => {
|
const handleUnauthorized = useCallback(() => {
|
||||||
@@ -290,6 +327,47 @@ function App() {
|
|||||||
[handleUnauthorized, session?.token]
|
[handleUnauthorized, session?.token]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const loadNotificationSettings = useCallback(async () => {
|
||||||
|
if (!session?.token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setNotificationLoading(true);
|
||||||
|
setNotificationError('');
|
||||||
|
try {
|
||||||
|
const response = await authorizedFetch('/api/notifications/settings');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
setNotificationSettings({
|
||||||
|
ntfy: {
|
||||||
|
enabled: Boolean(data?.settings?.ntfy?.enabled),
|
||||||
|
topic: data?.settings?.ntfy?.topic || '',
|
||||||
|
serverUrl: data?.settings?.ntfy?.serverUrl || ''
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
enabled: Boolean(data?.settings?.telegram?.enabled),
|
||||||
|
chatId: data?.settings?.telegram?.chatId || ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setNotificationCapabilities({
|
||||||
|
ntfy: {
|
||||||
|
enabled: Boolean(data?.capabilities?.ntfy?.enabled),
|
||||||
|
serverUrl: data?.capabilities?.ntfy?.serverUrl || '',
|
||||||
|
topicPrefix: data?.capabilities?.ntfy?.topicPrefix || ''
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
enabled: Boolean(data?.capabilities?.telegram?.enabled)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setNotificationDirty(false);
|
||||||
|
} catch (err) {
|
||||||
|
setNotificationError(`Benachrichtigungseinstellungen konnten nicht geladen werden: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setNotificationLoading(false);
|
||||||
|
}
|
||||||
|
}, [authorizedFetch, session?.token]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!session?.token || !session.isAdmin) {
|
if (!session?.token || !session.isAdmin) {
|
||||||
setAdminSettings(null);
|
setAdminSettings(null);
|
||||||
@@ -445,6 +523,13 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [session?.token, authorizedFetch]);
|
}, [session?.token, authorizedFetch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!session?.token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadNotificationSettings();
|
||||||
|
}, [session?.token, loadNotificationSettings]);
|
||||||
|
|
||||||
const syncStoresWithProgress = useCallback(
|
const syncStoresWithProgress = useCallback(
|
||||||
async ({ block = false, reason = 'manual', startJob = true, reuseOverlay = false, tokenOverride } = {}) => {
|
async ({ block = false, reason = 'manual', startJob = true, reuseOverlay = false, tokenOverride } = {}) => {
|
||||||
const effectiveToken = tokenOverride || session?.token;
|
const effectiveToken = tokenOverride || session?.token;
|
||||||
@@ -951,6 +1036,81 @@ function App() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNotificationFieldChange = (channel, field, value) => {
|
||||||
|
setNotificationSettings((prev) => {
|
||||||
|
const nextChannel = {
|
||||||
|
...prev[channel],
|
||||||
|
[field]: value
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[channel]: nextChannel
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setNotificationDirty(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveNotificationSettings = async () => {
|
||||||
|
if (!session?.token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setNotificationSaving(true);
|
||||||
|
setNotificationError('');
|
||||||
|
setNotificationMessage('');
|
||||||
|
try {
|
||||||
|
const response = await authorizedFetch('/api/notifications/settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ notifications: notificationSettings })
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
setNotificationSettings({
|
||||||
|
ntfy: {
|
||||||
|
enabled: Boolean(data?.ntfy?.enabled),
|
||||||
|
topic: data?.ntfy?.topic || notificationSettings.ntfy.topic,
|
||||||
|
serverUrl: data?.ntfy?.serverUrl || notificationSettings.ntfy.serverUrl
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
enabled: Boolean(data?.telegram?.enabled),
|
||||||
|
chatId: data?.telegram?.chatId || notificationSettings.telegram.chatId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setNotificationDirty(false);
|
||||||
|
setNotificationMessage('Benachrichtigungseinstellungen gespeichert.');
|
||||||
|
setTimeout(() => setNotificationMessage(''), 4000);
|
||||||
|
} catch (err) {
|
||||||
|
setNotificationError(`Speichern der Benachrichtigungen fehlgeschlagen: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setNotificationSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendNotificationTest = async (channel) => {
|
||||||
|
if (!session?.token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setNotificationError('');
|
||||||
|
setNotificationMessage('');
|
||||||
|
try {
|
||||||
|
const response = await authorizedFetch('/api/notifications/test', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ channel })
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
await response.json();
|
||||||
|
setNotificationMessage('Testbenachrichtigung gesendet.');
|
||||||
|
setTimeout(() => setNotificationMessage(''), 4000);
|
||||||
|
} catch (err) {
|
||||||
|
setNotificationError(`Testbenachrichtigung fehlgeschlagen: ${err.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAdminSettingChange = (field, value, isNumber = false) => {
|
const handleAdminSettingChange = (field, value, isNumber = false) => {
|
||||||
setAdminSettings((prev) => {
|
setAdminSettings((prev) => {
|
||||||
if (!prev) {
|
if (!prev) {
|
||||||
@@ -967,6 +1127,24 @@ function App() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAdminNotificationChange = (channel, field, value) => {
|
||||||
|
setAdminSettings((prev) => {
|
||||||
|
if (!prev) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
notifications: {
|
||||||
|
...(prev.notifications || {}),
|
||||||
|
[channel]: {
|
||||||
|
...(prev.notifications?.[channel] || {}),
|
||||||
|
[field]: value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleIgnoredSlotChange = (index, field, value) => {
|
const handleIgnoredSlotChange = (index, field, value) => {
|
||||||
setAdminSettings((prev) => {
|
setAdminSettings((prev) => {
|
||||||
if (!prev) {
|
if (!prev) {
|
||||||
@@ -1034,7 +1212,20 @@ function App() {
|
|||||||
ignoredSlots: (adminSettings.ignoredSlots || []).map((slot) => ({
|
ignoredSlots: (adminSettings.ignoredSlots || []).map((slot) => ({
|
||||||
storeId: slot.storeId || '',
|
storeId: slot.storeId || '',
|
||||||
description: slot.description || ''
|
description: slot.description || ''
|
||||||
}))
|
})),
|
||||||
|
notifications: {
|
||||||
|
ntfy: {
|
||||||
|
enabled: !!adminSettings.notifications?.ntfy?.enabled,
|
||||||
|
serverUrl: adminSettings.notifications?.ntfy?.serverUrl || '',
|
||||||
|
topicPrefix: adminSettings.notifications?.ntfy?.topicPrefix || '',
|
||||||
|
username: adminSettings.notifications?.ntfy?.username || '',
|
||||||
|
password: adminSettings.notifications?.ntfy?.password || ''
|
||||||
|
},
|
||||||
|
telegram: {
|
||||||
|
enabled: !!adminSettings.notifications?.telegram?.enabled,
|
||||||
|
botToken: adminSettings.notifications?.telegram?.botToken || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await authorizedFetch('/api/admin/settings', {
|
const response = await authorizedFetch('/api/admin/settings', {
|
||||||
@@ -1240,6 +1431,165 @@ function App() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-6 border border-gray-200 rounded-lg p-4 bg-gray-50">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800">Benachrichtigungen</h2>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Erhalte Hinweise über ntfy oder Telegram, sobald Slots gefunden oder gebucht wurden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{notificationLoading && <span className="text-sm text-gray-500">Lade Einstellungen…</span>}
|
||||||
|
</div>
|
||||||
|
{notificationError && (
|
||||||
|
<div className="mt-4 bg-red-100 border border-red-300 text-red-700 px-4 py-2 rounded">
|
||||||
|
{notificationError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{notificationMessage && (
|
||||||
|
<div className="mt-4 bg-green-100 border border-green-300 text-green-700 px-4 py-2 rounded">
|
||||||
|
{notificationMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="grid md:grid-cols-2 gap-4 mt-4">
|
||||||
|
<div className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800">ntfy</h3>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Push aufs Handy über die ntfy-App oder Browser (Themenkanal notwendig).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label className="inline-flex items-center space-x-2 text-sm text-gray-700">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={notificationSettings.ntfy.enabled}
|
||||||
|
onChange={(e) => handleNotificationFieldChange('ntfy', 'enabled', e.target.checked)}
|
||||||
|
disabled={!notificationCapabilities.ntfy.enabled}
|
||||||
|
className="h-4 w-4 text-blue-600 rounded focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span>Aktiv</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{!notificationCapabilities.ntfy.enabled ? (
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Diese Option wurde vom Admin deaktiviert. Bitte frage nach, wenn du ntfy nutzen möchtest.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-600 mb-1">Topic / Kanal</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={notificationSettings.ntfy.topic}
|
||||||
|
onChange={(e) => handleNotificationFieldChange('ntfy', 'topic', e.target.value)}
|
||||||
|
placeholder="z. B. mein-pickup-topic"
|
||||||
|
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
disabled={!notificationSettings.ntfy.enabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-600 mb-1">
|
||||||
|
Eigener Server (optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={notificationSettings.ntfy.serverUrl}
|
||||||
|
onChange={(e) => handleNotificationFieldChange('ntfy', 'serverUrl', e.target.value)}
|
||||||
|
placeholder={notificationCapabilities.ntfy.serverUrl || 'https://ntfy.sh'}
|
||||||
|
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
disabled={!notificationSettings.ntfy.enabled}
|
||||||
|
/>
|
||||||
|
{notificationCapabilities.ntfy.topicPrefix && (
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Vom Admin vorgegebenes Präfix: {notificationCapabilities.ntfy.topicPrefix}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => sendNotificationTest('ntfy')}
|
||||||
|
disabled={!notificationSettings.ntfy.enabled || notificationSaving}
|
||||||
|
className="text-sm text-blue-600 hover:text-blue-800"
|
||||||
|
>
|
||||||
|
Test senden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-800">Telegram</h3>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Nutze den Bot des Admins, um Nachrichten direkt in Telegram zu erhalten.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label className="inline-flex items-center space-x-2 text-sm text-gray-700">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={notificationSettings.telegram.enabled}
|
||||||
|
onChange={(e) => handleNotificationFieldChange('telegram', 'enabled', e.target.checked)}
|
||||||
|
disabled={!notificationCapabilities.telegram.enabled}
|
||||||
|
className="h-4 w-4 text-blue-600 rounded focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span>Aktiv</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{!notificationCapabilities.telegram.enabled ? (
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Telegram-Benachrichtigungen sind derzeit deaktiviert oder der Bot ist nicht konfiguriert.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-600 mb-1">Chat-ID</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={notificationSettings.telegram.chatId}
|
||||||
|
onChange={(e) => handleNotificationFieldChange('telegram', 'chatId', e.target.value)}
|
||||||
|
placeholder="z. B. 123456789"
|
||||||
|
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
disabled={!notificationSettings.telegram.enabled}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Tipp: Schreibe dem Telegram-Bot und nutze @userinfobot oder ein Chat-ID-Tool.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => sendNotificationTest('telegram')}
|
||||||
|
disabled={!notificationSettings.telegram.enabled || notificationSaving}
|
||||||
|
className="text-sm text-blue-600 hover:text-blue-800"
|
||||||
|
>
|
||||||
|
Test senden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end mt-4 gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={loadNotificationSettings}
|
||||||
|
className="text-sm text-gray-600 hover:text-gray-900"
|
||||||
|
disabled={notificationLoading}
|
||||||
|
>
|
||||||
|
Zurücksetzen
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={saveNotificationSettings}
|
||||||
|
disabled={!notificationDirty || notificationSaving}
|
||||||
|
className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{notificationSaving ? 'Speichere...' : 'Benachrichtigungen speichern'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-center mb-4 space-x-4">
|
<div className="flex justify-center mb-4 space-x-4">
|
||||||
<a
|
<a
|
||||||
href="https://foodsharing.de/?page=dashboard"
|
href="https://foodsharing.de/?page=dashboard"
|
||||||
@@ -1586,6 +1936,110 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<h2 className="text-lg font-semibold text-purple-900 mb-2">Benachrichtigungen</h2>
|
||||||
|
<p className="text-sm text-gray-600 mb-4">
|
||||||
|
Lege fest, welche Kanäle zur Verfügung stehen und welche Zugangsdaten verwendet werden.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="bg-white border border-purple-100 rounded-lg p-4 shadow-sm">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-purple-900">ntfy</h3>
|
||||||
|
<p className="text-xs text-gray-500">Server-basierte Push-Benachrichtigungen</p>
|
||||||
|
</div>
|
||||||
|
<label className="inline-flex items-center space-x-2 text-sm text-gray-600">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={adminSettings.notifications?.ntfy?.enabled || false}
|
||||||
|
onChange={(e) => handleAdminNotificationChange('ntfy', 'enabled', e.target.checked)}
|
||||||
|
className="h-4 w-4 text-purple-600 rounded focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
<span>Aktiv</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-600 mb-1">Server-URL</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={adminSettings.notifications?.ntfy?.serverUrl || ''}
|
||||||
|
onChange={(e) => handleAdminNotificationChange('ntfy', 'serverUrl', e.target.value)}
|
||||||
|
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||||
|
placeholder="https://ntfy.sh"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-600 mb-1">Topic-Präfix</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={adminSettings.notifications?.ntfy?.topicPrefix || ''}
|
||||||
|
onChange={(e) => handleAdminNotificationChange('ntfy', 'topicPrefix', e.target.value)}
|
||||||
|
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||||
|
placeholder="optional"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Wird vor jedes User-Topic gesetzt (z. B. <code>pickup/</code>).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-600 mb-1">Benutzername</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={adminSettings.notifications?.ntfy?.username || ''}
|
||||||
|
onChange={(e) => handleAdminNotificationChange('ntfy', 'username', e.target.value)}
|
||||||
|
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||||
|
placeholder="optional"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-600 mb-1">Passwort</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={adminSettings.notifications?.ntfy?.password || ''}
|
||||||
|
onChange={(e) => handleAdminNotificationChange('ntfy', 'password', e.target.value)}
|
||||||
|
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||||
|
placeholder="optional"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white border border-purple-100 rounded-lg p-4 shadow-sm">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-purple-900">Telegram</h3>
|
||||||
|
<p className="text-xs text-gray-500">Benachrichtigungen über einen Bot</p>
|
||||||
|
</div>
|
||||||
|
<label className="inline-flex items-center space-x-2 text-sm text-gray-600">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={adminSettings.notifications?.telegram?.enabled || false}
|
||||||
|
onChange={(e) => handleAdminNotificationChange('telegram', 'enabled', e.target.checked)}
|
||||||
|
className="h-4 w-4 text-purple-600 rounded focus:ring-purple-500"
|
||||||
|
/>
|
||||||
|
<span>Aktiv</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-semibold text-gray-600 mb-1">Bot Token</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={adminSettings.notifications?.telegram?.botToken || ''}
|
||||||
|
onChange={(e) => handleAdminNotificationChange('telegram', 'botToken', e.target.value)}
|
||||||
|
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||||
|
placeholder="123456:ABCDEF"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Erstelle einen Bot via @BotFather und trage den Token hier ein.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="border border-purple-200 rounded-lg bg-white p-4">
|
<div className="border border-purple-200 rounded-lg bg-white p-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h3 className="font-semibold text-purple-900">Ignorierte Slots</h3>
|
<h3 className="font-semibold text-purple-900">Ignorierte Slots</h3>
|
||||||
|
|||||||
Reference in New Issue
Block a user