import React, { useEffect, useState } from 'react';
import Modal from '../ui/Modal';
import toast from 'react-hot-toast';
/**
* A dialog component for selecting a ward from a list of wards belonging to a specific hospital.
* It displays wards grouped by block, allows searching, and includes a confirmation step before selection.
*
* @param {object} props - Component props.
* - isOpen: boolean
* - onClose: () => void
* - onSelect: (ward) => void
* - apiService: instance of ApiService
* - hospitalId: number | string
* @returns {JSX.Element} The WardSelectDialog component.
*/
const WardSelectDialog = ({ isOpen, onClose, onSelect, apiService, hospitalId }) => {
// State to hold the list of wards fetched from the API
const [wards, setWards] = useState([]);
// State to manage loading state during API calls
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState('');
const [activeBlock, setActiveBlock] = useState('');
const [selectedWard, setSelectedWard] = useState(null);
const [showConfirmation, setShowConfirmation] = useState(false);
useEffect(() => {
// Fetch wards when the dialog is opened or hospitalId/apiService changes
if (!isOpen) return;
const fetchWards = async () => {
setLoading(true);
try {
const data = await apiService.getWards(hospitalId);
setWards(data);
// Set the first block as active when data loads
if (data.length > 0) {
const firstBlock = data[0].block_name || data[0].block || 'Unknown';
setActiveBlock(firstBlock);
}
} catch (err) {
console.error(err);
toast.error('Failed to load wards');
} finally {
setLoading(false);
}
};
fetchWards();
}, [isOpen, apiService, hospitalId]);
// Memoized grouping of wards by their block name
const wardsByBlock = wards.reduce((acc, ward) => {
const blockName = ward.block_name || ward.block || 'Unknown';
if (!acc[blockName]) {
acc[blockName] = [];
}
acc[blockName].push(ward);
return acc;
}, {});
// Extract unique block names
const blocks = Object.keys(wardsByBlock);
// Filter wards within the currently active block based on the search query
const filteredWards = search.trim()
? (wardsByBlock[activeBlock] || []).filter(w =>
(w.ward_name || w.name || '').toLowerCase().includes(search.toLowerCase())
)
: (wardsByBlock[activeBlock] || []);
// Reset active block when search changes and results are found
useEffect(() => { // Effect to update the active block when search input changes and filters results
if (search.trim() && blocks.length > 0) {
// Find first block that has matching wards
const blockWithResults = blocks.find(block =>
wardsByBlock[block].some(w =>
(w.ward_name || w.name || '').toLowerCase().includes(search.toLowerCase())
)
);
if (blockWithResults && blockWithResults !== activeBlock) {
setActiveBlock(blockWithResults);
}
}
}, [search, blocks, wardsByBlock, activeBlock]);
// Handles the initial selection of a ward, setting the selected ward and showing confirmation
const handleWardSelect = (ward) => {
setSelectedWard(ward);
setShowConfirmation(true);
};
// Handles confirming the ward selection, calls the onSelect prop, and closes confirmation
const handleConfirmSelection = () => {
if (selectedWard) {
onSelect(selectedWard);
setShowConfirmation(false);
setSelectedWard(null);
onClose(); // Optionally close the main dialog as well
}
};
const handleCancelSelection = () => {
setShowConfirmation(false);
setSelectedWard(null);
};
return (
<Modal isOpen={isOpen} onClose={onClose} title="Select Ward" size="lg">
<div className="space-y-4">
{/* Search Input */}
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search wards..."
className="w-full border rounded-lg p-3 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
{loading && (
<div className="text-center py-8">
<p className="text-gray-500">Loading wards...</p>
</div>
)}
{!loading && blocks.length === 0 && (
<div className="text-center py-8">
<p className="text-gray-500">No wards found</p>
</div>
)}
{!loading && blocks.length > 0 && (
<div className="space-y-4">
{/* Block Tabs with Horizontal Scroll Indicator */}
<div className="border-b border-gray-200 relative">
<nav className="-mb-px flex space-x-8 overflow-x-auto scrollbar-hide">
{blocks.map((block) => {
const wardCount = search.trim()
? wardsByBlock[block].filter(w =>
(w.ward_name || w.name || '').toLowerCase().includes(search.toLowerCase())
).length
: wardsByBlock[block].length;
return (
<button
key={block}
onClick={() => setActiveBlock(block)}
className={`whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
activeBlock === block
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
{block}
<span className="ml-2 bg-gray-100 text-gray-600 py-1 px-2 rounded-full text-xs">
{wardCount}
</span>
</button>
);
})}
</nav>
{/* Horizontal Scroll Indicator */}
{blocks.length > 2 && (
<div className="absolute right-0 top-0 bottom-0 w-12 bg-gradient-to-l from-white via-white to-transparent pointer-events-none flex items-center justify-center z-10">
<div className="bg-blue-500 text-white p-2 rounded-full shadow-lg">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
)}
</div>
{/* Wards List with Vertical Scroll Indicator */}
<div className="relative">
<div className="max-h-96 overflow-y-auto space-y-2 scrollbar-hide">
{filteredWards.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-500">
{search.trim() ? 'No wards found matching your search' : 'No wards in this block'}
</p>
</div>
) : (
filteredWards.map((ward) => (
<button
key={ward.id}
onClick={() => handleWardSelect(ward)}
className="w-full text-left p-4 rounded-lg bg-white shadow hover:shadow-md border border-gray-200 transition-all duration-200 hover:border-blue-300"
>
<div className="font-semibold text-gray-900">
{ward.ward_name || ward.name}
</div>
<div className="text-sm text-gray-500 mt-1">
Block: {ward.block_name || ward.block} • Floor: {ward.floor || ward.floor_name}
</div>
</button>
))
)}
</div>
{/* Vertical Scroll Indicator */}
{filteredWards.length > 3 && (
<div className="absolute right-2 top-2 bottom-2 w-8 pointer-events-none flex flex-col items-center justify-between z-10">
<div className="bg-green-500 text-white p-1 rounded-full shadow-lg ">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 15l7-7 7 7" />
</svg>
</div>
<div className="bg-green-500 text-white p-1 rounded-full shadow-lg " style={{ animationDelay: '1s' }}>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M19 9l-7 7-7-7" />
</svg>
</div>
</div>
)}
</div>
</div>
)}
</div>
{/* Confirmation Dialog */}
{showConfirmation && selectedWard && (
// Fixed overlay for the confirmation dialog
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-sm w-full mx-4 shadow-xl">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Confirm Ward Selection
</h3>
<p className="text-gray-600 mb-2">
Are you sure you want to select this ward?
</p>
<div className="bg-gray-50 p-3 rounded-lg mb-4">
<div className="font-medium text-gray-900">
{selectedWard.ward_name || selectedWard.name}
</div>
<div className="text-sm text-gray-500">
Block: {selectedWard.block_name || selectedWard.block} • Floor: {selectedWard.floor || selectedWard.floor_name}
</div>
</div>
<div className="flex space-x-3">
<button
onClick={handleCancelSelection}
className="flex-1 px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
>
Cancel
</button>
<button
onClick={handleConfirmSelection}
className="flex-1 px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors"
>
Confirm
</button>
</div>
</div>
</div>
)}
{/* CSS styles to hide the scrollbars */}
<style jsx>{`
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
`}</style>
</Modal>
);
};
export default WardSelectDialog;
Source