diff --git a/.env.development b/.env.development index f6862db..2f392ff 100644 --- a/.env.development +++ b/.env.development @@ -1,4 +1,4 @@ VITE_API_URL=http://localhost:8088 VITE_WS_URL=http://localhost:8088 VITE_CHATBOT_URL=http://localhost:8088 -VITE_IOT_URL=http://localhost:8088 \ No newline at end of file +VITE_IOT_URL=http://localhost:8088 diff --git a/.env.production b/.env.production index 189f542..67c9e94 100644 --- a/.env.production +++ b/.env.production @@ -1,4 +1,4 @@ VITE_API_URL=https://apigatewaynew.mangobush-8de88b36.southeastasia.azurecontainerapps.io VITE_WS_URL=https://apigatewaynew.mangobush-8de88b36.southeastasia.azurecontainerapps.io VITE_CHATBOT_URL=https://apigatewaynew.mangobush-8de88b36.southeastasia.azurecontainerapps.io -VITE_IOT_URL=https://apigatewaynew.mangobush-8de88b36.southeastasia.azurecontainerapps.io \ No newline at end of file +VITE_IOT_URL=https://apigatewaynew.mangobush-8de88b36.southeastasia.azurecontainerapps.io diff --git a/src/App.jsx b/src/App.jsx index 2f998ad..42295f3 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -26,7 +26,7 @@ import DocDashboard from "./pages/DoctorsPage/DocDashboard"; import DocViewPatient from "./pages/DoctorViewPatient/DoctorPatientView"; import AdminConfirmAppointments from "./components/PatientPortal/bookings/AdminConfirmAppointments"; - +import PaymentSuccess from "./components/PatientPortal/bookings/PaymentSuccess"; function HomePage() { const stats = [ @@ -126,6 +126,7 @@ function AppContent() { path="/AdminDashboard/confirm-appointments" element={} /> + } /> ); diff --git a/src/api/api.js b/src/api/api.js index fdea9c1..dc604a4 100644 --- a/src/api/api.js +++ b/src/api/api.js @@ -93,6 +93,16 @@ export const getUserAppointments = async () => { return []; } }; +export const getDoctorAppointments = async (doctorId) => { + try { + if (!doctorId) return []; + const res = await API.get(`/doctor/appointments/${doctorId}`); + return res.data; + } catch (err) { + console.error("Error fetching doctor appointments:", err); + return []; + } +}; /* ========================= ADMIN – APPOINTMENTS @@ -116,4 +126,14 @@ export const confirmAppointment = async (appointmentId, params) => { return res.data; }; +// POST /api/doctor/appointments/confirm/{id}?physicalLocation=... OR ?zoomLink=... +export const confirmAppointmentDoctor = async (appointmentId, params) => { + const res = await API.post( + `/doctor/appointments/confirm/${appointmentId}`, + null, + { params } // axios will convert this to query string + ); + return res.data; +}; + export default API; diff --git a/src/components/AdminPortal/AdminDashboard.jsx b/src/components/AdminPortal/AdminDashboard.jsx index 87f1bfe..278bae7 100644 --- a/src/components/AdminPortal/AdminDashboard.jsx +++ b/src/components/AdminPortal/AdminDashboard.jsx @@ -25,6 +25,21 @@ function AdminDashboard() { const [patientCount, setPatientCount] = useState(null); const [deviceCount, setDeviceCount] = useState(null); + // ✅ FIX: read admin info from localStorage (set during login) + const [adminData, setAdminData] = useState({ name: "Admin" }); + + useEffect(() => { + try { + const stored = localStorage.getItem("user"); + if (stored) { + const parsed = JSON.parse(stored); + setAdminData(parsed); + } + } catch (e) { + console.warn("Could not parse user from localStorage", e); + } + }, []); + const handleLogout = () => { localStorage.removeItem("token"); localStorage.removeItem("user"); @@ -105,8 +120,10 @@ function AdminDashboard() { Admin Portal

- Welcome, Section 7{" "} - Janith + Welcome,{" "} + + {adminData?.name || "Admin"} +

@@ -136,7 +153,6 @@ function AdminDashboard() {

Active Doctors

-

{doctorCount ?? "..."}

@@ -149,7 +165,6 @@ function AdminDashboard() {

Active Patients

-

{patientCount ?? "..."}

@@ -157,12 +172,11 @@ function AdminDashboard() {
-
+

IOT Devices

-

{deviceCount ?? "..."}

@@ -174,18 +188,14 @@ function AdminDashboard() {
- - +
+ )}
); diff --git a/src/components/PatientPortal/ECGMonitor.jsx b/src/components/PatientPortal/ECGMonitor.jsx index c9f3419..fd72501 100644 --- a/src/components/PatientPortal/ECGMonitor.jsx +++ b/src/components/PatientPortal/ECGMonitor.jsx @@ -4,6 +4,38 @@ import { TrendingUp, Clock, AlertCircle } from 'lucide-react'; +const API_BASE = import.meta.env.VITE_API_URL; + +const parseRecordedAt = (value) => { + if (!value) return null; + if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value; + if (Array.isArray(value) && value.length >= 5) { + const [year, month, day, hour = 0, minute = 0, second = 0, nano = 0] = value; + return new Date(year, month - 1, day, hour, minute, second, Math.floor(nano / 1000000)); + } + if (typeof value === "string") { + const normalized = value.includes("T") ? value : value.replace(" ", "T"); + const parsed = new Date(normalized); + if (!Number.isNaN(parsed.getTime())) return parsed; + } + return null; +}; + +const formatRecordedDate = (value) => { + const parsed = parseRecordedAt(value); + return parsed ? parsed.toLocaleDateString() : "Date unavailable"; +}; + +const formatRecordedTime = (value) => { + const parsed = parseRecordedAt(value); + return parsed ? parsed.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : "--:--"; +}; + +const formatRecordedDateTime = (value) => { + const parsed = parseRecordedAt(value); + return parsed ? parsed.toLocaleString() : "Date unavailable"; +}; + const ECGMonitor = ({ isFullPage = false, patientId }) => { const [isPaused, setIsPaused] = useState(false); const [history, setHistory] = useState([]); @@ -29,7 +61,7 @@ const ECGMonitor = ({ isFullPage = false, patientId }) => { } try { const token = localStorage.getItem("token"); - const res = await fetch(`http://localhost:8084/api/patient/ecg/history/${patientId}`, { + const res = await fetch(`${API_BASE}/api/patient/ecg/history/${patientId}`, { headers: { Authorization: `Bearer ${token}` } }); if (res.ok) { diff --git a/src/components/PatientPortal/ManualEntryForm.jsx b/src/components/PatientPortal/ManualEntryForm.jsx index 5082114..00be62e 100644 --- a/src/components/PatientPortal/ManualEntryForm.jsx +++ b/src/components/PatientPortal/ManualEntryForm.jsx @@ -84,10 +84,13 @@ const ManualEntryForm = ({ onClose, onSuccess }) => { console.log("Submitting vital signs:", apiData); + const token = localStorage.getItem("token") || ""; + const response = await fetch(`${import.meta.env.VITE_API_URL}/api/vital-signs/submit`, { method: "POST", headers: { "Content-Type": "application/json", + "Authorization": `Bearer ${token}` }, body: JSON.stringify(apiData), }); diff --git a/src/components/PatientPortal/ProfileTab.jsx b/src/components/PatientPortal/ProfileTab.jsx index 87da01e..171d6f6 100644 --- a/src/components/PatientPortal/ProfileTab.jsx +++ b/src/components/PatientPortal/ProfileTab.jsx @@ -1,26 +1,25 @@ import { useState, useEffect } from "react"; -import { User, Mail, Phone, MapPin, Calendar, Heart, Pill, AlertCircle } from "lucide-react"; +import { + User, Mail, Phone, MapPin, Calendar, Heart, + Pill, AlertCircle, Pencil, X, Save, Loader2, +} from "lucide-react"; import { API_BASE_URL } from "../../api"; -// Parse comma-separated string into array of trimmed non-empty strings +/* ── helpers ─────────────────────────────────────────────────────── */ const parseList = (str) => { if (!str || typeof str !== "string") return []; return str.split(",").map((s) => s.trim()).filter(Boolean); }; -// Parse medications: "Name - frequency" or just "Name" const parseMedications = (str) => { const items = parseList(str); return items.map((item) => { const dash = item.indexOf(" - "); - if (dash > 0) { - return { name: item.slice(0, dash).trim(), frequency: item.slice(dash + 3).trim() }; - } + if (dash > 0) return { name: item.slice(0, dash).trim(), frequency: item.slice(dash + 3).trim() }; return { name: item, frequency: "" }; }); }; -// Parse past surgeries: "Name (year)" or just "Name" const parseSurgeries = (str) => { const items = parseList(str); return items.map((item) => { @@ -42,8 +41,7 @@ const calculateAge = (dateOfBirth) => { const formatPatientId = (id) => { if (!id) return "N/A"; - const year = new Date().getFullYear(); - return `P-${year}-${String(id).padStart(3, "0")}`; + return `P-${new Date().getFullYear()}-${String(id).padStart(3, "0")}`; }; const formatAddress = (address, city, district, postalCode) => { @@ -51,253 +49,346 @@ const formatAddress = (address, city, district, postalCode) => { return parts.length ? parts.join(", ") : "—"; }; +/* ── MedSection: read-only chip/card display ────────────────────── */ +const ChipList = ({ items, color }) => + items.length > 0 ? ( +
+ {items.map((item, i) => ( + + {item} + + ))} +
+ ) : ( + None recorded + ); + +/* ── main component ─────────────────────────────────────────────── */ const ProfileTab = ({ patientId: propPatientId }) => { - const [patient, setPatient] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchProfile = async () => { - try { - const patientIdToFetch = propPatientId || localStorage.getItem("patientId"); - const token = localStorage.getItem("token"); - if (!patientIdToFetch) { - setError("Please log in again."); - setLoading(false); - return; - } - const response = await fetch(`${API_BASE_URL}/api/patient/get/${patientIdToFetch}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - ...(token && { Authorization: `Bearer ${token}` }), - }, - }); - if (!response.ok) throw new Error("Failed to load profile"); - const data = await response.json(); - setPatient(data); - setError(null); - } catch (err) { - console.error("Profile fetch error:", err); - setError(err.message || "Failed to load profile"); - } finally { - setLoading(false); - } - }; - fetchProfile(); - }, [propPatientId]); - - if (loading) { - return ( -
-
-
-
- {[1, 2, 3, 4, 5, 6].map((i) => ( -
- ))} -
-
-
-
-
-
-
-
+ const [patient, setPatient] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + /* edit-mode state */ + const [editing, setEditing] = useState(false); + const [saving, setSaving] = useState(false); + const [saveError, setSaveError] = useState(""); + const [form, setForm] = useState({ + allergies: "", + currentMedications: "", + pastSurgeries: "", + chronicConditions: "", + }); + + /* ── fetch profile ── */ + const fetchProfile = async () => { + try { + const id = propPatientId || localStorage.getItem("patientId"); + const token = localStorage.getItem("token"); + if (!id) { setError("Please log in again."); setLoading(false); return; } + const res = await fetch(`${API_BASE_URL}/api/patient/get/${id}`, { + headers: { "Content-Type": "application/json", ...(token && { Authorization: `Bearer ${token}` }) }, + }); + if (!res.ok) throw new Error("Failed to load profile"); + const data = await res.json(); + setPatient(data); + setError(null); + } catch (err) { + setError(err.message || "Failed to load profile"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchProfile(); }, [propPatientId]); + + /* ── open edit form ── */ + const handleEditClick = () => { + setForm({ + allergies: patient?.allergies || "", + currentMedications: patient?.currentMedications || "", + pastSurgeries: patient?.pastSurgeries || "", + chronicConditions: patient?.chronicConditions || "", + }); + setSaveError(""); + setEditing(true); + }; + + /* ── save edits ── */ + const handleSave = async () => { + setSaving(true); + setSaveError(""); + try { + const id = propPatientId || localStorage.getItem("patientId"); + const token = localStorage.getItem("token"); + const res = await fetch(`${API_BASE_URL}/api/patient/update/${id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + ...(token && { Authorization: `Bearer ${token}` }), + }, + body: JSON.stringify(form), + }); + if (!res.ok) throw new Error("Failed to save changes"); + const updated = await res.json(); + setPatient((prev) => ({ ...prev, ...updated })); + setEditing(false); + } catch (err) { + setSaveError(err.message || "Failed to save changes"); + } finally { + setSaving(false); + } + }; + + /* ── loading / error states ── */ + if (loading) return ( +
+
+
+
+ {[1,2,3,4,5,6].map((i) =>
)}
- ); - } - - if (error) { - return ( -
-

{error}

- -
- ); - } +
+ ); + + if (error) return ( +
+

{error}

+ +
+ ); if (!patient) return null; - const age = calculateAge(patient.dateOfBirth); - const patientIdFormatted = formatPatientId(patient.id || patient.Id); - const addressFull = formatAddress(patient.address, patient.city, patient.district, patient.postalCode); - const allergiesList = parseList(patient.allergies); - const medicationsList = parseMedications(patient.currentMedications); - const chronicConditionsList = parseList(patient.medicalConditions); - const pastSurgeriesList = parseSurgeries(patient.pastSurgeries); + /* ── derived display values ── */ + const age = calculateAge(patient.dateOfBirth); + const patientIdFmt = formatPatientId(patient.id || patient.Id); + const addressFull = formatAddress(patient.address, patient.city, patient.district, patient.postalCode); + const allergiesList = parseList(patient.allergies); + const medsList = parseMedications(patient.currentMedications); + const chronicList = parseList(patient.chronicConditions); // ← correct field + const surgeriesList = parseSurgeries(patient.pastSurgeries); + + /* shared textarea style */ + const taClass = "w-full border border-gray-300 rounded-xl px-3 py-2 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-400 resize-none"; return (
- {/* Personal Information Card */} + + {/* ══════════════ Personal Information ══════════════ */}

Personal Information

+ {/* Left column */}
-
- Full Name - {patient.name || "—"} -
-
- Patient ID - {patientIdFormatted} -
-
- Age - - {age != null ? `${age} years` : "—"} - -
-
- Blood Type - {patient.bloodType || "—"} -
-
- NIC No - {patient.nicNo || "—"} -
-
- Gender - {patient.gender || "—"} -
+ {[ + ["Full Name", patient.name], + ["Patient ID", patientIdFmt], + ["Age", age != null ? `${age} years` : "—"], + ["Blood Type", patient.bloodType], + ["NIC No", patient.nicNo], + ["Gender", patient.gender], + ].map(([label, val]) => ( +
+ {label} + {val || "—"} +
+ ))}
+ {/* Right column */}
-
- -
- Email - {patient.email || "—"} -
-
-
- -
- Phone - {patient.contactNo || "—"} -
-
-
- -
- Address - {addressFull} -
-
-
- -
- Emergency Contact - {patient.guardiansName || "—"} - {patient.guardianRelationship && ( - ({patient.guardianRelationship}) - )} + {[ + [Mail, "Email", patient.email, true], + [Phone, "Phone", patient.contactNo, false], + [MapPin, "Address", addressFull, true], + [User, "Emergency Contact", patient.guardiansName, false], + [Phone, "Emergency Phone", patient.guardiansContactNo, false], + ].map(([Icon, label, val, wrap]) => ( +
+ +
+ {label} + {val || "—"} + {label === "Emergency Contact" && patient.guardianRelationship && ( + ({patient.guardianRelationship}) + )} +
-
-
- -
- Emergency Phone - {patient.guardiansContactNo || "—"} -
-
+ ))}
- {/* Medical History Card */} + {/* ══════════════ Medical History ══════════════ */}
-

Medical History

-
-
-
-

- - Allergies -

-
- {allergiesList.length > 0 ? ( - allergiesList.map((allergy, index) => ( - - {allergy} - - )) - ) : ( - None recorded - )} + {/* Header + Edit / Save / Cancel buttons */} +
+

Medical History

+ {!editing ? ( + + ) : ( +
+ + +
+ )} +
+ + {saveError && ( +
+ {saveError} +
+ )} + + {/* ── Read-only view ── */} + {!editing && ( +
+ {/* Left column */} +
+
+

+ Allergies +

+ +
+ +
+

+ Current Medications +

+ {medsList.length > 0 ? ( +
+ {medsList.map((med, i) => ( +
+

{med.name}

+ {med.frequency &&

{med.frequency}

} +
+ ))} +
+ ) : None recorded}
-
-

- - Current Medications -

-
- {medicationsList.length > 0 ? ( - medicationsList.map((med, index) => ( -
-

{med.name}

- {med.frequency &&

{med.frequency}

} -
- )) - ) : ( - None recorded - )} + {/* Right column */} +
+
+

+ Chronic Conditions +

+ +
+ +
+

+ Past Surgeries +

+ {surgeriesList.length > 0 ? ( +
+ {surgeriesList.map((s, i) => ( +
+

{s.name}

+ {s.date &&

{s.date}

} +
+ ))} +
+ ) : None recorded}
+ )} -
-
-

- - Chronic Conditions -

-
- {chronicConditionsList.length > 0 ? ( - chronicConditionsList.map((condition, index) => ( -
-

{condition}

-
- )) - ) : ( - None recorded - )} + {/* ── Edit form ── */} + {editing && ( +
+ {/* Left column */} +
+
+ +