Source

components/memos/WorkerStatus.jsx

import React, { useState, useEffect, useRef } from 'react';
import { MessageCircle, Tag, Trash2, Plus, X, Clock, Reply, Mic, MicOff, Phone, Lock } from 'lucide-react';
import { useAuth } from '../../hooks/useAuth';
import { toast } from 'react-hot-toast';
import PhoneLink from './PhoneLink';

/**
 * WorkerStatus component displays and manages worker status updates for a memo.
 * It includes features for adding updates, tagging roles, OTP verification,
 * speech-to-text input (if supported by the environment), and deleting updates.
 */

const WorkerStatus = ({ memoId = 1, data = [], onChange = () => { } }) => {
  const { user, hospital, apiService } = useAuth();
  const isResponder = user?.is_responder;

  // Modal state
  const [isModalOpen, setIsModalOpen] = useState(false);

  // Form state
  const [comment, setComment] = useState('');
  const [statusType, setStatusType] = useState('attended');
  const [taggedRoleId, setTaggedRoleId] = useState('');
  const [otp, setOtp] = useState('');
  const [roles, setRoles] = useState([]);
  const [saving, setSaving] = useState(false);
  const [deletingId, setDeletingId] = useState(null);

  // OTP logic state
  const [hasUserCommentedBefore, setHasUserCommentedBefore] = useState(false);
  const [needsOtp, setNeedsOtp] = useState(false);

  // Speech-to-text state
  const [isListening, setIsListening] = useState(false);
  const [speechSupported, setSpeechSupported] = useState(false);
  const [currentLanguage, setCurrentLanguage] = useState('Tamil');
  const [availableLanguages, setAvailableLanguages] = useState([]);
  const [speechFeedback, setSpeechFeedback] = useState('');

  // NEW: Track accumulated speech text separately
  const [accumulatedSpeechText, setAccumulatedSpeechText] = useState('');
  const [lastPartialText, setLastPartialText] = useState('');

  const commentInputRef = useRef(null);

  // Process data with threading logic (existing logic)
  const processData = (rawData) => {
    const processed = rawData.map(item => ({
      ...item,
      can_delete: item.by === user?.id,
      is_reply: false,
      parent_tag: null
    }));

    const threaded = [];
    const taggedConversations = new Map();

    processed.forEach(item => {
      if (item.tagged_role_id && !item.deleted) {
        const conversationKey = `${item.tagged_role_id}_${item.event_id}`;
        taggedConversations.set(conversationKey, {
          parent: item,
          replies: []
        });
        threaded.push(item);
      } else if (!item.deleted) {
        const possibleParent = threaded.find(parent =>
          parent.tagged_role_id &&
          new Date(item.timestamp) > new Date(parent.timestamp) &&
          Math.abs(new Date(item.timestamp) - new Date(parent.timestamp)) < 24 * 60 * 60 * 1000
        );

        if (possibleParent) {
          const conversationKey = `${possibleParent.tagged_role_id}_${possibleParent.event_id}`;
          const conversation = taggedConversations.get(conversationKey);
          if (conversation) {
            item.is_reply = true;
            item.parent_tag = possibleParent.tagged_role_name;
            conversation.replies.push(item);
          }
        } else {
          threaded.push(item);
        }
      }
    });

    const final = [];
    threaded.forEach(item => {
      final.push(item);
      if (item.tagged_role_id) {
        const conversationKey = `${item.tagged_role_id}_${item.event_id}`;
        const conversation = taggedConversations.get(conversationKey);
        if (conversation && conversation.replies.length > 0) {
          final.push(...conversation.replies.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)));
        }
      }
    });

    return final;
  };

  const processedData = processData(data);
  const visibleItems = processedData.filter(i => !i.deleted);

  // Check if user has commented before
  useEffect(() => {
    if (user?.id && data.length > 0) {
      const userHasCommented = data.some(item => 
        item.by === user.id && !item.deleted
      );
      setHasUserCommentedBefore(userHasCommented);
    }
  }, [user?.id, data]);

  // Update OTP requirement based on user history and status type
  useEffect(() => {
    const shouldRequireOtp = () => {
      // Always require OTP for completed status
      if (statusType === 'completed') {
        return true;
      }
      
      // For other status types, only require OTP if user hasn't commented before
      if (!hasUserCommentedBefore) {
        return true;
      }
      
      return false;
    };

    setNeedsOtp(shouldRequireOtp());
    
    // Clear OTP when it's not needed
    if (!shouldRequireOtp()) {
      setOtp('');
    }
  }, [statusType, hasUserCommentedBefore]);

  const formatDate = (ts) => {
    try {
      const date = new Date(ts);
      const now = new Date();
      const diffMs = now - date;
      const diffMins = Math.floor(diffMs / 60000);
      const diffHours = Math.floor(diffMs / 3600000);
      const diffDays = Math.floor(diffMs / 86400000);

      if (diffMins < 1) return 'Just now';
      if (diffMins < 60) return `${diffMins}m ago`;
      if (diffHours < 24) return `${diffHours}h ago`;
      if (diffDays < 7) return `${diffDays}d ago`;
      return date.toLocaleDateString();
    } catch {
      return ts;
    }
  };

  // Initialize speech functionality
  useEffect(() => {
    // Setup Flutter message listener
    const handleFlutterMessage = (event) => {
      const data = event.detail;
      console.log('📨 Received from Flutter:', data);

      switch (data.type) {
        case 'speechStatus':
          handleSpeechStatus(data);
          break;
        case 'speechResult':
          handleSpeechResult(data);
          break;
        case 'speechLanguages':
          setAvailableLanguages(data.languages);
          break;
      }
    };

    window.addEventListener('flutterMessage', handleFlutterMessage);

    // Check if speech is supported and get available languages
    if (window.SpeechToText) {
      setSpeechSupported(true);
      sendSpeechCommand('getAvailableLanguages');
    }

    return () => {
      window.removeEventListener('flutterMessage', handleFlutterMessage);
    };
  }, []);

  useEffect(() => {
    const fetchRoles = async () => {
      try {
        const data = await apiService.getRoles(hospital.id);
        const filtered = (data || []).filter((r) => (r.name?.toLowerCase() !== 'admin') && (r.is_responder == true));
        setRoles(filtered);
      } catch (err) {
        console.error(err);
        toast.error('Failed to fetch roles');
      }
    };
    fetchRoles();
  }, [apiService, hospital.id]);

  // Speech command sender
  const sendSpeechCommand = (action, params = {}) => {
    if (window.SpeechToText) {
      window.SpeechToText.postMessage(JSON.stringify({
        action,
        ...params
      }));
    }
  };

  // Handle speech status updates from Flutter
  const handleSpeechStatus = (data) => {
    const { status, message, isListening: listening } = data;
    setIsListening(listening);
    setSpeechFeedback(message);

    // Reset accumulated speech text when speech stops
    if (!listening) {
      setAccumulatedSpeechText('');
      setLastPartialText('');
    }

    if (status === 'error') {
      console.error('Speech error:', message);
      // Reset speech state on error
      setAccumulatedSpeechText('');
      setLastPartialText('');
    }
  };

  // FIXED: Handle speech results with proper accumulation
  // FIXED: Handle speech results with proper concatenation
  const handleSpeechResult = (data) => {
    const { text, isFinal } = data;

    if (isFinal) {
      // Final result - concatenate with existing comment
      setComment(prev => {
        const newText = prev ? `${prev} ${text}`.trim() : text;
        return newText;
      });

      // Clear accumulated speech text and partial text
      setAccumulatedSpeechText('');
      setLastPartialText('');
      setSpeechFeedback('');
    } else {
      // Partial result - show as feedback only
      setLastPartialText(text);
      setSpeechFeedback(`Listening: "${text}"`);
    }
  };

  // FIXED: Start speech recognition - don't reset accumulated text
  const startSpeechRecognition = () => {
    if (!speechSupported) {
      alert('Speech recognition not available');
      return;
    }

    // Don't reset accumulated text when starting new recognition
    // setAccumulatedSpeechText(comment); // Remove this line
    setLastPartialText('');
    sendSpeechCommand('startListening', { language: currentLanguage });
  };
  // Stop speech recognition
  const stopSpeechRecognition = () => {
    sendSpeechCommand('stopListening');
  };

  // Change speech language
  const changeSpeechLanguage = (language) => {
    setCurrentLanguage(language);
    sendSpeechCommand('changeLanguage', { language });
  };

  const handleSubmit = async () => {
    if (!comment.trim()) {
      toast.error('Please enter a comment');
      return;
    }

    // Validate OTP if required
    if (needsOtp && (!otp || otp.length !== 6)) {
      toast.error('Please enter a valid 6-digit OTP');
      return;
    }

    // Validate tagged role selection
    if (statusType === 'tagged' && !taggedRoleId) {
      toast.error('Please select a role to tag');
      return;
    }

    setSaving(true);
    try {
      const payload = {
        comment,
        by: user.id,
        by_institution_id: user.institution_id,
        by_phone: user.phone_number
      };

      // Add OTP if required
      if (needsOtp) {
        payload.otp = otp;
      }

      if (statusType === 'tagged') {
        const roleObj = roles.find(r => String(r.id) === String(taggedRoleId));
        payload.tagged_role_id = taggedRoleId;
        payload.tagged_role_name = roleObj?.name || '';
      }

      const res = await apiService.addWorkerStatus(hospital.id, memoId, {
        event_type: statusType,
        payload,
        metadata: {}
      });

      if (res.isValid){
        toast.success('Status update posted successfully');
      }else{
        toast.error("Wrong OTP , Try again");
      }
      setComment('');
      setOtp('');
      setTaggedRoleId('');
      setStatusType('attended');
      setIsModalOpen(false);
      // Reset speech state when closing modal
      setAccumulatedSpeechText('');
      setLastPartialText('');
      setSpeechFeedback('');
      onChange?.();
    } catch (error) {
      console.error('Error posting status:', error);
      toast.error(error.message || 'Failed to post status update');
    } finally {
      setSaving(false);
    }
  };

  const handleDelete = async (item) => {
    if (!item.can_delete) return;
    setDeletingId(item.event_id);
    try {
      await apiService.deleteWorkerStatus(hospital.id, memoId, item.event_id, {
        metadata: {}
      });
      onChange?.();
    } finally {
      setDeletingId(null);
    }
  };

  const getStatusIcon = (item) => {
    if (item.tagged_role_id) return <Tag className="w-4 h-4 text-orange-600" />;
    return <MessageCircle className="w-4 h-4 text-blue-600" />;
  };

  const getStatusColor = (item) => {
    if (item.is_reply) return 'bg-green-100';
    if (item.tagged_role_id) return 'bg-orange-100';
    return 'bg-blue-100';
  };

  function makePhoneCall(phoneNumber) {
    const cleanNumber = phoneNumber.replace(/[^\d+]/g, '');

    // Try window.open with _system first (most effective for WebViews)
    try {
      window.open(`tel:${cleanNumber}`, '_system');
      return;
    } catch (error) {
      console.log('_system failed, trying _blank');
    }

    // Fallback to _blank
    try {
      window.open(`tel:${cleanNumber}`, '_blank');
      return;
    } catch (error) {
      console.log('_blank failed, trying location.href');
    }

    // Final fallback
    window.location.href = `tel:${cleanNumber}`;
  }

  const getStatusBadge = (type) => {
    const badges = {
      attended: { text: 'Attended', color: 'bg-green-100 text-green-800' },
      completed: { text: 'Completed', color: 'bg-blue-100 text-blue-800' },
      incomplete: { text: 'Incomplete', color: 'bg-yellow-100 text-yellow-800' },
      tagged: { text: 'Tagged', color: 'bg-orange-100 text-orange-800' }
    };
    return badges[type] || badges.attended;
  };

  // NEW: Handle modal close to reset speech state
  const handleModalClose = () => {
    setIsModalOpen(false);
    setComment('');
    setOtp('');
    setTaggedRoleId('');
    setStatusType('attended');
    setAccumulatedSpeechText('');
    setLastPartialText('');
    setSpeechFeedback('');
    if (isListening) {
      stopSpeechRecognition();
    }
  };

  // Get OTP requirement message
  const getOtpMessage = () => {
    if (statusType === 'completed') {
      return 'OTP is required for completed status';
    }
    if (!hasUserCommentedBefore) {
      return 'OTP is required for first-time commenters';
    }
    return '';
  };

  return (
    <>
      {/* Main Card */}
      <div className="bg-white rounded-xl shadow-lg border border-gray-100 overflow-hidden">
        {/* Header */}
        <div className="bg-gradient-to-r from-blue-600 to-blue-700 px-6 py-4">
          <div className="flex items-center justify-between">
            <div className="flex items-center gap-3">
              <div className="w-10 h-10 bg-white/20 rounded-lg flex items-center justify-center">
                <MessageCircle className="w-5 h-5 text-white" />
              </div>
              <div>
                <h3 className="text-lg font-semibold text-white">Worker Status</h3>
                <p className="text-blue-100 text-sm">{visibleItems.length} updates</p>
              </div>
            </div>
            {isResponder && (
              <button
                onClick={() => setIsModalOpen(true)}
                className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors"
              >
                <Plus className="w-4 h-4" />
                <span className="hidden sm:inline">Add Update</span>
              </button>
            )}
          </div>
        </div>

        {/* Content */}
        <div className="p-6">
          {visibleItems.length === 0 ? (
            <div className="text-center py-12">
              <div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
                <MessageCircle className="w-8 h-8 text-gray-400" />
              </div>
              <p className="text-gray-500 mb-4">No worker status updates yet</p>
              {isResponder && (
                <button
                  onClick={() => setIsModalOpen(true)}
                  className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg transition-colors"
                >
                  Add First Update
                </button>
              )}
            </div>
          ) : (
            <div className="space-y-4">
              {visibleItems
                .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
                .map((item, index) => (
                  <div
                    key={item.event_id || `${item.timestamp}-${index}`}
                    className={`relative ${item.is_reply ? 'ml-8 border-l-4 border-green-200 pl-4' : ''}`}
                  >
                    {item.is_reply && (
                      <div className="absolute -left-6 top-3 text-green-500">
                        <Reply className="w-4 h-4" />
                      </div>
                    )}

                    <div className="flex items-start gap-4 p-4 bg-gray-50 rounded-xl hover:bg-gray-100 transition-colors">
                      <div className={`flex-shrink-0 w-10 h-10 ${getStatusColor(item)} rounded-full flex items-center justify-center`}>
                        {getStatusIcon(item)}
                      </div>

                      <div className="flex-1 min-w-0">
                        <div className="flex items-start justify-between gap-4">
                          <div className="flex-1">
                            <p className="text-gray-900 text-sm leading-relaxed break-words">
                              {item.comment} <span className="text-gray-500 text-xs">– {item.by_institution_id}</span>
                            </p>

                            <div className="flex flex-wrap items-center gap-3 mt-3">
                              <div className="flex items-center gap-1 text-xs text-gray-500">
                                <Clock className="w-3 h-3" />
                                {formatDate(item.timestamp)} -
                                {item.by_phone ? (
                                  <PhoneLink phone={item.by_phone} />
                                ) : (
                                  <span className="ml-2 text-gray-400 italic">Phone number not available</span>
                                )}
                              </div>

                              {item.tagged_role_name && (
                                <div className="flex items-center gap-1 bg-orange-100 text-orange-700 px-2 py-1 rounded-full text-xs font-medium">
                                  <Tag className="w-3 h-3" />
                                  {item.tagged_role_name}
                                </div>
                              )}

                              {item.is_reply && (
                                <div className="flex items-center gap-1 bg-green-100 text-green-700 px-2 py-1 rounded-full text-xs font-medium">
                                  <Reply className="w-3 h-3" />
                                  Reply to {item.parent_tag}
                                </div>
                              )}
                            </div>
                          </div>

                          {/* Actions */}
                          {item.can_delete && (
                            <button
                              onClick={() => handleDelete(item)}
                              disabled={deletingId === item.event_id}
                              className="flex-shrink-0 p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors disabled:opacity-50"
                              title="Delete update"
                            >
                              <Trash2 className="w-4 h-4" />
                            </button>
                          )}
                        </div>
                      </div>
                    </div>
                  </div>
                ))}
            </div>
          )}
        </div>
      </div>

      {/* Modal */}
      {isModalOpen && (
        <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
          <div className="bg-white rounded-xl shadow-2xl w-full max-w-md max-h-[90vh] overflow-y-auto">
            {/* Modal Header */}
            <div className="bg-gradient-to-r from-blue-600 to-blue-700 px-6 py-4">
              <div className="flex items-center justify-between">
                <h3 className="text-lg font-semibold text-white">Add Status Update</h3>
                <button
                  onClick={handleModalClose}
                  className="text-white/80 hover:text-white p-1 rounded"
                >
                  <X className="w-5 h-5" />
                </button>
              </div>
            </div>

            {/* Modal Content */}
            <div className="p-6 space-y-4">
              {/* Status Type */}
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-2">
                  Status Type
                </label>
                <select
                  value={statusType}
                  onChange={(e) => setStatusType(e.target.value)}
                  className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                >
                  <option value="attended">Attended</option>
                  <option value="completed">Completed</option>
                  <option value="incomplete">Incomplete</option>
                  <option value="tagged">Tag other role</option>
                </select>
              </div>

              {/* Tagged Role */}
              {statusType === 'tagged' && (
                <div>
                  <label className="block text-sm font-medium text-gray-700 mb-2">
                    Tag Role
                  </label>
                  <select
                    value={taggedRoleId}
                    onChange={(e) => setTaggedRoleId(e.target.value)}
                    className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                    required
                  >
                    <option value="">Select role to tag...</option>
                    {roles.map((role) => (
                      <option key={role.id} value={role.id}>
                        {role.name}
                      </option>
                    ))}
                  </select>
                </div>
              )}

              {/* OTP Field */}
              {needsOtp && (
                <div>
                  <div className="flex items-center gap-2 mb-2">
                    <label className="block text-sm font-medium text-gray-700">
                      OTP (6 digits)
                    </label>
                    <Lock className="w-4 h-4 text-orange-500" />
                  </div>
                  <input
                    type="text"
                    value={otp}
                    onChange={(e) => {
                      const value = e.target.value.slice(0, 6);
                      setOtp(value);
                    }}
                    placeholder="Enter 6-digit OTP"
                    className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
                    maxLength={6}
                    required
                  />
                  {getOtpMessage() && (
                    <p className="text-xs text-orange-600 mt-1 flex items-center gap-1">
                      <Lock className="w-3 h-3" />
                      {getOtpMessage()}
                    </p>
                  )}
                </div>
              )}

              {/* Comment with Speech Controls */}
              <div>
                <div className="flex items-center justify-between mb-2">
                  <label className="block text-sm font-medium text-gray-700">
                    Comment
                  </label>

                  {/* Speech Controls */}
                  {speechSupported && (
                    <div className="flex items-center gap-2">
                      <select
                        value={currentLanguage}
                        onChange={(e) => changeSpeechLanguage(e.target.value)}
                        className="text-xs border border-gray-300 rounded px-2 py-1 focus:ring-1 focus:ring-blue-500"
                        disabled={isListening}
                      >
                        <option value="English">English</option>
                        <option value="Tamil">தமிழ்</option>
                      </select>

                      <button
                        type="button"
                        onClick={isListening ? stopSpeechRecognition : startSpeechRecognition}
                        className={`p-2 rounded-lg transition-colors ${isListening
                          ? 'bg-red-100 text-red-600 hover:bg-red-200'
                          : 'bg-blue-100 text-blue-600 hover:bg-blue-200'
                          }`}
                        title={isListening ? 'Stop Recording' : 'Start Voice Input'}
                      >
                        {isListening ? <MicOff className="w-4 h-4" /> : <Mic className="w-4 h-4" />}
                      </button>
                    </div>
                  )}
                </div>

                <textarea
                  ref={commentInputRef}
                  value={comment}
                  onChange={(e) => setComment(e.target.value)}
                  placeholder="Describe the status update... (You can use voice input)"
                  rows={3}
                  className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
                  required
                />

                {/* Speech Feedback */}
                {speechFeedback && (
                  <div className="mt-2 p-2 bg-blue-50 border border-blue-200 rounded-lg">
                    <div className="flex items-center gap-2 text-blue-700 text-sm">
                      {isListening && <div className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></div>}
                      <span>{speechFeedback}</span>
                    </div>
                  </div>
                )}
              </div>

              {/* Status Preview */}
              {comment && (
                <div className="bg-gray-50 rounded-lg p-3">
                  <p className="text-xs text-gray-500 mb-1">Preview:</p>
                  <div className="flex items-center gap-2">
                    <span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusBadge(statusType).color}`}>
                      {getStatusBadge(statusType).text}
                    </span>
                    <span className="text-sm text-gray-700">{comment}</span>
                  </div>
                </div>
              )}
              {/* Upload Image Button */}
               {typeof window !== 'undefined' && window.ReactToFlutter && (
                 <button type="button" onClick={() => {
                  if (window.ReactToFlutter?.postMessage) {
                      window.ReactToFlutter.postMessage(JSON.stringify({
                        action: 'uploadImage',
                        memoId: memoId
                      }));
                  } else {
                  alert("Image upload not supported in this environment");
                  }

                  // Optional: close modal after calling Flutter
                  // setIsModalOpen(false);
                  }}
                  className="w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors">
                  Upload Image
                </button>)}
              {/* Actions */}
              <div className="flex gap-3 pt-4">
                <button
                  type="button"
                  onClick={handleModalClose}
                  className="flex-1 px-4 py-2 text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
                >
                  Cancel
                </button>
                <button
                  type="button"
                  onClick={handleSubmit}
                  disabled={saving || !comment.trim() || isListening || (needsOtp && otp.length !== 6)}
                  className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
                >
                  {saving ? 'Posting...' : 'Post Update'}
                </button>
              </div>
            </div>
          </div>
        </div>
      )}
    </>
  );
};

export default WorkerStatus;