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 */} +
+
+ +