add HospitalNetwork management page and service integration
This commit is contained in:
+847
-281
File diff suppressed because it is too large
Load Diff
+538
-99
@@ -47,9 +47,13 @@ export default function HospitalNetwork({ notify }) {
|
||||
return saved ? JSON.parse(saved) : DEFAULT_STAFF
|
||||
})
|
||||
|
||||
// Providers (Doctors) fetched from real API per hospital
|
||||
const [providers, setProviders] = useState([])
|
||||
const [providersLoading, setProvidersLoading] = useState(false)
|
||||
|
||||
// Selection & UI States
|
||||
const [selectedGroupId, setSelectedGroupId] = useState('HG001')
|
||||
const [activeHospitalStaffId, setActiveHospitalStaffId] = useState(null) // ID of hospital whose staff modal is open
|
||||
const [activeHospitalStaffId, setActiveHospitalStaffId] = useState(null)
|
||||
const [staffTab, setStaffTab] = useState('doctor') // 'doctor' | 'caregiver'
|
||||
|
||||
// Searches & Filters
|
||||
@@ -90,7 +94,17 @@ export default function HospitalNetwork({ notify }) {
|
||||
adminPassword: '',
|
||||
adminUsername: '',
|
||||
})
|
||||
const [staffForm, setStaffForm] = useState({ type: 'doctor', name: '', specialty: '', role: 'Registered Nurse', experience: '1 Yr', certification: '', status: 'On Duty', phone: '', email: '' })
|
||||
const [staffForm, setStaffForm] = useState({
|
||||
type: 'doctor',
|
||||
// Doctor-specific (API provider fields)
|
||||
firstName: '', lastName: '', email: '', username: '', password: '',
|
||||
mobile: '', isdCode: '+91', gender: 'male', dob: '', bloodGroup: '',
|
||||
licenseNo: '', practicingSince: '',
|
||||
providerSpeciality: '', // comma-separated input → array on submit
|
||||
// Caregiver-specific (local only)
|
||||
name: '', specialty: '', role: 'Registered Nurse', experience: '1 Yr',
|
||||
certification: '', status: 'On Duty', phone: '',
|
||||
})
|
||||
|
||||
// Synchronize with LocalStorage
|
||||
useEffect(() => {
|
||||
@@ -119,8 +133,6 @@ export default function HospitalNetwork({ notify }) {
|
||||
fetchedGroups = groupsRes
|
||||
}
|
||||
|
||||
// Normalize backend shape → frontend shape
|
||||
// Backend uses additional_info.city for HQ, reg_no as identifier, no established/description fields
|
||||
fetchedGroups = fetchedGroups.map(g => {
|
||||
const info = g.additional_info
|
||||
? (typeof g.additional_info === 'string' ? JSON.parse(g.additional_info) : g.additional_info)
|
||||
@@ -135,7 +147,6 @@ export default function HospitalNetwork({ notify }) {
|
||||
|
||||
if (fetchedGroups.length > 0) {
|
||||
setGroups(fetchedGroups)
|
||||
// Automatically select the first fetched group ID
|
||||
setSelectedGroupId(fetchedGroups[0].id)
|
||||
}
|
||||
|
||||
@@ -173,7 +184,7 @@ export default function HospitalNetwork({ notify }) {
|
||||
console.log('Hospitals API bypass')
|
||||
}
|
||||
|
||||
// Fetch staff
|
||||
// Fetch caregiver staff
|
||||
try {
|
||||
const staffRes = await adminService.hospitalNetwork.getStaff()
|
||||
let fetchedStaff = []
|
||||
@@ -196,10 +207,41 @@ export default function HospitalNetwork({ notify }) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Fetch providers (doctors) for a specific hospital from the real API
|
||||
const fetchProviders = useCallback(async (hospitalId) => {
|
||||
if (!hospitalId) return
|
||||
setProvidersLoading(true)
|
||||
setProviders([])
|
||||
try {
|
||||
const res = await adminService.hospitalNetwork.getProviders(hospitalId)
|
||||
// Response shape: { success, data: { providers: [...], pagination: {...} } }
|
||||
let list = []
|
||||
if (res?.data?.providers && Array.isArray(res.data.providers)) {
|
||||
list = res.data.providers
|
||||
} else if (res?.data && Array.isArray(res.data)) {
|
||||
list = res.data
|
||||
} else if (Array.isArray(res)) {
|
||||
list = res
|
||||
}
|
||||
setProviders(list)
|
||||
} catch (err) {
|
||||
console.log('Providers fetch failed, showing local doctors')
|
||||
setProviders([])
|
||||
} finally {
|
||||
setProvidersLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
syncWithApi()
|
||||
}, [syncWithApi])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeHospitalStaffId) {
|
||||
fetchProviders(activeHospitalStaffId)
|
||||
}
|
||||
}, [activeHospitalStaffId, fetchProviders])
|
||||
|
||||
// Analytics Helpers
|
||||
const analytics = useMemo(() => {
|
||||
const totalGroups = groups.length
|
||||
@@ -230,16 +272,27 @@ export default function HospitalNetwork({ notify }) {
|
||||
}, [groups, groupQuery])
|
||||
|
||||
const selectedGroup = useMemo(() => {
|
||||
if (selectedGroupId === 'standalone') {
|
||||
return {
|
||||
id: 'standalone',
|
||||
name: 'Independent Facilities',
|
||||
description: 'Standalone hospitals and clinic networks operating independently of corporate provider groups.'
|
||||
}
|
||||
}
|
||||
return groups.find(g => g.id === selectedGroupId) || groups[0]
|
||||
}, [groups, selectedGroupId])
|
||||
|
||||
const filteredHospitals = useMemo(() => {
|
||||
if (!selectedGroupId) return []
|
||||
return hospitals.filter(h =>
|
||||
(h.groupId === selectedGroupId || h.group_id === selectedGroupId || String(h.groupId) === String(selectedGroupId) || String(h.group_id) === String(selectedGroupId)) &&
|
||||
return hospitals.filter(h => {
|
||||
const matchesGroup = selectedGroupId === 'standalone'
|
||||
? (!h.groupId && !h.group_id)
|
||||
: (h.groupId === selectedGroupId || h.group_id === selectedGroupId || String(h.groupId) === String(selectedGroupId) || String(h.group_id) === String(selectedGroupId));
|
||||
|
||||
return matchesGroup &&
|
||||
(h.name.toLowerCase().includes(hospitalQuery.toLowerCase()) ||
|
||||
h.city.toLowerCase().includes(hospitalQuery.toLowerCase()))
|
||||
)
|
||||
(h.city || '').toLowerCase().includes(hospitalQuery.toLowerCase()))
|
||||
})
|
||||
}, [hospitals, selectedGroupId, hospitalQuery])
|
||||
|
||||
const activeHospital = useMemo(() => {
|
||||
@@ -265,11 +318,39 @@ export default function HospitalNetwork({ notify }) {
|
||||
})
|
||||
}, [staff, activeHospitalStaffId, staffTab, staffQuery, specialtyFilter, roleFilter])
|
||||
|
||||
// Unique list of providers (doctors) filtered from the API response
|
||||
const filteredProviders = useMemo(() => {
|
||||
if (staffTab !== 'doctor') return []
|
||||
return providers.filter(p => {
|
||||
const user = p.user || {}
|
||||
const fullName = `${user.first_name || ''} ${user.last_name || ''}`.toLowerCase()
|
||||
const email = (user.email || '').toLowerCase()
|
||||
const username = (user.username || '').toLowerCase()
|
||||
const matchesSearch = fullName.includes(staffQuery.toLowerCase()) ||
|
||||
email.includes(staffQuery.toLowerCase()) ||
|
||||
username.includes(staffQuery.toLowerCase())
|
||||
|
||||
const specs = p.provider_speciality || []
|
||||
const specialtyNames = specs.map(s => s.speciality || '').filter(Boolean)
|
||||
const matchesSpec = specialtyFilter === 'All' ||
|
||||
specialtyNames.some(sName => sName.toLowerCase() === specialtyFilter.toLowerCase())
|
||||
|
||||
return matchesSearch && matchesSpec
|
||||
})
|
||||
}, [providers, staffTab, staffQuery, specialtyFilter])
|
||||
|
||||
// Unique lists for filtering dropdowns
|
||||
const specialties = useMemo(() => {
|
||||
const list = staff.filter(s => s.type === 'doctor').map(s => s.specialty)
|
||||
return ['All', ...new Set(list)]
|
||||
}, [staff])
|
||||
providers.forEach(p => {
|
||||
if (p.provider_speciality && Array.isArray(p.provider_speciality)) {
|
||||
p.provider_speciality.forEach(s => {
|
||||
if (s.speciality) list.push(s.speciality)
|
||||
})
|
||||
}
|
||||
})
|
||||
return ['All', ...new Set(list.filter(Boolean))]
|
||||
}, [staff, providers])
|
||||
|
||||
const roles = useMemo(() => {
|
||||
const list = staff.filter(s => s.type === 'caregiver').map(s => s.role)
|
||||
@@ -516,19 +597,40 @@ export default function HospitalNetwork({ notify }) {
|
||||
setEditItem(item)
|
||||
setStaffForm({
|
||||
type: item.type,
|
||||
name: item.name,
|
||||
// Doctor fields
|
||||
firstName: item.user?.first_name || item.firstName || '',
|
||||
lastName: item.user?.last_name || item.lastName || '',
|
||||
email: item.user?.email || item.email || '',
|
||||
username: item.user?.username || item.username || '',
|
||||
password: '',
|
||||
mobile: item.user?.mobile || item.mobile || '',
|
||||
isdCode: item.user?.isd_code || item.isdCode || '+91',
|
||||
gender: item.user?.gender || item.gender || 'male',
|
||||
dob: item.user?.dob || item.dob || '',
|
||||
bloodGroup: item.user?.blood_group || item.bloodGroup || '',
|
||||
licenseNo: item.license_no || item.licenseNo || '',
|
||||
practicingSince: item.practicing_since || item.practicingSince || '',
|
||||
providerSpeciality: Array.isArray(item.provider_speciality)
|
||||
? item.provider_speciality.join(', ')
|
||||
: (item.providerSpeciality || item.specialty || ''),
|
||||
// Caregiver fields
|
||||
name: item.name || '',
|
||||
specialty: item.specialty || '',
|
||||
role: item.role || 'Registered Nurse',
|
||||
experience: item.experience || '1 Yr',
|
||||
certification: item.certification || '',
|
||||
status: item.status,
|
||||
phone: item.phone,
|
||||
email: item.email
|
||||
status: item.status || 'On Duty',
|
||||
phone: item.phone || '',
|
||||
})
|
||||
} else {
|
||||
setEditItem(null)
|
||||
setStaffForm({
|
||||
type: staffTab,
|
||||
// Doctor fields
|
||||
firstName: '', lastName: '', email: '', username: '', password: '',
|
||||
mobile: '', isdCode: '+91', gender: 'male', dob: '', bloodGroup: '',
|
||||
licenseNo: '', practicingSince: '', providerSpeciality: '',
|
||||
// Caregiver fields
|
||||
name: '',
|
||||
specialty: '',
|
||||
role: 'Registered Nurse',
|
||||
@@ -536,30 +638,95 @@ export default function HospitalNetwork({ notify }) {
|
||||
certification: '',
|
||||
status: staffTab === 'doctor' ? 'On Duty' : 'Active',
|
||||
phone: '',
|
||||
email: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleStaffSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!staffForm.name.trim()) return notify('Provider Name is required', true)
|
||||
|
||||
// ── DOCTOR: call the real provider API ──
|
||||
if (staffForm.type === 'doctor') {
|
||||
if (!staffForm.firstName.trim()) return notify('First Name is required', true)
|
||||
if (!staffForm.lastName.trim()) return notify('Last Name is required', true)
|
||||
if (!staffForm.email.trim()) return notify('Email is required', true)
|
||||
if (modalMode === 'create' && !staffForm.password.trim()) return notify('Password is required', true)
|
||||
if (!staffForm.mobile.trim()) return notify('Mobile number is required', true)
|
||||
if (!staffForm.licenseNo.trim()) return notify('License number is required', true)
|
||||
|
||||
try {
|
||||
const providerPayload = {
|
||||
user: {
|
||||
role: 'provider',
|
||||
first_name: staffForm.firstName,
|
||||
last_name: staffForm.lastName,
|
||||
email: staffForm.email,
|
||||
username: staffForm.username || `${staffForm.firstName.toLowerCase()}_${staffForm.lastName.toLowerCase()}`,
|
||||
password: staffForm.password,
|
||||
mobile: staffForm.mobile,
|
||||
isd_code: staffForm.isdCode,
|
||||
gender: staffForm.gender,
|
||||
dob: staffForm.dob || '1980-01-01',
|
||||
blood_group: staffForm.bloodGroup || 'O+',
|
||||
timezone_id: 1,
|
||||
country_iso: 'IN',
|
||||
is_active: 1,
|
||||
},
|
||||
license_no: staffForm.licenseNo,
|
||||
practicing_since: staffForm.practicingSince || '2010-01-01',
|
||||
provider_speciality: staffForm.providerSpeciality
|
||||
? staffForm.providerSpeciality.split(',').map(s => s.trim()).filter(Boolean)
|
||||
: [],
|
||||
}
|
||||
|
||||
const res = await adminService.hospitalNetwork.addProvider(activeHospitalStaffId, providerPayload)
|
||||
|
||||
if (res?.success || res?.status === 201 || res?.data) {
|
||||
notify(`Dr. ${staffForm.firstName} ${staffForm.lastName} registered successfully.`)
|
||||
// Add optimistic local entry so table updates immediately
|
||||
const localEntry = {
|
||||
id: res?.data?.id || ('S' + Date.now().toString().slice(-4)),
|
||||
hospitalId: activeHospitalStaffId,
|
||||
hospital_id: activeHospitalStaffId,
|
||||
type: 'doctor',
|
||||
name: `Dr. ${staffForm.firstName} ${staffForm.lastName}`,
|
||||
specialty: staffForm.providerSpeciality.split(',')[0]?.trim() || 'General Physician',
|
||||
experience: staffForm.practicingSince
|
||||
? `${new Date().getFullYear() - new Date(staffForm.practicingSince).getFullYear()} Yrs`
|
||||
: '1 Yr',
|
||||
status: 'On Duty',
|
||||
phone: `${staffForm.isdCode}${staffForm.mobile}`,
|
||||
email: staffForm.email,
|
||||
}
|
||||
setStaff(prev => [...prev, localEntry])
|
||||
setModalType(null)
|
||||
} else {
|
||||
notify(res?.message || 'Failed to register provider', true)
|
||||
}
|
||||
} catch (err) {
|
||||
notify(err.message || 'An error occurred while registering provider', true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ── CAREGIVER: local state only ──
|
||||
if (!staffForm.name.trim()) return notify('Caregiver Name is required', true)
|
||||
|
||||
if (modalMode === 'create') {
|
||||
const newStaff = {
|
||||
id: 'S' + Date.now().toString().slice(-4),
|
||||
hospitalId: activeHospitalStaffId,
|
||||
type: staffForm.type,
|
||||
hospital_id: activeHospitalStaffId,
|
||||
type: 'caregiver',
|
||||
name: staffForm.name,
|
||||
status: staffForm.status,
|
||||
phone: staffForm.phone,
|
||||
email: staffForm.email,
|
||||
...(staffForm.type === 'doctor'
|
||||
? { specialty: staffForm.specialty || 'General Physician', experience: staffForm.experience }
|
||||
: { role: staffForm.role, certification: staffForm.certification })
|
||||
email: staffForm.email || '',
|
||||
role: staffForm.role,
|
||||
certification: staffForm.certification,
|
||||
}
|
||||
setStaff(prev => [...prev, newStaff])
|
||||
notify(`${staffForm.type === 'doctor' ? 'Doctor' : 'Caregiver'} "${staffForm.name}" registered successfully.`)
|
||||
notify(`Caregiver "${staffForm.name}" registered successfully.`)
|
||||
} else if (modalMode === 'edit' && editItem) {
|
||||
setStaff(prev => prev.map(s => s.id === editItem.id ? {
|
||||
...s,
|
||||
@@ -647,8 +814,7 @@ export default function HospitalNetwork({ notify }) {
|
||||
<div className="network-left-sidebar">
|
||||
<div className="pane-header">
|
||||
<div className="header-search-row">
|
||||
<h3 className="section-pane-title">Hospital Groups</h3>
|
||||
<button className="add-mini-btn" title="Create New Hospital Group" onClick={() => handleOpenGroupModal('create')}>
|
||||
<h3 className="section-pane-title">Healthcare Network</h3> <button className="add-mini-btn" title="Create New Hospital Group" onClick={() => handleOpenGroupModal('create')}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19" />
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
@@ -670,46 +836,97 @@ export default function HospitalNetwork({ notify }) {
|
||||
</div>
|
||||
|
||||
<div className="groups-list">
|
||||
|
||||
{/* Hospital Groups Section */}
|
||||
<div className="network-section-label">
|
||||
<span>HOSPITAL GROUPS</span>
|
||||
<span>{groups.length}</span>
|
||||
</div>
|
||||
|
||||
{/* Hospital Groups */}
|
||||
{filteredGroups.length === 0 ? (
|
||||
<div className="empty-state">No hospital groups found.</div>
|
||||
groupQuery && (
|
||||
<div className="empty-state">
|
||||
No hospital groups found.
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
filteredGroups.map(g => {
|
||||
const isSelected = g.id === selectedGroupId
|
||||
const hospitalCount = hospitals.filter(h => h.groupId === g.id || h.group_id === g.id || String(h.groupId) === String(g.id) || String(h.group_id) === String(g.id)).length
|
||||
|
||||
// Safely extract hq and established year from backend-specific keys if missing
|
||||
const hospitalCount = hospitals.filter(
|
||||
h =>
|
||||
h.groupId === g.id ||
|
||||
h.group_id === g.id ||
|
||||
String(h.groupId) === String(g.id) ||
|
||||
String(h.group_id) === String(g.id)
|
||||
).length
|
||||
|
||||
let hqVal = g.hq
|
||||
let estVal = g.established
|
||||
|
||||
if (!hqVal && g.additional_info) {
|
||||
try {
|
||||
const info = typeof g.additional_info === 'string' ? JSON.parse(g.additional_info) : g.additional_info
|
||||
const info =
|
||||
typeof g.additional_info === 'string'
|
||||
? JSON.parse(g.additional_info)
|
||||
: g.additional_info
|
||||
|
||||
hqVal = info.city || info.hq
|
||||
} catch (e) {
|
||||
hqVal = 'N/A'
|
||||
}
|
||||
}
|
||||
|
||||
if (!estVal && g.created_at) {
|
||||
estVal = g.created_at.substring(0, 4)
|
||||
}
|
||||
|
||||
hqVal = hqVal || 'N/A'
|
||||
estVal = estVal || 'N/A'
|
||||
|
||||
return (
|
||||
<div
|
||||
key={g.id}
|
||||
className={`group-item-card ${isSelected ? 'selected' : ''}`}
|
||||
className={`group-item-card ${
|
||||
isSelected ? 'selected' : ''
|
||||
}`}
|
||||
onClick={() => setSelectedGroupId(g.id)}
|
||||
>
|
||||
<div className="group-item-header">
|
||||
<h4 className="group-name">{g.name}</h4>
|
||||
<span className="group-badge">{hospitalCount} facilities</span>
|
||||
<h4 className="group-name">
|
||||
{g.name}
|
||||
</h4>
|
||||
|
||||
<span className="group-badge">
|
||||
{hospitalCount} hospital
|
||||
</span>
|
||||
</div>
|
||||
<p className="group-hq">HQ: {hqVal} · Est: {estVal}</p>
|
||||
|
||||
<p className="group-hq">
|
||||
HQ: {hqVal} · Est: {estVal}
|
||||
</p>
|
||||
|
||||
<div className="group-actions-inline">
|
||||
<button className="icon-link-btn" title="Edit Group" onClick={(e) => { e.stopPropagation(); handleOpenGroupModal('edit', g); }}>
|
||||
<button
|
||||
className="icon-link-btn"
|
||||
title="Edit Group"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleOpenGroupModal('edit', g)
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button className="icon-link-btn danger-link" title="Delete Group" onClick={(e) => { e.stopPropagation(); handleDeleteGroup(g.id, g.name); }}>
|
||||
|
||||
<button
|
||||
className="icon-link-btn danger-link"
|
||||
title="Delete Group"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteGroup(g.id, g.name)
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
@@ -717,6 +934,66 @@ export default function HospitalNetwork({ notify }) {
|
||||
)
|
||||
})
|
||||
)}
|
||||
|
||||
{/* Independent Facilities Section */}
|
||||
<div className="network-section-label standalone-section-label">
|
||||
<span>STANDALONE HOSPITALS</span>
|
||||
|
||||
<span>
|
||||
{
|
||||
hospitals.filter(
|
||||
h => !h.groupId && !h.group_id
|
||||
).length
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Standalone Hospitals Card */}
|
||||
{(!groupQuery ||
|
||||
'standalone hospitals'.includes(groupQuery.toLowerCase()) ||
|
||||
'standalone'.includes(groupQuery.toLowerCase())) && (
|
||||
<div
|
||||
className={`group-item-card standalone-card ${
|
||||
selectedGroupId === 'standalone'
|
||||
? 'selected'
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => setSelectedGroupId('standalone')}
|
||||
>
|
||||
<div className="group-item-header">
|
||||
<h4
|
||||
className="group-name"
|
||||
style={{
|
||||
color:
|
||||
selectedGroupId === 'standalone'
|
||||
? '#a855f7'
|
||||
: 'inherit'
|
||||
}}
|
||||
>
|
||||
Standalone Hospitals
|
||||
</h4>
|
||||
|
||||
<span className="group-badge">
|
||||
{
|
||||
hospitals.filter(
|
||||
h => !h.groupId && !h.group_id
|
||||
).length
|
||||
} hospitals
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p
|
||||
className="group-hq"
|
||||
style={{
|
||||
fontSize: '0.85rem',
|
||||
opacity: 0.75
|
||||
}}
|
||||
>
|
||||
Hospitals not affiliated with a hospital group
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -799,7 +1076,7 @@ export default function HospitalNetwork({ notify }) {
|
||||
<button className="mini-action-btn danger" title="Remove Facility" onClick={() => handleDeleteHospital(h.id, h.name)}>
|
||||
Delete
|
||||
</button>
|
||||
<button className="action-btn-primary" onClick={() => { setActiveHospitalStaffId(h.id); setStaffTab('doctor'); }}>
|
||||
<button className="manage-staff-btn" onClick={() => { setActiveHospitalStaffId(h.id); setStaffTab('doctor'); }}>
|
||||
Manage Staff
|
||||
</button>
|
||||
</div>
|
||||
@@ -839,7 +1116,7 @@ export default function HospitalNetwork({ notify }) {
|
||||
className={`roster-tab-btn ${staffTab === 'doctor' ? 'active' : ''}`}
|
||||
onClick={() => { setStaffTab('doctor'); setStaffQuery(''); }}
|
||||
>
|
||||
Medical Doctors ({hospitalStaffStats[activeHospital.id]?.doctors || 0})
|
||||
Medical Doctors ({providers.length})
|
||||
</button>
|
||||
<button
|
||||
className={`roster-tab-btn ${staffTab === 'caregiver' ? 'active' : ''}`}
|
||||
@@ -893,20 +1170,61 @@ export default function HospitalNetwork({ notify }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Roster Table */}
|
||||
{/* Doctors (API-backed cards grid) or Caregivers (local table) */}
|
||||
{staffTab === 'doctor' ? (
|
||||
providersLoading ? (
|
||||
<div className="roster-loading">
|
||||
<div className="roster-spinner"></div>
|
||||
<span>Loading registered providers...</span>
|
||||
</div>
|
||||
) : filteredProviders.length === 0 ? (
|
||||
<div className="empty-state-full" style={{ padding: '32px' }}>
|
||||
<p>No matching doctors found at this facility.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="providers-cards-grid">
|
||||
{filteredProviders.map(p => {
|
||||
const user = p.user || {}
|
||||
const name = `Dr. ${user.first_name || ''} ${user.last_name || ''}`
|
||||
const initial = user.first_name ? user.first_name[0].toUpperCase() : 'D'
|
||||
const specs = p.provider_speciality || []
|
||||
const specialtyText = specs.map(s => s.speciality).join(', ') || 'General Physician'
|
||||
|
||||
return (
|
||||
<div key={p.id} className="provider-card">
|
||||
<div className="provider-card-header">
|
||||
<div className="provider-avatar">
|
||||
{initial}
|
||||
</div>
|
||||
<div className="provider-card-meta">
|
||||
<h4 className="provider-name">{name}</h4>
|
||||
<p className="provider-email">{user.email || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="provider-card-body">
|
||||
<span className="provider-tag tag-specialty">{specialtyText}</span>
|
||||
<span className="provider-tag tag-license">Lic: {p.license_no}</span>
|
||||
<span className="provider-tag tag-since">Since: {p.practicing_since || 'N/A'}</span>
|
||||
{user.mobile && (
|
||||
<span className="provider-tag tag-mobile">📞 {user.isd_code || ''} {user.mobile}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="provider-card-footer">
|
||||
<span className="provider-status-dot">Active</span>
|
||||
<div className="provider-actions">
|
||||
<button className="mini-action-btn" onClick={() => handleOpenStaffModal('edit', p)}>Edit</button>
|
||||
<button className="mini-action-btn danger" onClick={() => handleDeleteStaff(p.id, name)}>Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="table-responsive roster-table-container">
|
||||
<table className="telemetry-table">
|
||||
<thead>
|
||||
{staffTab === 'doctor' ? (
|
||||
<tr>
|
||||
<th>Clinician Name</th>
|
||||
<th>Specialty</th>
|
||||
<th>Experience</th>
|
||||
<th>Contact Number</th>
|
||||
<th>Operations Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
) : (
|
||||
<tr>
|
||||
<th>Caregiver Name</th>
|
||||
<th>Role category</th>
|
||||
@@ -915,7 +1233,6 @@ export default function HospitalNetwork({ notify }) {
|
||||
<th>Work Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredStaff.length === 0 ? (
|
||||
@@ -932,7 +1249,7 @@ export default function HospitalNetwork({ notify }) {
|
||||
<td>
|
||||
<div className="staff-avatar-cell">
|
||||
<div className="cg-avatar mini-av">
|
||||
{s.type === 'doctor' ? 'DR' : (s.role ? s.role.substring(0, 2).toUpperCase() : 'CG')}
|
||||
{s.role ? s.role.substring(0, 2).toUpperCase() : 'CG'}
|
||||
</div>
|
||||
<div className="staff-meta-details">
|
||||
<strong className="staff-cell-name">{s.name}</strong>
|
||||
@@ -940,8 +1257,8 @@ export default function HospitalNetwork({ notify }) {
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{s.type === 'doctor' ? s.specialty : s.role}</td>
|
||||
<td>{s.type === 'doctor' ? s.experience : (s.certification || 'Verified')}</td>
|
||||
<td>{s.role}</td>
|
||||
<td>{s.certification || 'Verified'}</td>
|
||||
<td>{s.phone || 'N/A'}</td>
|
||||
<td>
|
||||
<span className={`status-indicator-tag status-${statusClass}`}>
|
||||
@@ -961,6 +1278,7 @@ export default function HospitalNetwork({ notify }) {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1284,53 +1602,183 @@ export default function HospitalNetwork({ notify }) {
|
||||
|
||||
{/* 3. Staff Form */}
|
||||
{modalType === 'staff' && (
|
||||
<form onSubmit={handleStaffSubmit} className="modal-form">
|
||||
<div className="form-row-double">
|
||||
<div className="form-group">
|
||||
<form onSubmit={handleStaffSubmit} className={`modal-form ${staffForm.type === 'doctor' ? 'form-grid-two-col' : ''}`}>
|
||||
|
||||
{/* Roster type selector — always full width */}
|
||||
<div className="form-group" style={{ gridColumn: staffForm.type === 'doctor' ? '1 / -1' : undefined }}>
|
||||
<label>Roster Type</label>
|
||||
<select
|
||||
disabled={modalMode === 'edit'}
|
||||
value={staffForm.type}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, type: e.target.value }))}
|
||||
>
|
||||
<option value="doctor">Medical Doctor</option>
|
||||
<option value="doctor">Medical Doctor (Provider)</option>
|
||||
<option value="caregiver">Caregiver / Nurse</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* ── DOCTOR FORM (two-column, API-backed) ── */}
|
||||
{staffForm.type === 'doctor' ? (
|
||||
<>
|
||||
{/* Left Column */}
|
||||
<div className="form-column">
|
||||
<h4 className="form-section-title">Personal Information</h4>
|
||||
|
||||
<div className="form-row-double">
|
||||
<div className="form-group">
|
||||
<label>Full Legal Name *</label>
|
||||
<label>First Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g., Dr. Robert Chen"
|
||||
value={staffForm.name}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, name: e.target.value }))}
|
||||
type="text" required
|
||||
placeholder="e.g., Arjun"
|
||||
value={staffForm.firstName}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, firstName: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Last Name *</label>
|
||||
<input
|
||||
type="text" required
|
||||
placeholder="e.g., Patel"
|
||||
value={staffForm.lastName}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, lastName: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{staffForm.type === 'doctor' ? (
|
||||
<div className="form-group">
|
||||
<label>Email Address *</label>
|
||||
<input
|
||||
type="email" required
|
||||
placeholder="e.g., arjun.patel@healthplus.com"
|
||||
value={staffForm.email}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, email: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-row-double">
|
||||
<div className="form-group">
|
||||
<label>Specialty Department</label>
|
||||
<label>ISD Code</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g., Oncologist"
|
||||
value={staffForm.specialty}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, specialty: e.target.value }))}
|
||||
placeholder="e.g., +91"
|
||||
value={staffForm.isdCode}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, isdCode: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Clinical Experience</label>
|
||||
<label>Mobile Number *</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g., 10 Yrs"
|
||||
value={staffForm.experience}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, experience: e.target.value }))}
|
||||
type="text" required
|
||||
placeholder="e.g., 9123456789"
|
||||
value={staffForm.mobile}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, mobile: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row-double">
|
||||
<div className="form-group">
|
||||
<label>Gender</label>
|
||||
<select
|
||||
value={staffForm.gender}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, gender: e.target.value }))}
|
||||
>
|
||||
<option value="male">Male</option>
|
||||
<option value="female">Female</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Date of Birth</label>
|
||||
<input
|
||||
type="date"
|
||||
value={staffForm.dob}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, dob: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Blood Group</label>
|
||||
<select
|
||||
value={staffForm.bloodGroup}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, bloodGroup: e.target.value }))}
|
||||
>
|
||||
<option value="">— Select —</option>
|
||||
{['A+','A-','B+','B-','AB+','AB-','O+','O-'].map(bg => (
|
||||
<option key={bg} value={bg}>{bg}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column */}
|
||||
<div className="form-column">
|
||||
<h4 className="form-section-title">Credentials & Account</h4>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Username *</label>
|
||||
<input
|
||||
type="text" required
|
||||
placeholder="e.g., dr_arjunpatel"
|
||||
value={staffForm.username}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, username: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Password {modalMode === 'create' ? '*' : '(leave blank to keep current)'}</label>
|
||||
<input
|
||||
type="password"
|
||||
required={modalMode === 'create'}
|
||||
placeholder="e.g., HealthPlus@2026"
|
||||
value={staffForm.password}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, password: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Medical License No. *</label>
|
||||
<input
|
||||
type="text" required
|
||||
placeholder="e.g., LIC98765432"
|
||||
value={staffForm.licenseNo}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, licenseNo: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Practicing Since</label>
|
||||
<input
|
||||
type="date"
|
||||
value={staffForm.practicingSince}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, practicingSince: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Specialities (comma-separated) *</label>
|
||||
<input
|
||||
type="text" required
|
||||
placeholder="e.g., Cardiology, Internal Medicine"
|
||||
value={staffForm.providerSpeciality}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, providerSpeciality: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-info-box">
|
||||
<p>💡 The doctor will be registered as a provider under <strong>{activeHospital?.name || 'this facility'}</strong> and can log in to the provider portal.</p>
|
||||
</div>
|
||||
|
||||
<div className="form-actions form-actions-grid-span">
|
||||
<button type="button" className="action-btn cancel-btn" onClick={() => setModalType(null)}>Cancel</button>
|
||||
<button type="submit" className="action-btn-primary">Register Doctor</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* ── CAREGIVER FORM (local) ── */
|
||||
<>
|
||||
<div className="form-row-double">
|
||||
<div className="form-group">
|
||||
<label>Role Category</label>
|
||||
@@ -1344,6 +1792,18 @@ export default function HospitalNetwork({ notify }) {
|
||||
<option value="Occupational Therapist">Occupational Therapist</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Full Legal Name *</label>
|
||||
<input
|
||||
type="text" required
|
||||
placeholder="e.g., Emily Davis"
|
||||
value={staffForm.name}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, name: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row-double">
|
||||
<div className="form-group">
|
||||
<label>Certifications / Licenses</label>
|
||||
<input
|
||||
@@ -1353,56 +1813,35 @@ export default function HospitalNetwork({ notify }) {
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, certification: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-row-double">
|
||||
<div className="form-group">
|
||||
<label>Primary Phone</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="+1 (555) 000-0000"
|
||||
placeholder="+91 9000000000"
|
||||
value={staffForm.phone}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, phone: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Work Email</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="e.g., r.chen@hospitals.com"
|
||||
value={staffForm.email}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, email: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Operations Status</label>
|
||||
<label>Work Status</label>
|
||||
<select
|
||||
value={staffForm.status}
|
||||
onChange={(e) => setStaffForm(prev => ({ ...prev, status: e.target.value }))}
|
||||
>
|
||||
{staffForm.type === 'doctor' ? (
|
||||
<>
|
||||
<option value="On Duty">On Duty</option>
|
||||
<option value="Off Duty">Off Duty</option>
|
||||
<option value="On Call">On Call</option>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<option value="Active">Active</option>
|
||||
<option value="On Leave">On Leave</option>
|
||||
<option value="Scheduled">Scheduled</option>
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-actions">
|
||||
<button type="button" className="action-btn cancel-btn" onClick={() => setModalType(null)}>Cancel</button>
|
||||
<button type="submit" className="action-btn-primary">Register Staff</button>
|
||||
<button type="submit" className="action-btn-primary">Register Caregiver</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -145,13 +145,20 @@ export const adminService = {
|
||||
saveGroup: (data) => data.id ? api.put(`/v1/resource/hospitalgroups/${data.id}`, data) : api.post('/v1/resource/hospitalgroups', data),
|
||||
deleteGroup: (id) => api.delete(`/v1/resource/hospitalgroups/${id}`),
|
||||
|
||||
getHospitals: () => api.get('/v1/resource/hospitals'),
|
||||
getHospitals: (params = {}) => api.get('/v1/resource/hospitals', params),
|
||||
saveHospital: (data) => data.id ? api.put(`/v1/resource/hospitals/${data.id}`, data) : api.post('/v1/resource/hospitals', data),
|
||||
deleteHospital: (id) => api.delete(`/v1/resource/hospitals/${id}`),
|
||||
|
||||
getStaff: () => api.get('/v1/resource/staff'),
|
||||
saveStaff: (data) => data.id ? api.put(`/v1/resource/staff/${data.id}`, data) : api.post('/v1/resource/staff', data),
|
||||
deleteStaff: (id) => api.delete(`/v1/resource/staff/${id}`),
|
||||
|
||||
// Add Provider (Doctor) — POST /v1/resource/providers
|
||||
addProvider: (hospitalId, providerData) => api.post('/v1/resource/providers', {
|
||||
...providerData,
|
||||
hospital_id: hospitalId,
|
||||
}),
|
||||
getProviders: (hospitalId) => api.get('/v1/resource/providers', { hospital_id: hospitalId }),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user