Untitled

 avatar
unknown
plain_text
a month ago
56 kB
6
Indexable
<<<<<<< HEAD
import { useState, useEffect, useMemo, useRef } from 'react'
=======
import { useState, useEffect } from 'react'
>>>>>>> develop
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { PageLayout } from '@/components/layout/page-layout'
import { Button } from '@/components/ui/button'
import { ConversationListPanel } from '@/components/whatsapp/conversation-list/ConversationListPanel'
import { buildConnectorLabel } from '@/components/whatsapp/conversation-list/ConnectorSwitcher'
import { apiRequest } from '@/lib/queryClient'
import { cn } from '@/lib/utils'
import { WHATSAPP_CONNECTORS_QUERY_KEY } from '@/lib/connectors-query'
import {
  buildMarkWhatsappMessageReadRequest,
  buildSendConversationMessageRequest,
  buildUpdateWhatsappMessageReactionRequest,
} from '@/lib/whatsapp-message-request'
import {
  isPlaceholderMediaToken,
  readContentPayloadValue,
  normalizeContentLibraryObjectPath,
  buildReplyComposerContext,
} from '@/lib/whatsapp-message-utils'
import { useWhatsappMessages } from '@/hooks/use-whatsapp-messages'
import { useWhatsappConversations } from '@/hooks/use-whatsapp-conversations'
import { MessageCircle, ArrowLeft } from 'lucide-react'
import { useAuth } from '@/hooks/use-auth'
import { usePermissions } from '@/hooks/use-permissions'
import type { WhatsappConversation, WhatsappMessage, ReplyComposerContext } from '@/types/whatsapp'
import { toastFor } from '@/lib/toast-for'
import { formatWhatsappConnectorLabel } from '@/lib/whatsapp-connectors'
import { useWebSocket } from '@/hooks/use-websocket'
import { CustomerContextPanel } from '@/components/whatsapp/customer-context/CustomerContextPanel'
import {
  shouldSendCompanionAfterMedia,
  shouldSendCompanionAsCaption,
} from '@/components/whatsapp/content-library-send-behavior'
import { ChatInput } from '@/components/whatsapp/chat-input'
import { ChatHeader } from '@/components/whatsapp/chat-panel/ChatHeader'
import { MessagesArea } from '@/components/whatsapp/chat-panel/MessagesArea'
import { WindowWarningBanner } from '@/components/whatsapp/chat-panel/WindowWarningBanner'
import { NewConversation } from '@/components/whatsapp/new-conversation'
import { getContentPayloadFileName } from '@/components/whatsapp/content-library-preview-utils'
import type { ContentLibraryRecord } from '@/contracts'
import type { ConfiguredConnector } from '@/types/connectors'
<<<<<<< HEAD

interface WhatsappConversation {
  id: number
  customerPhone: string
  customerName?: string
  connectorId?: string | null
  connectorName?: string | null
  title: string
  status: string
  priority: string
  isUnread: boolean
  unreadCount: number
  lastMessageAt?: string
  lastMessagePreview?: string
  orderId?: number
  assignedUserId?: number
  assignedTeamId?: number
  tags?: string[]
  isVip?: boolean
  isArchived?: boolean
}

interface WhatsappMessage {
  id: number | string
  conversationId: number
  whatsappMessageId?: string
  content?: string
  messageType: string
  direction: 'inbound' | 'outbound'
  sentByUserId?: number
  senderPhone?: string
  senderName?: string
  deliveryStatus: string
  errorMessage?: string
  isSystemMessage: boolean
  systemMessageType?: string
  mediaUrl?: string
  mediaMimeType?: string
  mediaSize?: number
  mediaCaption?: string
  replyToWhatsappMessageId?: string
  replyToSenderPhone?: string
  replyToSenderName?: string
  replyPreview?: string
  replyMessageType?: string
  replyIsOutbound?: boolean
  reactionMessageId?: string
  reactionEmoji?: string
  metadata?: {
    rawMessage?: {
      context?: {
        id?: string
        from?: string
      }
      reaction?: {
        message_id?: string
        emoji?: string
      }
    }
  }
  createdAt: string
  deliveredAt?: string
  readAt?: string
}

type MessageReactionSummary = {
  emoji: string
  count: number
}

type AutomationAttemptButtonMapping = {
  buttonId: string
  buttonLabel: string
  targetPipelineStageId: number
}

type AutomationAttempt = {
  id: number
  orderId: number
  conversationId?: number | null
  status: string
  buttonMappings?: AutomationAttemptButtonMapping[]
  selectedButtonLabel?: string | null
  mediationAction?: string | null
  lateResponseAt?: string | null
  lateResponseButtonId?: string | null
  lateResponseApplied?: boolean | null
}

type ReplyComposerContext = {
  replyToWhatsappMessageId: string
  replyToSenderPhone?: string
  replyToSenderName?: string
  replyPreview?: string
  replyMessageType?: string
  replyIsOutbound?: boolean
}
=======
import type { ConvoFilterValue } from '@/components/whatsapp/conversation-list/ConvoFilterTabs'
>>>>>>> develop

type SendContentItemInput = {
  conversationId: number
  item: ContentLibraryRecord
  replyContext?: ReplyComposerContext | null
}

const WHATSAPP_WORKSPACE_CONNECTOR_STORAGE_KEY = 'whatsapp:selected-workspace-connector'

<<<<<<< HEAD
const buildConnectorWorkspaceLabel = (
  connector: Pick<ConfiguredConnector, 'id' | 'name' | 'config'> | null | undefined,
) => {
  if (!connector) {
    return ''
  }

  return formatWhatsappConnectorLabel({
    id: connector.id,
    name: connector.name,
    config: connector.config,
  })
}

function TextReplyActionBanner({
  attempt,
  disabled,
  onApplyButton,
  onDismiss,
}: {
  attempt: AutomationAttempt
  disabled: boolean
  onApplyButton: (buttonId: string) => void
  onDismiss: () => void
}) {
  const mappings = Array.isArray(attempt.buttonMappings) ? attempt.buttonMappings : []

  return (
    <div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-3 text-sm text-amber-950 shadow-sm">
      <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
        <div className="min-w-0">
          <div className="font-semibold">Reponse texte a traiter</div>
          <div className="text-xs text-amber-800">
            Choisissez une action prevue par le template ou continuez sans action pipeline.
          </div>
        </div>
        <div className="flex flex-wrap gap-2">
          {mappings.map((mapping) => (
            <Button
              key={`${attempt.id}-${mapping.buttonId}`}
              size="sm"
              variant="outline"
              className="border-amber-300 bg-white text-amber-950 hover:bg-amber-100"
              disabled={disabled}
              onClick={() => {
                onApplyButton(mapping.buttonId)
              }}
            >
              {mapping.buttonLabel}
            </Button>
          ))}
          <Button size="sm" variant="ghost" disabled={disabled} onClick={onDismiss}>
            Continuer sans action
          </Button>
        </div>
      </div>
    </div>
  )
}

function LateButtonReviewBanner({ attempt }: { attempt: AutomationAttempt }) {
  const lateResponseDate = attempt.lateResponseAt ? new Date(attempt.lateResponseAt) : null
  const lateResponseLabel =
    lateResponseDate && !Number.isNaN(lateResponseDate.getTime())
      ? format(lateResponseDate, 'dd/MM HH:mm', { locale: fr })
      : null
  const buttonLabel = attempt.selectedButtonLabel || attempt.lateResponseButtonId || 'bouton'

  return (
    <div className="rounded-md border border-orange-200 bg-orange-50 px-3 py-3 text-sm text-orange-950 shadow-sm">
      <div className="flex gap-3">
        <Clock className="mt-0.5 h-4 w-4 flex-shrink-0 text-orange-600" />
        <div className="min-w-0">
          <div className="font-semibold">Clic tardif a verifier</div>
          <div className="text-xs text-orange-800">
            Le client a clique {buttonLabel}
            {lateResponseLabel ? ` le ${lateResponseLabel}` : ''}, mais la commande avait deja
            quitte le fallback. Aucune action pipeline automatique n'a ete appliquee.
          </div>
        </div>
      </div>
    </div>
  )
}

=======
>>>>>>> develop
function WhatsAppChatPage() {
  const [selectedConversationId, setSelectedConversationId] = useState<number | null>(null)
  const [selectedConnectorId, setSelectedConnectorId] = useState('')
  const [newMessage, setNewMessage] = useState('')
  const [statusFilter, setStatusFilter] = useState<ConvoFilterValue>('all')
  const [searchTerm, setSearchTerm] = useState('')
  const [showSidebar, setShowSidebar] = useState(true) // Sidebar droite visible par défaut
  const [isMobile, setIsMobile] = useState(window.innerWidth < 768)
  const [view, setView] = useState<'conversations' | 'chat' | 'sidebar'>('conversations')
  const [showNewConversation, setShowNewConversation] = useState(false)
  const [replyContext, setReplyContext] = useState<ReplyComposerContext | null>(null)

  const queryClient = useQueryClient()
  const { user } = useAuth()
  const { isConnected } = useWebSocket()
  const { hasPermission } = usePermissions()
  const canDelete = hasPermission('whatsapp.manage')

  // Responsive handler
  useEffect(() => {
    const handleResize = () => {
      setIsMobile(window.innerWidth < 768)
    }
    window.addEventListener('resize', handleResize)
    return () => {
      window.removeEventListener('resize', handleResize)
    }
  }, [])

  const { data: whatsappConnectors = [], isLoading: connectorsLoading } = useQuery<
    ConfiguredConnector[]
  >({
    queryKey: WHATSAPP_CONNECTORS_QUERY_KEY,
    select: (rows) =>
      rows
        .filter((connector) => connector.type === 'whatsapp')
        .sort((left, right) => {
          const leftIsActive = left.status === 'active' ? 1 : 0
          const rightIsActive = right.status === 'active' ? 1 : 0
          if (leftIsActive !== rightIsActive) {
            return rightIsActive - leftIsActive
          }

          return right.createdAt.localeCompare(left.createdAt)
        }),
  })

  useEffect(() => {
    if (whatsappConnectors.length === 0) {
      if (selectedConnectorId) {
        setSelectedConnectorId('')
      }
      return
    }

    const hasSelectedConnector = whatsappConnectors.some(
      (connector) => connector.id === selectedConnectorId,
    )
    if (hasSelectedConnector) {
      return
    }

    const savedConnectorId =
      typeof window !== 'undefined'
        ? window.localStorage.getItem(WHATSAPP_WORKSPACE_CONNECTOR_STORAGE_KEY)?.trim() || ''
        : ''
    const savedConnector = whatsappConnectors.find((connector) => connector.id === savedConnectorId)

    setSelectedConnectorId(savedConnector?.id || whatsappConnectors[0].id)
  }, [selectedConnectorId, whatsappConnectors])

  // BP-3 fix: typeof window !== 'undefined' inutile dans un SPA React
  useEffect(() => {
    if (!selectedConnectorId) {
      window.localStorage.removeItem(WHATSAPP_WORKSPACE_CONNECTOR_STORAGE_KEY)
      return
    }
    window.localStorage.setItem(WHATSAPP_WORKSPACE_CONNECTOR_STORAGE_KEY, selectedConnectorId)
  }, [selectedConnectorId])

  const {
    conversationList,
    filteredConversations,
    isLoading: conversationsLoading,
  } = useWhatsappConversations({
    connectorId: selectedConnectorId,
    searchTerm,
    statusFilter,
    isConnected,
  })

  const {
    visibleConversationMessages,
    isLoading: messagesLoading,
    messageReactions,
    currentUserReactions,
    resolveQuotedMessage,
    customerWindowExpired,
  } = useWhatsappMessages({ conversationId: selectedConversationId, userId: user?.id, isConnected })

  useEffect(() => {
    setReplyContext(null)
  }, [selectedConversationId])

  const selectedWorkspaceConnector =
    whatsappConnectors.find((connector) => connector.id === selectedConnectorId) || null
  const selectedConversationCandidate = conversationList.find(
    (conv: WhatsappConversation) => conv.id === selectedConversationId,
  )

  // Mark conversation as read when selected
  useEffect(() => {
    if (selectedConversationId && selectedConversationCandidate?.isUnread) {
      const timer = setTimeout(async () => {
        try {
          await apiRequest('POST', `/api/whatsapp/conversations/${selectedConversationId}/read`)
          queryClient.invalidateQueries({ queryKey: ['/api/whatsapp/conversations'] })
        } catch (error) {
          console.error('Error marking conversation as read:', error)
        }
      }, 1000)
      return () => {
        clearTimeout(timer)
      }
    }
  }, [selectedConversationCandidate?.isUnread, selectedConversationId])

  // Send message mutation
  const sendMessageMutation = useMutation({
    mutationFn: async (
      data: {
        conversationId: number
        content: string
        messageType?: string
        templateId?: number
      } & Partial<ReplyComposerContext>,
    ) => {
      const request = buildSendConversationMessageRequest(data)
      return apiRequest('POST', request.url, request.body)
    },
    onSuccess: () => {
      setNewMessage('')
      setReplyContext(null)
      queryClient.invalidateQueries({
        queryKey: ['/api/whatsapp/conversations', selectedConversationId, 'messages'],
      })
      queryClient.invalidateQueries({ queryKey: ['/api/whatsapp/conversations'] })
    },
    onError: (error: any) => {
      const raw = (error?.message || '').toString()
      let hint = ''
      if (raw.startsWith('402:')) hint = 'Message hors fenêtre 24h: utilisez un template Utility.'
      else if (raw.startsWith('403:')) hint = "Consentement requis: obtenez l'opt-in du client."
      else if (raw.startsWith('404:'))
        hint = 'Conversation introuvable: réessayez après avoir actualisé.'
      else if (raw.startsWith('429:')) hint = "Trop d'envois: patientez avant de réessayer."
      else if (raw.startsWith('401:')) hint = 'Token expiré: reconnectez le connecteur WhatsApp.'
      toastFor({
        kind: 'error',
        titleKey: 'common.dynamicMsg',
        params: {
          message: `Erreur d'envoi: ${raw || "Impossible d'envoyer le message"}${hint ? ` — ${hint}` : ''}`,
        },
      })
    },
  })

  // Retry message mutation (SOLUTION: réutilise les messages existants)
  const retryMessageMutation = useMutation({
    mutationFn: async (messageId: string | number) => {
      return apiRequest('POST', `/api/whatsapp/messages/${messageId}/retry`)
    },
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ['/api/whatsapp/conversations', selectedConversationId, 'messages'],
      })
      queryClient.invalidateQueries({ queryKey: ['/api/whatsapp/conversations'] })
      toastFor({
        kind: 'success',
        titleKey: 'common.dynamicMsg',
        params: { message: 'Message renvoyé avec succès' },
      })
    },
    onError: (error: any) => {
      toastFor({
        kind: 'error',
        titleKey: 'common.dynamicMsg',
        params: { message: error?.message || 'Impossible de renvoyer le message' },
      })
    },
  })

  const markMessageReadMutation = useMutation({
    mutationFn: async (messageId: string | number) => {
      const request = buildMarkWhatsappMessageReadRequest({ messageId })
      return apiRequest('POST', request.url, request.body)
    },
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ['/api/whatsapp/conversations', selectedConversationId, 'messages'],
      })
    },
    onError: (error: any) => {
      toastFor({
        kind: 'error',
        titleKey: 'common.dynamicMsg',
        params: { message: error?.message || 'Impossible de marquer le message comme lu' },
      })
    },
  })

  const updateReactionMutation = useMutation({
    mutationFn: async ({
      messageId,
      emoji,
    }: {
      messageId: string | number
      emoji?: string | null
    }) => {
      const request = buildUpdateWhatsappMessageReactionRequest({ messageId, emoji })
      return apiRequest('POST', request.url, request.body)
    },
    onSuccess: (_data, variables) => {
      queryClient.invalidateQueries({
        queryKey: ['/api/whatsapp/conversations', selectedConversationId, 'messages'],
      })
      toastFor({
        kind: 'success',
        titleKey: 'common.dynamicMsg',
        params: {
          message: variables.emoji ? `Réaction ${variables.emoji} ajoutée` : 'Réaction supprimée',
        },
      })
    },
    onError: (error: any) => {
      toastFor({
        kind: 'error',
        titleKey: 'common.dynamicMsg',
        params: { message: error?.message || 'Impossible de mettre à jour la réaction' },
      })
    },
  })

  const sendMediaMessageMutation = useMutation({
    mutationFn: async (data: { conversationId: number; file: File; caption?: string }) => {
      const formData = new FormData()
      formData.append('file', data.file)
      if (data.caption) {
        formData.append('caption', data.caption)
      }

      return apiRequest(
        'POST',
        `/api/whatsapp/conversations/${data.conversationId}/messages/media`,
        formData,
      )
    },
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ['/api/whatsapp/conversations', selectedConversationId, 'messages'],
      })
      queryClient.invalidateQueries({ queryKey: ['/api/whatsapp/conversations'] })
    },
    onError: (error: any) => {
      toastFor({
        kind: 'error',
        titleKey: 'common.dynamicMsg',
        params: { message: error?.message || "Impossible d'envoyer le fichier" },
      })
    },
  })

  const sendContentItemMutation = useMutation({
    mutationFn: async ({ conversationId, item, replyContext }: SendContentItemInput) => {
      const sendTextMessage = async (
        content: string,
        nextReplyContext?: ReplyComposerContext | null,
      ) => {
        const request = buildSendConversationMessageRequest({
          conversationId,
          content,
          messageType: 'text',
          ...(nextReplyContext || {}),
        })
        return apiRequest('POST', request.url, request.body)
      }

      const sendMediaMessage = async (params: {
        objectPath: string
        fileName?: string
        caption?: string
        messageTypeOverride?: 'document'
        nextReplyContext?: ReplyComposerContext | null
      }) => {
        return apiRequest('POST', `/api/whatsapp/conversations/${conversationId}/media-message`, {
          objectPath: params.objectPath,
          fileName: params.fileName,
          caption: params.caption,
          messageTypeOverride: params.messageTypeOverride,
          ...(params.nextReplyContext || {}),
        })
      }

      const companionText = item.companionText?.trim() || ''
      const textPayload = readContentPayloadValue(item.payload, 'text')
      const linkPayload = readContentPayloadValue(item.payload, 'url')
      const filePayload = readContentPayloadValue(item.payload, 'fileUrl')

      switch (item.primaryType) {
        case 'text': {
          if (!textPayload) {
            throw new Error('Le contenu texte selectionne est vide.')
          }
          await sendTextMessage(textPayload, replyContext)
          return
        }

        case 'link': {
          if (!linkPayload) {
            throw new Error('Le lien selectionne est invalide.')
          }
          await sendTextMessage(
            companionText ? `${companionText}\n${linkPayload}` : linkPayload,
            replyContext,
          )
          return
        }

        default: {
          if (!filePayload) {
            throw new Error('Le media selectionne ne contient aucun fichier.')
          }

          const objectPath = normalizeContentLibraryObjectPath(filePayload)
          if (!objectPath) {
            throw new Error('Le media selectionne ne peut pas etre converti pour WhatsApp.')
          }

          const fileName = getContentPayloadFileName(item.payload, 'file')
          const shouldSendCaption = shouldSendCompanionAsCaption(item.primaryType, companionText)
          const shouldSendCompanionAfter = shouldSendCompanionAfterMedia(
            item.primaryType,
            companionText,
          )

          if (shouldSendCompanionAfter) {
            await sendMediaMessage({
              objectPath,
              fileName,
              ...(item.primaryType === 'document' ? { messageTypeOverride: 'document' } : {}),
              nextReplyContext: replyContext,
            })
            await sendTextMessage(companionText)
            return
          }

          await sendMediaMessage({
            objectPath,
            fileName,
            caption: shouldSendCaption ? companionText : undefined,
            ...(item.primaryType === 'document' ? { messageTypeOverride: 'document' } : {}),
            nextReplyContext: replyContext,
          })
          return
        }
      }
    },
    onSuccess: () => {
      setNewMessage('')
      setReplyContext(null)
      queryClient.invalidateQueries({
        queryKey: ['/api/whatsapp/conversations', selectedConversationId, 'messages'],
      })
      queryClient.invalidateQueries({ queryKey: ['/api/whatsapp/conversations'] })
    },
    onError: (error: any) => {
      toastFor({
        kind: 'error',
        titleKey: 'common.dynamicMsg',
        params: { message: error?.message || "Impossible d'envoyer le contenu selectionne" },
      })
    },
  })

  const handleSendMessage = () => {
    if (!newMessage.trim() || !selectedConversationId) return

    sendMessageMutation.mutate({
      conversationId: selectedConversationId,
      content: newMessage.trim(),
      messageType: 'text',
      ...(replyContext || {}),
    })
  }

  const handleTemplateSend = (templateId: number) => {
    if (!selectedConversationId) return

    sendMessageMutation.mutate({
      conversationId: selectedConversationId,
      content: '',
      messageType: 'template',
      templateId,
    })
  }

  const handleContentSend = (item: ContentLibraryRecord) => {
    if (!selectedConversationId) return

    sendContentItemMutation.mutate({
      conversationId: selectedConversationId,
      item,
      replyContext,
    })
  }

  const handleReplySelection = (message: WhatsappMessage) => {
    const nextReplyContext = buildReplyComposerContext(message)

    if (!nextReplyContext) {
      toastFor({
        kind: 'error',
        titleKey: 'common.dynamicMsg',
        params: { message: "Ce message n'a pas encore d'identifiant WhatsApp exploitable." },
      })
      return
    }

    setReplyContext(nextReplyContext)
  }

  const handleChooseTemplate = (message: WhatsappMessage) => {
    setNewMessage('/templates')
  }

  const handleMarkMessageRead = (message: WhatsappMessage) => {
    markMessageReadMutation.mutate(message.whatsappMessageId || message.id)
  }

  const handleReactionUpdate = (
    message: WhatsappMessage,
    emoji?: string | null,
    currentReaction?: string | null,
  ) => {
    updateReactionMutation.mutate({
      messageId: message.whatsappMessageId || message.id,
      emoji: currentReaction && currentReaction === emoji ? null : emoji,
    })
  }

  // 🔧 NOUVEAU: Mutation pour supprimer un message
  const deleteMessageMutation = useMutation({
    mutationFn: async (messageId: string | number) => {
      return apiRequest('DELETE', `/api/whatsapp/messages/${messageId}`)
    },
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ['/api/whatsapp/conversations', selectedConversationId, 'messages'],
      })
      queryClient.invalidateQueries({ queryKey: ['/api/whatsapp/conversations'] })
      toastFor({
        kind: 'success',
        titleKey: 'common.dynamicMsg',
        params: { message: 'Message supprimé avec succès' },
      })
    },
    onError: (error: any) => {
      toastFor({
        kind: 'error',
        titleKey: 'common.dynamicMsg',
        params: { message: error?.message || 'Impossible de supprimer le message' },
      })
    },
  })

  // La confirmation se fait dans MessageBubble (AlertDialog) — ici on exécute juste.
  const handleDelete = (message: WhatsappMessage) => {
    deleteMessageMutation.mutate(message.id)
  }

  const handleRetry = (message: WhatsappMessage) => retryMessageMutation.mutate(message.id)

  const handleConversationCreated = (conversationId: number) => {
    setShowNewConversation(false)
    setSelectedConversationId(conversationId)
    setView('chat')
    queryClient.invalidateQueries({ queryKey: ['/api/whatsapp/conversations'] })
  }

  const selectedConversation = filteredConversations.find(
    (conv: WhatsappConversation) => conv.id === selectedConversationId,
  )
  const selectedConversationOrderId = selectedConversation?.orderId
  const { data: selectedOrderAutomationAttempts = [] } = useQuery<AutomationAttempt[]>({
    queryKey: [
      '/api/message-template-automations/attempts',
      selectedConversationOrderId,
      selectedConversationId,
    ],
    enabled: Boolean(selectedConversationId),
    queryFn: async () => {
      const params = new URLSearchParams()
      if (selectedConversationOrderId) {
        params.set('orderId', String(selectedConversationOrderId))
      }
      params.set('conversationId', String(selectedConversationId))
      params.set('limit', '10')
      const response = await apiRequest(
        'GET',
        `/api/message-template-automations/attempts?${params.toString()}`,
      )
      const payload = (await response.json()) as unknown
      return Array.isArray(payload) ? (payload as AutomationAttempt[]) : []
    },
  })
  const pendingTextReplyAttempt = useMemo(
    () =>
      selectedOrderAutomationAttempts.find(
        (attempt) =>
          attempt.status === 'awaiting_human_routing' &&
          attempt.conversationId === selectedConversationId,
      ) || null,
    [selectedConversationId, selectedOrderAutomationAttempts],
  )
  const lateButtonReviewAttempt = useMemo(
    () =>
      selectedOrderAutomationAttempts.find(
        (attempt) =>
          attempt.status === 'responded_late_ignored' &&
          attempt.conversationId === selectedConversationId,
      ) || null,
    [selectedConversationId, selectedOrderAutomationAttempts],
  )
  const routeTextReplyMutation = useMutation({
    mutationFn: async ({
      attemptId,
      action,
      buttonId,
    }: {
      attemptId: number
      action: 'apply_button' | 'dismiss'
      buttonId?: string
    }) => {
      if (!selectedConversationId) {
        throw new Error('Conversation manquante')
      }

      return apiRequest(
        'POST',
        `/api/whatsapp/conversations/${String(selectedConversationId)}/route-text-reply`,
        {
          attemptId,
          action,
          ...(buttonId ? { buttonId } : {}),
        },
      )
    },
    onSuccess: () => {
      void queryClient.invalidateQueries({
        queryKey: ['/api/message-template-automations/attempts', selectedConversationOrderId],
      })
      void queryClient.invalidateQueries({ queryKey: ['/api/whatsapp/conversations'] })
      toastFor({
        kind: 'success',
        titleKey: 'common.dynamicMsg',
        params: { message: 'Reponse texte traitee' },
      })
    },
    onError: (error: unknown) => {
      const message =
        error instanceof Error ? error.message : 'Impossible de traiter la reponse texte'
      toastFor({
        kind: 'error',
        titleKey: 'common.dynamicMsg',
        params: { message },
      })
    },
  })

  useEffect(() => {
    if (!selectedConversationId) {
      return
    }

    const isConversationVisible = filteredConversations.some(
      (conversation) => conversation.id === selectedConversationId,
    )
    if (isConversationVisible) {
      return
    }

    setSelectedConversationId(null)
    setReplyContext(null)
    if (isMobile) {
      setView('conversations')
    }
  }, [filteredConversations, isMobile, selectedConversationId])

  // BP-1 fix: buildConnectorLabel importé depuis ConnectorSwitcher (plus de render function inline)
  const connectorWorkspaceSummary = selectedWorkspaceConnector
    ? buildConnectorLabel(selectedWorkspaceConnector)
    : 'Aucun compte WhatsApp'

<<<<<<< HEAD
  const renderConnectorWorkspaceSwitcher = (compact = false) => (
    <div className={compact ? 'space-y-2' : 'space-y-2 min-w-[260px]'}>
      <div className="flex items-center justify-between">
        <p className="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
          Numéro WhatsApp
        </p>
        {selectedWorkspaceConnector && (
          <Badge
            variant="outline"
            className="text-[10px] bg-emerald-50 text-emerald-700 border-emerald-200"
          >
            {selectedWorkspaceConnector.status}
          </Badge>
        )}
      </div>
      <Select
        value={selectedConnectorId}
        onValueChange={(value) => {
          setSelectedConnectorId(value)
          setSearchTerm('')
          if (isMobile) {
            setView('conversations')
          }
        }}
        disabled={connectorsLoading || whatsappConnectors.length === 0}
      >
        <SelectTrigger className={compact ? 'h-9' : 'h-10'}>
          <SelectValue
            placeholder={
              connectorsLoading
                ? 'Chargement des comptes...'
                : whatsappConnectors.length > 0
                  ? 'Sélectionnez un compte WhatsApp'
                  : 'Aucun compte WhatsApp'
            }
          />
        </SelectTrigger>
        <SelectContent>
          {whatsappConnectors.map((connector) => (
            <SelectItem key={connector.id} value={connector.id}>
              {buildConnectorWorkspaceLabel(connector)}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    </div>
  )

  const renderConversationActionBanners = () => {
    if (!pendingTextReplyAttempt && !lateButtonReviewAttempt) {
      return null
    }

    return (
      <div className="flex-shrink-0 border-b border-gray-200 bg-white px-4 py-3 dark:border-gray-700 dark:bg-gray-800">
        <div className="space-y-2">
          {pendingTextReplyAttempt ? (
            <TextReplyActionBanner
              attempt={pendingTextReplyAttempt}
              disabled={routeTextReplyMutation.isPending}
              onApplyButton={(buttonId) => {
                routeTextReplyMutation.mutate({
                  attemptId: pendingTextReplyAttempt.id,
                  action: 'apply_button',
                  buttonId,
                })
              }}
              onDismiss={() => {
                routeTextReplyMutation.mutate({
                  attemptId: pendingTextReplyAttempt.id,
                  action: 'dismiss',
                })
              }}
            />
          ) : null}
          {lateButtonReviewAttempt ? (
            <LateButtonReviewBanner attempt={lateButtonReviewAttempt} />
          ) : null}
        </div>
      </div>
    )
  }

  if (isMobile) {
    if (view === 'conversations') {
      return (
        <div className="h-screen flex flex-col overflow-hidden">
          <div className="h-16 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 flex items-center justify-center">
=======
  // ─── Rendu unifié (#25) — un seul arbre JSX ─────────────────────────────
  // L'état `view` ne fait plus que basculer des classes CSS sur mobile ; les
  // variantes `md:` forcent l'affichage des 3 panneaux côte à côte en desktop.
  // Fini les deux arbres dupliqués où chaque feature devait être câblée 2×.
  const page = (
    <>
      <div className="flex h-full overflow-hidden bg-[#f0f2f5] dark:bg-gray-900">
        {/* Panneau 1 — Liste des conversations */}
        <aside
          className={cn(
            'h-full w-full flex-col overflow-hidden md:flex md:w-80 md:flex-shrink-0',
            view === 'conversations' ? 'flex' : 'hidden',
          )}
        >
          {/* Barre de titre — mobile uniquement (en desktop l'en-tête de l'app la fournit) */}
          <div className="flex h-16 flex-shrink-0 items-center justify-center border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800 md:hidden">
>>>>>>> develop
            <h1 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
              WhatsApp Chat
            </h1>
          </div>
          <div className="min-h-0 flex-1">
            <ConversationListPanel
              connectors={whatsappConnectors}
              selectedConnectorId={selectedConnectorId}
              connectorsLoading={connectorsLoading}
              onConnectorChange={(value) => {
                setSelectedConnectorId(value)
                setSearchTerm('')
              }}
              connectorSummary={connectorWorkspaceSummary}
              searchTerm={searchTerm}
              onSearchChange={setSearchTerm}
              onNewConversation={() => setShowNewConversation(true)}
              statusFilter={statusFilter}
              onFilterChange={setStatusFilter}
              conversations={filteredConversations}
              selectedConversationId={selectedConversationId}
              conversationsLoading={conversationsLoading}
              onSelectConversation={(id) => {
                setSelectedConversationId(id)
                setView('chat')
              }}
            />
          </div>
        </aside>

<<<<<<< HEAD
              <div className="flex-1 overflow-hidden">
                <TabsContent value="info" className="h-full m-4 mt-4 overflow-y-auto">
                  <CustomerInfoPanel
                    customerPhone={selectedConversation.customerPhone}
                    customerName={selectedConversation.customerName}
                  />
                </TabsContent>

                <TabsContent value="notes" className="h-full m-4 mt-4 overflow-y-auto">
                  <ConversationNotes conversationId={selectedConversation.id} />
                </TabsContent>

                <TabsContent value="calls" className="h-full m-4 mt-4 overflow-y-auto">
                  <ConversationCalls
                    conversationId={selectedConversation.id}
                    customerPhone={selectedConversation.customerPhone}
                    customerName={selectedConversation.customerName}
                  />
                </TabsContent>

                <TabsContent value="media" className="h-full m-4 mt-4 overflow-y-auto">
                  <MediaHistory conversationId={selectedConversation.id} onClose={() => {}} />
                </TabsContent>

                <TabsContent value="management" className="h-full m-4 mt-4 overflow-y-auto">
                  <ConversationManagement
                    conversationId={selectedConversation.id}
                    currentConversation={selectedConversation}
                    onUpdate={() => {
                      queryClient.invalidateQueries({ queryKey: ['/api/whatsapp/conversations'] })
                    }}
                  />
                </TabsContent>
              </div>
            </Tabs>
          </div>
        </div>
      )
    }

    if (view === 'chat') {
      return (
        <div className="h-screen flex flex-col overflow-hidden">
          {/* Chat content for mobile */}
          <div className="flex-1 flex flex-col h-full overflow-hidden">
            <div className="flex-shrink-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
              <div className="p-4">
                <div className="flex items-center justify-between">
                  {selectedConversation && (
                    <ConversationHeader
                      conversation={selectedConversation}
                      onBack={() => {
                        setView('conversations')
                      }}
                      showBackButton={true}
                      isOnline={true}
                      lastSeen={
                        selectedConversation.lastMessageAt
                          ? format(new Date(selectedConversation.lastMessageAt), 'HH:mm', {
                              locale: fr,
                            })
                          : undefined
                      }
                    />
                  )}

                  <div className="flex items-center gap-2">
                    <Button
                      variant="ghost"
                      size="icon"
                      className="text-gray-600 hover:text-[#00a884]"
                    >
                      <Phone className="h-5 w-5" />
                    </Button>
                    <Button
                      variant="ghost"
                      size="icon"
                      onClick={() => {
                        setView('sidebar')
                      }}
                      className="text-gray-600 hover:text-[#00a884]"
                    >
                      <Settings className="h-5 w-5" />
                    </Button>
                  </div>
                </div>
              </div>
            </div>

            {renderConversationActionBanners()}

            {/* Messages Area */}
            <div className="flex-1 overflow-hidden bg-[#e5ddd5] dark:bg-gray-900">
              <div className="h-full overflow-y-auto p-4">
                {messagesLoading ? (
                  <div className="flex items-center justify-center h-full">
                    <div className="text-gray-500">Chargement des messages...</div>
                  </div>
                ) : visibleConversationMessages.length > 0 ? (
                  visibleConversationMessages.map((message: WhatsappMessage, index: number) => (
                    <MessageBubble
                      key={message.id}
                      message={message}
                      quotedMessage={resolveQuotedMessage(message)}
                      reactions={messageReactions.get(resolveMessageIdentity(message)) || []}
                      currentUserReaction={
                        currentUserReactions.get(resolveMessageIdentity(message)) || null
                      }
                      isLast={index === visibleConversationMessages.length - 1}
                      onRetry={() => {
                        retryMessageMutation.mutate(message.id)
                      }}
                      onReact={handleReactionUpdate}
                      onReply={handleReplySelection}
                      onCopy={(content: string) => {
                        navigator.clipboard.writeText(content)
                        toastFor({
                          kind: 'success',
                          titleKey: 'common.dynamicMsg',
                          params: { message: 'Texte copié dans le presse-papiers' },
                        })
                      }}
                      onDelete={handleDelete}
                      onChooseTemplate={handleChooseTemplate}
                    />
                  ))
                ) : (
                  <div className="flex items-center justify-center h-full">
                    <div className="text-center p-8">
                      <MessageCircle className="h-16 w-16 text-gray-400 mx-auto mb-4" />
                      <h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
                        Aucun message
                      </h3>
                      <p className="text-gray-500 dark:text-gray-400">
                        Commencez la conversation avec votre client
                      </p>
                    </div>
                  </div>
                )}
                <div ref={messagesEndRef} />
              </div>
            </div>

            {/* Message Input */}
            <div className="flex-shrink-0">
              <ChatInput
                value={newMessage}
                onChange={setNewMessage}
                onSend={handleSendMessage}
                onTemplateSend={handleTemplateSend}
                onContentSend={handleContentSend}
                onMediaSend={handleMediaSend}
                onAudioSend={handleAudioSend}
                isLoading={
                  sendMessageMutation.isPending ||
                  sendMediaMessageMutation.isPending ||
                  sendContentItemMutation.isPending
                }
                placeholder="Tapez votre message..."
                conversationId={selectedConversationId || undefined}
                replyContext={replyContext}
                onCancelReply={() => {
                  setReplyContext(null)
                }}
              />
            </div>
          </div>

          {/* Mobile Sidebar Access */}
          {showSidebar && (
            <div className="fixed inset-0 bg-white dark:bg-gray-800 z-50">
              <div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
                <h2 className="text-lg font-semibold">Options</h2>
                <Button
                  variant="ghost"
                  size="icon"
                  onClick={() => {
                    setShowSidebar(false)
                  }}
                >
                  <ArrowLeft className="h-5 w-5" />
                </Button>
              </div>
              <div className="flex-1 overflow-hidden">
                <Tabs
                  value={activeTab}
                  onValueChange={setActiveTab}
                  className="h-full flex flex-col"
                >
                  <TabsList className="grid w-full grid-cols-4 m-4 mb-0">
                    <TabsTrigger value="notes" className="text-xs">
                      <StickyNote className="h-4 w-4 mr-1" />
                      Notes
                    </TabsTrigger>
                    <TabsTrigger value="calls" className="text-xs">
                      <PhoneCall className="h-4 w-4 mr-1" />
                      Appels
                    </TabsTrigger>
                    <TabsTrigger value="media" className="text-xs">
                      <ImageIcon className="h-4 w-4 mr-1" />
                      Médias
                    </TabsTrigger>
                    <TabsTrigger value="management" className="text-xs">
                      <Settings className="h-4 w-4 mr-1" />
                      Gestion
                    </TabsTrigger>
                  </TabsList>

                  <div className="flex-1 overflow-hidden">
                    <TabsContent value="notes" className="h-full m-4 mt-4 overflow-y-auto">
                      {selectedConversation && (
                        <ConversationNotes conversationId={selectedConversation.id} />
                      )}
                    </TabsContent>

                    <TabsContent value="calls" className="h-full m-4 mt-4 overflow-y-auto">
                      {selectedConversation && (
                        <ConversationCalls
                          conversationId={selectedConversation.id}
                          customerPhone={selectedConversation.customerPhone}
                          customerName={selectedConversation.customerName}
                        />
                      )}
                    </TabsContent>

                    <TabsContent value="media" className="h-full m-4 mt-4 overflow-y-auto">
                      {selectedConversation && (
                        <MediaHistory conversationId={selectedConversation.id} onClose={() => {}} />
                      )}
                    </TabsContent>

                    <TabsContent value="management" className="h-full m-4 mt-4 overflow-y-auto">
                      {selectedConversation && (
                        <ConversationManagement
                          conversationId={selectedConversation.id}
                          currentConversation={selectedConversation}
                          onUpdate={() => {
                            queryClient.invalidateQueries({
                              queryKey: ['/api/whatsapp/conversations'],
                            })
                          }}
                        />
                      )}
                    </TabsContent>
                  </div>
                </Tabs>
              </div>
            </div>
=======
        {/* Panneau 2 — Zone de chat */}
        <section
          className={cn(
            'h-full min-w-0 flex-1 flex-col overflow-hidden md:flex',
            view === 'chat' ? 'flex' : 'hidden',
>>>>>>> develop
          )}
        >
          {!selectedConversation ? (
            <div className="flex flex-1 items-center justify-center bg-[#e5ddd5] dark:bg-gray-900">
              <div className="p-8 text-center">
                <MessageCircle className="mx-auto mb-4 h-16 w-16 text-gray-400" />
                <h3 className="mb-2 text-lg font-medium text-gray-900 dark:text-gray-100">
                  {selectedWorkspaceConnector
                    ? `Workspace ${selectedWorkspaceConnector.name}`
                    : 'WhatsApp Business pour Sendocki'}
                </h3>
                <p className="text-gray-500 dark:text-gray-400">
                  Sélectionnez une conversation pour commencer à discuter avec vos clients
                </p>
              </div>
            </div>
          ) : (
            <>
<<<<<<< HEAD
              {/* Chat Header */}
              <div className="flex-shrink-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
                <div className="p-4">
                  <div className="flex items-center justify-between">
                    <ConversationHeader
                      conversation={selectedConversation}
                      onBack={undefined}
                      showBackButton={false}
                      isOnline={true}
                      lastSeen={
                        selectedConversation.lastMessageAt
                          ? format(new Date(selectedConversation.lastMessageAt), 'HH:mm', {
                              locale: fr,
                            })
                          : undefined
                      }
                    />

                    <div className="flex items-center gap-2">
                      <Button
                        variant="ghost"
                        size="icon"
                        className="text-gray-600 hover:text-[#00a884]"
                      >
                        <Phone className="h-5 w-5" />
                      </Button>
                      <Button
                        variant="ghost"
                        size="icon"
                        onClick={() => {
                          setShowSidebar(!showSidebar)
                        }}
                        className={`text-gray-600 hover:text-[#00a884] ${showSidebar ? 'text-[#00a884] bg-gray-100 dark:bg-gray-700' : ''}`}
                        title="Informations client"
                      >
                        <Settings className="h-5 w-5" />
                      </Button>
                    </div>
                  </div>
                </div>

                {/* Quick Actions Bar */}
                <div className="px-4 py-2 bg-gray-50 dark:bg-gray-700 border-t border-gray-100 dark:border-gray-600">
                  <div className="flex items-center gap-2">
                    <CreateOrderDialog
                      conversationId={selectedConversation.id}
                      customerName={selectedConversation.customerName}
                      customerPhone={selectedConversation.customerPhone}
                      customerId={selectedConversation.orderId}
                    >
                      <Button size="sm" variant="outline" className="gap-2 text-xs">
                        <ShoppingCart className="h-4 w-4" />
                        Créer commande
                      </Button>
                    </CreateOrderDialog>
                  </div>
                </div>
              </div>

              {renderConversationActionBanners()}

              {/* Messages Area */}
              <div className="flex-1 overflow-hidden bg-[#e5ddd5] dark:bg-gray-900">
                <div className="h-full overflow-y-auto p-4">
                  {messagesLoading ? (
                    <div className="flex items-center justify-center h-full">
                      <div className="text-gray-500">Chargement des messages...</div>
                    </div>
                  ) : visibleConversationMessages.length > 0 ? (
                    visibleConversationMessages.map((message: WhatsappMessage, index: number) => (
                      <MessageBubble
                        key={message.id}
                        message={message}
                        quotedMessage={resolveQuotedMessage(message)}
                        reactions={messageReactions.get(resolveMessageIdentity(message)) || []}
                        currentUserReaction={
                          currentUserReactions.get(resolveMessageIdentity(message)) || null
                        }
                        isLast={index === visibleConversationMessages.length - 1}
                        onRetry={() => {
                          retryMessageMutation.mutate(message.id)
                        }}
                        onReact={handleReactionUpdate}
                        onReply={handleReplySelection}
                        onCopy={(content: string) => {
                          navigator.clipboard.writeText(content)
                          toastFor({
                            kind: 'success',
                            titleKey: 'common.dynamicMsg',
                            params: { message: 'Texte copié dans le presse-papiers' },
                          })
                        }}
                        onDelete={handleDelete}
                        onChooseTemplate={handleChooseTemplate}
                      />
                    ))
                  ) : (
                    <div className="flex items-center justify-center h-full">
                      <div className="text-center p-8">
                        <MessageCircle className="h-16 w-16 text-gray-400 mx-auto mb-4" />
                        <h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
                          Aucun message
                        </h3>
                        <p className="text-gray-500 dark:text-gray-400">
                          Commencez la conversation avec votre client
                        </p>
                      </div>
                    </div>
                  )}
                  <div ref={messagesEndRef} />
                </div>
              </div>

              {/* Message Input */}
=======
              <ChatHeader
                conversation={selectedConversation}
                showBackButton={isMobile}
                onBack={() => setView('conversations')}
                sidebarActive={!isMobile && showSidebar}
                onToggleSidebar={() => {
                  if (isMobile) {
                    setView('sidebar')
                  } else {
                    setShowSidebar((previous) => !previous)
                  }
                }}
                showQuickActions={!isMobile}
              />
              {customerWindowExpired && (
                <WindowWarningBanner onChooseTemplate={() => setNewMessage('/templates')} />
              )}
              <MessagesArea
                messages={visibleConversationMessages}
                isLoading={messagesLoading}
                messageReactions={messageReactions}
                currentUserReactions={currentUserReactions}
                resolveQuotedMessage={resolveQuotedMessage}
                conversationId={selectedConversationId}
                onRetry={handleRetry}
                onReact={handleReactionUpdate}
                onReply={handleReplySelection}
                onChooseTemplate={handleChooseTemplate}
                onDelete={canDelete ? handleDelete : undefined}
              />
>>>>>>> develop
              <div className="flex-shrink-0">
                <ChatInput
                  value={newMessage}
                  onChange={setNewMessage}
                  onSend={handleSendMessage}
                  onTemplateSend={handleTemplateSend}
                  onContentSend={handleContentSend}
                  isLoading={
                    sendMessageMutation.isPending ||
                    sendMediaMessageMutation.isPending ||
                    sendContentItemMutation.isPending
                  }
                  placeholder="Tapez votre message..."
                  conversationId={selectedConversationId || undefined}
                  replyContext={replyContext}
                  onCancelReply={() => setReplyContext(null)}
                />
              </div>
            </>
          )}
        </section>

        {/* Panneau 3 — Contexte client */}
        <aside
          className={cn(
            'h-full w-full flex-col overflow-hidden bg-white dark:bg-gray-800 md:w-80 md:flex-shrink-0 md:border-l md:border-gray-200 md:dark:border-gray-700',
            view === 'sidebar' && selectedConversation ? 'flex' : 'hidden',
            showSidebar && selectedConversation ? 'md:flex' : 'md:hidden',
          )}
        >
          {/* En-tête retour — mobile uniquement */}
          {selectedConversation && (
            <div className="flex h-16 flex-shrink-0 items-center border-b border-gray-200 px-4 dark:border-gray-700 md:hidden">
              <Button variant="ghost" size="icon" onClick={() => setView('chat')} className="mr-2">
                <ArrowLeft className="h-5 w-5" />
              </Button>
              <h1 className="truncate text-lg font-semibold text-gray-900 dark:text-gray-100">
                {selectedConversation.customerName || selectedConversation.customerPhone}
              </h1>
            </div>
          )}
          <div className="min-h-0 flex-1 overflow-hidden">
            {selectedConversation && (
              <CustomerContextPanel
                conversation={selectedConversation}
                onUpdate={() =>
                  queryClient.invalidateQueries({ queryKey: ['/api/whatsapp/conversations'] })
                }
              />
            )}
          </div>
        </aside>
      </div>

      {/* Nouvelle conversation — Dialog autonome (gère son propre overlay, mobile + desktop) */}
      <NewConversation
        open={showNewConversation}
        forcedConnectorId={selectedConnectorId || undefined}
        onClose={() => {
          setShowNewConversation(false)
        }}
        onConversationCreated={handleConversationCreated}
      />
    </>
  )

  // Coquille extérieure : plein écran sur mobile, PageLayout (nav de l'app) sur desktop.
  if (isMobile) {
    return <div className="h-screen overflow-hidden">{page}</div>
  }

  return (
    <PageLayout className="overflow-hidden p-0">
      <div style={{ height: 'calc(100vh - 64px)' }}>{page}</div>
    </PageLayout>
  )
}

export default WhatsAppChatPage
Editor is loading...
Leave a Comment