feat(admin): make telegram bot config and senior helper id management fully functional with live test ping and verification

This commit is contained in:
Shanmuga Krishnan S M
2026-08-24 14:04:11 +05:30
parent 6740400788
commit 9275722e16
3 changed files with 1001 additions and 160 deletions

View File

@@ -14,6 +14,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -257,15 +258,47 @@ public class AdminController {
if (Files.exists(path)) { if (Files.exists(path)) {
try { try {
byte[] bytes = Files.readAllBytes(path); byte[] bytes = Files.readAllBytes(path);
return objectMapper.readValue(bytes, Map.class); Map<String, Object> loaded = objectMapper.readValue(bytes, Map.class);
if (loaded != null) {
List<Object> rawChatIds = (List<Object>) loaded.get("helper_chat_ids");
List<Map<String, Object>> helpers = (List<Map<String, Object>>) loaded.get("seniorHelpers");
if (helpers == null && rawChatIds != null) {
helpers = new ArrayList<>();
for (Object id : rawChatIds) {
Map<String, Object> h = new HashMap<>();
h.put("chatId", Long.parseLong(id.toString()));
h.put("name", "Senior Responder");
helpers.add(h);
}
loaded.put("seniorHelpers", helpers);
} else if (helpers != null && rawChatIds == null) {
List<Long> ids = new ArrayList<>();
for (Map<String, Object> h : helpers) {
if (h.get("chatId") != null) {
try {
ids.add(Long.parseLong(h.get("chatId").toString()));
} catch (Exception ignored) {}
}
}
loaded.put("helper_chat_ids", ids);
}
return loaded;
}
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
Map<String, Object> defaultCfg = new HashMap<>(); Map<String, Object> defaultCfg = new HashMap<>();
defaultCfg.put("community_bot_token", ""); defaultCfg.put("community_bot_token", "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY");
defaultCfg.put("telegram_bot_token", ""); defaultCfg.put("telegram_bot_token", "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY");
defaultCfg.put("helper_chat_ids", new ArrayList<Long>(List.of(971749136L, 5567776672L, 1873240361L))); defaultCfg.put("helper_chat_ids", new ArrayList<Long>(List.of(1873240361L, 5567776672L, 8518850169L, 7238144438L)));
List<Map<String, Object>> defaultHelpers = new ArrayList<>();
defaultHelpers.add(Map.of("chatId", 1873240361L, "name", "Senior Mentor (CSE)"));
defaultHelpers.add(Map.of("chatId", 5567776672L, "name", "Senior Responder (ECE)"));
defaultHelpers.add(Map.of("chatId", 8518850169L, "name", "Senior Responder (IT)"));
defaultHelpers.add(Map.of("chatId", 7238144438L, "name", "Senior Responder (AIDS)"));
defaultCfg.put("seniorHelpers", defaultHelpers);
defaultCfg.put("spring_backend_url", "http://localhost:8085"); defaultCfg.put("spring_backend_url", "http://localhost:8085");
return defaultCfg; return defaultCfg;
} }
@@ -293,6 +326,25 @@ public class AdminController {
if (payload.containsKey("community_bot_token")) current.put("community_bot_token", payload.get("community_bot_token")); if (payload.containsKey("community_bot_token")) current.put("community_bot_token", payload.get("community_bot_token"));
if (payload.containsKey("telegram_bot_token")) current.put("telegram_bot_token", payload.get("telegram_bot_token")); if (payload.containsKey("telegram_bot_token")) current.put("telegram_bot_token", payload.get("telegram_bot_token"));
if (payload.containsKey("spring_backend_url")) current.put("spring_backend_url", payload.get("spring_backend_url")); if (payload.containsKey("spring_backend_url")) current.put("spring_backend_url", payload.get("spring_backend_url"));
if (payload.containsKey("bot_username")) current.put("bot_username", payload.get("bot_username"));
if (payload.containsKey("seniorHelpers")) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> helpers = (List<Map<String, Object>>) payload.get("seniorHelpers");
current.put("seniorHelpers", helpers);
List<Long> ids = new ArrayList<>();
if (helpers != null) {
for (Map<String, Object> h : helpers) {
if (h.get("chatId") != null) {
try {
ids.add(Long.parseLong(h.get("chatId").toString()));
} catch (Exception ignored) {}
}
}
}
current.put("helper_chat_ids", ids);
} else if (payload.containsKey("helper_chat_ids")) {
current.put("helper_chat_ids", payload.get("helper_chat_ids"));
}
saveTelegramConfigFile(current); saveTelegramConfigFile(current);
return ResponseEntity.ok(Map.of("success", true, "config", current)); return ResponseEntity.ok(Map.of("success", true, "config", current));
} }
@@ -300,6 +352,7 @@ public class AdminController {
@PostMapping("/telegram/helpers") @PostMapping("/telegram/helpers")
public ResponseEntity<?> addSeniorHelper(@RequestBody Map<String, Object> payload) { public ResponseEntity<?> addSeniorHelper(@RequestBody Map<String, Object> payload) {
Object chatIdObj = payload.get("chatId"); Object chatIdObj = payload.get("chatId");
String nameStr = payload.get("name") != null ? payload.get("name").toString().trim() : "Senior Responder";
if (chatIdObj == null) { if (chatIdObj == null) {
return ResponseEntity.badRequest().body(Map.of("error", "chatId is required")); return ResponseEntity.badRequest().body(Map.of("error", "chatId is required"));
} }
@@ -313,38 +366,142 @@ public class AdminController {
Map<String, Object> config = readTelegramConfigFile(); Map<String, Object> config = readTelegramConfigFile();
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<Object> rawHelpers = (List<Object>) config.getOrDefault("helper_chat_ids", new ArrayList<>()); List<Map<String, Object>> helpers = (List<Map<String, Object>>) config.getOrDefault("seniorHelpers", new ArrayList<>());
List<Long> helpers = new ArrayList<>(); if (helpers == null) helpers = new ArrayList<>();
for (Object o : rawHelpers) { List<Long> helperIds = new ArrayList<>();
helpers.add(Long.parseLong(o.toString()));
boolean found = false;
for (Map<String, Object> h : helpers) {
if (h.get("chatId") != null) {
Long cId = Long.parseLong(h.get("chatId").toString());
helperIds.add(cId);
if (cId.equals(chatId)) {
h.put("name", nameStr);
found = true;
}
}
} }
if (!helpers.contains(chatId)) { if (!found) {
helpers.add(chatId); Map<String, Object> newHelper = new HashMap<>();
config.put("helper_chat_ids", helpers); newHelper.put("chatId", chatId);
newHelper.put("name", nameStr);
helpers.add(newHelper);
helperIds.add(chatId);
}
config.put("seniorHelpers", helpers);
config.put("helper_chat_ids", helperIds);
saveTelegramConfigFile(config); saveTelegramConfigFile(config);
}
return ResponseEntity.ok(Map.of("success", true, "helpers", helpers)); return ResponseEntity.ok(Map.of("success", true, "helpers", helpers, "helper_chat_ids", helperIds));
} }
@DeleteMapping("/telegram/helpers/{chatId}") @DeleteMapping("/telegram/helpers/{chatId}")
public ResponseEntity<?> removeSeniorHelper(@PathVariable Long chatId) { public ResponseEntity<?> removeSeniorHelper(@PathVariable Long chatId) {
Map<String, Object> config = readTelegramConfigFile(); Map<String, Object> config = readTelegramConfigFile();
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<Object> rawHelpers = (List<Object>) config.getOrDefault("helper_chat_ids", new ArrayList<>()); List<Map<String, Object>> helpers = (List<Map<String, Object>>) config.getOrDefault("seniorHelpers", new ArrayList<>());
List<Long> helpers = new ArrayList<>(); if (helpers != null) {
for (Object o : rawHelpers) { helpers.removeIf(h -> h.get("chatId") != null && Long.parseLong(h.get("chatId").toString()) == chatId);
helpers.add(Long.parseLong(o.toString())); } else {
helpers = new ArrayList<>();
} }
boolean removed = helpers.removeIf(id -> id.equals(chatId)); List<Long> helperIds = new ArrayList<>();
if (removed) { for (Map<String, Object> h : helpers) {
config.put("helper_chat_ids", helpers); if (h.get("chatId") != null) {
try {
helperIds.add(Long.parseLong(h.get("chatId").toString()));
} catch (Exception ignored) {}
}
}
config.put("seniorHelpers", helpers);
config.put("helper_chat_ids", helperIds);
saveTelegramConfigFile(config); saveTelegramConfigFile(config);
return ResponseEntity.ok(Map.of("success", true, "helpers", helpers, "helper_chat_ids", helperIds));
} }
return ResponseEntity.ok(Map.of("success", true, "helpers", helpers)); @PostMapping("/telegram/test-bot")
public ResponseEntity<?> testTelegramBot(@RequestBody Map<String, Object> payload) {
String token = (String) payload.get("token");
if (token == null || token.trim().isEmpty()) {
Map<String, Object> cfg = readTelegramConfigFile();
token = (String) cfg.getOrDefault("community_bot_token", "");
}
if (token == null || token.trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("ok", false, "description", "Bot token is required"));
}
try {
RestTemplate restTemplate = new RestTemplate();
String url = "https://api.telegram.org/bot" + token.trim() + "/getMe";
ResponseEntity<Map> response = restTemplate.getForEntity(url, Map.class);
return ResponseEntity.ok(response.getBody());
} catch (Exception e) {
return ResponseEntity.ok(Map.of("ok", false, "description", e.getMessage()));
}
}
@PostMapping("/telegram/test-dispatch")
public ResponseEntity<?> testTelegramDispatch(@RequestBody Map<String, Object> payload) {
String token = (String) payload.get("token");
Object chatIdObj = payload.get("chatId");
String message = (String) payload.get("message");
if (token == null || token.trim().isEmpty()) {
Map<String, Object> cfg = readTelegramConfigFile();
token = (String) cfg.getOrDefault("community_bot_token", "");
}
if (token == null || token.trim().isEmpty() || chatIdObj == null) {
return ResponseEntity.badRequest().body(Map.of("ok", false, "description", "Bot token and chatId are required"));
}
if (message == null || message.trim().isEmpty()) {
message = "🔔 *RIT Nexus Admin Test Ping*\n\nThis is a verified test notification confirming your Telegram Chat ID is active and connected to the Q&A dispatch system!";
}
try {
RestTemplate restTemplate = new RestTemplate();
String url = "https://api.telegram.org/bot" + token.trim() + "/sendMessage";
Map<String, Object> body = new HashMap<>();
body.put("chat_id", chatIdObj);
body.put("text", message);
body.put("parse_mode", "Markdown");
ResponseEntity<Map> response = restTemplate.postForEntity(url, body, Map.class);
return ResponseEntity.ok(response.getBody());
} catch (Exception e) {
return ResponseEntity.ok(Map.of("ok", false, "description", e.getMessage()));
}
}
@PostMapping("/telegram/test-broadcast")
public ResponseEntity<?> testTelegramBroadcast(@RequestBody Map<String, Object> payload) {
Map<String, Object> cfg = readTelegramConfigFile();
String token = (String) payload.getOrDefault("token", cfg.get("community_bot_token"));
String message = (String) payload.getOrDefault("message", "📢 *RIT Nexus Q&A Dispatch Network Test*\n\nAll senior responders are currently being pinged to verify real-time dispatch connectivity.");
@SuppressWarnings("unchecked")
List<Object> rawHelpers = (List<Object>) cfg.getOrDefault("helper_chat_ids", new ArrayList<>());
int delivered = 0;
List<Map<String, Object>> results = new ArrayList<>();
RestTemplate restTemplate = new RestTemplate();
if (token != null && !token.trim().isEmpty() && rawHelpers != null) {
for (Object idObj : rawHelpers) {
try {
String url = "https://api.telegram.org/bot" + token.trim() + "/sendMessage";
Map<String, Object> body = new HashMap<>();
body.put("chat_id", idObj);
body.put("text", message);
body.put("parse_mode", "Markdown");
ResponseEntity<Map> res = restTemplate.postForEntity(url, body, Map.class);
results.add(Map.of("chatId", idObj, "status", "success", "response", res.getBody()));
delivered++;
} catch (Exception e) {
results.add(Map.of("chatId", idObj, "status", "failed", "error", e.getMessage()));
}
}
}
return ResponseEntity.ok(Map.of("success", true, "delivered", delivered, "total", rawHelpers != null ? rawHelpers.size() : 0, "details", results));
} }
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────

View File

@@ -7,7 +7,9 @@ import {
Clock, Palette, KeyRound, ExternalLink, ArrowRight, Clock, Palette, KeyRound, ExternalLink, ArrowRight,
Code2, Check, HelpCircle, Users, Activity, Code2, Check, HelpCircle, Users, Activity,
UserCheck, Building2, GraduationCap, Calculator, Search, GitMerge, UserCheck, Building2, GraduationCap, Calculator, Search, GitMerge,
Power, Play, SlidersHorizontal, ToggleLeft, ToggleRight Power, Play, SlidersHorizontal, ToggleLeft, ToggleRight,
Eye, EyeOff, Zap, Copy, CheckCheck, Sparkles, Radio, Terminal,
MessageSquare, ShieldAlert
} from 'lucide-react'; } from 'lucide-react';
import { getBackendUrl } from '@/lib/utils'; import { getBackendUrl } from '@/lib/utils';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@@ -39,11 +41,24 @@ interface BusRouteItem {
interface SeniorHelperItem { interface SeniorHelperItem {
chatId: number; chatId: number;
name: string; name: string;
department?: string;
lastPingStatus?: 'success' | 'failed' | 'idle';
lastPingTime?: string;
}
interface TelegramBotInfo {
id?: number;
is_bot?: boolean;
first_name?: string;
username?: string;
can_join_groups?: boolean;
can_read_all_group_messages?: boolean;
} }
interface TelegramConfig { interface TelegramConfig {
community_bot_token?: string; community_bot_token?: string;
telegram_bot_token?: string; telegram_bot_token?: string;
bot_username?: string;
spring_backend_url?: string; spring_backend_url?: string;
helper_chat_ids?: number[]; helper_chat_ids?: number[];
seniorHelpers?: SeniorHelperItem[]; seniorHelpers?: SeniorHelperItem[];
@@ -175,12 +190,25 @@ export default function AdminDashboard() {
const [busStatusTabFilter, setBusStatusTabFilter] = useState<'ALL' | 'ACTIVE' | 'STOPPED'>('ALL'); const [busStatusTabFilter, setBusStatusTabFilter] = useState<'ALL' | 'ACTIVE' | 'STOPPED'>('ALL');
const [gridStatusFilter, setGridStatusFilter] = useState<'ALL' | 'ACTIVE' | 'STOPPED'>('ACTIVE'); const [gridStatusFilter, setGridStatusFilter] = useState<'ALL' | 'ACTIVE' | 'STOPPED'>('ACTIVE');
// Telegram Q&A State // Telegram Q&A & Bot State
const [telegramConfig, setTelegramConfig] = useState<TelegramConfig | null>(null); const [telegramConfig, setTelegramConfig] = useState<TelegramConfig | null>(null);
const [loadingTelegram, setLoadingTelegram] = useState(false); const [loadingTelegram, setLoadingTelegram] = useState(false);
const [newHelperChatId, setNewHelperChatId] = useState(''); const [newHelperChatId, setNewHelperChatId] = useState('');
const [newHelperName, setNewHelperName] = useState(''); const [newHelperName, setNewHelperName] = useState('');
const [newHelperDept, setNewHelperDept] = useState('CSE');
const [savingBotConfig, setSavingBotConfig] = useState(false); const [savingBotConfig, setSavingBotConfig] = useState(false);
const [showCommunityToken, setShowCommunityToken] = useState(false);
const [showCollabToken, setShowCollabToken] = useState(false);
const [testingBotType, setTestingBotType] = useState<'community' | 'collab' | null>(null);
const [botInfoCommunity, setBotInfoCommunity] = useState<TelegramBotInfo | null>(null);
const [botInfoCollab, setBotInfoCollab] = useState<TelegramBotInfo | null>(null);
const [pingingHelperId, setPingingHelperId] = useState<number | null>(null);
const [broadcastingAlert, setBroadcastingAlert] = useState(false);
const [copiedChatId, setCopiedChatId] = useState<number | null>(null);
const [editingHelper, setEditingHelper] = useState<SeniorHelperItem | null>(null);
const [editHelperName, setEditHelperName] = useState('');
const [editHelperDept, setEditHelperDept] = useState('CSE');
const [activeGuideTab, setActiveGuideTab] = useState<'botfather' | 'chatid' | 'workflow'>('botfather');
// Notes State // Notes State
const [notes, setNotes] = useState<NoteItem[]>([]); const [notes, setNotes] = useState<NoteItem[]>([]);
@@ -634,9 +662,6 @@ export default function AdminDashboard() {
showToast('success', `All routes set to ${status === 'ACTIVE' ? '🟢 Active' : '🔴 Stopped'}`); showToast('success', `All routes set to ${status === 'ACTIVE' ? '🟢 Active' : '🔴 Stopped'}`);
}; };
// ─────────────────────────────────────────────────────────────
// TELEGRAM BOT & SENIOR HELPERS HANDLERS
// ─────────────────────────────────────────────────────────────
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// TELEGRAM BOT & SENIOR HELPERS HANDLERS // TELEGRAM BOT & SENIOR HELPERS HANDLERS
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
@@ -651,11 +676,15 @@ export default function AdminDashboard() {
if (!localConfig || (!localConfig.seniorHelpers?.length && !localConfig.helper_chat_ids?.length)) { if (!localConfig || (!localConfig.seniorHelpers?.length && !localConfig.helper_chat_ids?.length)) {
localConfig = { localConfig = {
community_bot_token: '8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY', community_bot_token: '8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY',
spring_backend_url: 'https://rit-services.in/api', telegram_bot_token: '8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY',
helper_chat_ids: [971749136, 982314512], bot_username: 'FreshersCommunityBot',
spring_backend_url: 'http://localhost:8085',
helper_chat_ids: [1873240361, 5567776672, 8518850169, 7238144438],
seniorHelpers: [ seniorHelpers: [
{ chatId: 971749136, name: 'Rahul (CSE 4th Yr)' }, { chatId: 1873240361, name: 'Senior Mentor', department: 'CSE' },
{ chatId: 982314512, name: 'Sneha (ECE 3rd Yr)' } { chatId: 5567776672, name: 'Senior Responder', department: 'ECE' },
{ chatId: 8518850169, name: 'Senior Responder', department: 'IT' },
{ chatId: 7238144438, name: 'Senior Responder', department: 'AIDS' },
] ]
}; };
localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(localConfig)); localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(localConfig));
@@ -663,6 +692,11 @@ export default function AdminDashboard() {
setTelegramConfig(localConfig); setTelegramConfig(localConfig);
// Initial silent verification of bot token
if (localConfig?.community_bot_token) {
verifyBotTokenSilent(localConfig.community_bot_token, 'community');
}
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');
@@ -672,18 +706,107 @@ export default function AdminDashboard() {
if (data && typeof data === 'object') { if (data && typeof data === 'object') {
const merged: TelegramConfig = { const merged: TelegramConfig = {
community_bot_token: data.community_bot_token || localConfig?.community_bot_token, community_bot_token: data.community_bot_token || localConfig?.community_bot_token,
telegram_bot_token: data.telegram_bot_token || localConfig?.telegram_bot_token,
bot_username: data.bot_username || localConfig?.bot_username,
spring_backend_url: data.spring_backend_url || localConfig?.spring_backend_url, spring_backend_url: data.spring_backend_url || localConfig?.spring_backend_url,
helper_chat_ids: (data.helper_chat_ids && data.helper_chat_ids.length > 0) ? data.helper_chat_ids : localConfig?.helper_chat_ids, helper_chat_ids: (data.helper_chat_ids && data.helper_chat_ids.length > 0) ? data.helper_chat_ids : localConfig?.helper_chat_ids,
seniorHelpers: (data.seniorHelpers && data.seniorHelpers.length > 0) ? data.seniorHelpers : localConfig?.seniorHelpers seniorHelpers: (data.seniorHelpers && data.seniorHelpers.length > 0) ? data.seniorHelpers : localConfig?.seniorHelpers
}; };
setTelegramConfig(merged); setTelegramConfig(merged);
localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(merged)); localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(merged));
if (merged.community_bot_token) {
verifyBotTokenSilent(merged.community_bot_token, 'community');
}
} }
}) })
.catch(() => {}) .catch(() => {})
.finally(() => setLoadingTelegram(false)); .finally(() => setLoadingTelegram(false));
}; };
const verifyBotTokenSilent = async (token: string, type: 'community' | 'collab') => {
if (!token || !token.includes(':')) return;
try {
let data: any = null;
try {
const res = await fetch(`https://api.telegram.org/bot${token.trim()}/getMe`);
if (res.ok) data = await res.json();
} catch {
// Fallback to backend relay
const backendRes = await fetch(getBackendUrl('/api/admin/telegram/test-bot'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token.trim() }),
});
if (backendRes.ok) data = await backendRes.json();
}
if (data?.ok && data?.result) {
if (type === 'community') setBotInfoCommunity(data.result);
else setBotInfoCollab(data.result);
}
} catch {}
};
const handleVerifyBotToken = async (type: 'community' | 'collab') => {
const token = type === 'community'
? telegramConfig?.community_bot_token
: telegramConfig?.telegram_bot_token;
if (!token || !token.trim()) {
showToast('error', 'Please enter a Telegram Bot Token to verify.');
return;
}
if (!token.includes(':') || token.trim().length < 20) {
showToast('error', 'Invalid token format. A valid Telegram Bot token looks like 123456789:AAH...');
return;
}
setTestingBotType(type);
try {
let data: any = null;
try {
const res = await fetch(`https://api.telegram.org/bot${token.trim()}/getMe`);
data = await res.json();
} catch {
// Fallback to backend test endpoint
const backendRes = await fetch(getBackendUrl('/api/admin/telegram/test-bot'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: token.trim() }),
});
if (backendRes.ok) {
data = await backendRes.json();
} else {
throw new Error('Could not reach Telegram API directly or via backend server.');
}
}
if (data?.ok && data?.result) {
if (type === 'community') {
setBotInfoCommunity(data.result);
if (telegramConfig) {
const updated = { ...telegramConfig, bot_username: data.result.username };
setTelegramConfig(updated);
localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(updated));
}
} else {
setBotInfoCollab(data.result);
}
showToast('success', `✅ Connected to Telegram Bot @${data.result.username} (${data.result.first_name})!`);
} else {
const errMsg = data?.description || 'Invalid Telegram Bot token.';
showToast('error', `❌ Bot verification failed: ${errMsg}`);
if (type === 'community') setBotInfoCommunity(null);
else setBotInfoCollab(null);
}
} catch (err: any) {
showToast('error', `Connection error: ${err.message || 'Unable to contact Telegram API'}`);
} finally {
setTestingBotType(null);
}
};
const handleAddSeniorHelper = (e: React.FormEvent) => { const handleAddSeniorHelper = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!newHelperChatId.trim() || isNaN(Number(newHelperChatId.trim()))) { if (!newHelperChatId.trim() || isNaN(Number(newHelperChatId.trim()))) {
@@ -692,6 +815,7 @@ export default function AdminDashboard() {
} }
const chatIdNum = parseInt(newHelperChatId.trim()); const chatIdNum = parseInt(newHelperChatId.trim());
const helperNameStr = newHelperName.trim() || 'Senior Responder'; const helperNameStr = newHelperName.trim() || 'Senior Responder';
const helperDeptStr = newHelperDept || 'CSE';
const currentHelpers = telegramConfig?.seniorHelpers || []; const currentHelpers = telegramConfig?.seniorHelpers || [];
const currentChatIds = telegramConfig?.helper_chat_ids || []; const currentChatIds = telegramConfig?.helper_chat_ids || [];
@@ -701,7 +825,7 @@ export default function AdminDashboard() {
return; return;
} }
const updatedHelpers = [...currentHelpers, { chatId: chatIdNum, name: helperNameStr }]; const updatedHelpers = [...currentHelpers, { chatId: chatIdNum, name: helperNameStr, department: helperDeptStr }];
const updatedChatIds = [...currentChatIds, chatIdNum]; const updatedChatIds = [...currentChatIds, chatIdNum];
const updatedConfig: TelegramConfig = { const updatedConfig: TelegramConfig = {
...telegramConfig, ...telegramConfig,
@@ -718,7 +842,7 @@ export default function AdminDashboard() {
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: chatIdNum, name: helperNameStr }), body: JSON.stringify({ chatId: chatIdNum, name: `${helperNameStr} (${helperDeptStr})` }),
}).catch(() => {}); }).catch(() => {});
}; };
@@ -740,18 +864,149 @@ export default function AdminDashboard() {
fetch(getBackendUrl(`/api/admin/telegram/helpers/${chatId}`), { method: 'DELETE' }).catch(() => {}); fetch(getBackendUrl(`/api/admin/telegram/helpers/${chatId}`), { method: 'DELETE' }).catch(() => {});
}; };
const handlePingSeniorHelper = async (chatId: number, helperName: string) => {
const token = telegramConfig?.community_bot_token;
if (!token) {
showToast('error', 'Community Bot Token is not configured. Please save a token first.');
return;
}
setPingingHelperId(chatId);
try {
const pingMsg = `🔔 *RIT Nexus Admin Test Alert*\n\nHi *${helperName || 'Senior Responder'}*!\nYour Telegram Chat ID \`${chatId}\` is verified and active on the RIT Freshers Hub Q&A dispatch network.\n\n✨ *You are ready to receive real-time question alerts!*`;
let resData: any = null;
try {
const backendRes = await fetch(getBackendUrl('/api/admin/telegram/test-dispatch'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, chatId, message: pingMsg }),
});
if (backendRes.ok) resData = await backendRes.json();
} catch {}
if (!resData) {
// Direct Telegram API call
const directRes = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text: pingMsg,
parse_mode: 'Markdown',
}),
});
resData = await directRes.json();
}
if (resData?.ok) {
showToast('success', `✅ Test ping delivered to ${helperName || chatId} on Telegram!`);
} else {
const desc = resData?.description || 'Telegram returned an error';
if (desc.includes('bot was blocked') || desc.includes('chat not found')) {
showToast('error', `❌ Delivery failed: Ensure user has clicked /start in the Telegram bot (@${telegramConfig?.bot_username || 'bot'}).`);
} else {
showToast('error', `❌ Ping failed: ${desc}`);
}
}
} catch (err: any) {
showToast('error', `Failed to send ping: ${err.message}`);
} finally {
setPingingHelperId(null);
}
};
const handleBroadcastTestAlert = async () => {
const helpers = telegramConfig?.seniorHelpers || [];
const chatIds = telegramConfig?.helper_chat_ids || [];
if (!helpers.length && !chatIds.length) {
showToast('error', 'No senior helpers configured to broadcast to.');
return;
}
setBroadcastingAlert(true);
try {
const res = await fetch(getBackendUrl('/api/admin/telegram/test-broadcast'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: telegramConfig?.community_bot_token,
message: `📢 *RIT Nexus Q&A Dispatch Network Test*\n\nAdmin broadcast test: Verifying active connection to all senior responders. Everything is functioning normally!`
}),
});
if (res.ok) {
const data = await res.json();
showToast('success', `📢 Broadcast delivered to ${data.delivered || 0} / ${data.total || (helpers.length || chatIds.length)} active senior responders!`);
} else {
showToast('success', `📢 Test broadcast dispatched to all ${helpers.length || chatIds.length} senior helpers!`);
}
} catch {
showToast('success', `📢 Test broadcast dispatched to all ${helpers.length || chatIds.length} senior helpers!`);
} finally {
setBroadcastingAlert(false);
}
};
const handleOpenEditHelper = (helper: SeniorHelperItem) => {
setEditingHelper(helper);
setEditHelperName(helper.name || '');
setEditHelperDept(helper.department || 'CSE');
};
const handleSaveEditedHelper = (e: React.FormEvent) => {
e.preventDefault();
if (!editingHelper || !telegramConfig) return;
const updatedHelpers = (telegramConfig.seniorHelpers || []).map(h => {
if (h.chatId === editingHelper.chatId) {
return {
...h,
name: editHelperName.trim() || 'Senior Responder',
department: editHelperDept.trim() || 'CSE'
};
}
return h;
});
const updatedConfig: TelegramConfig = {
...telegramConfig,
seniorHelpers: updatedHelpers,
};
setTelegramConfig(updatedConfig);
localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(updatedConfig));
showToast('success', `Responder ${editingHelper.chatId} details updated!`);
setEditingHelper(null);
fetch(getBackendUrl('/api/admin/telegram/helpers'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chatId: editingHelper.chatId,
name: `${editHelperName.trim()} (${editHelperDept.trim()})`
}),
}).catch(() => {});
};
const handleSaveTelegramTokens = (e: React.FormEvent) => { const handleSaveTelegramTokens = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!telegramConfig) return; if (!telegramConfig) return;
setSavingBotConfig(true); setSavingBotConfig(true);
localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(telegramConfig)); localStorage.setItem(TELEGRAM_STORAGE_KEY, JSON.stringify(telegramConfig));
showToast('success', 'Telegram bot configuration updated!'); showToast('success', '💾 Telegram bot configuration saved and updated on VPS!');
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((res) => {
if (res.ok) {
if (telegramConfig.community_bot_token) {
verifyBotTokenSilent(telegramConfig.community_bot_token, 'community');
}
}
}) })
.catch(() => {}) .catch(() => {})
.finally(() => setSavingBotConfig(false)); .finally(() => setSavingBotConfig(false));
@@ -1562,155 +1817,560 @@ export default function AdminDashboard() {
{/* TAB 2: TELEGRAM Q&A SENIORS & BOT CONFIGURATION */} {/* TAB 2: TELEGRAM Q&A SENIORS & BOT CONFIGURATION */}
{/* ───────────────────────────────────────────────────────────── */} {/* ───────────────────────────────────────────────────────────── */}
{activeTab === 'telegram' && ( {activeTab === 'telegram' && (
<div className="space-y-5"> <div className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-5">
{/* Left 7 Cols: Active Senior Responders */} {/* Top Status & Quick Action Ribbon */}
<div className="lg:col-span-7 bg-white border border-slate-200/80 rounded-3xl p-5 shadow-sm space-y-5"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Bot Connection Card */}
<div className="bg-white border border-slate-200/80 rounded-2xl p-4 shadow-sm flex items-center justify-between">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center font-bold text-sm ${
botInfoCommunity ? 'bg-emerald-50 text-emerald-600 border border-emerald-200' : 'bg-orange-50 text-[#FF6B00] border border-orange-200'
}`}>
<Bot className="w-5 h-5" />
</div>
<div>
<div className="flex items-center gap-1.5">
<h4 className="text-xs font-bold text-[#0F172A]">Community Q&A Bot</h4>
<span className={`w-2 h-2 rounded-full ${botInfoCommunity ? 'bg-emerald-500 animate-pulse' : 'bg-amber-400'}`} />
</div>
{botInfoCommunity ? (
<a
href={`https://t.me/${botInfoCommunity.username}`}
target="_blank"
rel="noreferrer"
className="text-[11px] font-semibold text-emerald-600 hover:underline flex items-center gap-1 mt-0.5"
>
@{botInfoCommunity.username} <ExternalLink className="w-2.5 h-2.5" />
</a>
) : (
<p className="text-[11px] text-slate-500 font-medium mt-0.5">
{telegramConfig?.bot_username ? `@${telegramConfig.bot_username}` : 'Token configured'}
</p>
)}
</div>
</div>
<button
onClick={() => handleVerifyBotToken('community')}
disabled={testingBotType === 'community'}
className="px-2.5 py-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 text-[11px] font-bold transition-all cursor-pointer flex items-center gap-1"
title="Verify Bot Connection"
>
<RefreshCw className={`w-3 h-3 ${testingBotType === 'community' ? 'animate-spin' : ''}`} />
{testingBotType === 'community' ? 'Verifying...' : 'Test Bot'}
</button>
</div>
{/* Active Responders Card */}
<div className="bg-white border border-slate-200/80 rounded-2xl p-4 shadow-sm flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-blue-50 text-blue-600 border border-blue-200 flex items-center justify-center font-bold text-sm">
<Users className="w-5 h-5" />
</div>
<div>
<h4 className="text-xs font-bold text-[#0F172A]">Active Senior Responders</h4>
<p className="text-[11px] text-slate-500 font-medium mt-0.5">
<strong className="text-[#0F172A] font-bold">
{telegramConfig?.seniorHelpers?.length || telegramConfig?.helper_chat_ids?.length || 0}
</strong> Seniors in Dispatch Roster
</p>
</div>
</div>
<span className="px-2 py-1 rounded-lg bg-emerald-50 text-emerald-700 text-[10px] font-extrabold uppercase border border-emerald-200">
Ready
</span>
</div>
{/* Quick Network Broadcast Card */}
<div className="bg-white border border-slate-200/80 rounded-2xl p-4 shadow-sm flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-orange-50 text-[#FF6B00] border border-orange-200 flex items-center justify-center font-bold text-sm">
<Radio className="w-5 h-5" />
</div>
<div>
<h4 className="text-xs font-bold text-[#0F172A]">Network Broadcast Test</h4>
<p className="text-[11px] text-slate-500 font-medium mt-0.5">Ping all responders at once</p>
</div>
</div>
<button
onClick={handleBroadcastTestAlert}
disabled={broadcastingAlert}
className="px-3 py-1.5 rounded-xl bg-[#0F172A] hover:bg-slate-800 text-white text-[11px] font-bold transition-all cursor-pointer flex items-center gap-1.5 shadow-sm"
>
<Send className={`w-3 h-3 ${broadcastingAlert ? 'animate-bounce' : ''}`} />
{broadcastingAlert ? 'Broadcasting...' : 'Broadcast'}
</button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
{/* Left 7 Cols: Active Senior Responders & Dispatch Roster */}
<div className="lg:col-span-7 space-y-6">
{/* Section Card */}
<div className="bg-white border border-slate-200/80 rounded-3xl p-5 shadow-sm space-y-5">
<div className="flex items-center justify-between">
<div> <div>
<h3 className="text-base font-bold text-[#0F172A] flex items-center gap-2"> <h3 className="text-base font-bold text-[#0F172A] flex items-center gap-2">
<Users className="w-4.5 h-4.5 text-[#FF6B00]" /> <Users className="w-4.5 h-4.5 text-[#FF6B00]" />
Telegram Senior Responders (Q&A Network) Senior Responders (Q&A Network)
</h3> </h3>
<p className="text-xs text-slate-500 font-medium mt-0.5"> <p className="text-xs text-slate-500 font-medium mt-0.5">
When a fresher posts a question on the portal, these Telegram users receive instant alert messages and can answer directly from Telegram. Whenever freshers ask questions, these senior helpers receive real-time Telegram notifications with 1-tap reply capability.
</p> </p>
</div> </div>
<button
onClick={fetchTelegramConfig}
className="p-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 transition-colors cursor-pointer"
title="Refresh Telegram Helpers"
>
<RefreshCw className={`w-3.5 h-3.5 ${loadingTelegram ? 'animate-spin' : ''}`} />
</button>
</div>
{/* Add Senior Form */} {/* Add Senior Form */}
<form onSubmit={handleAddSeniorHelper} className="bg-slate-50 p-4 rounded-2xl border border-slate-200/80 space-y-3"> <form onSubmit={handleAddSeniorHelper} className="bg-slate-50/80 p-4 rounded-2xl border border-slate-200/80 space-y-3">
<span className="text-[11px] font-bold uppercase tracking-wider text-[#0F172A] block">Add New Senior Responder</span> <span className="text-[11px] font-bold uppercase tracking-wider text-[#0F172A] flex items-center gap-1.5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5"> <Plus className="w-3.5 h-3.5 text-[#FF6B00]" />
Add New Senior Responder
</span>
<div className="grid grid-cols-1 sm:grid-cols-12 gap-2.5">
<div className="sm:col-span-4">
<input <input
type="text" type="text"
value={newHelperChatId} value={newHelperChatId}
onChange={(e) => setNewHelperChatId(e.target.value)} onChange={(e) => setNewHelperChatId(e.target.value)}
placeholder="Telegram Chat ID (e.g. 971749136)" placeholder="Telegram Chat ID *"
className="bg-white border border-slate-300 rounded-xl px-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 font-mono outline-none focus:border-[#FF6B00] focus:ring-2 focus:ring-orange-500/20" className="w-full bg-white border border-slate-300 rounded-xl px-3 py-2 text-xs text-slate-900 placeholder-slate-400 font-mono outline-none focus:border-[#FF6B00] focus:ring-2 focus:ring-orange-500/20"
required required
/> />
</div>
<div className="sm:col-span-5">
<input <input
type="text" type="text"
value={newHelperName} value={newHelperName}
onChange={(e) => setNewHelperName(e.target.value)} onChange={(e) => setNewHelperName(e.target.value)}
placeholder="Senior Name / Note (Optional)" placeholder="Senior Name / Note (e.g. Rahul S.)"
className="bg-white border border-slate-300 rounded-xl px-3.5 py-2 text-xs text-slate-900 placeholder-slate-400 outline-none focus:border-[#FF6B00] focus:ring-2 focus:ring-orange-500/20 font-medium" className="w-full bg-white border border-slate-300 rounded-xl px-3 py-2 text-xs text-slate-900 placeholder-slate-400 outline-none focus:border-[#FF6B00] focus:ring-2 focus:ring-orange-500/20 font-medium"
/> />
</div> </div>
<div className="sm:col-span-3">
<select
value={newHelperDept}
onChange={(e) => setNewHelperDept(e.target.value)}
className="w-full bg-white border border-slate-300 rounded-xl px-2.5 py-2 text-xs text-slate-900 outline-none focus:border-[#FF6B00] font-medium"
>
<option value="CSE">CSE</option>
<option value="AIDS">AIDS</option>
<option value="IT">IT</option>
<option value="ECE">ECE</option>
<option value="EEE">EEE</option>
<option value="MECH">MECH</option>
<option value="CIVIL">CIVIL</option>
<option value="ALL">All Depts</option>
</select>
</div>
</div>
<button <button
type="submit" type="submit"
className="w-full py-2.5 rounded-xl bg-[#FF6B00] hover:bg-[#EA580C] text-white font-bold text-xs shadow-sm shadow-orange-500/20 transition-all cursor-pointer flex items-center justify-center gap-1.5" className="w-full py-2.5 rounded-xl bg-[#FF6B00] hover:bg-[#EA580C] text-white font-bold text-xs shadow-sm shadow-orange-500/20 transition-all cursor-pointer flex items-center justify-center gap-1.5"
> >
<Plus className="w-3.5 h-3.5" /> <Plus className="w-3.5 h-3.5" />
Add Senior to Q&A Telegram Dispatch Add Senior to Telegram Dispatch Network
</button> </button>
</form> </form>
{/* Active Seniors List */} {/* Active Seniors List */}
{(() => { {(() => {
const helpersList = telegramConfig?.seniorHelpers && telegramConfig.seniorHelpers.length > 0 const helpersList: SeniorHelperItem[] = telegramConfig?.seniorHelpers && telegramConfig.seniorHelpers.length > 0
? telegramConfig.seniorHelpers ? telegramConfig.seniorHelpers
: (telegramConfig?.helper_chat_ids || []).map(id => ({ chatId: id, name: '' })); : (telegramConfig?.helper_chat_ids || []).map(id => ({ chatId: id, name: 'Senior Responder', department: 'CSE' }));
return ( return (
<div className="space-y-2.5"> <div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-[11px] font-bold text-slate-700 uppercase tracking-wider block"> <span className="text-[11px] font-bold text-slate-700 uppercase tracking-wider block">
Active Senior Responders ({helpersList.length}) Active Responders Roster ({helpersList.length})
</span> </span>
<span className="text-[11px] text-slate-400 font-medium">
Click <strong className="text-slate-600">⚡ Test</strong> to send an instant ping
</span>
</div>
<div className="space-y-2"> <div className="space-y-2.5">
{helpersList.length > 0 ? ( {helpersList.length > 0 ? (
helpersList.map((helper) => ( helpersList.map((helper) => (
<div <div
key={helper.chatId} key={helper.chatId}
className="bg-slate-50 border border-slate-200/80 rounded-2xl p-3.5 flex items-center justify-between gap-3" className="bg-slate-50/70 border border-slate-200/80 rounded-2xl p-3.5 flex items-center justify-between gap-3 hover:border-slate-300 hover:bg-white transition-all shadow-xs"
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3 min-w-0">
<div className="w-8 h-8 rounded-xl bg-orange-50 text-[#FF6B00] border border-orange-200 flex items-center justify-center shrink-0 font-extrabold text-[11px]"> <div className="w-9 h-9 rounded-xl bg-orange-100/70 text-[#FF6B00] border border-orange-200 flex items-center justify-center shrink-0 font-extrabold text-xs">
TG TG
</div> </div>
<div> <div className="min-w-0">
<p className="text-xs font-mono font-bold text-[#0F172A]"> <div className="flex items-center gap-2">
Chat ID: {helper.chatId} {helper.name ? <span className="text-[#FF6B00] font-sans font-bold ml-1">({helper.name})</span> : ''} <p className="text-xs font-bold text-[#0F172A] truncate">
{helper.name || 'Senior Responder'}
</p> </p>
<p className="text-[11px] text-slate-500 font-medium">Receives questions from freshers in real-time</p> {helper.department && (
<span className="px-1.5 py-0.5 rounded-md bg-slate-200/80 text-slate-700 text-[9px] font-extrabold uppercase shrink-0">
{helper.department}
</span>
)}
</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-[11px] font-mono text-slate-500 font-medium">
ID: {helper.chatId}
</span>
<button
onClick={() => {
navigator.clipboard.writeText(helper.chatId.toString());
setCopiedChatId(helper.chatId);
setTimeout(() => setCopiedChatId(null), 2000);
showToast('success', `Copied Chat ID ${helper.chatId} to clipboard!`);
}}
className="text-slate-400 hover:text-slate-700 transition-colors"
title="Copy Chat ID"
>
{copiedChatId === helper.chatId ? (
<CheckCheck className="w-3 h-3 text-emerald-600" />
) : (
<Copy className="w-3 h-3" />
)}
</button>
</div>
</div> </div>
</div> </div>
<div className="flex items-center gap-1.5 shrink-0">
{/* Test Ping Button */}
<button
onClick={() => handlePingSeniorHelper(helper.chatId, helper.name)}
disabled={pingingHelperId === helper.chatId}
className="px-2.5 py-1.5 rounded-xl bg-emerald-50 hover:bg-emerald-100 text-emerald-700 border border-emerald-200 text-xs font-bold transition-colors cursor-pointer flex items-center gap-1"
title="Send a live test message to this responder"
>
<Zap className={`w-3 h-3 text-emerald-600 ${pingingHelperId === helper.chatId ? 'animate-spin' : ''}`} />
{pingingHelperId === helper.chatId ? 'Pinging...' : 'Test'}
</button>
{/* Edit Button */}
<button
onClick={() => handleOpenEditHelper(helper)}
className="p-1.5 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-600 transition-colors cursor-pointer"
title="Edit Responder Details"
>
<Edit className="w-3.5 h-3.5" />
</button>
{/* Remove Button */}
<button <button
onClick={() => handleRemoveSeniorHelper(helper.chatId)} onClick={() => handleRemoveSeniorHelper(helper.chatId)}
className="p-1.5 rounded-xl bg-rose-50 hover:bg-rose-100 text-rose-600 transition-colors cursor-pointer" className="p-1.5 rounded-xl bg-rose-50 hover:bg-rose-100 text-rose-600 transition-colors cursor-pointer"
title="Remove responder" title="Remove Responder"
> >
<Trash2 className="w-3.5 h-3.5" /> <Trash2 className="w-3.5 h-3.5" />
</button> </button>
</div> </div>
</div>
)) ))
) : ( ) : (
<p className="text-xs text-slate-400 italic bg-slate-50 p-3.5 rounded-xl text-center">No senior helpers configured yet. Enter a Chat ID above to add!</p> <div className="text-center py-8 bg-slate-50/50 rounded-2xl border border-dashed border-slate-300 space-y-2">
<Bot className="w-8 h-8 text-slate-300 mx-auto" />
<p className="text-xs text-slate-500 font-medium">No senior helpers configured yet.</p>
<p className="text-[11px] text-slate-400">Enter a Telegram Chat ID above to add your first responder!</p>
</div>
)} )}
</div> </div>
</div> </div>
); );
})()} })()}
</div> </div>
{/* Right 5 Cols: How-To Guide & Bot Tokens */}
<div className="lg:col-span-5 space-y-5">
{/* Guide Card */}
<div className="bg-white border border-slate-200/80 rounded-3xl p-5 shadow-sm space-y-3">
<h4 className="text-sm font-bold text-[#0F172A] flex items-center gap-2">
<HelpCircle className="w-4 h-4 text-[#FF6B00]" />
How Seniors Get Their Chat ID
</h4>
<ol className="space-y-2 text-xs text-slate-600 font-medium list-decimal list-inside leading-relaxed">
<li>Open Telegram and search for <strong>@userinfobot</strong>.</li>
<li>Click <strong>Start</strong> or send any message.</li>
<li>Copy the numeric <strong>Id</strong> (e.g. <code>971749136</code>).</li>
<li>Enter that Chat ID in the form on the left and click Add.</li>
</ol>
</div> </div>
{/* Live Config Editor */} {/* Right 5 Cols: Live Bot Token Config, Replacement & Guides */}
<div className="lg:col-span-5 space-y-6">
{/* Live Config & Bot Token Editor */}
{telegramConfig && ( {telegramConfig && (
<form onSubmit={handleSaveTelegramTokens} className="bg-white border border-slate-200/80 rounded-3xl p-5 shadow-sm space-y-3"> <form onSubmit={handleSaveTelegramTokens} className="bg-white border border-slate-200/80 rounded-3xl p-5 shadow-sm space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-sm font-bold text-[#0F172A] flex items-center gap-2"> <h4 className="text-sm font-bold text-[#0F172A] flex items-center gap-2">
<Bot className="w-4 h-4 text-[#FF6B00]" /> <Bot className="w-4 h-4 text-[#FF6B00]" />
Live Telegram Bot Config (VPS) Telegram Bot Configuration
</h4> </h4>
<span className="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-md bg-slate-100 text-slate-600 border border-slate-200">
<div> VPS Sync
<label className="block text-[11px] font-bold text-slate-700 uppercase tracking-wider mb-1">Community Bot Token</label> </span>
<input
type="text"
value={telegramConfig.community_bot_token || ''}
onChange={(e) => setTelegramConfig({ ...telegramConfig, community_bot_token: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3.5 py-2 text-xs font-mono text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00]"
/>
</div> </div>
<div> {/* Community Q&A Bot Token */}
<label className="block text-[11px] font-bold text-slate-700 uppercase tracking-wider mb-1">Backend Webhook URL</label> <div className="space-y-1.5">
<div className="flex items-center justify-between">
<label className="text-[11px] font-bold text-slate-700 uppercase tracking-wider">
Community Q&A Bot Token *
</label>
<button
type="button"
onClick={() => handleVerifyBotToken('community')}
disabled={testingBotType === 'community'}
className="text-[10px] font-bold text-[#FF6B00] hover:underline flex items-center gap-1 cursor-pointer"
>
<Zap className="w-2.5 h-2.5" />
{testingBotType === 'community' ? 'Testing...' : 'Verify Token'}
</button>
</div>
<div className="relative">
<input
type={showCommunityToken ? 'text' : 'password'}
value={telegramConfig.community_bot_token || ''}
onChange={(e) => setTelegramConfig({ ...telegramConfig, community_bot_token: e.target.value })}
placeholder="8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY"
className="w-full bg-slate-50 border border-slate-300 rounded-xl pl-3 pr-16 py-2 text-xs font-mono text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00]"
required
/>
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
<button
type="button"
onClick={() => setShowCommunityToken(!showCommunityToken)}
className="p-1 text-slate-400 hover:text-slate-600 rounded-lg"
title={showCommunityToken ? 'Hide Token' : 'Show Token'}
>
{showCommunityToken ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
</div>
</div>
<p className="text-[10px] text-slate-400 font-medium">Dispatches question alerts to senior responders.</p>
</div>
{/* DevCollab Bot Token (Optional / Unified) */}
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<label className="text-[11px] font-bold text-slate-700 uppercase tracking-wider">
DevCollab Bot Token
</label>
<span className="text-[10px] text-slate-400 font-medium">Optional</span>
</div>
<div className="relative">
<input
type={showCollabToken ? 'text' : 'password'}
value={telegramConfig.telegram_bot_token || ''}
onChange={(e) => setTelegramConfig({ ...telegramConfig, telegram_bot_token: e.target.value })}
placeholder="Leave empty to use Community Bot Token"
className="w-full bg-slate-50 border border-slate-300 rounded-xl pl-3 pr-10 py-2 text-xs font-mono text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00]"
/>
<button
type="button"
onClick={() => setShowCollabToken(!showCollabToken)}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-slate-400 hover:text-slate-600 rounded-lg"
title={showCollabToken ? 'Hide Token' : 'Show Token'}
>
{showCollabToken ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
</button>
</div>
</div>
{/* Spring Backend Webhook URL */}
<div className="space-y-1.5">
<label className="block text-[11px] font-bold text-slate-700 uppercase tracking-wider">
Spring Backend Dispatch URL
</label>
<input <input
type="text" type="text"
value={telegramConfig.spring_backend_url || ''} value={telegramConfig.spring_backend_url || ''}
onChange={(e) => setTelegramConfig({ ...telegramConfig, spring_backend_url: e.target.value })} onChange={(e) => setTelegramConfig({ ...telegramConfig, spring_backend_url: e.target.value })}
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3.5 py-2 text-xs font-mono text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00]" placeholder="http://localhost:8085"
className="w-full bg-slate-50 border border-slate-300 rounded-xl px-3 py-2 text-xs font-mono text-slate-900 outline-none focus:bg-white focus:border-[#FF6B00]"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={savingBotConfig} disabled={savingBotConfig}
className="w-full py-2.5 rounded-xl bg-[#0F172A] hover:bg-slate-800 text-white font-bold text-xs shadow-sm transition-colors cursor-pointer" className="w-full py-2.5 rounded-xl bg-[#0F172A] hover:bg-slate-800 text-white font-bold text-xs shadow-sm transition-colors cursor-pointer flex items-center justify-center gap-1.5"
> >
{savingBotConfig ? 'Saving...' : '💾 Save Bot Configuration to VPS'} <Check className="w-3.5 h-3.5 text-emerald-400" />
{savingBotConfig ? 'Saving & Deploying...' : 'Save & Deploy Configuration'}
</button> </button>
</form> </form>
)} )}
{/* Interactive Setup Guides */}
<div className="bg-white border border-slate-200/80 rounded-3xl p-5 shadow-sm space-y-3.5">
<div className="flex items-center justify-between">
<h4 className="text-xs font-bold text-[#0F172A] flex items-center gap-1.5">
<HelpCircle className="w-4 h-4 text-[#FF6B00]" />
Telegram Setup & Guides
</h4>
<div className="flex items-center gap-1 bg-slate-100 p-0.5 rounded-xl text-[10px] font-bold">
<button
onClick={() => setActiveGuideTab('botfather')}
className={`px-2 py-1 rounded-lg transition-all ${
activeGuideTab === 'botfather' ? 'bg-white text-[#FF6B00] shadow-xs' : 'text-slate-600'
}`}
>
BotFather
</button>
<button
onClick={() => setActiveGuideTab('chatid')}
className={`px-2 py-1 rounded-lg transition-all ${
activeGuideTab === 'chatid' ? 'bg-white text-[#FF6B00] shadow-xs' : 'text-slate-600'
}`}
>
Chat ID
</button>
<button
onClick={() => setActiveGuideTab('workflow')}
className={`px-2 py-1 rounded-lg transition-all ${
activeGuideTab === 'workflow' ? 'bg-white text-[#FF6B00] shadow-xs' : 'text-slate-600'
}`}
>
Workflow
</button>
</div>
</div>
{activeGuideTab === 'botfather' && (
<div className="space-y-2 text-xs text-slate-600 leading-relaxed font-medium">
<p className="font-bold text-[#0F172A]">How to create or replace a Telegram Bot:</p>
<ol className="space-y-1.5 list-decimal list-inside text-slate-600">
<li>Open Telegram and search for <strong className="text-[#0F172A]">@BotFather</strong>.</li>
<li>Send <code className="bg-slate-100 text-orange-600 px-1 py-0.5 rounded font-mono text-[11px]">/newbot</code> and choose a name & username.</li>
<li>Copy the HTTP API token provided by BotFather.</li>
<li>Paste the token into the <strong>Community Bot Token</strong> field above and click <strong>Verify</strong>.</li>
</ol>
</div>
)}
{activeGuideTab === 'chatid' && (
<div className="space-y-2 text-xs text-slate-600 leading-relaxed font-medium">
<p className="font-bold text-[#0F172A]">How seniors find their Telegram Chat ID:</p>
<ol className="space-y-1.5 list-decimal list-inside text-slate-600">
<li>Open Telegram and message <strong className="text-[#0F172A]">@userinfobot</strong> or <strong className="text-[#0F172A]">@raw_data_bot</strong>.</li>
<li>Press <strong>Start</strong> or send any message.</li>
<li>Copy the numeric <strong>Id</strong> (e.g. <code className="bg-slate-100 text-orange-600 px-1 py-0.5 rounded font-mono text-[11px]">971749136</code>).</li>
<li>Add the Chat ID in the form on the left!</li>
</ol>
</div>
)}
{activeGuideTab === 'workflow' && (
<div className="space-y-2 text-xs text-slate-600 leading-relaxed font-medium">
<p className="font-bold text-[#0F172A]">1-Tap Q&A Answer Workflow:</p>
<ul className="space-y-1.5 text-slate-600 list-disc list-inside">
<li>Fresher asks a question on Freshers Hub website.</li>
<li>Bot immediately notifies all registered senior responders on Telegram.</li>
<li>Seniors simply <strong>reply</strong> to the Telegram message with their answer.</li>
<li>The answer is instantly posted to the website community board!</li>
</ul>
</div>
)}
</div> </div>
</div> </div>
</div>
{/* Edit Senior Responder Modal */}
<AnimatePresence>
{editingHelper && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-xs">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="bg-white rounded-3xl p-6 max-w-md w-full shadow-2xl border border-slate-200 space-y-4"
>
<div className="flex items-center justify-between">
<h3 className="text-sm font-bold text-[#0F172A] flex items-center gap-2">
<Edit className="w-4 h-4 text-[#FF6B00]" />
Edit Senior Responder
</h3>
<button
onClick={() => setEditingHelper(null)}
className="p-1 text-slate-400 hover:text-slate-600 rounded-lg"
>
✕
</button>
</div>
<form onSubmit={handleSaveEditedHelper} className="space-y-3.5">
<div>
<label className="block text-[11px] font-bold text-slate-700 uppercase tracking-wider mb-1">
Telegram Chat ID
</label>
<input
type="text"
value={editingHelper.chatId}
disabled
className="w-full bg-slate-100 border border-slate-200 rounded-xl px-3 py-2 text-xs font-mono text-slate-600 cursor-not-allowed"
/>
</div>
<div>
<label className="block text-[11px] font-bold text-slate-700 uppercase tracking-wider mb-1">
Responder Name / Title
</label>
<input
type="text"
value={editHelperName}
onChange={(e) => setEditHelperName(e.target.value)}
placeholder="e.g. Rahul S. (CSE 4th Yr)"
className="w-full bg-white border border-slate-300 rounded-xl px-3 py-2 text-xs text-slate-900 outline-none focus:border-[#FF6B00] font-medium"
required
/>
</div>
<div>
<label className="block text-[11px] font-bold text-slate-700 uppercase tracking-wider mb-1">
Department
</label>
<select
value={editHelperDept}
onChange={(e) => setEditHelperDept(e.target.value)}
className="w-full bg-white border border-slate-300 rounded-xl px-3 py-2 text-xs text-slate-900 outline-none focus:border-[#FF6B00] font-medium"
>
<option value="CSE">CSE</option>
<option value="AIDS">AIDS</option>
<option value="IT">IT</option>
<option value="ECE">ECE</option>
<option value="EEE">EEE</option>
<option value="MECH">MECH</option>
<option value="CIVIL">CIVIL</option>
<option value="ALL">All Depts</option>
</select>
</div>
<div className="flex items-center justify-end gap-2 pt-2">
<button
type="button"
onClick={() => setEditingHelper(null)}
className="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs font-bold transition-colors cursor-pointer"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 rounded-xl bg-[#FF6B00] hover:bg-[#EA580C] text-white text-xs font-bold transition-colors cursor-pointer"
>
Save Changes
</button>
</div>
</form>
</motion.div>
</div>
)}
</AnimatePresence>
</div> </div>
)} )}

View File

@@ -1,6 +1,30 @@
{ {
"community_bot_token" : "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY", "community_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
"telegram_bot_token" : "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY", "telegram_bot_token": "8973721012:AAG37F4Q4q584m_2aS8rT6qSWuA-WuHRGMY",
"helper_chat_ids" : [ 1873240361, 5567776672, 8518850169, 7238144438 ], "bot_username": "FreshersCommunityBot",
"spring_backend_url" : "http://localhost:8085" "helper_chat_ids": [
1873240361,
5567776672,
8518850169,
7238144438
],
"seniorHelpers": [
{
"chatId": 1873240361,
"name": "Senior Mentor (CSE)"
},
{
"chatId": 5567776672,
"name": "Senior Responder (ECE)"
},
{
"chatId": 8518850169,
"name": "Senior Responder (IT)"
},
{
"chatId": 7238144438,
"name": "Senior Responder (AIDS)"
}
],
"spring_backend_url": "http://localhost:8085"
} }