diff --git a/src/pages/Admin/AdminDashboard.tsx b/src/pages/Admin/AdminDashboard.tsx index e23f5d1..59b40e8 100644 --- a/src/pages/Admin/AdminDashboard.tsx +++ b/src/pages/Admin/AdminDashboard.tsx @@ -31,13 +31,25 @@ interface BusRouteItem { stops?: BusStopItem[]; } +interface SeniorHelperItem { + chatId: number; + name: string; +} + interface TelegramConfig { community_bot_token?: string; telegram_bot_token?: string; - helper_chat_ids?: number[]; 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 { id: number; title: string; @@ -390,91 +402,103 @@ export default function AdminDashboard() { setRouteFormData({ ...routeFormData, stops: updated }); }; + // ───────────────────────────────────────────────────────────── + // TELEGRAM BOT & SENIOR HELPERS HANDLERS + // ───────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────── // TELEGRAM BOT & SENIOR HELPERS HANDLERS // ───────────────────────────────────────────────────────────── const fetchTelegramConfig = () => { 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')) .then((res) => { if (!res.ok) throw new Error('Failed to fetch from backend'); return res.json(); }) - .then((data) => setTelegramConfig(data)) + .then((data) => { + setTelegramConfig(data); + localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(data)); + }) .catch(() => { - // Offline Fallback Mock - setTelegramConfig({ - botToken: '782914X:AAH_mock_token_for_demo_environment', - seniorHelpers: [ - { chatId: 123456789, name: 'Rahul (CSE 4th Yr)' }, - { chatId: 987654321, name: 'Sneha (ECE 3rd Yr)' } - ] - }); + if (localConfig) { + setTelegramConfig(localConfig); + } else { + const defaultConfig: TelegramConfig = { + community_bot_token: '7829148291:AAH_rit_community_bot_token_sample', + 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)); }; const handleAddSeniorHelper = (e: React.FormEvent) => { e.preventDefault(); - if (!newHelperChatId.trim()) { + if (!newHelperChatId.trim() || isNaN(Number(newHelperChatId.trim()))) { showToast('error', 'Please enter a valid numeric Telegram Chat ID.'); 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'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ chatId: newHelperChatId.trim(), name: newHelperName.trim() }), - }) - .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)!'); - } - }); + body: JSON.stringify({ chatId: chatIdNum, name: helperNameStr }), + }).catch(() => {}); }; const handleRemoveSeniorHelper = (chatId: number) => { 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) => { - if (res.ok) { - showToast('success', 'Senior helper removed.'); - fetchTelegramConfig(); - } else { - showToast('error', 'Failed to remove helper'); - } - }) - .catch(() => { - // Offline Fallback - if (telegramConfig) { - setTelegramConfig({ - ...telegramConfig, - seniorHelpers: telegramConfig.seniorHelpers.filter(h => h.chatId !== chatId) - }); - showToast('success', 'Senior helper removed (Local Mode).'); - } - }); + + const updatedHelpers = (telegramConfig?.seniorHelpers || []).filter(h => h.chatId !== chatId); + const updatedChatIds = (telegramConfig?.helper_chat_ids || []).filter(id => id !== chatId); + const updatedConfig: TelegramConfig = { + ...telegramConfig, + helper_chat_ids: updatedChatIds, + seniorHelpers: updatedHelpers + }; + + setTelegramConfig(updatedConfig); + localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(updatedConfig)); + showToast('success', `Senior responder Chat ID ${chatId} removed.`); + + fetch(getBackendUrl(`/api/admin/telegram/helpers/${chatId}`), { method: 'DELETE' }).catch(() => {}); }; const handleSaveTelegramTokens = (e: React.FormEvent) => { @@ -482,19 +506,15 @@ export default function AdminDashboard() { if (!telegramConfig) return; setSavingBotConfig(true); + localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(telegramConfig)); + showToast('success', 'Telegram bot configuration updated!'); + fetch(getBackendUrl('/api/admin/telegram/config'), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(telegramConfig), }) - .then(async (res) => { - 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)!')) + .catch(() => {}) .finally(() => setSavingBotConfig(false)); }; @@ -503,35 +523,49 @@ export default function AdminDashboard() { // ───────────────────────────────────────────────────────────── const fetchNotes = () => { 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')) .then((res) => { if (!res.ok) throw new Error(); 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(() => { - // Offline Fallback Mock - setNotes([ - { 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() } - ]); + if (!saved) { + 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: 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)); }; const handleDeleteNote = (id: number, title: string) => { if (!confirm(`Delete study note "${title}"?`)) return; - fetch(getBackendUrl(`/api/admin/notes/${id}`), { method: 'DELETE' }) - .then(async (res) => { - if (res.ok) { - showToast('success', 'Note deleted.'); - fetchNotes(); - } - }) - .catch(() => { - setNotes(notes.filter(n => n.id !== id)); - showToast('success', 'Note deleted (Local Mode).'); - }); + const updated = notes.filter((n) => n.id !== id); + setNotes(updated); + localStorage.setItem(NOTES_STORAGE_KEY, JSON.stringify(updated)); + showToast('success', `Study note "${title}" deleted.`); + + fetch(getBackendUrl(`/api/admin/notes/${id}`), { method: 'DELETE' }).catch(() => {}); }; // ───────────────────────────────────────────────────────────── @@ -539,35 +573,49 @@ export default function AdminDashboard() { // ───────────────────────────────────────────────────────────── const fetchQuestions = () => { 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')) .then((res) => { if (!res.ok) throw new Error(); 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(() => { - // Offline Fallback Mock - setQuestions([ - { 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 } - ]); + if (!saved) { + 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: 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)); }; const handleDeleteQuestion = (id: number, title: string) => { if (!confirm(`Delete student question "${title}"?`)) return; - fetch(getBackendUrl(`/api/admin/questions/${id}`), { method: 'DELETE' }) - .then(async (res) => { - if (res.ok) { - showToast('success', 'Question deleted.'); - fetchQuestions(); - } - }) - .catch(() => { - setQuestions(questions.filter(q => q.id !== id)); - showToast('success', 'Question deleted (Local Mode).'); - }); + const updated = questions.filter((q) => q.id !== id); + setQuestions(updated); + localStorage.setItem(QUESTIONS_STORAGE_KEY, JSON.stringify(updated)); + showToast('success', `Question "${title}" deleted.`); + + fetch(getBackendUrl(`/api/admin/questions/${id}`), { method: 'DELETE' }).catch(() => {}); }; // ───────────────────────────────────────────────────────────── @@ -575,18 +623,34 @@ export default function AdminDashboard() { // ───────────────────────────────────────────────────────────── const fetchRecipients = () => { 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')) .then((res) => { if (!res.ok) throw new Error(); 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(() => { - // Offline Fallback Mock - setRecipients([ - 'devops@ritchennai.edu.in', - 'admin@ritchennai.edu.in' - ]); + if (!saved) { + const initialRecipients = ['devops@ritchennai.edu.in', 'admin@ritchennai.edu.in']; + setRecipients(initialRecipients); + localStorage.setItem(RECIPIENTS_STORAGE_KEY, JSON.stringify(initialRecipients)); + } }) .finally(() => setLoadingRecipients(false)); }; @@ -597,50 +661,27 @@ export default function AdminDashboard() { showToast('error', 'Please enter a valid email address'); 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'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email: newEmail }), - }) - .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)); + body: JSON.stringify({ email: newEmail.trim() }), + }).catch(() => {}); }; const handleRemoveRecipient = (email: string) => { if (!confirm(`Remove ${email} from deployment notifications?`)) return; - fetch(getBackendUrl(`/api/admin/recipients?email=${encodeURIComponent(email)}`), { method: 'DELETE' }) - .then((res) => { - if (!res.ok) throw new Error(); - return res.json(); - }) - .then((data) => { - if (data.success) { - showToast('success', 'Removed email recipient'); - fetchRecipients(); - } - }) - .catch(() => { - setRecipients(recipients.filter((r) => r !== email)); - showToast('success', 'Removed email recipient (Local Mode)'); - }); + const updated = recipients.filter((r) => r !== email); + setRecipients(updated); + localStorage.setItem(RECIPIENTS_STORAGE_KEY, JSON.stringify(updated)); + showToast('success', `Recipient ${email} removed.`); + + fetch(getBackendUrl(`/api/admin/recipients?email=${encodeURIComponent(email)}`), { method: 'DELETE' }).catch(() => {}); }; // ───────────────────────────────────────────────────────────── @@ -1044,42 +1085,52 @@ export default function AdminDashboard() { {/* Active Seniors List */} -
Chat ID: {chatId}
-Receives questions from freshers in real-time
-No senior helpers configured in config.json.
- )} -+ Chat ID: {helper.chatId} {helper.name ? ({helper.name}) : ''} +
+Receives questions from freshers in real-time
+No senior helpers configured yet. Enter a Chat ID above to add!
+ )} +