Make Telegram Q&A, Notes, Community Questions and Email Alerts fully functional with persistent localStorage state
This commit is contained in:
@@ -31,13 +31,25 @@ interface BusRouteItem {
|
|||||||
stops?: BusStopItem[];
|
stops?: BusStopItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SeniorHelperItem {
|
||||||
|
chatId: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface TelegramConfig {
|
interface TelegramConfig {
|
||||||
community_bot_token?: string;
|
community_bot_token?: string;
|
||||||
telegram_bot_token?: string;
|
telegram_bot_token?: string;
|
||||||
helper_chat_ids?: number[];
|
|
||||||
spring_backend_url?: string;
|
spring_backend_url?: string;
|
||||||
|
helper_chat_ids?: number[];
|
||||||
|
seniorHelpers?: SeniorHelperItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ROUTE_STORAGE_KEY = 'RIT_LOCAL_BUS_ROUTES';
|
||||||
|
const TELEGRAM_STORAGE_KEY = 'RIT_LOCAL_TELEGRAM_CONFIG';
|
||||||
|
const NOTES_STORAGE_KEY = 'RIT_LOCAL_NOTES';
|
||||||
|
const QUESTIONS_STORAGE_KEY = 'RIT_LOCAL_QUESTIONS';
|
||||||
|
const RECIPIENTS_STORAGE_KEY = 'RIT_LOCAL_RECIPIENTS';
|
||||||
|
|
||||||
interface NoteItem {
|
interface NoteItem {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -390,91 +402,103 @@ export default function AdminDashboard() {
|
|||||||
setRouteFormData({ ...routeFormData, stops: updated });
|
setRouteFormData({ ...routeFormData, stops: updated });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// TELEGRAM BOT & SENIOR HELPERS HANDLERS
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
// TELEGRAM BOT & SENIOR HELPERS HANDLERS
|
// TELEGRAM BOT & SENIOR HELPERS HANDLERS
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
const fetchTelegramConfig = () => {
|
const fetchTelegramConfig = () => {
|
||||||
setLoadingTelegram(true);
|
setLoadingTelegram(true);
|
||||||
|
const saved = localStorage.getItem(TELEGRAM_STORAGE_KEY);
|
||||||
|
let localConfig: TelegramConfig | null = null;
|
||||||
|
if (saved) {
|
||||||
|
try { localConfig = JSON.parse(saved); } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
fetch(getBackendUrl('/api/admin/telegram/config'))
|
fetch(getBackendUrl('/api/admin/telegram/config'))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res.ok) throw new Error('Failed to fetch from backend');
|
if (!res.ok) throw new Error('Failed to fetch from backend');
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then((data) => setTelegramConfig(data))
|
.then((data) => {
|
||||||
|
setTelegramConfig(data);
|
||||||
|
localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(data));
|
||||||
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Offline Fallback Mock
|
if (localConfig) {
|
||||||
setTelegramConfig({
|
setTelegramConfig(localConfig);
|
||||||
botToken: '782914X:AAH_mock_token_for_demo_environment',
|
} else {
|
||||||
seniorHelpers: [
|
const defaultConfig: TelegramConfig = {
|
||||||
{ chatId: 123456789, name: 'Rahul (CSE 4th Yr)' },
|
community_bot_token: '7829148291:AAH_rit_community_bot_token_sample',
|
||||||
{ chatId: 987654321, name: 'Sneha (ECE 3rd Yr)' }
|
spring_backend_url: 'https://rit-services.in/api',
|
||||||
]
|
helper_chat_ids: [971749136, 982314512],
|
||||||
});
|
seniorHelpers: [
|
||||||
|
{ chatId: 971749136, name: 'Rahul (CSE 4th Yr)' },
|
||||||
|
{ chatId: 982314512, name: 'Sneha (ECE 3rd Yr)' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
setTelegramConfig(defaultConfig);
|
||||||
|
localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(defaultConfig));
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoadingTelegram(false));
|
.finally(() => setLoadingTelegram(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddSeniorHelper = (e: React.FormEvent) => {
|
const handleAddSeniorHelper = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!newHelperChatId.trim()) {
|
if (!newHelperChatId.trim() || isNaN(Number(newHelperChatId.trim()))) {
|
||||||
showToast('error', 'Please enter a valid numeric Telegram Chat ID.');
|
showToast('error', 'Please enter a valid numeric Telegram Chat ID.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const chatIdNum = parseInt(newHelperChatId.trim());
|
||||||
|
const helperNameStr = newHelperName.trim() || 'Senior Responder';
|
||||||
|
|
||||||
|
const currentHelpers = telegramConfig?.seniorHelpers || [];
|
||||||
|
const currentChatIds = telegramConfig?.helper_chat_ids || [];
|
||||||
|
|
||||||
|
if (currentHelpers.some(h => h.chatId === chatIdNum) || currentChatIds.includes(chatIdNum)) {
|
||||||
|
showToast('error', `Chat ID ${chatIdNum} is already registered as a responder.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedHelpers = [...currentHelpers, { chatId: chatIdNum, name: helperNameStr }];
|
||||||
|
const updatedChatIds = [...currentChatIds, chatIdNum];
|
||||||
|
const updatedConfig: TelegramConfig = {
|
||||||
|
...telegramConfig,
|
||||||
|
helper_chat_ids: updatedChatIds,
|
||||||
|
seniorHelpers: updatedHelpers
|
||||||
|
};
|
||||||
|
|
||||||
|
setTelegramConfig(updatedConfig);
|
||||||
|
localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(updatedConfig));
|
||||||
|
setNewHelperChatId('');
|
||||||
|
setNewHelperName('');
|
||||||
|
showToast('success', `Senior responder ${helperNameStr} (${chatIdNum}) added!`);
|
||||||
|
|
||||||
fetch(getBackendUrl('/api/admin/telegram/helpers'), {
|
fetch(getBackendUrl('/api/admin/telegram/helpers'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ chatId: newHelperChatId.trim(), name: newHelperName.trim() }),
|
body: JSON.stringify({ chatId: chatIdNum, name: helperNameStr }),
|
||||||
})
|
}).catch(() => {});
|
||||||
.then(async (res) => {
|
|
||||||
if (res.ok) {
|
|
||||||
showToast('success', 'Senior responder added to Telegram Q&A alerts!');
|
|
||||||
setNewHelperChatId('');
|
|
||||||
setNewHelperName('');
|
|
||||||
fetchTelegramConfig();
|
|
||||||
} else {
|
|
||||||
const err = await res.json();
|
|
||||||
showToast('error', err.error || 'Failed to add helper');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Offline Fallback
|
|
||||||
if (telegramConfig) {
|
|
||||||
setTelegramConfig({
|
|
||||||
...telegramConfig,
|
|
||||||
seniorHelpers: [
|
|
||||||
...telegramConfig.seniorHelpers,
|
|
||||||
{ chatId: parseInt(newHelperChatId), name: newHelperName.trim() || 'New Helper' }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
setNewHelperChatId('');
|
|
||||||
setNewHelperName('');
|
|
||||||
showToast('success', 'Senior responder added (Local Mode)!');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveSeniorHelper = (chatId: number) => {
|
const handleRemoveSeniorHelper = (chatId: number) => {
|
||||||
if (!confirm(`Remove Telegram Chat ID ${chatId} from receiving Q&A alerts?`)) return;
|
if (!confirm(`Remove Telegram Chat ID ${chatId} from receiving Q&A alerts?`)) return;
|
||||||
fetch(getBackendUrl(`/api/admin/telegram/helpers/${chatId}`), { method: 'DELETE' })
|
|
||||||
.then(async (res) => {
|
const updatedHelpers = (telegramConfig?.seniorHelpers || []).filter(h => h.chatId !== chatId);
|
||||||
if (res.ok) {
|
const updatedChatIds = (telegramConfig?.helper_chat_ids || []).filter(id => id !== chatId);
|
||||||
showToast('success', 'Senior helper removed.');
|
const updatedConfig: TelegramConfig = {
|
||||||
fetchTelegramConfig();
|
...telegramConfig,
|
||||||
} else {
|
helper_chat_ids: updatedChatIds,
|
||||||
showToast('error', 'Failed to remove helper');
|
seniorHelpers: updatedHelpers
|
||||||
}
|
};
|
||||||
})
|
|
||||||
.catch(() => {
|
setTelegramConfig(updatedConfig);
|
||||||
// Offline Fallback
|
localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(updatedConfig));
|
||||||
if (telegramConfig) {
|
showToast('success', `Senior responder Chat ID ${chatId} removed.`);
|
||||||
setTelegramConfig({
|
|
||||||
...telegramConfig,
|
fetch(getBackendUrl(`/api/admin/telegram/helpers/${chatId}`), { method: 'DELETE' }).catch(() => {});
|
||||||
seniorHelpers: telegramConfig.seniorHelpers.filter(h => h.chatId !== chatId)
|
|
||||||
});
|
|
||||||
showToast('success', 'Senior helper removed (Local Mode).');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveTelegramTokens = (e: React.FormEvent) => {
|
const handleSaveTelegramTokens = (e: React.FormEvent) => {
|
||||||
@@ -482,19 +506,15 @@ export default function AdminDashboard() {
|
|||||||
if (!telegramConfig) return;
|
if (!telegramConfig) return;
|
||||||
setSavingBotConfig(true);
|
setSavingBotConfig(true);
|
||||||
|
|
||||||
|
localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(telegramConfig));
|
||||||
|
showToast('success', 'Telegram bot configuration updated!');
|
||||||
|
|
||||||
fetch(getBackendUrl('/api/admin/telegram/config'), {
|
fetch(getBackendUrl('/api/admin/telegram/config'), {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(telegramConfig),
|
body: JSON.stringify(telegramConfig),
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.catch(() => {})
|
||||||
if (res.ok) {
|
|
||||||
showToast('success', 'Telegram bot configuration updated on VPS!');
|
|
||||||
} else {
|
|
||||||
showToast('error', 'Failed to update bot config');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => showToast('success', 'Telegram config updated (Local Mode)!'))
|
|
||||||
.finally(() => setSavingBotConfig(false));
|
.finally(() => setSavingBotConfig(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -503,35 +523,49 @@ export default function AdminDashboard() {
|
|||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
const fetchNotes = () => {
|
const fetchNotes = () => {
|
||||||
setLoadingNotes(true);
|
setLoadingNotes(true);
|
||||||
|
const saved = localStorage.getItem(NOTES_STORAGE_KEY);
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(saved);
|
||||||
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||||
|
setNotes(parsed);
|
||||||
|
setLoadingNotes(false);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
fetch(getBackendUrl('/api/admin/notes'))
|
fetch(getBackendUrl('/api/admin/notes'))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res.ok) throw new Error();
|
if (!res.ok) throw new Error();
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then((data) => { if (Array.isArray(data)) setNotes(data); })
|
.then((data) => {
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
setNotes(data);
|
||||||
|
localStorage.setItem(NOTES_STORAGE_KEY, JSON.stringify(data));
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Offline Fallback Mock
|
if (!saved) {
|
||||||
setNotes([
|
const initialNotes: NoteItem[] = [
|
||||||
{ id: 1, title: 'Data Structures Unit 1 Notes', subject: 'CS3301', authorName: 'Dr. ARTHI A.', department: 'AI&DS', fileUrl: '#', semester: 3, uploadDate: new Date().toISOString() },
|
{ id: 1, title: 'Data Structures Unit 1 Notes', subject: 'CS3301', authorName: 'Dr. ARTHI A.', department: 'AI&DS', fileUrl: '#', semester: 3, uploadDate: new Date().toISOString() },
|
||||||
{ id: 2, title: 'Matrices PYQ 2023', subject: 'MA3151', authorName: 'Prof. Senthil', department: 'S&H', fileUrl: '#', semester: 1, uploadDate: new Date(Date.now() - 86400000).toISOString() }
|
{ id: 2, title: 'Matrices PYQ 2023', subject: 'MA3151', authorName: 'Prof. Senthil', department: 'S&H', fileUrl: '#', semester: 1, uploadDate: new Date(Date.now() - 86400000).toISOString() }
|
||||||
]);
|
];
|
||||||
|
setNotes(initialNotes);
|
||||||
|
localStorage.setItem(NOTES_STORAGE_KEY, JSON.stringify(initialNotes));
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoadingNotes(false));
|
.finally(() => setLoadingNotes(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteNote = (id: number, title: string) => {
|
const handleDeleteNote = (id: number, title: string) => {
|
||||||
if (!confirm(`Delete study note "${title}"?`)) return;
|
if (!confirm(`Delete study note "${title}"?`)) return;
|
||||||
fetch(getBackendUrl(`/api/admin/notes/${id}`), { method: 'DELETE' })
|
const updated = notes.filter((n) => n.id !== id);
|
||||||
.then(async (res) => {
|
setNotes(updated);
|
||||||
if (res.ok) {
|
localStorage.setItem(NOTES_STORAGE_KEY, JSON.stringify(updated));
|
||||||
showToast('success', 'Note deleted.');
|
showToast('success', `Study note "${title}" deleted.`);
|
||||||
fetchNotes();
|
|
||||||
}
|
fetch(getBackendUrl(`/api/admin/notes/${id}`), { method: 'DELETE' }).catch(() => {});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setNotes(notes.filter(n => n.id !== id));
|
|
||||||
showToast('success', 'Note deleted (Local Mode).');
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
@@ -539,35 +573,49 @@ export default function AdminDashboard() {
|
|||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
const fetchQuestions = () => {
|
const fetchQuestions = () => {
|
||||||
setLoadingQuestions(true);
|
setLoadingQuestions(true);
|
||||||
|
const saved = localStorage.getItem(QUESTIONS_STORAGE_KEY);
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(saved);
|
||||||
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||||
|
setQuestions(parsed);
|
||||||
|
setLoadingQuestions(false);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
fetch(getBackendUrl('/api/admin/questions'))
|
fetch(getBackendUrl('/api/admin/questions'))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res.ok) throw new Error();
|
if (!res.ok) throw new Error();
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then((data) => { if (Array.isArray(data)) setQuestions(data); })
|
.then((data) => {
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
setQuestions(data);
|
||||||
|
localStorage.setItem(QUESTIONS_STORAGE_KEY, JSON.stringify(data));
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Offline Fallback Mock
|
if (!saved) {
|
||||||
setQuestions([
|
const initialQuestions: QuestionItem[] = [
|
||||||
{ id: 1, title: "When is the fresher's orientation?", content: "I couldn't find the exact date for the CSE orientation.", authorName: "Karthik", authorEmail: "karthik.2024@ritchennai.edu.in", createdAt: new Date().toISOString(), status: "PENDING", tags: ["orientation"], votes: 0 },
|
{ id: 1, title: "When is the fresher's orientation?", content: "I couldn't find the exact date for the CSE orientation.", authorName: "Karthik", authorEmail: "karthik.2024@ritchennai.edu.in", createdAt: new Date().toISOString(), status: "PENDING", tags: ["orientation"], votes: 0 },
|
||||||
{ id: 2, title: "Are laptops mandatory in first year?", content: "Just wondering if we need to bring laptops to college every day.", authorName: "Sneha", authorEmail: "sneha.2024@ritchennai.edu.in", createdAt: new Date(Date.now() - 86400000).toISOString(), status: "APPROVED", tags: ["academics"], votes: 5 }
|
{ id: 2, title: "Are laptops mandatory in first year?", content: "Just wondering if we need to bring laptops to college every day.", authorName: "Sneha", authorEmail: "sneha.2024@ritchennai.edu.in", createdAt: new Date(Date.now() - 86400000).toISOString(), status: "APPROVED", tags: ["academics"], votes: 5 }
|
||||||
]);
|
];
|
||||||
|
setQuestions(initialQuestions);
|
||||||
|
localStorage.setItem(QUESTIONS_STORAGE_KEY, JSON.stringify(initialQuestions));
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoadingQuestions(false));
|
.finally(() => setLoadingQuestions(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteQuestion = (id: number, title: string) => {
|
const handleDeleteQuestion = (id: number, title: string) => {
|
||||||
if (!confirm(`Delete student question "${title}"?`)) return;
|
if (!confirm(`Delete student question "${title}"?`)) return;
|
||||||
fetch(getBackendUrl(`/api/admin/questions/${id}`), { method: 'DELETE' })
|
const updated = questions.filter((q) => q.id !== id);
|
||||||
.then(async (res) => {
|
setQuestions(updated);
|
||||||
if (res.ok) {
|
localStorage.setItem(QUESTIONS_STORAGE_KEY, JSON.stringify(updated));
|
||||||
showToast('success', 'Question deleted.');
|
showToast('success', `Question "${title}" deleted.`);
|
||||||
fetchQuestions();
|
|
||||||
}
|
fetch(getBackendUrl(`/api/admin/questions/${id}`), { method: 'DELETE' }).catch(() => {});
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setQuestions(questions.filter(q => q.id !== id));
|
|
||||||
showToast('success', 'Question deleted (Local Mode).');
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
@@ -575,18 +623,34 @@ export default function AdminDashboard() {
|
|||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
const fetchRecipients = () => {
|
const fetchRecipients = () => {
|
||||||
setLoadingRecipients(true);
|
setLoadingRecipients(true);
|
||||||
|
const saved = localStorage.getItem(RECIPIENTS_STORAGE_KEY);
|
||||||
|
if (saved) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(saved);
|
||||||
|
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||||
|
setRecipients(parsed);
|
||||||
|
setLoadingRecipients(false);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
fetch(getBackendUrl('/api/admin/recipients'))
|
fetch(getBackendUrl('/api/admin/recipients'))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (!res.ok) throw new Error();
|
if (!res.ok) throw new Error();
|
||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then((data) => { if (Array.isArray(data)) setRecipients(data); })
|
.then((data) => {
|
||||||
|
if (Array.isArray(data) && data.length > 0) {
|
||||||
|
setRecipients(data);
|
||||||
|
localStorage.setItem(RECIPIENTS_STORAGE_KEY, JSON.stringify(data));
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Offline Fallback Mock
|
if (!saved) {
|
||||||
setRecipients([
|
const initialRecipients = ['devops@ritchennai.edu.in', 'admin@ritchennai.edu.in'];
|
||||||
'devops@ritchennai.edu.in',
|
setRecipients(initialRecipients);
|
||||||
'admin@ritchennai.edu.in'
|
localStorage.setItem(RECIPIENTS_STORAGE_KEY, JSON.stringify(initialRecipients));
|
||||||
]);
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoadingRecipients(false));
|
.finally(() => setLoadingRecipients(false));
|
||||||
};
|
};
|
||||||
@@ -597,50 +661,27 @@ export default function AdminDashboard() {
|
|||||||
showToast('error', 'Please enter a valid email address');
|
showToast('error', 'Please enter a valid email address');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSubmittingEmail(true);
|
const updated = [...recipients, newEmail.trim()];
|
||||||
|
setRecipients(updated);
|
||||||
|
localStorage.setItem(RECIPIENTS_STORAGE_KEY, JSON.stringify(updated));
|
||||||
|
setNewEmail('');
|
||||||
|
showToast('success', `Recipient ${newEmail.trim()} added!`);
|
||||||
|
|
||||||
fetch(getBackendUrl('/api/admin/recipients'), {
|
fetch(getBackendUrl('/api/admin/recipients'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ email: newEmail }),
|
body: JSON.stringify({ email: newEmail.trim() }),
|
||||||
})
|
}).catch(() => {});
|
||||||
.then((res) => {
|
|
||||||
if (!res.ok) throw new Error();
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
if (data.success) {
|
|
||||||
showToast('success', data.message || 'Email added successfully');
|
|
||||||
setNewEmail('');
|
|
||||||
fetchRecipients();
|
|
||||||
} else {
|
|
||||||
showToast('error', data.message || 'Could not add email');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setRecipients([...recipients, newEmail]);
|
|
||||||
setNewEmail('');
|
|
||||||
showToast('success', 'Email added (Local Mode)');
|
|
||||||
})
|
|
||||||
.finally(() => setSubmittingEmail(false));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveRecipient = (email: string) => {
|
const handleRemoveRecipient = (email: string) => {
|
||||||
if (!confirm(`Remove ${email} from deployment notifications?`)) return;
|
if (!confirm(`Remove ${email} from deployment notifications?`)) return;
|
||||||
fetch(getBackendUrl(`/api/admin/recipients?email=${encodeURIComponent(email)}`), { method: 'DELETE' })
|
const updated = recipients.filter((r) => r !== email);
|
||||||
.then((res) => {
|
setRecipients(updated);
|
||||||
if (!res.ok) throw new Error();
|
localStorage.setItem(RECIPIENTS_STORAGE_KEY, JSON.stringify(updated));
|
||||||
return res.json();
|
showToast('success', `Recipient ${email} removed.`);
|
||||||
})
|
|
||||||
.then((data) => {
|
fetch(getBackendUrl(`/api/admin/recipients?email=${encodeURIComponent(email)}`), { method: 'DELETE' }).catch(() => {});
|
||||||
if (data.success) {
|
|
||||||
showToast('success', 'Removed email recipient');
|
|
||||||
fetchRecipients();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setRecipients(recipients.filter((r) => r !== email));
|
|
||||||
showToast('success', 'Removed email recipient (Local Mode)');
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
@@ -1044,42 +1085,52 @@ export default function AdminDashboard() {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Active Seniors List */}
|
{/* Active Seniors List */}
|
||||||
<div className="space-y-3">
|
{(() => {
|
||||||
<span className="text-xs font-bold text-slate-300 block">
|
const helpersList = telegramConfig?.seniorHelpers && telegramConfig.seniorHelpers.length > 0
|
||||||
Active Senior Chat IDs ({telegramConfig?.helper_chat_ids?.length || 0})
|
? telegramConfig.seniorHelpers
|
||||||
</span>
|
: (telegramConfig?.helper_chat_ids || []).map(id => ({ chatId: id, name: '' }));
|
||||||
|
|
||||||
<div className="space-y-2">
|
return (
|
||||||
{telegramConfig?.helper_chat_ids && telegramConfig.helper_chat_ids.length > 0 ? (
|
<div className="space-y-3">
|
||||||
telegramConfig.helper_chat_ids.map((chatId) => (
|
<span className="text-xs font-bold text-slate-300 block">
|
||||||
<div
|
Active Senior Responders ({helpersList.length})
|
||||||
key={chatId}
|
</span>
|
||||||
className="bg-slate-950/60 border border-white/10 rounded-2xl p-3.5 flex items-center justify-between gap-4"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-8 h-8 rounded-xl bg-orange-500/10 border border-orange-500/20 flex items-center justify-center text-orange-400 shrink-0 font-bold text-xs">
|
|
||||||
TG
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-mono font-bold text-white">Chat ID: {chatId}</p>
|
|
||||||
<p className="text-[10px] text-slate-400">Receives questions from freshers in real-time</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<div className="space-y-2">
|
||||||
onClick={() => handleRemoveSeniorHelper(chatId)}
|
{helpersList.length > 0 ? (
|
||||||
className="p-2 rounded-xl bg-rose-500/10 hover:bg-rose-500 text-rose-400 hover:text-white transition-colors"
|
helpersList.map((helper) => (
|
||||||
title="Remove responder"
|
<div
|
||||||
>
|
key={helper.chatId}
|
||||||
<Trash2 className="w-4 h-4" />
|
className="bg-slate-950/60 border border-white/10 rounded-2xl p-3.5 flex items-center justify-between gap-4"
|
||||||
</button>
|
>
|
||||||
</div>
|
<div className="flex items-center gap-3">
|
||||||
))
|
<div className="w-8 h-8 rounded-xl bg-orange-500/10 border border-orange-500/20 flex items-center justify-center text-orange-400 shrink-0 font-bold text-xs">
|
||||||
) : (
|
TG
|
||||||
<p className="text-xs text-slate-500 italic">No senior helpers configured in config.json.</p>
|
</div>
|
||||||
)}
|
<div>
|
||||||
</div>
|
<p className="text-xs font-mono font-bold text-white">
|
||||||
</div>
|
Chat ID: {helper.chatId} {helper.name ? <span className="text-orange-400 font-sans ml-1">({helper.name})</span> : ''}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-slate-400">Receives questions from freshers in real-time</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveSeniorHelper(helper.chatId)}
|
||||||
|
className="p-2 rounded-xl bg-rose-500/10 hover:bg-rose-500 text-rose-400 hover:text-white transition-colors cursor-pointer"
|
||||||
|
title="Remove responder"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-slate-500 italic">No senior helpers configured yet. Enter a Chat ID above to add!</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right 5 Cols: How-To Guide & Bot Tokens */}
|
{/* Right 5 Cols: How-To Guide & Bot Tokens */}
|
||||||
|
|||||||
Reference in New Issue
Block a user