Source

components/common/WardSelectDialog.jsx

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;