Source

components/dashboard/DashboardIframe.jsx

import { BarChart3, Calendar, Clock, RefreshCw, ChevronDown, Download, FileSpreadsheet } from 'lucide-react';
import { useEffect, useState } from 'react';
import ApiService from '../../services/api';
import { useAuth } from '../../contexts/AuthContext';
import toast from 'react-hot-toast';


/**
 * Renders the dashboard and associated memo table for a specific hospital.
 * It allows users to view dashboard statistics based on selected periods (today, week, month)
 * and date, and also displays a table of memos for the chosen period.
 * Users can filter and search memos and export them to Excel.
 *
 * @param {object} props - The component props.
 * @param {string} [props.baseUrl="https://app.memotrack.net/media/dashboard/"] - The base URL for the dashboard JSON files.
 * @param {string} [props.iframeHtmlUrl="/dashboard.html"] - The URL of the HTML file that loads the dashboard visualization.
 * @param {string} [props.apiBaseUrl='https://app.memotrack.net' ||'http://localhost:8000'] - The base URL for the API.
 * */
const DashboardIframe = ({
  baseUrl = "https://app.memotrack.net/media/dashboard/",
  iframeHtmlUrl = "/dashboard.html",
  apiBaseUrl = 'https://app.memotrack.net' ||'http://localhost:8000'
}) => {
  const {hospital,apiService} = useAuth();
  const hospitalId = hospital.id;
  const [selectedPeriod, setSelectedPeriod] = useState('month');
  const [selectedDate, setSelectedDate] = useState(new Date());
  const [showDatePicker, setShowDatePicker] = useState(false);
  const [jsonUrl, setJsonUrl] = useState('');
  const [iframeUrl, setIframeUrl] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [memos, setMemos] = useState([]);
  const [memosLoading, setMemosLoading] = useState(false);
  const [memosError, setMemosError] = useState(null);
  const [currentPage, setCurrentPage] = useState(1);
  const memosPerPage = 10;
  const [searchTerm, setSearchTerm] = useState("");
  const [filteredMemos, setFilteredMemos] = useState(memos);
  const [selectedResponder, setSelectedResponder] = useState('');
  const [selectedBlock, setSelectedBlock] = useState('');
  


  useEffect(() => {
  const lowerSearch = searchTerm.toLowerCase();

  const filtered = memos.filter((memo) => {
    const complaintMatch = memo.latest_snapshot.complaint
      .toLowerCase()
      .includes(lowerSearch);

    const responderMatch =
      selectedResponder === '' ||
      memo.latest_snapshot.tagged_role_name === selectedResponder;

    const blockMatch =
      selectedBlock === '' ||
      memo.latest_snapshot.current_block_name === selectedBlock;

    return complaintMatch && responderMatch && blockMatch;
  });

  setFilteredMemos(filtered);
}, [searchTerm, selectedResponder, selectedBlock, memos]);

  // Calculate the memos for the current page
  const paginatedMemos = filteredMemos.slice(
    (currentPage - 1) * memosPerPage,
    currentPage * memosPerPage
  );
  
  const totalPages = Math.ceil(memos.length / memosPerPage);
  // Function to format date based on period and selected date
  const formatDateForPeriod = (period, date) => {
    const targetDate = new Date(date);
    
    switch (period) {
      case 'today': {
        const year = targetDate.getFullYear();
        const month = String(targetDate.getMonth() + 1).padStart(2, '0');
        const day = String(targetDate.getDate()).padStart(2, '0');
        return `${year}-${month}-${day}`;
      }
      case 'week': {
        const year = targetDate.getFullYear();
        const month = String(targetDate.getMonth() + 1).padStart(2, '0');
        
        // Calculate ISO week number
        const startOfYear = new Date(year, 0, 1);
        const pastDaysOfYear = (targetDate - startOfYear) / 86400000;
        const weekNumber = Math.ceil((pastDaysOfYear + startOfYear.getDay() + 1) / 7);
        const weekStr = String(weekNumber).padStart(2, '0');
        
        return `${year}-W${weekStr}`;
      }
      case 'month': {
        const year = targetDate.getFullYear();
        const month = String(targetDate.getMonth() + 1).padStart(2, '0');
        return `${year}-${month}`;
      }
      default:
        return '';
    }
  };

  // Function to format date for API call
  const formatDateForAPI = (period, date) => {
    const targetDate = new Date(date);
    
    switch (period) {
      case 'today':
        return {
          type: 'date',
          value: targetDate.toISOString().split('T')[0]
        };
      case 'week':
        const year = targetDate.getFullYear();
        const startOfYear = new Date(year, 0, 1);
        const pastDaysOfYear = (targetDate - startOfYear) / 86400000;
        const weekNumber = Math.ceil((pastDaysOfYear + startOfYear.getDay() + 1) / 7);
        return {
          type: 'week',
          year: year,
          week: weekNumber
        };
      case 'month':
        return {
          type: 'month',
          year: targetDate.getFullYear(),
          month: targetDate.getMonth() + 1
        };
      default:
        return {};
    }
  };

  // Generate the JSON URL and iframe URL based on hospital ID and selected period
  useEffect(() => {
    const dateString = formatDateForPeriod(selectedPeriod, selectedDate);
    const jsonDataUrl = `${baseUrl}${hospital.id}_${dateString}.json`;
    const fullIframeUrl = `${iframeHtmlUrl}?source=${encodeURIComponent(jsonDataUrl)}`;
    
    setJsonUrl(jsonDataUrl);
    setIframeUrl(fullIframeUrl);
  }, [hospitalId, selectedPeriod, selectedDate, baseUrl, iframeHtmlUrl]);

  // function to get query param
  function getQueryString(){
    try {
      const dateParams = formatDateForAPI(selectedPeriod, selectedDate);
      const queryParams = new URLSearchParams({
        hospital_id: hospital.id,
        period_type: dateParams.type,
        ...dateParams
      });
      return queryParams;
    }catch(err){
      toast.error("Failed to Download");
    }
  }

  // Function to fetch memos from backend
  const fetchMemos = async () => {
    setMemosLoading(true);
    setMemosError(null);
    
    try {
      const dateParams = formatDateForAPI(selectedPeriod, selectedDate);
      const queryParams = new URLSearchParams({
        hospital_id: hospital.id,
        period_type: dateParams.type,
        ...dateParams
      });

      const response = await apiService.getDashboardMemos(hospitalId,queryParams);
      console.log(response);
      if (response.count <= 0 ) {
        throw new Error(`Failed to fetch memos (${response.status})`);
      }
      setMemos(response.memos);
    } catch (err) {
      setMemosError(err.message);
      setMemos([]);
    } finally {
      setMemosLoading(false);
    }
  };

  // Function to check if JSON URL is accessible for dashboard
  const loadDashboard = async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch(jsonUrl);
      if (!response.ok) {
        throw new Error(`Dashboard data not available (${response.status})`);
      }
      
      const data = await response.json();
      console.log('Dashboard data loaded:', data);
      
      // Simulate dashboard loading time
      setTimeout(() => {
        setLoading(false);
      }, 500);
      
    } catch (err) {
      setError(err.message);
      setLoading(false);
    }
  };

  // Auto-load when JSON URL changes
  useEffect(() => {
    if (jsonUrl) {
      loadDashboard();
      fetchMemos(); // Also fetch memos when date changes
    }
  }, [jsonUrl]);

  // Handle date input change
  const handleDateChange = (e) => {
    const newDate = new Date(e.target.value);
    setSelectedDate(newDate);
    setShowDatePicker(false);
  };

  // Handle week input change
  const handleWeekChange = (e) => {
    const [year, week] = e.target.value.split('-W');
    const newDate = new Date(year, 0, 1 + (week - 1) * 7);
    setSelectedDate(newDate);
    setShowDatePicker(false);
  };

  // Handle month input change
  const handleMonthChange = (e) => {
    const [year, month] = e.target.value.split('-');
    const newDate = new Date(year, month - 1, 1);
    setSelectedDate(newDate);
    setShowDatePicker(false);
  };

  // Get formatted date string for display
  const getDisplayDate = () => {
    const date = new Date(selectedDate);
    switch (selectedPeriod) {
      case 'today':
        return date.toLocaleDateString();
      case 'week':
        const year = date.getFullYear();
        const startOfYear = new Date(year, 0, 1);
        const pastDaysOfYear = (date - startOfYear) / 86400000;
        const weekNumber = Math.ceil((pastDaysOfYear + startOfYear.getDay() + 1) / 7);
        return `Week ${weekNumber}, ${year}`;
      case 'month':
        return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
      default:
        return '';
    }
  };

  // Get the appropriate input type and value for the date picker
  const getDateInputProps = () => {
    const date = new Date(selectedDate);
    switch (selectedPeriod) {
      case 'today':
        return {
          type: 'date',
          value: date.toISOString().split('T')[0],
          onChange: handleDateChange
        };
      case 'week':
        const year = date.getFullYear();
        const startOfYear = new Date(year, 0, 1);
        const pastDaysOfYear = (date - startOfYear) / 86400000;
        const weekNumber = Math.ceil((pastDaysOfYear + startOfYear.getDay() + 1) / 7);
        return {
          type: 'week',
          value: `${year}-W${String(weekNumber).padStart(2, '0')}`,
          onChange: handleWeekChange
        };
      case 'month':
        return {
          type: 'month',
          value: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`,
          onChange: handleMonthChange
        };
      default:
        return {};
    }
  };

  // Navigate to previous period
  const goToPrevious = () => {
    const newDate = new Date(selectedDate);
    switch (selectedPeriod) {
      case 'today':
        newDate.setDate(newDate.getDate() - 1);
        break;
      case 'week':
        newDate.setDate(newDate.getDate() - 7);
        break;
      case 'month':
        newDate.setMonth(newDate.getMonth() - 1);
        break;
    }
    setSelectedDate(newDate);
  };

  // Navigate to next period
  const goToNext = () => {
    const newDate = new Date(selectedDate);
    switch (selectedPeriod) {
      case 'today':
        newDate.setDate(newDate.getDate() + 1);
        break;
      case 'week':
        newDate.setDate(newDate.getDate() + 7);
        break;
      case 'month':
        newDate.setMonth(newDate.getMonth() + 1);
        break;
    }
    setSelectedDate(newDate);
  };

  // Reset to current date
  const goToCurrent = () => {
    setSelectedDate(new Date());
  };

  // Export single memo
  const exportMemo = (memoId) => {
    const exportUrl = `${apiBaseUrl}/api/export/memo/${memoId}`;
    window.open(exportUrl, '_blank');
  };

  // Export all memos
  const exportAllMemos = () => {
    const dateParams = formatDateForAPI(selectedPeriod, selectedDate);
    const queryParams = new URLSearchParams({
      hospital_id: hospitalId,
      period_type: dateParams.type,
      ...dateParams
    });
    const exportUrl = `${apiBaseUrl}/api/export/memos/${hospitalId}/?${queryParams}`;
    window.open(exportUrl, '_blank');
  };

  // Format date for display
  const formatDisplayDate = (dateString) => {
    return new Date(dateString).toLocaleDateString('en-US', {
      year: 'numeric',
      month: 'short',
      day: 'numeric',
      hour: '2-digit',
      minute: '2-digit'
    });
  };

  // Get status badge color
  const getStatusBadge = (status, displayStatus) => {
    const effectiveStatus = displayStatus || status;
    const colors = {
      ongoing: 'bg-yellow-100 text-yellow-800',
      completed: 'bg-green-100 text-green-800',
      attended: 'bg-blue-100 text-blue-800',
      incomplete: 'bg-red-100 text-red-800'
    };
    return colors[effectiveStatus] || 'bg-gray-100 text-gray-800';
  };

  return (
    <div className="w-full">
      <h1 className='text-2xl font-bold p-3 mb-3'>Dashboard</h1>
      
      {/* Controls */}
      <div className="mb-6 flex flex-wrap gap-4 items-center justify-between bg-white p-4 rounded-lg border">
        {/* Period Selection Buttons */}
        <div className="flex gap-2">
          <button
            onClick={() => setSelectedPeriod('today')}
            className={`px-4 py-2 rounded-lg font-medium transition-all flex items-center gap-2 ${
              selectedPeriod === 'today'
                ? 'bg-blue-600 text-white shadow-md'
                : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
            }`}
          >
            <Clock size={16} />
            Today
          </button>
          <button
            onClick={() => setSelectedPeriod('week')}
            className={`px-4 py-2 rounded-lg font-medium transition-all flex items-center gap-2 ${
              selectedPeriod === 'week'
                ? 'bg-blue-600 text-white shadow-md'
                : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
            }`}
          >
            <Calendar size={16} />
            Week
          </button>
          <button
            onClick={() => setSelectedPeriod('month')}
            className={`px-4 py-2 rounded-lg font-medium transition-all flex items-center gap-2 ${
              selectedPeriod === 'month'
                ? 'bg-blue-600 text-white shadow-md'
                : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
            }`}
          >
            <BarChart3 size={16} />
            Month
          </button>
        </div>

        {/* Date Navigation and Selection */}
        <div className="flex items-center gap-2">
          {/* Previous Button */}
          <button
            onClick={goToPrevious}
            className="px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-all"
          >
            ←
          </button>

          {/* Current Date Display and Picker */}
          <div className="relative">
            <button
              onClick={() => setShowDatePicker(!showDatePicker)}
              className="px-4 py-2 bg-gray-50 text-gray-700 rounded-lg hover:bg-gray-100 transition-all flex items-center gap-2 min-w-[200px] justify-between"
            >
              <span>{getDisplayDate()}</span>
              <ChevronDown size={16} className={`transition-transform ${showDatePicker ? 'rotate-180' : ''}`} />
            </button>
            
            {showDatePicker && (
              <div className="absolute top-full left-0 mt-2 bg-white border rounded-lg shadow-lg p-4 z-50 min-w-[200px]">
                <input
                  {...getDateInputProps()}
                  className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
                />
                <div className="flex gap-2 mt-3">
                  <button
                    onClick={goToCurrent}
                    className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-all"
                  >
                    Today
                  </button>
                  <button
                    onClick={() => setShowDatePicker(false)}
                    className="px-3 py-1 text-sm bg-gray-200 text-gray-700 rounded hover:bg-gray-300 transition-all"
                  >
                    Close
                  </button>
                </div>
              </div>
            )}
          </div>

          {/* Next Button */}
          <button
            onClick={goToNext}
            className="px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-all"
          >
            →
          </button>
        </div>

        {/* Refresh Button */}
        <button
          onClick={() => {
            loadDashboard();
            fetchMemos();
          }}
          disabled={loading || memosLoading}
          className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center gap-2"
        >
          <RefreshCw size={16} className={(loading || memosLoading) ? 'animate-spin' : ''} />
          Refresh
        </button>
      </div>

      {/* Error Display */}
      {(error || memosError) && (
        <div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
          {error && <p className="text-red-700">Dashboard: {error}</p>}
          {memosError && <p className="text-red-700">Memos: {memosError}</p>}
        </div>
      )}

      {/* Loading State */}
      {(loading || memosLoading) && (
        <div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
          <p className="text-blue-700 flex items-center gap-2">
            <RefreshCw size={16} className="animate-spin" />
            Loading data...
          </p>
        </div>
      )}

      {/* Dashboard Iframe */}
      <iframe
        src={iframeUrl}
        className="w-full border-none mb-6"
        title="Hospital Dashboard"
        key={iframeUrl}
        style={{minHeight: '100vh'}}
      />

      {/* Memos Table */}
      <div className="p-4 border-b flex flex-col md:flex-row md:justify-between md:items-center gap-4">
  <h2 className="text-xl font-semibold">Memos ({filteredMemos.length})</h2>

  <div className="flex flex-col md:flex-row gap-2 w-full md:w-auto">
    {/* Search Field */}
    <input
      type="text"
      placeholder="Search complaints..."
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
      className="px-3 py-2 border rounded-md text-sm w-full md:w-64"
    />

    {/* Responder Dropdown */}
    <select
      value={selectedResponder}
      onChange={(e) => setSelectedResponder(e.target.value)}
      className="px-3 py-2 border rounded-md text-sm w-full md:w-48"
    >
      <option value="">All Responders</option>
      {Array.from(new Set(memos.map(m => m.latest_snapshot.tagged_role_name)))
        .filter(Boolean)
        .map((responder) => (
          <option key={responder} value={responder}>
            {responder}
          </option>
      ))}
    </select>
    
{/* Block Dropdown */}
<select
  value={selectedBlock}
  onChange={(e) => setSelectedBlock(e.target.value)}
  className="px-3 py-2 border rounded-md text-sm w-full md:w-48"
>
  <option value="">All Blocks</option>
  {Array.from(new Set(memos.map(m => m.latest_snapshot.current_block_name))).map((block) => (
    <option key={block} value={block}>{block}</option>
  ))}
</select>
  </div>

  {/* Export Button */}
  <button
    onClick={exportAllMemos}
    disabled={filteredMemos.length === 0}
    className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all flex items-center gap-2"
  >
    <Download size={16} />
    Export All to Excel
  </button>
</div>




        
        {memos.length > 0 ? (
          <div className="overflow-x-auto">
            <table className="w-full">
              <thead className="bg-gray-50">
                <tr>
                  {/* <th className="px-4 py-3 text-left text-sm font-medium text-gray-900">Memo ID</th> */}
                  <th className="px-4 py-3 text-left text-sm font-medium text-gray-900">Created</th>
                  <th className="px-4 py-3 text-left text-sm font-medium text-gray-900">Complaint</th>
                  <th className="px-4 py-3 text-left text-sm font-medium text-gray-900">Department</th>
                  <th className="px-4 py-3 text-left text-sm font-medium text-gray-900">Status</th>
                  <th className="px-4 py-3 text-left text-sm font-medium text-gray-900">Location</th>
                  <th className="px-4 py-3 text-left text-sm font-medium text-gray-900">Tagged Role</th>
                  <th className="px-4 py-3 text-left text-sm font-medium text-gray-900">Actions</th>
                </tr>
              </thead>
              <tbody className="divide-y divide-gray-200">
  {paginatedMemos.map((memo, index) => (
    <tr key={memo.memoId} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
      {/* <td className="px-4 py-3 text-sm text-gray-900 font-mono">
        {memo.memoId.slice(0, 8)}...
      </td> */}
      <td className="px-4 py-3 text-sm text-gray-900">
        {formatDisplayDate(memo.created_at)}
      </td>
      <td className="px-4 py-3 text-sm text-gray-900 max-w-xs truncate" title={memo.latest_snapshot.complaint}>
        {memo.latest_snapshot.complaint}
      </td>
      <td className="px-4 py-3 text-sm text-gray-900">
        {memo.latest_snapshot.department}
      </td>
      <td className="px-4 py-3 text-sm">
        <span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusBadge(memo.latest_snapshot.status, memo.latest_snapshot.display_status)}`}>
          {memo.latest_snapshot.display_status || memo.latest_snapshot.status}
        </span>
      </td>
      <td className="px-4 py-3 text-sm text-gray-900">
        <div className="text-xs">
          <div>{memo.latest_snapshot.current_block_name}</div>
          <div className="text-gray-500">{memo.latest_snapshot.current_ward_name}</div>
        </div>
      </td>
      <td className="px-4 py-3 text-sm text-gray-900">
        {memo.latest_snapshot.tagged_role_name}
      </td>
      <td className="px-4 py-3 text-sm">
        <button
          onClick={() => exportMemo(memo.memoId)}
          className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 transition-all flex items-center gap-1 text-xs"
        >
          <FileSpreadsheet size={12} />
          Export
        </button>
      </td>
    </tr>
  ))}
</tbody>

            </table>
          <div className="flex justify-center items-center gap-2 my-4">
            <button
              onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
              disabled={currentPage === 1}
              className="px-3 py-1 text-sm bg-gray-200 rounded disabled:opacity-50"
            >
              Previous
            </button>
{Array.from({ length: totalPages }, (_, i) => i + 1)
  .filter((page) => {
    if (currentPage <= 4) {
      return page <= 7;
    }
    return (
      page >= Math.max(currentPage - 3, 1) &&
      page <= Math.min(currentPage + 3, totalPages)
    );
  })
  .map((page) => (
    <button
      key={page}
      onClick={() => setCurrentPage(page)}
      className={`px-3 py-1 text-sm rounded ${
        page === currentPage ? 'bg-blue-600 text-white' : 'bg-gray-100'
      }`}
    >
      {page}
    </button>
))}


            <button
              onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
              disabled={currentPage === totalPages}
              className="px-3 py-1 text-sm bg-gray-200 rounded disabled:opacity-50"
              >
              Next
            </button>
          </div>
        </div>

        ) : (
          <div className="p-8 text-center text-gray-500">
            <FileSpreadsheet size={48} className="mx-auto mb-4 text-gray-300" />
            <p>No memos found for the selected period</p>
          </div>
        )}
      </div>
    
    
  );
};

export default DashboardIframe;