+
+
+ {isOpen && (
+ <>
+
setIsOpen(false)}
+ />
+
+
+
+
+ Notifications
+ {unreadCount > 0 && (
+
+ {unreadCount} new
+
+ )}
+
+ {unreadCount > 0 && (
+
+ )}
+
+
+
+ {notifications.length === 0 ? (
+
+ No notifications yet.
+
+ ) : (
+ notifications.map((notif) => (
+
{
+ if (!notif.isRead) handleMarkAsRead(notif._id);
+ setIsOpen(false);
+ }}
+ className={`p-3.5 transition-colors cursor-pointer flex flex-col gap-1 ${
+ notif.isRead ? 'bg-transparent hover:bg-white/[0.03]' : 'bg-cyan-500/[0.06] hover:bg-cyan-500/[0.1]'
+ }`}
+ >
+
+
+ {notif.title}
+
+
+ {new Date(notif.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+
+
+
+ {notif.message}
+
+ {notif.link && notif.link !== '/' && (
+
+ setIsOpen(false)}
+ className="inline-flex items-center gap-1 text-[11px] font-medium text-cyan-400 hover:text-cyan-300"
+ >
+ View Details
+
+
+ )}
+
+ ))
+ )}
+
+
+ >
+ )}
+
+ );
+};
+
+export default NotificationDropdown;
diff --git a/frontend/src/pages/AdminDashboard.jsx b/frontend/src/pages/AdminDashboard.jsx
new file mode 100644
index 0000000..9c7e948
--- /dev/null
+++ b/frontend/src/pages/AdminDashboard.jsx
@@ -0,0 +1,438 @@
+import { useState, useEffect } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import { ShieldCheck, Users, Stethoscope, Activity, FileText, CheckCircle2, XCircle, AlertCircle, Search, UserCheck, ShieldAlert, Lock, Unlock, Database } from 'lucide-react';
+import { adminAPI } from '../services/api';
+import toast from 'react-hot-toast';
+
+const AdminDashboard = () => {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const initialTab = searchParams.get('tab') || 'overview';
+ const [activeTab, setActiveTab] = useState(initialTab);
+
+ const [analytics, setAnalytics] = useState({
+ totalUsers: 0,
+ totalDoctors: 0,
+ verifiedDoctors: 0,
+ totalPatients: 0,
+ totalAppointments: 0,
+ completedAppointments: 0
+ });
+ const [pendingDoctors, setPendingDoctors] = useState([]);
+ const [usersList, setUsersList] = useState([]);
+ const [auditLogs, setAuditLogs] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [userSearch, setUserSearch] = useState('');
+
+ const fetchAdminData = async () => {
+ setLoading(true);
+ try {
+ const [analyticsRes, usersRes, logsRes] = await Promise.all([
+ adminAPI.getAnalytics(),
+ adminAPI.getAllUsers(),
+ adminAPI.getAuditLogs({ limit: 50 })
+ ]);
+
+ if (analyticsRes.data && analyticsRes.data.success) {
+ setAnalytics(analyticsRes.data.analytics || {});
+ setPendingDoctors(analyticsRes.data.pendingVerifications || []);
+ }
+ if (usersRes.data && usersRes.data.success) {
+ setUsersList(usersRes.data.users || []);
+ }
+ if (logsRes.data && logsRes.data.success) {
+ setAuditLogs(logsRes.data.logs || []);
+ }
+ } catch (err) {
+ console.error('Admin data fetch error:', err);
+ toast.error('Could not load admin portal data');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchAdminData();
+ }, []);
+
+ useEffect(() => {
+ const tabParam = searchParams.get('tab');
+ if (tabParam) setActiveTab(tabParam);
+ }, [searchParams]);
+
+ const handleVerifyDoctor = async (profileId, status, rejectionReason = '') => {
+ try {
+ await adminAPI.verifyDoctor(profileId, { status, rejectionReason });
+ toast.success(`Doctor status updated to ${status}`);
+ fetchAdminData();
+ } catch (err) {
+ toast.error('Could not update doctor verification status');
+ }
+ };
+
+ const handleUserStatusToggle = async (userId, currentStatus) => {
+ const newStatus = currentStatus === 'active' ? 'suspended' : 'active';
+ try {
+ await adminAPI.updateUserStatus(userId, { status: newStatus });
+ toast.success(`User status changed to ${newStatus}`);
+ fetchAdminData();
+ } catch (err) {
+ toast.error('Failed to update user status');
+ }
+ };
+
+ return (
+
+
+
+ {/* Header Banner */}
+
+
+
+ System Administrator Supervision Portal
+
+
MedTech Platform Command Center
+
+ Supervise user registrations, audit real-time system logs, and verify medical license submissions from doctors to ensure highest clinical compliance.
+
+
+
+
+
+
+ {/* Overview Analytics Cards */}
+
+
+
+
Total Registered Users
+
+
+
+
+
{analytics.totalUsers || usersList.length}
+
Patients, Doctors & Admins
+
+
+
+
+
Verified Doctors
+
+
+
+
+
+ {analytics.verifiedDoctors || usersList.filter(u => u.role === 'doctor' && u.doctorProfile?.verificationStatus === 'verified').length} / {analytics.totalDoctors || usersList.filter(u => u.role === 'doctor').length}
+
+
Verified active specialists
+
+
+
+
+
Total Consultations
+
+
+
+
+
{analytics.totalAppointments || 0}
+
{analytics.completedAppointments || 0} completed sessions
+
+
+
+
+
Pending License Checks
+
+
+
+
+
{pendingDoctors.length}
+
Awaiting review below
+
+
+
+ {/* Tabs */}
+
+ {[
+ { id: 'overview', label: `Overview & Doctor Verification (${pendingDoctors.length})`, icon: ShieldCheck },
+ { id: 'users', label: `All Users Directory (${usersList.length})`, icon: Users },
+ { id: 'logs', label: `System Security Audit Logs (${auditLogs.length})`, icon: Database }
+ ].map((t) => {
+ const Icon = t.icon;
+ return (
+
+ );
+ })}
+
+
+ {/* Tab 1: Doctor Verification List */}
+ {activeTab === 'overview' && (
+
+
+
+ Pending Doctor License Verifications
+
+
+
+ {loading ? (
+
+ {[1, 2].map((n) => (
+
+ ))}
+
+ ) : pendingDoctors.length === 0 ? (
+
+
+
+
+
All Doctors Verified!
+
+ There are currently no doctor license verifications pending administrative review.
+
+
+ ) : (
+
+ {pendingDoctors.map((doc) => (
+
+
+
+ {doc.userId?.avatarUrl ? (
+

+ ) : (
+ doc.userId?.name ? doc.userId.name.charAt(0).toUpperCase() : 'D'
+ )}
+
+
+
+
Dr. {doc.userId?.name || 'Specialist'}
+
+ Pending Verification
+
+
+
{doc.specialization || 'General Physician'} • {doc.qualifications?.join(', ') || 'MBBS'}
+
License Number: {doc.licenseNumber || 'Not provided'}
+
+ {doc.licenseDocumentUrl && (
+
+ )}
+
+
+
+ {/* Verification Actions */}
+
+
+
+
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* Tab 2: User Management Directory */}
+ {activeTab === 'users' && (
+
+
+
+ All Platform Accounts ({usersList.length})
+
+
+
+ setUserSearch(e.target.value)}
+ className="w-full bg-white/5 border border-white/10 rounded-xl pl-9 pr-4 py-2.5 text-xs text-white focus:outline-none focus:border-cyan-500"
+ />
+
+
+
+
+
+
+
+
+ | User Account |
+ Role |
+ Joined Date |
+ Account Status |
+ Supervision Actions |
+
+
+
+ {usersList
+ .filter(u => !userSearch || u.name?.toLowerCase().includes(userSearch.toLowerCase()) || u.email?.toLowerCase().includes(userSearch.toLowerCase()) || u.role?.toLowerCase().includes(userSearch.toLowerCase()))
+ .map((u) => (
+
+
+
+
+ {u.avatarUrl ? (
+ 
+ ) : (
+ u.name?.charAt(0).toUpperCase() || 'U'
+ )}
+
+
+ {u.name}
+ {u.email}
+
+
+ |
+
+
+ {u.role || 'patient'}
+
+ |
+
+ {new Date(u.createdAt || Date.now()).toLocaleDateString()}
+ |
+
+
+ {u.status || 'active'}
+
+ |
+
+ {u.role !== 'admin' && (
+
+ )}
+ |
+
+ ))}
+
+
+
+
+
+ )}
+
+ {/* Tab 3: Security & Audit Logs */}
+ {activeTab === 'logs' && (
+
+
+
+ Security & API Audit Trail
+
+ Showing latest 50 security transactions
+
+
+
+
+
+
+
+ | Action & Route |
+ User ID / Role |
+ Status |
+ IP Address |
+ Timestamp |
+
+
+
+ {auditLogs.length === 0 ? (
+
+ |
+ No audit logs recorded yet. Subsequent API actions and authentication requests will be logged automatically here.
+ |
+
+ ) : (
+ auditLogs.map((log) => (
+
+ |
+ {log.action || log.method + ' ' + log.url}
+ {log.details || 'Processed API request'}
+ |
+
+ {log.userRole?.toUpperCase() || 'GUEST'}
+ {log.userId || 'N/A'}
+ |
+
+ = 400 ? 'bg-rose-500/20 text-rose-300 border border-rose-500/30' : 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'
+ }`}>
+ {log.status || 200}
+
+ |
+
+ {log.ipAddress || '127.0.0.1'}
+ |
+
+ {new Date(log.createdAt || Date.now()).toLocaleString()}
+ |
+
+ ))
+ )}
+
+
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default AdminDashboard;
diff --git a/frontend/src/pages/ChatRoom.jsx b/frontend/src/pages/ChatRoom.jsx
new file mode 100644
index 0000000..54de315
--- /dev/null
+++ b/frontend/src/pages/ChatRoom.jsx
@@ -0,0 +1,498 @@
+import { useState, useEffect, useRef } from 'react';
+import { useSearchParams, useNavigate } from 'react-router-dom';
+import { MessageSquare, Send, Paperclip, Image as ImageIcon, FileText, Mic, Video, Phone, Check, CheckCheck, Smile, Search, User } from 'lucide-react';
+import { chatAPI } from '../services/api';
+import { getSocket, connectSocket } from '../services/socket';
+import CallModal from '../components/CallModal';
+import toast from 'react-hot-toast';
+
+const ChatRoom = () => {
+ const [searchParams] = useSearchParams();
+ const navigate = useNavigate();
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
+ const targetDocId = searchParams.get('doctor') || searchParams.get('participant');
+ const targetAppId = searchParams.get('appointment');
+
+ const [conversations, setConversations] = useState([]);
+ const [activeConv, setActiveConv] = useState(null);
+ const [messages, setMessages] = useState([]);
+ const [loadingMessages, setLoadingMessages] = useState(false);
+ const [inputMsg, setInputMsg] = useState('');
+ const [searchFilter, setSearchFilter] = useState('');
+ const [onlineUsers, setOnlineUsers] = useState([]);
+ const [isTyping, setIsTyping] = useState(false);
+ const [typingUser, setTypingUser] = useState('');
+ const [uploadingFile, setUploadingFile] = useState(false);
+
+ // Call modal state
+ const [callModalOpen, setCallModalOpen] = useState(false);
+ const [callIsVideo, setCallIsVideo] = useState(true);
+
+ const messagesEndRef = useRef(null);
+ const fileInputRef = useRef(null);
+ const typingTimeoutRef = useRef(null);
+
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ };
+
+ const fetchConversations = async () => {
+ try {
+ const res = await chatAPI.getConversations();
+ if (res.data && res.data.success) {
+ const list = res.data.conversations || [];
+ setConversations(list);
+
+ // If URL has targetDocId, check or create conversation
+ if (targetDocId && !activeConv) {
+ const existing = list.find(c => c.participants?.some(p => p._id === targetDocId || p.id === targetDocId));
+ if (existing) {
+ setActiveConv(existing);
+ } else {
+ const createRes = await chatAPI.createConversation({ participantId: targetDocId, appointmentId: targetAppId });
+ if (createRes.data?.conversation) {
+ setConversations((prev) => [createRes.data.conversation, ...prev]);
+ setActiveConv(createRes.data.conversation);
+ }
+ }
+ } else if (!activeConv && list.length > 0) {
+ setActiveConv(list[0]);
+ }
+ }
+ } catch (err) {
+ console.error('Fetch conversations error:', err);
+ }
+ };
+
+ const fetchMessages = async (convId) => {
+ if (!convId) return;
+ setLoadingMessages(true);
+ try {
+ const res = await chatAPI.getMessages(convId);
+ if (res.data && res.data.success) {
+ setMessages(res.data.messages || []);
+ }
+ } catch (err) {
+ console.error('Fetch messages error:', err);
+ } finally {
+ setLoadingMessages(false);
+ setTimeout(scrollToBottom, 100);
+ }
+ };
+
+ useEffect(() => {
+ fetchConversations();
+
+ const socket = connectSocket(user.id);
+ if (socket) {
+ socket.on('online-users', (usersList) => {
+ setOnlineUsers(usersList || []);
+ });
+
+ socket.on('receive-message', (incomingMsg) => {
+ if (incomingMsg.conversationId === activeConv?._id) {
+ setMessages((prev) => [...prev, incomingMsg]);
+ setTimeout(scrollToBottom, 100);
+ }
+ // Also refresh conversation summary list
+ fetchConversations();
+ });
+
+ socket.on('user-typing', ({ senderName }) => {
+ setIsTyping(true);
+ setTypingUser(senderName || 'Participant');
+ });
+
+ socket.on('user-stop-typing', () => {
+ setIsTyping(false);
+ setTypingUser('');
+ });
+ }
+
+ return () => {
+ const s = getSocket();
+ if (s) {
+ s.off('online-users');
+ s.off('receive-message');
+ s.off('user-typing');
+ s.off('user-stop-typing');
+ }
+ };
+ }, []);
+
+ useEffect(() => {
+ if (activeConv) {
+ fetchMessages(activeConv._id);
+ const socket = getSocket();
+ if (socket) {
+ socket.emit('join-room', activeConv._id);
+ }
+ }
+ }, [activeConv]);
+
+ const handleInputChange = (e) => {
+ setInputMsg(e.target.value);
+ const socket = getSocket();
+ if (socket && activeConv) {
+ socket.emit('typing', { room: activeConv._id, senderName: user.name });
+ if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current);
+ typingTimeoutRef.current = setTimeout(() => {
+ socket.emit('stop-typing', { room: activeConv._id });
+ }, 2000);
+ }
+ };
+
+ const handleSendMessage = async (e) => {
+ if (e) e.preventDefault();
+ if (!inputMsg.trim() || !activeConv) return;
+
+ const textToSend = inputMsg;
+ setInputMsg('');
+
+ try {
+ const res = await chatAPI.sendMessage({
+ conversationId: activeConv._id,
+ text: textToSend
+ });
+
+ if (res.data && res.data.success) {
+ const newMsg = res.data.message;
+ setMessages((prev) => [...prev, newMsg]);
+ setTimeout(scrollToBottom, 100);
+
+ const socket = getSocket();
+ if (socket) {
+ socket.emit('send-message', {
+ ...newMsg,
+ conversationId: activeConv._id
+ });
+ socket.emit('stop-typing', { room: activeConv._id });
+ }
+ fetchConversations();
+ }
+ } catch (err) {
+ console.error('Send message error:', err);
+ toast.error('Could not send message');
+ }
+ };
+
+ const handleFileUpload = async (e) => {
+ const file = e.target.files?.[0];
+ if (!file || !activeConv) return;
+
+ const formData = new FormData();
+ formData.append('attachment', file);
+ setUploadingFile(true);
+
+ try {
+ const uploadRes = await chatAPI.uploadAttachment(formData);
+ if (uploadRes.data && uploadRes.data.success) {
+ const attachment = uploadRes.data.attachment;
+
+ const msgRes = await chatAPI.sendMessage({
+ conversationId: activeConv._id,
+ text: `Attachment: ${file.name}`,
+ attachments: [attachment]
+ });
+
+ if (msgRes.data && msgRes.data.success) {
+ const newMsg = msgRes.data.message;
+ setMessages((prev) => [...prev, newMsg]);
+ setTimeout(scrollToBottom, 100);
+
+ const socket = getSocket();
+ if (socket) {
+ socket.emit('send-message', {
+ ...newMsg,
+ conversationId: activeConv._id
+ });
+ }
+ fetchConversations();
+ }
+ }
+ } catch (err) {
+ console.error('Upload attachment error:', err);
+ toast.error('File upload failed');
+ } finally {
+ setUploadingFile(false);
+ if (fileInputRef.current) fileInputRef.current.value = '';
+ }
+ };
+
+ const getOtherParticipant = (conv) => {
+ if (!conv || !conv.participants) return {};
+ return conv.participants.find(p => p._id !== user.id && p.id !== user.id) || conv.participants[0] || {};
+ };
+
+ const activeOtherUser = getOtherParticipant(activeConv);
+ const isTargetOnline = activeOtherUser._id && onlineUsers.includes(activeOtherUser._id);
+
+ return (
+
+
+
+ {/* Left Sidebar: Conversations List */}
+
+
+
+ Consultations Chat
+
+
+
+ setSearchFilter(e.target.value)}
+ className="w-full bg-white/5 border border-white/10 rounded-xl pl-9 pr-3 py-2 text-xs text-white focus:outline-none focus:border-cyan-500"
+ />
+
+
+
+
+ {conversations.length === 0 ? (
+
+ No active conversations yet. Start a chat from the Doctor Directory or Appointments page.
+
+ ) : (
+ conversations
+ .filter(c => {
+ const p = getOtherParticipant(c);
+ return !searchFilter || p.name?.toLowerCase().includes(searchFilter.toLowerCase());
+ })
+ .map((conv) => {
+ const p = getOtherParticipant(conv);
+ const isOnline = p._id && onlineUsers.includes(p._id);
+ const isSelected = activeConv?._id === conv._id;
+
+ return (
+
setActiveConv(conv)}
+ className={`p-4 transition cursor-pointer flex items-center gap-3.5 ${
+ isSelected
+ ? 'bg-cyan-500/15 border-l-4 border-cyan-400'
+ : 'hover:bg-white/[0.03]'
+ }`}
+ >
+
+
+ {p.avatarUrl ? (
+

+ ) : (
+ p.name?.charAt(0).toUpperCase() || 'U'
+ )}
+
+ {isOnline && (
+
+ )}
+
+
+
+
+ {p.role === 'doctor' ? `Dr. ${p.name}` : p.name || 'Participant'}
+
+
+ {conv.lastMessage?.createdAt ? new Date(conv.lastMessage.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''}
+
+
+
+ {conv.lastMessage?.text || 'Started consultation'}
+
+
+
+ );
+ })
+ )}
+
+
+
+ {/* Right Panel: Active Chat Room */}
+
+ {activeConv ? (
+ <>
+ {/* Top Header */}
+
+
+
+
+ {activeOtherUser.avatarUrl ? (
+

+ ) : (
+ activeOtherUser.name?.charAt(0).toUpperCase() || 'U'
+ )}
+
+ {isTargetOnline && (
+
+ )}
+
+
+
+ {activeOtherUser.role === 'doctor' ? `Dr. ${activeOtherUser.name}` : activeOtherUser.name || 'Participant'}
+
+
+ {isTyping ? (
+ {typingUser} is typing...
+ ) : isTargetOnline ? (
+ <>• Online>
+ ) : (
+ • Offline
+ )}
+
+
+
+
+ {/* Call Action Buttons */}
+
+
+
+
+
+
+ {/* Messages Area */}
+
+ {loadingMessages ? (
+
+ ) : messages.length === 0 ? (
+
+
+
+
+
No messages here yet. Type your consultation notes or attach a medical report below!
+
+ ) : (
+ messages.map((msg, idx) => {
+ const isMe = msg.sender?._id === user.id || msg.sender === user.id;
+ return (
+
+
+ {msg.text}
+
+ {/* Attachments rendering */}
+ {msg.attachments && msg.attachments.length > 0 && (
+
+ {msg.attachments.map((att, i) => (
+
+ ))}
+
+ )}
+
+
+ {new Date(msg.createdAt || Date.now()).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+ {isMe && }
+
+
+ );
+ })
+ )}
+
+
+
+ {/* Bottom Input Area */}
+
+ >
+ ) : (
+
+
+
+
+
Select a Consultation Chat
+
+ Choose a doctor or patient from the left sidebar to access consultation message history, share medical reports, or start HD video calls.
+
+
+ )}
+
+
+
+
+ {/* WebRTC Call Modal */}
+
setCallModalOpen(false)}
+ user={user}
+ targetUser={activeOtherUser}
+ initialIsVideo={callIsVideo}
+ roomId={`room_conv_${activeConv?._id}`}
+ />
+
+
+ );
+};
+
+export default ChatRoom;
diff --git a/frontend/src/pages/DigitalPrescriptionGenerator.jsx b/frontend/src/pages/DigitalPrescriptionGenerator.jsx
new file mode 100644
index 0000000..55cf37b
--- /dev/null
+++ b/frontend/src/pages/DigitalPrescriptionGenerator.jsx
@@ -0,0 +1,501 @@
+import { useState, useEffect } from 'react';
+import { useSearchParams, useNavigate } from 'react-router-dom';
+import { Pill, Plus, Trash2, Printer, CheckCircle2, ShieldCheck, User, Stethoscope, Heart, Calendar, FileText } from 'lucide-react';
+import { digitalPrescriptionsAPI, adminAPI } from '../services/api';
+import toast from 'react-hot-toast';
+
+const DigitalPrescriptionGenerator = () => {
+ const [searchParams] = useSearchParams();
+ const navigate = useNavigate();
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
+
+ const urlPatientId = searchParams.get('patient') || '';
+ const urlAppId = searchParams.get('appointment') || '';
+ const urlDiagnosis = searchParams.get('diagnosis') || '';
+
+ const [patientId, setPatientId] = useState(urlPatientId);
+ const [appointmentId, setAppointmentId] = useState(urlAppId);
+ const [diagnosis, setDiagnosis] = useState(urlDiagnosis || 'Acute Upper Respiratory Tract Infection');
+ const [patientName, setPatientName] = useState('Patient Consultation');
+ const [patientAge, setPatientAge] = useState(32);
+ const [patientGender, setPatientGender] = useState('Male');
+
+ // Vitals
+ const [bp, setBp] = useState('120/80 mmHg');
+ const [pulse, setPulse] = useState('74 bpm');
+ const [temp, setTemp] = useState('98.6 °F');
+ const [weight, setWeight] = useState('68 kg');
+
+ // Medicines dynamic list
+ const [medicines, setMedicines] = useState([
+ {
+ name: 'Amoxicillin 500mg',
+ dosage: '1 Tablet',
+ frequency: 'Twice a day (1-0-1)',
+ duration: '5 Days',
+ beforeAfterFood: 'After Food',
+ instructions: 'Take with full glass of warm water'
+ },
+ {
+ name: 'Paracetamol 650mg',
+ dosage: '1 Tablet',
+ frequency: 'As needed for fever',
+ duration: '3 Days',
+ beforeAfterFood: 'After Food',
+ instructions: 'Do not exceed 4 tablets in 24 hours'
+ }
+ ]);
+
+ const [generalNotes, setGeneralNotes] = useState('Drink adequate warm fluids, maintain voice rest, and take vitamin C supplements. Seek emergency care if shortness of breath develops.');
+ const [followUpDate, setFollowUpDate] = useState(new Date(Date.now() + 5 * 86400000).toISOString().split('T')[0]);
+ const [submitting, setSubmitting] = useState(false);
+ const [issuedRxId, setIssuedRxId] = useState(null);
+
+ const addMedicineRow = () => {
+ setMedicines([...medicines, {
+ name: '',
+ dosage: '1 Tablet',
+ frequency: 'Twice a day (1-0-1)',
+ duration: '5 Days',
+ beforeAfterFood: 'After Food',
+ instructions: ''
+ }]);
+ };
+
+ const removeMedicineRow = (index) => {
+ setMedicines(medicines.filter((_, idx) => idx !== index));
+ };
+
+ const updateMedicine = (index, field, value) => {
+ const copy = [...medicines];
+ copy[index][field] = value;
+ setMedicines(copy);
+ };
+
+ const handleSubmitPrescription = async (e) => {
+ e.preventDefault();
+ if (!patientId && !appointmentId && !patientName) {
+ return toast.error('Please specify patient details');
+ }
+ if (medicines.length === 0 || !medicines[0].name.trim()) {
+ return toast.error('Please add at least one medication');
+ }
+
+ setSubmitting(true);
+ try {
+ const payload = {
+ patientId: patientId || user.id || '64101234567890abcdef1234',
+ appointmentId: appointmentId || undefined,
+ diagnosis,
+ vitalsObserved: {
+ bloodPressure: bp,
+ pulseRate: pulse,
+ temperature: temp,
+ weight
+ },
+ medicines,
+ generalNotes,
+ followUpDate
+ };
+
+ const res = await digitalPrescriptionsAPI.create(payload);
+ if (res.data && res.data.success) {
+ toast.success('Digital Prescription Issued Successfully!');
+ setIssuedRxId(res.data.prescription?._id || 'RX-2026-9812');
+ }
+ } catch (err) {
+ console.error('Issue Rx error:', err);
+ toast.error(err.response?.data?.message || 'Could not issue digital prescription');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ return (
+
+
+
+ {/* Header Section */}
+
+
+
+ Digital Prescription Studio
+
+
+ Construct interactive, legally formatted prescriptions with real-time live preview and instant vault delivery to patients.
+
+
+
+
+ {issuedRxId && (
+
+ Issued to Vault
+
+ )}
+
+
+
+
+ {/* Studio Grid: Form + Live Preview */}
+
+
+ {/* Left Panel: Form Inputs */}
+
+
+ {/* Right Panel: Printable Live Prescription Sheet Preview */}
+
+
+
+ {/* Prescription Header */}
+
+
+
+
+ MedTech Healthcare
+
+
Advanced Telemedicine Consultation Studio
+
+
+
+
Dr. {user.name || 'Senior Consultant'}
+
{user.specialization || 'General & Telemedicine Specialist'}
+
Reg / License: {user.licenseNumber || 'MCI-REG-98231'}
+
+
+
+ {/* Patient Box */}
+
+
+ Patient Name
+ {patientName || 'Consulting Patient'}
+
+
+ Date & Age
+ {new Date().toLocaleDateString()} • {patientAge} Yrs / {patientGender}
+
+
+
+ {/* Vitals observed strip */}
+
+ BP: {bp}
+ Pulse: {pulse}
+ Temp: {temp}
+ Wt: {weight}
+
+
+ {/* Diagnosis */}
+
+
Diagnosis / Clinical Finding:
+
{diagnosis || 'None specified'}
+
+
+ {/* Rx Symbol & Medicines Table */}
+
+
℞
+
+
+
+
+ | # |
+ Medicine Name |
+ Dosage & Frequency |
+ Timing |
+ Duration |
+
+
+
+ {medicines.map((med, i) => (
+
+ | {i + 1} |
+
+ {med.name || 'Medication Name'}
+ {med.instructions && {med.instructions} }
+ |
+
+ {med.dosage}
+ {med.frequency}
+ |
+ {med.beforeAfterFood} |
+ {med.duration} |
+
+ ))}
+
+
+
+
+ {/* Notes & Follow-up */}
+ {generalNotes && (
+
+
Advice & Diet Instructions:
+
{generalNotes}
+
+ )}
+
+ {/* Footer / Signature Block */}
+
+
+ Next Review / Follow-up:
+ {followUpDate ? new Date(followUpDate).toLocaleDateString() : 'As required'}
+
+
+
+
Dr. {user.name || 'Signature'}
+
+
Authorized Digital Signature
+
Issued via MedTech Encrypted Tele-Consultation
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DigitalPrescriptionGenerator;
diff --git a/frontend/src/pages/DoctorDashboard.jsx b/frontend/src/pages/DoctorDashboard.jsx
new file mode 100644
index 0000000..53b1866
--- /dev/null
+++ b/frontend/src/pages/DoctorDashboard.jsx
@@ -0,0 +1,516 @@
+import { useState, useEffect } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import { Stethoscope, Calendar, Clock, CheckCircle2, XCircle, Video, MessageSquare, DollarSign, Users, ShieldAlert, ShieldCheck, Upload, Award, Plus } from 'lucide-react';
+import { appointmentsAPI, doctorsAPI } from '../services/api';
+import CallModal from '../components/CallModal';
+import toast from 'react-hot-toast';
+
+const DoctorDashboard = () => {
+ const navigate = useNavigate();
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
+ const [appointments, setAppointments] = useState([]);
+ const [doctorProfile, setDoctorProfile] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [activeTab, setActiveTab] = useState('upcoming');
+
+ // Call modal state
+ const [callModalOpen, setCallModalOpen] = useState(false);
+ const [activeCallApp, setActiveCallApp] = useState(null);
+
+ // License upload state
+ const [uploadingLicense, setUploadingLicense] = useState(false);
+
+ // Availability schedule edit state
+ const [availabilityDays, setAvailabilityDays] = useState(['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']);
+ const [startTime, setStartTime] = useState('09:00 AM');
+ const [endTime, setEndTime] = useState('06:00 PM');
+ const [fee, setFee] = useState(500);
+ const [savingSchedule, setSavingSchedule] = useState(false);
+
+ const fetchData = async () => {
+ setLoading(true);
+ try {
+ const [appRes, docRes] = await Promise.all([
+ appointmentsAPI.getAll(),
+ doctorsAPI.getById(user.id || user._id)
+ ]);
+
+ if (appRes.data && appRes.data.success) {
+ setAppointments(appRes.data.appointments || []);
+ }
+ if (docRes.data && docRes.data.success) {
+ const doc = docRes.data.doctor;
+ setDoctorProfile(doc);
+ if (doc.availabilityDays) setAvailabilityDays(doc.availabilityDays);
+ if (doc.availabilityHours?.start) setStartTime(doc.availabilityHours.start);
+ if (doc.availabilityHours?.end) setEndTime(doc.availabilityHours.end);
+ if (doc.consultationFee) setFee(doc.consultationFee);
+ }
+ } catch (err) {
+ console.error('Fetch doctor dashboard data error:', err);
+ toast.error('Could not refresh dashboard data');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ const handleApprove = async (id) => {
+ try {
+ await appointmentsAPI.updateStatus(id, { status: 'approved' });
+ toast.success('Consultation Request Approved!');
+ fetchData();
+ } catch (err) {
+ toast.error('Failed to approve request');
+ }
+ };
+
+ const handleReject = async (id) => {
+ const reason = window.prompt('Enter reason for rejection/cancellation:', 'Time slot unavailable');
+ if (reason === null) return;
+ try {
+ await appointmentsAPI.updateStatus(id, { status: 'rejected', rejectionReason: reason });
+ toast.success('Consultation Request Rejected');
+ fetchData();
+ } catch (err) {
+ toast.error('Failed to reject request');
+ }
+ };
+
+ const handleLicenseUpload = async (e) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ const formData = new FormData();
+ formData.append('license', file);
+ setUploadingLicense(true);
+
+ try {
+ const res = await doctorsAPI.uploadLicense(formData);
+ if (res.data && res.data.success) {
+ toast.success('Medical license uploaded and submitted for verification!');
+ fetchData();
+ }
+ } catch (err) {
+ toast.error('License upload failed');
+ } finally {
+ setUploadingLicense(false);
+ }
+ };
+
+ const handleSaveSchedule = async (e) => {
+ e.preventDefault();
+ setSavingSchedule(true);
+ try {
+ await doctorsAPI.updateProfile({
+ availabilityDays,
+ availabilityHours: { start: startTime, end: endTime },
+ consultationFee: Number(fee)
+ });
+ toast.success('Availability schedule & consultation fee updated!');
+ fetchData();
+ } catch (err) {
+ toast.error('Could not save schedule');
+ } finally {
+ setSavingSchedule(false);
+ }
+ };
+
+ const toggleDay = (day) => {
+ if (availabilityDays.includes(day)) {
+ setAvailabilityDays(availabilityDays.filter(d => d !== day));
+ } else {
+ setAvailabilityDays([...availabilityDays, day]);
+ }
+ };
+
+ // Stats calculation
+ const pendingRequests = appointments.filter(a => a.status === 'booked');
+ const upcomingApps = appointments.filter(a => ['booked', 'approved'].includes(a.status));
+ const completedApps = appointments.filter(a => a.status === 'completed');
+ const totalEarnings = completedApps.reduce((sum, a) => sum + (a.feeAmount || 500), 0);
+ const uniquePatients = new Set(appointments.map(a => a.patientId?._id || a.patientId?.id)).size;
+
+ return (
+
+
+
+ {/* Top Header Banner */}
+
+
+
+ {doctorProfile?.avatarUrl ? (
+

+ ) : (
+ user.name?.charAt(0).toUpperCase() || 'D'
+ )}
+
+
+
+
Dr. {user.name}
+ {doctorProfile?.verificationStatus === 'verified' ? (
+
+ Verified Doctor
+
+ ) : (
+
+ {doctorProfile?.verificationStatus || 'Pending'} Verification
+
+ )}
+
+
+ {doctorProfile?.specialization || 'General Physician'} • {doctorProfile?.qualifications?.join(', ') || 'MBBS'}
+
+
+
+
+
+
+
Issue Digital Prescription
+
+
+
View Earnings Ledger
+
+
+
+
+ {/* Verification Alert if not verified */}
+ {doctorProfile?.verificationStatus !== 'verified' && (
+
+
+
+
+
Complete Your Medical License Verification
+
+ Upload your medical registration certificate or license document for rapid administrator review and verification badge.
+
+
+
+
+
+ )}
+
+ {/* Overview Stats Cards */}
+
+
+
+
Upcoming Sessions
+
+
+
+
+
{upcomingApps.length}
+
Scheduled video/voice consults
+
+
+
+
+
Pending Requests
+
+
+
+
+
{pendingRequests.length}
+
Awaiting your confirmation
+
+
+
+
+
Patients Treated
+
+
+
+
+
{uniquePatients || completedApps.length}
+
Unique consulting patients
+
+
+
+
+
₹{totalEarnings}
+
From completed consultations
+
+
+
+ {/* Workspace Navigation Tabs */}
+
+ {[
+ { id: 'upcoming', label: `Upcoming Consultations (${upcomingApps.length})` },
+ { id: 'pending', label: `Pending Approval (${pendingRequests.length})` },
+ { id: 'completed', label: `Completed History (${completedApps.length})` },
+ { id: 'schedule', label: 'Availability & Fee Settings' }
+ ].map((t) => (
+
+ ))}
+
+
+ {/* Tab 1: Upcoming & Pending & Completed Consultations Grid */}
+ {['upcoming', 'pending', 'completed'].includes(activeTab) && (
+
+ {loading ? (
+
+ {[1, 2].map((n) => (
+
+ ))}
+
+ ) : (
+ (() => {
+ const list = activeTab === 'upcoming' ? upcomingApps : activeTab === 'pending' ? pendingRequests : completedApps;
+ if (list.length === 0) {
+ return (
+
+
+
+
+
No {activeTab} consultations
+
+ Your patient appointments matching this tab will be organized chronologically right here.
+
+
+ );
+ }
+
+ return (
+
+ {list.map((app) => (
+
+
+
+
+ {app.patientId?.avatarUrl ? (
+

+ ) : (
+ app.patientId?.name?.charAt(0).toUpperCase() || 'P'
+ )}
+
+
+
+
{app.patientId?.name || 'Patient'}
+
+ {app.status}
+
+
+
+ {app.consultationType || 'Video Call'} • ₹{app.feeAmount || 500}
+
+
+ 📅 {new Date(app.appointmentDate).toLocaleDateString()}
+ ⏰ {app.timeSlot}
+
+
+
+
+ {/* Actions */}
+
+ {app.status === 'booked' && (
+ <>
+
+
+ >
+ )}
+
+ {['booked', 'approved'].includes(app.status) && (
+
+ )}
+
+
+
+
+ + Issue Rx
+
+
+
+
+ {/* Patient visit note & symptoms */}
+
+
+ Consultation Reason: {app.reasonForVisit}
+
+ {app.symptoms && app.symptoms.length > 0 && (
+
+ {app.symptoms.map((s, idx) => (
+
+ {s}
+
+ ))}
+
+ )}
+
+
+ ))}
+
+ );
+ })()
+ )}
+
+ )}
+
+ {/* Tab 4: Availability & Consultation Fee Schedule Settings */}
+ {activeTab === 'schedule' && (
+
+ )}
+
+
+
+ {/* WebRTC Call Modal */}
+
setCallModalOpen(false)}
+ user={user}
+ targetUser={activeCallApp?.patientId || {}}
+ initialIsVideo={activeCallApp?.consultationType !== 'Voice Call'}
+ roomId={activeCallApp?.meetingRoomId || activeCallApp?._id}
+ />
+
+
+ );
+};
+
+export default DoctorDashboard;
diff --git a/frontend/src/pages/DoctorEarnings.jsx b/frontend/src/pages/DoctorEarnings.jsx
new file mode 100644
index 0000000..67109ef
--- /dev/null
+++ b/frontend/src/pages/DoctorEarnings.jsx
@@ -0,0 +1,154 @@
+import { useState, useEffect } from 'react';
+import { DollarSign, TrendingUp, Calendar, CheckCircle2, ArrowUpRight, Award, ShieldCheck, Download, FileSpreadsheet } from 'lucide-react';
+import { paymentsAPI, appointmentsAPI } from '../services/api';
+
+const DoctorEarnings = () => {
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
+ const [payments, setPayments] = useState([]);
+ const [appointments, setAppointments] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ const fetchEarnings = async () => {
+ setLoading(true);
+ try {
+ const [payRes, appRes] = await Promise.all([
+ paymentsAPI.getAll(),
+ appointmentsAPI.getAll()
+ ]);
+ if (payRes.data && payRes.data.success) {
+ setPayments(payRes.data.payments || []);
+ }
+ if (appRes.data && appRes.data.success) {
+ setAppointments(appRes.data.appointments || []);
+ }
+ } catch (err) {
+ console.error('Fetch earnings error:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchEarnings();
+ }, []);
+
+ const completedApps = appointments.filter(a => a.status === 'completed');
+ const totalRevenue = completedApps.reduce((sum, a) => sum + (a.feeAmount || 500), 0);
+ const avgFee = completedApps.length > 0 ? Math.round(totalRevenue / completedApps.length) : (user.consultationFee || 500);
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+ Revenue & Earnings Analytics
+
+
+ Transparent ledger of all your completed online consultations, consultation fee payouts, and transaction receipts.
+
+
+
+
+
+ {/* Stats Grid */}
+
+
+
+
Total Consultation Revenue
+
+
+
+
+
₹{totalRevenue}
+
+ 100% payout directly deposited
+
+
+
+
+
+
Completed Sessions
+
+
+
+
+
{completedApps.length}
+
Paid video/voice consultations
+
+
+
+
+
Average Fee per Session
+
+
+
+
+
₹{avgFee}
+
Standard fee configured in settings
+
+
+
+ {/* Ledger Table */}
+
+
+ Transaction Ledger & Settlement History
+
+
+
+
+
+
+ | Invoice ID |
+ Patient Name |
+ Consultation Type |
+ Date |
+ Status |
+ Amount (INR) |
+
+
+
+ {completedApps.length === 0 ? (
+
+ |
+ No completed paid sessions yet. Completed patient consultations will be recorded automatically here.
+ |
+
+ ) : (
+ completedApps.map((app) => (
+
+ |
+ INV-{new Date(app.appointmentDate).getFullYear()}-{app._id.substring(app._id.length - 6).toUpperCase()}
+ |
+ {app.patientId?.name || 'Patient'} |
+ {app.consultationType || 'Video Call'} |
+ {new Date(app.appointmentDate).toLocaleDateString()} |
+
+
+ SETTLED
+
+ |
+
+ ₹{app.feeAmount || 500}.00
+ |
+
+ ))
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default DoctorEarnings;
diff --git a/frontend/src/pages/DoctorProfileView.jsx b/frontend/src/pages/DoctorProfileView.jsx
new file mode 100644
index 0000000..3b41383
--- /dev/null
+++ b/frontend/src/pages/DoctorProfileView.jsx
@@ -0,0 +1,437 @@
+import { useState, useEffect } from 'react';
+import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
+import { Star, ShieldCheck, Calendar, Clock, DollarSign, Award, CheckCircle2, AlertCircle, ArrowLeft, MessageSquare } from 'lucide-react';
+import { doctorsAPI, appointmentsAPI, paymentsAPI } from '../services/api';
+import toast from 'react-hot-toast';
+
+const DoctorProfileView = () => {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const shouldOpenBook = searchParams.get('book') === 'true';
+
+ const [doctor, setDoctor] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
+ const [slots, setSlots] = useState([]);
+ const [slotsLoading, setSlotsLoading] = useState(false);
+ const [selectedSlot, setSelectedSlot] = useState('');
+ const [consultationType, setConsultationType] = useState('Video Call');
+ const [reasonForVisit, setReasonForVisit] = useState('');
+ const [symptomTags, setSymptomTags] = useState([]);
+ const [symptomInput, setSymptomInput] = useState('');
+ const [bookingLoading, setBookingLoading] = useState(false);
+ const [confirmedBooking, setConfirmedBooking] = useState(null);
+
+ const fetchDoctor = async () => {
+ setLoading(true);
+ try {
+ const res = await doctorsAPI.getById(id);
+ if (res.data && res.data.success) {
+ setDoctor(res.data.doctor);
+ }
+ } catch (err) {
+ console.error('Fetch doctor details error:', err);
+ toast.error('Could not load doctor details');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchSlots = async (dateStr) => {
+ if (!id || !dateStr) return;
+ setSlotsLoading(true);
+ try {
+ const res = await appointmentsAPI.getSlots({ doctorId: id, date: dateStr });
+ if (res.data && res.data.success) {
+ setSlots(res.data.slots || []);
+ }
+ } catch (err) {
+ console.error('Fetch slots error:', err);
+ } finally {
+ setSlotsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchDoctor();
+ }, [id]);
+
+ useEffect(() => {
+ if (doctor) {
+ fetchSlots(selectedDate);
+ }
+ }, [selectedDate, doctor]);
+
+ const handleAddSymptom = (e) => {
+ if (e.key === 'Enter' && symptomInput.trim()) {
+ e.preventDefault();
+ if (!symptomTags.includes(symptomInput.trim())) {
+ setSymptomTags([...symptomTags, symptomInput.trim()]);
+ }
+ setSymptomInput('');
+ }
+ };
+
+ const handleBookAndPay = async (e) => {
+ e.preventDefault();
+ if (!selectedSlot) {
+ return toast.error('Please select an available time slot');
+ }
+ if (!reasonForVisit.trim()) {
+ return toast.error('Please provide a reason for the consultation');
+ }
+
+ setBookingLoading(true);
+ try {
+ // Book appointment
+ const bookRes = await appointmentsAPI.book({
+ doctorId: doctor.id,
+ appointmentDate: selectedDate,
+ timeSlot: selectedSlot,
+ consultationType,
+ reasonForVisit,
+ symptoms: symptomTags,
+ feeAmount: doctor.consultationFee || 500
+ });
+
+ if (bookRes.data && bookRes.data.success) {
+ const appointment = bookRes.data.appointment;
+
+ // Create instant simulated payment order / invoice
+ const payRes = await paymentsAPI.createOrder({
+ appointmentId: appointment._id,
+ amount: doctor.consultationFee || 500,
+ paymentGateway: 'Razorpay'
+ });
+
+ toast.success('Consultation Booked & Paid Successfully!');
+ setConfirmedBooking({
+ appointment,
+ payment: payRes.data?.payment
+ });
+ }
+ } catch (err) {
+ console.error('Booking error:', err);
+ toast.error(err.response?.data?.message || 'Booking failed. Please try again.');
+ } finally {
+ setBookingLoading(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (!doctor) {
+ return (
+
+
Doctor Profile Not Found
+
+
+ );
+ }
+
+ return (
+
+
+
+ {/* Back navigation */}
+
+
+ {/* Doctor Header Banner */}
+
+
+
+
+
+
+ {doctor.avatarUrl ? (
+

+ ) : (
+ doctor.name?.charAt(0).toUpperCase() || 'D'
+ )}
+
+
+
+
Dr. {doctor.name}
+ {doctor.verificationStatus === 'verified' && (
+
+ Verified Specialist
+
+ )}
+
+
{doctor.specialization}
+
+ {doctor.qualifications?.join(', ') || 'MBBS, MD'} • {doctor.experienceYears || 5} Years Clinical Experience
+
+
+
+ {doctor.rating || 4.8} ({doctor.totalReviews || 12} reviews)
+
+
+ ₹{doctor.consultationFee || 500} per consultation
+
+
+
+
+
+
+
+ Book Appointment
+
+
+
+
+
+
+ {/* Content Layout */}
+
+
+ {/* Left Column: Bio & Reviews */}
+
+
+
+ Professional Biography & Medical Background
+
+
+ {doctor.bio || 'Dr. ' + doctor.name + ' is a highly esteemed ' + doctor.specialization + ' with over ' + (doctor.experienceYears || 5) + ' years of dedicated clinical practice. Focused on preventive healthcare, advanced diagnostics, and compassionate patient relationships, providing thorough online consultations and personalized treatment recommendations.'}
+
+
+
+
+ Availability
+ {doctor.availabilityDays?.slice(0, 4).join(', ') || 'Mon - Sat'}
+
+
+ Consultation Hours
+ {doctor.availabilityHours?.start || '09:00 AM'} - {doctor.availabilityHours?.end || '06:00 PM'}
+
+
+ License Registration
+ {doctor.licenseNumber || 'MCI-REG-98231'}
+
+
+
+
+ {/* Patient Reviews Section */}
+
+
+
+ Patient Reviews & Ratings
+
+ {doctor.reviews?.length || 0} reviews
+
+
+ {doctor.reviews && doctor.reviews.length > 0 ? (
+
+ {doctor.reviews.map((rev) => (
+
+
+
+
+ {rev.patientId?.name?.charAt(0).toUpperCase() || 'P'}
+
+
+ {rev.patientId?.name || 'Verified Patient'}
+ {new Date(rev.createdAt).toLocaleDateString()}
+
+
+
+ ★ {rev.rating}
+
+
+
{rev.comment}
+
+ ))}
+
+ ) : (
+
+ No written reviews yet for this doctor. Be the first to leave a review after your consultation!
+
+ )}
+
+
+
+ {/* Right Column: Interactive Booking Card */}
+
+
+
+
+
Book Consultation
+
Instant online scheduling & payment
+
+
+ Consultation Fee
+ ₹{doctor.consultationFee || 500}
+
+
+
+ {confirmedBooking ? (
+
+
+
+
+
+
Appointment Confirmed!
+
+ Your consultation with Dr. {doctor.name} is scheduled for {selectedDate} at {selectedSlot}.
+
+
+
+
Invoice: {confirmedBooking.payment?.invoiceNumber || 'INV-2026-89123'}
+
Payment Status: PAID
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default DoctorProfileView;
diff --git a/frontend/src/pages/DoctorSearch.jsx b/frontend/src/pages/DoctorSearch.jsx
new file mode 100644
index 0000000..a30620f
--- /dev/null
+++ b/frontend/src/pages/DoctorSearch.jsx
@@ -0,0 +1,300 @@
+import { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import { Search, Star, DollarSign, Filter, Calendar, Award, ShieldCheck, UserCheck } from 'lucide-react';
+import { doctorsAPI } from '../services/api';
+
+const DoctorSearch = () => {
+ const [doctors, setDoctors] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [specialization, setSpecialization] = useState('All');
+ const [maxFee, setMaxFee] = useState(2000);
+ const [minRating, setMinRating] = useState(0);
+
+ const specialties = [
+ 'All',
+ 'General Physician',
+ 'Cardiologist',
+ 'Dermatologist',
+ 'Pediatrician',
+ 'Neurologist',
+ 'Orthopedic',
+ 'Psychiatrist',
+ 'Gynecologist'
+ ];
+
+ const fetchDoctors = async () => {
+ setLoading(true);
+ try {
+ const params = {};
+ if (specialization !== 'All') params.specialization = specialization;
+ if (maxFee) params.maxFee = maxFee;
+ if (minRating > 0) params.rating = minRating;
+ if (searchQuery) params.search = searchQuery;
+
+ const res = await doctorsAPI.getAll(params);
+ if (res.data && res.data.success) {
+ setDoctors(res.data.doctors || []);
+ }
+ } catch (err) {
+ console.error('Fetch doctors error:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchDoctors();
+ }, [specialization, maxFee, minRating]);
+
+ const handleSearchSubmit = (e) => {
+ e.preventDefault();
+ fetchDoctors();
+ };
+
+ return (
+
+
+
+ {/* Header Section */}
+
+
+
+
+ Verified Medical Professionals
+
+
+ Find & Book Expert Doctors Online
+
+
+ Browse verified specialists, check live consultation fees and reviews, and schedule instant HD video or voice consultations from the comfort of your home.
+
+
+
+ {/* Search Bar Form */}
+
+
+
+ {/* Filters and Results Layout */}
+
+
+ {/* Sidebar Filters */}
+
+
+
+ Filters
+
+
+
+
+ {/* Specialization Filter */}
+
+
+
+ {specialties.map((spec) => (
+
+ ))}
+
+
+
+ {/* Max Fee Slider */}
+
+
+
+ ₹{maxFee}
+
+
setMaxFee(Number(e.target.value))}
+ className="w-full accent-cyan-400 bg-slate-800 rounded-lg h-2 cursor-pointer"
+ />
+
+
+ {/* Minimum Rating */}
+
+
+
+ {[0, 3, 4, 4.5].map((rate) => (
+
+ ))}
+
+
+
+
+ {/* Doctor Cards Grid */}
+
+
+
+ Showing {doctors.length} available specialists
+
+
+
+ {loading ? (
+
+ {[1, 2, 3, 4].map((n) => (
+
+ ))}
+
+ ) : doctors.length === 0 ? (
+
+
+
+
+
No doctors found matching criteria
+
+ Try adjusting your filters or resetting the search parameters to browse more medical specialists.
+
+
+
+ ) : (
+
+ {doctors.map((doc) => (
+
+
+ {/* Top Header */}
+
+
+
+ {doc.avatarUrl ? (
+

+ ) : (
+ doc.name?.charAt(0).toUpperCase() || 'D'
+ )}
+
+
+
+
+ Dr. {doc.name}
+
+ {doc.verificationStatus === 'verified' && (
+
+ )}
+
+
{doc.specialization}
+
+ {doc.qualifications?.join(', ') || 'MBBS'} • {doc.experienceYears || 5} Yrs Exp
+
+
+
+
+ {/* Rating badge */}
+
+
+ {doc.rating || 4.8}
+ ({doc.totalReviews || 12})
+
+
+
+ {/* Bio */}
+
+ {doc.bio || 'Experienced medical practitioner committed to comprehensive patient wellness and personalized consultations.'}
+
+
+ {/* Info Pills */}
+
+
+
+ ₹{doc.consultationFee || 500} / session
+
+
+
+ {doc.availabilityDays?.slice(0, 3).join(', ') || 'Mon - Sat'}
+
+
+
+
+ {/* Action Buttons */}
+
+
+ View Profile
+
+
+ Book Slot
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default DoctorSearch;
diff --git a/frontend/src/pages/MedicalRecordsPortal.jsx b/frontend/src/pages/MedicalRecordsPortal.jsx
new file mode 100644
index 0000000..64378cc
--- /dev/null
+++ b/frontend/src/pages/MedicalRecordsPortal.jsx
@@ -0,0 +1,259 @@
+import { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import { FileText, Activity, FileSpreadsheet, Download, ShieldCheck, Calendar, User, Pill, Heart, CheckCircle2, AlertCircle } from 'lucide-react';
+import { digitalPrescriptionsAPI } from '../services/api';
+import toast from 'react-hot-toast';
+
+const MedicalRecordsPortal = () => {
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
+ const [activeTab, setActiveTab] = useState('digital-rx');
+ const [digitalRxList, setDigitalRxList] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ const fetchDigitalPrescriptions = async () => {
+ setLoading(true);
+ try {
+ const res = await digitalPrescriptionsAPI.getAll();
+ if (res.data && res.data.success) {
+ setDigitalRxList(res.data.prescriptions || []);
+ }
+ } catch (err) {
+ console.error('Fetch prescriptions error:', err);
+ toast.error('Could not load digital prescriptions');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchDigitalPrescriptions();
+ }, []);
+
+ const handlePrintPrescription = (rx) => {
+ window.print();
+ };
+
+ return (
+
+
+
+ {/* Header Section */}
+
+
+
+ Encrypted Health Vault
+
+
Combined Medical Records Portal
+
+ All your health documents in one synchronized vault. Access verified prescriptions issued by doctors, digitized OCR prescriptions, blood test reports, and symptom timelines.
+
+
+
+
+
+
OCR Prescriptions
+
+
+
Lab Reports
+
+
+
Symptom Timeline
+
+
+
+
+ {/* Portal Tabs */}
+
+
+
+
OCR Prescription Scanner →
+
+
+
Blood & Lab Reports Organizer →
+
+
+
Symptom Progression Graph →
+
+
+
+ {/* Digital Prescriptions Tab Content */}
+ {activeTab === 'digital-rx' && (
+
+ {loading ? (
+
+ {[1, 2].map((n) => (
+
+ ))}
+
+ ) : digitalRxList.length === 0 ? (
+
+
+
No Digital Prescriptions Issued Yet
+
+ When you complete online video or voice consultations, your doctor's official digital prescriptions with full dosage instructions will appear instantly right here.
+
+
+ Book Consultation Now
+
+
+ ) : (
+
+ {digitalRxList.map((rx) => (
+
+ {/* Header bar */}
+
+
+
+ {rx.doctorId?.name ? rx.doctorId.name.charAt(0).toUpperCase() : 'D'}
+
+
+
+
Dr. {rx.doctorId?.name || 'Specialist'}
+ Verified Rx
+
+
Diagnosis: {rx.diagnosis}
+
Date Issued: {new Date(rx.createdAt).toLocaleDateString()}
+
+
+
+
+
+
+ {/* Vitals Observed (if any) */}
+ {rx.vitalsObserved && Object.values(rx.vitalsObserved).some(v => v) && (
+
+ {rx.vitalsObserved.bloodPressure && (
+
+ Blood Pressure
+ {rx.vitalsObserved.bloodPressure}
+
+ )}
+ {rx.vitalsObserved.pulseRate && (
+
+ Pulse Rate
+ {rx.vitalsObserved.pulseRate}
+
+ )}
+ {rx.vitalsObserved.temperature && (
+
+ Temperature
+ {rx.vitalsObserved.temperature}
+
+ )}
+ {rx.vitalsObserved.weight && (
+
+ Weight
+ {rx.vitalsObserved.weight}
+
+ )}
+
+ )}
+
+ {/* Medicines Table */}
+
+
+ Prescribed Medications
+
+
+
+
+
+
+ | Medicine Name |
+ Dosage |
+ Frequency |
+ Timing |
+ Duration |
+ Instructions |
+
+
+
+ {rx.medicines?.map((med, idx) => (
+
+ | {med.name} |
+ {med.dosage} |
+ {med.frequency} |
+
+
+ {med.beforeAfterFood}
+
+ |
+ {med.duration} |
+ {med.instructions || 'As directed'} |
+
+ ))}
+
+
+
+
+
+ {/* General notes / follow up */}
+ {(rx.generalNotes || rx.followUpDate) && (
+
+ {rx.generalNotes && (
+
+
Doctor's Advice & Notes:
+
{rx.generalNotes}
+
+ )}
+ {rx.followUpDate && (
+
+ Recommended Follow-up:
+ {new Date(rx.followUpDate).toLocaleDateString()}
+
+ )}
+
+ )}
+
+ ))}
+
+ )}
+
+ )}
+
+
+
+ );
+};
+
+export default MedicalRecordsPortal;
diff --git a/frontend/src/pages/PatientAppointments.jsx b/frontend/src/pages/PatientAppointments.jsx
new file mode 100644
index 0000000..1298393
--- /dev/null
+++ b/frontend/src/pages/PatientAppointments.jsx
@@ -0,0 +1,441 @@
+import { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Calendar, Clock, Video, Phone, MessageSquare, RefreshCw, XCircle, FileText, Star, ShieldCheck, CheckCircle2, AlertCircle } from 'lucide-react';
+import { appointmentsAPI, paymentsAPI } from '../services/api';
+import CallModal from '../components/CallModal';
+import toast from 'react-hot-toast';
+
+const PatientAppointments = () => {
+ const navigate = useNavigate();
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
+ const [appointments, setAppointments] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [activeTab, setActiveTab] = useState('All');
+
+ // Call modal state
+ const [callModalOpen, setCallModalOpen] = useState(false);
+ const [activeCallAppointment, setActiveCallAppointment] = useState(null);
+
+ // Reschedule modal state
+ const [rescheduleModalOpen, setRescheduleModalOpen] = useState(false);
+ const [rescheduleTarget, setRescheduleTarget] = useState(null);
+ const [newDate, setNewDate] = useState('');
+ const [newTimeSlot, setNewTimeSlot] = useState('10:00 AM');
+
+ // Invoice modal state
+ const [invoiceModalOpen, setInvoiceModalOpen] = useState(false);
+ const [activeInvoiceApp, setActiveInvoiceApp] = useState(null);
+
+ const fetchAppointments = async () => {
+ setLoading(true);
+ try {
+ const params = {};
+ if (activeTab !== 'All') params.status = activeTab.toLowerCase();
+ const res = await appointmentsAPI.getAll(params);
+ if (res.data && res.data.success) {
+ setAppointments(res.data.appointments || []);
+ }
+ } catch (err) {
+ console.error('Fetch appointments error:', err);
+ toast.error('Could not load appointments');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchAppointments();
+ }, [activeTab]);
+
+ const handleStatusChange = async (id, newStatus, reason = '') => {
+ try {
+ await appointmentsAPI.updateStatus(id, { status: newStatus, rejectionReason: reason });
+ toast.success(`Appointment marked as ${newStatus}`);
+ fetchAppointments();
+ } catch (err) {
+ console.error('Update status error:', err);
+ toast.error('Could not update status');
+ }
+ };
+
+ const handleRescheduleSubmit = async (e) => {
+ e.preventDefault();
+ if (!rescheduleTarget || !newDate || !newTimeSlot) return;
+ try {
+ await appointmentsAPI.reschedule(rescheduleTarget._id, { newDate, newTimeSlot });
+ toast.success('Appointment rescheduled successfully');
+ setRescheduleModalOpen(false);
+ fetchAppointments();
+ } catch (err) {
+ console.error('Reschedule error:', err);
+ toast.error('Could not reschedule appointment');
+ }
+ };
+
+ const handleJoinCall = (app) => {
+ setActiveCallAppointment(app);
+ setCallModalOpen(true);
+ };
+
+ const getStatusBadge = (status) => {
+ switch (status) {
+ case 'approved':
+ case 'booked':
+ return
Upcoming;
+ case 'completed':
+ return
Completed;
+ case 'cancelled':
+ case 'rejected':
+ return
Cancelled;
+ default:
+ return
{status};
+ }
+ };
+
+ const getOtherUser = (app) => {
+ if (user.role === 'doctor') return app.patientId || {};
+ return app.doctorId || {};
+ };
+
+ return (
+
+
+
+ {/* Header Section */}
+
+
+
+ {user.role === 'doctor' ? 'Patient Consultations & Schedule' : 'My Appointments & Consultations'}
+
+
+ Manage your upcoming video consultations, check notes, and join live WebRTC call rooms.
+
+
+
+ {user.role !== 'doctor' && (
+
+ )}
+
+
+ {/* Status Tabs */}
+
+ {['All', 'Booked', 'Approved', 'Completed', 'Cancelled'].map((tab) => (
+
+ ))}
+
+
+ {/* Appointments List Grid */}
+ {loading ? (
+
+ {[1, 2, 3].map((n) => (
+
+ ))}
+
+ ) : appointments.length === 0 ? (
+
+
+
+
+
No {activeTab !== 'All' ? activeTab : ''} appointments found
+
+ {user.role === 'doctor'
+ ? 'When patients schedule online consultations with you, they will appear right here.'
+ : 'You have no appointments right now. Browse our specialist directory to schedule your next health consultation.'}
+
+ {user.role !== 'doctor' && (
+
+ )}
+
+ ) : (
+
+ {appointments.map((app) => {
+ const otherUser = getOtherUser(app);
+ const isUpcoming = ['booked', 'approved'].includes(app.status);
+
+ return (
+
+
+
+ {/* User info */}
+
+
+ {otherUser?.avatarUrl ? (
+

+ ) : (
+ otherUser?.name ? otherUser.name.charAt(0).toUpperCase() : 'U'
+ )}
+
+
+
+
+ {user.role === 'doctor' ? `Patient: ${otherUser.name || 'Anonymous'}` : `Dr. ${otherUser.name || 'Specialist'}`}
+
+ {getStatusBadge(app.status)}
+
+
+ {app.consultationType || 'Video Call'} Consultation • ₹{app.feeAmount || 500}
+
+
+ {new Date(app.appointmentDate).toLocaleDateString()}
+ {app.timeSlot}
+
+
+
+
+ {/* Right side status / invoice */}
+
+
+ Payment:
+ {app.paymentStatus || 'PAID'}
+
+
+
+
+
+ {/* Visit reason & symptom tags */}
+
+
+ Reason for visit: {app.reasonForVisit}
+
+ {app.symptoms && app.symptoms.length > 0 && (
+
+ Symptoms:
+ {app.symptoms.map((s, idx) => (
+
+ {s}
+
+ ))}
+
+ )}
+
+
+ {/* Action Buttons Bar */}
+
+
+ {/* Chat Button */}
+
+
+ {/* Join Call Button (Upcoming or Completed demo) */}
+ {isUpcoming && (
+
+ )}
+
+ {/* Doctor Action buttons */}
+ {user.role === 'doctor' && app.status === 'booked' && (
+ <>
+
+ >
+ )}
+
+ {user.role === 'doctor' && isUpcoming && (
+
+ )}
+
+ {/* Reschedule Button */}
+ {isUpcoming && (
+
+ )}
+
+ {/* Cancel Button */}
+ {isUpcoming && (
+
+ )}
+
+
+
+ );
+ })}
+
+ )}
+
+
+
+ {/* Reschedule Modal */}
+ {rescheduleModalOpen && rescheduleTarget && (
+
+
+
+
Reschedule Consultation
+
+
+
+
+
+ )}
+
+ {/* Invoice Modal */}
+ {invoiceModalOpen && activeInvoiceApp && (
+
+
+
+
+
+ Consultation Tax Invoice
+
+
+
+
+
+
+
+ Invoice Number
+ INV-{new Date(activeInvoiceApp.appointmentDate).getFullYear()}-{activeInvoiceApp._id.substring(activeInvoiceApp._id.length - 6).toUpperCase()}
+
+
+ Date Issued
+ {new Date(activeInvoiceApp.createdAt || Date.now()).toLocaleDateString()}
+
+
+
+
+
+ Patient Details
+ {user.role === 'patient' ? user.name : activeInvoiceApp.patientId?.name || 'Patient'}
+ {user.role === 'patient' ? user.email : activeInvoiceApp.patientId?.email}
+
+
+ Consulting Doctor
+ Dr. {activeInvoiceApp.doctorId?.name || 'Specialist'}
+ {activeInvoiceApp.consultationType || 'HD Video Call'}
+
+
+
+
+
+ Professional Consultation Charges
+ ₹{activeInvoiceApp.feeAmount || 500}.00
+
+
+ Platform & Processing Fee (0%)
+ ₹0.00
+
+
+ Total Amount Paid
+ ₹{activeInvoiceApp.feeAmount || 500}.00
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* WebRTC Video/Voice Call Modal */}
+
setCallModalOpen(false)}
+ user={user}
+ targetUser={getOtherUser(activeCallAppointment || {})}
+ initialIsVideo={activeCallAppointment?.consultationType !== 'Voice Call'}
+ roomId={activeCallAppointment?.meetingRoomId || activeCallAppointment?._id}
+ />
+
+
+ );
+};
+
+export default PatientAppointments;
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index 71c3520..4bfa58d 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -22,13 +22,95 @@ api.interceptors.request.use(
api.interceptors.response.use(
(response) => response,
- (error) => {
- if (error.response?.status === 401) {
+ async (error) => {
+ const originalRequest = error.config;
+ if (error.response?.status === 401 && !originalRequest._retry) {
+ originalRequest._retry = true;
+ const refreshToken = localStorage.getItem('refreshToken');
+ if (refreshToken) {
+ try {
+ const res = await axios.post('http://localhost:4500/api/auth/refresh', { token: refreshToken });
+ if (res.data.success && res.data.token) {
+ localStorage.setItem('token', res.data.token);
+ originalRequest.headers.Authorization = `Bearer ${res.data.token}`;
+ return axios(originalRequest);
+ }
+ } catch (refreshErr) {
+ console.error('Refresh Token Error:', refreshErr);
+ }
+ }
localStorage.removeItem('token');
+ localStorage.removeItem('refreshToken');
+ localStorage.removeItem('user');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
+// Helper API services for all modules
+export const authAPI = {
+ login: (data) => api.post('/auth/login', data),
+ register: (data) => api.post('/auth/register', data),
+ getMe: () => api.get('/auth/me'),
+ forgotPassword: (data) => api.post('/auth/forgot-password', data),
+ resetPassword: (data) => api.post('/auth/reset-password', data),
+ updateProfile: (data) => api.put('/auth/profile', data)
+};
+
+export const doctorsAPI = {
+ getAll: (params) => api.get('/doctors', { params }),
+ getById: (id) => api.get(`/doctors/${id}`),
+ updateProfile: (data) => api.put('/doctors/profile', data),
+ uploadLicense: (formData) => api.post('/doctors/upload-license', formData, { headers: { 'Content-Type': 'multipart/form-data' } })
+};
+
+export const patientAPI = {
+ getProfile: () => api.get('/patients/profile'),
+ updateProfile: (data) => api.put('/patients/profile', data),
+ getHistory: (params) => api.get('/patients/history', { params })
+};
+
+export const appointmentsAPI = {
+ book: (data) => api.post('/appointments/book', data),
+ getAll: (params) => api.get('/appointments', { params }),
+ getSlots: (params) => api.get('/appointments/slots', { params }),
+ updateStatus: (id, data) => api.put(`/appointments/${id}/status`, data),
+ reschedule: (id, data) => api.put(`/appointments/${id}/reschedule`, data)
+};
+
+export const chatAPI = {
+ getConversations: () => api.get('/chat/conversations'),
+ createConversation: (data) => api.post('/chat/conversations', data),
+ getMessages: (conversationId) => api.get(`/chat/messages/${conversationId}`),
+ sendMessage: (data) => api.post('/chat/messages', data),
+ searchMessages: (query) => api.get('/chat/messages/search', { params: { query } }),
+ uploadAttachment: (formData) => api.post('/chat/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } })
+};
+
+export const digitalPrescriptionsAPI = {
+ create: (data) => api.post('/digital-prescriptions', data),
+ getAll: (params) => api.get('/digital-prescriptions', { params }),
+ getById: (id) => api.get(`/digital-prescriptions/${id}`)
+};
+
+export const paymentsAPI = {
+ createOrder: (data) => api.post('/payments/order', data),
+ getAll: () => api.get('/payments'),
+ refund: (id, data) => api.put(`/payments/${id}/refund`, data)
+};
+
+export const notificationsAPI = {
+ getAll: () => api.get('/notifications'),
+ markRead: (id) => api.put(`/notifications/${id}/read`)
+};
+
+export const adminAPI = {
+ getAnalytics: () => api.get('/admin/analytics'),
+ verifyDoctor: (profileId, data) => api.put(`/admin/doctors/${profileId}/verify`, data),
+ getAllUsers: (params) => api.get('/admin/users', { params }),
+ updateUserStatus: (userId, data) => api.put(`/admin/users/${userId}`, data),
+ getAuditLogs: (params) => api.get('/admin/audit-logs', { params })
+};
+
export default api;
\ No newline at end of file
diff --git a/frontend/src/services/socket.js b/frontend/src/services/socket.js
new file mode 100644
index 0000000..376bc11
--- /dev/null
+++ b/frontend/src/services/socket.js
@@ -0,0 +1,38 @@
+import { io } from 'socket.io-client';
+
+let socket = null;
+
+export const connectSocket = (userId) => {
+ if (!socket) {
+ socket = io('http://localhost:4500', {
+ withCredentials: true,
+ transports: ['websocket', 'polling']
+ });
+
+ socket.on('connect', () => {
+ console.log('Connected to Socket.IO server:', socket.id);
+ if (userId) {
+ socket.emit('register-user', userId);
+ }
+ });
+
+ socket.on('disconnect', () => {
+ console.log('Disconnected from Socket.IO server');
+ });
+ } else if (socket && socket.connected === false) {
+ socket.connect();
+ if (userId) {
+ socket.emit('register-user', userId);
+ }
+ }
+ return socket;
+};
+
+export const getSocket = () => socket;
+
+export const disconnectSocket = () => {
+ if (socket) {
+ socket.disconnect();
+ socket = null;
+ }
+};