'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { useSearchParams } from 'next/navigation' import ProtectedRoute from '@/components/ProtectedRoute' import Modal from '@/components/Modal' import LoadingSpinner from '@/components/LoadingSpinner' import Link from 'next/link' import { toast } from 'react-hot-toast' import { TrendingUp, Plus, Search, Filter, DollarSign, Target, Award, Clock, ArrowLeft, Users, CheckCircle2, XCircle, AlertCircle, Edit, Trash2, Calendar, Building2, User, Loader2, FileText, TrendingDown } from 'lucide-react' import { dealsAPI, Deal, CreateDealData, UpdateDealData, DealFilters } from '@/lib/api/deals' import { contactsAPI } from '@/lib/api/contacts' import { pipelinesAPI, Pipeline } from '@/lib/api/pipelines' import { useLanguage } from '@/contexts/LanguageContext' function CRMContent() { const { t } = useLanguage() const searchParams = useSearchParams() // State Management const [deals, setDeals] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Pagination const [currentPage, setCurrentPage] = useState(1) const [totalPages, setTotalPages] = useState(1) const [total, setTotal] = useState(0) const pageSize = 10 // Filters const [searchTerm, setSearchTerm] = useState('') const [selectedStructure, setSelectedStructure] = useState('all') const [selectedStage, setSelectedStage] = useState('all') const [selectedStatus, setSelectedStatus] = useState('all') // Modals const [showCreateModal, setShowCreateModal] = useState(false) const [showEditModal, setShowEditModal] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [showWinDialog, setShowWinDialog] = useState(false) const [showLoseDialog, setShowLoseDialog] = useState(false) const [selectedDeal, setSelectedDeal] = useState(null) // Form Data const [formData, setFormData] = useState({ name: '', contactId: '', structure: 'B2B', pipelineId: '', stage: 'LEAD', estimatedValue: 0, probability: 50, expectedCloseDate: '' }) const [formErrors, setFormErrors] = useState>({}) const [submitting, setSubmitting] = useState(false) // Win/Lose Forms const [winData, setWinData] = useState({ actualValue: 0, wonReason: '' }) const [loseData, setLoseData] = useState({ lostReason: '' }) // Contacts for dropdown const [contacts, setContacts] = useState([]) const [loadingContacts, setLoadingContacts] = useState(false) // Pipelines for dropdown const [pipelines, setPipelines] = useState([]) const [loadingPipelines, setLoadingPipelines] = useState(false) const editHandledRef = useRef(null) // Fetch Contacts for dropdown useEffect(() => { const fetchContacts = async () => { setLoadingContacts(true) try { const data = await contactsAPI.getAll({ pageSize: 100 }) setContacts(data.contacts) } catch (err) { console.error('Failed to load contacts:', err) } finally { setLoadingContacts(false) } } fetchContacts() }, []) // Fetch Pipelines for dropdown useEffect(() => { const fetchPipelines = async () => { setLoadingPipelines(true) try { const data = await pipelinesAPI.getAll() setPipelines(data) } catch (err) { console.error('Failed to load pipelines:', err) toast.error('Failed to load pipelines') } finally { setLoadingPipelines(false) } } fetchPipelines() }, []) // Fetch Deals (with debouncing for search) const fetchDeals = useCallback(async () => { setLoading(true) setError(null) try { const filters: DealFilters = { page: currentPage, pageSize, } if (searchTerm) filters.search = searchTerm if (selectedStructure !== 'all') filters.structure = selectedStructure if (selectedStage !== 'all') filters.stage = selectedStage if (selectedStatus !== 'all') filters.status = selectedStatus const data = await dealsAPI.getAll(filters) setDeals(data.deals) setTotal(data.total) setTotalPages(data.totalPages) } catch (err: any) { setError(err.response?.data?.message || 'Failed to load deals') toast.error('Failed to load deals') } finally { setLoading(false) } }, [currentPage, searchTerm, selectedStructure, selectedStage, selectedStatus]) // Debounced search useEffect(() => { const debounce = setTimeout(() => { setCurrentPage(1) fetchDeals() }, 500) return () => clearTimeout(debounce) }, [searchTerm]) // Fetch on filter/page change useEffect(() => { fetchDeals() }, [currentPage, selectedStructure, selectedStage, selectedStatus]) // Handle ?edit=dealId from URL (e.g. from deal detail page) const editId = searchParams.get('edit') useEffect(() => { if (!editId || editHandledRef.current === editId) return const deal = deals.find(d => d.id === editId) if (deal) { editHandledRef.current = editId setSelectedDeal(deal) setFormData({ name: deal.name, contactId: deal.contactId, structure: deal.structure, pipelineId: deal.pipelineId, stage: deal.stage, estimatedValue: deal.estimatedValue, probability: deal.probability, expectedCloseDate: deal.expectedCloseDate?.split('T')[0] || '' }) setShowEditModal(true) } else if (!loading) { editHandledRef.current = editId dealsAPI.getById(editId).then((d) => { setSelectedDeal(d) setFormData({ name: d.name, contactId: d.contactId, structure: d.structure, pipelineId: d.pipelineId, stage: d.stage, estimatedValue: d.estimatedValue, probability: d.probability, expectedCloseDate: d.expectedCloseDate?.split('T')[0] || '' }) setShowEditModal(true) }).catch(() => toast.error('Deal not found')) } }, [editId, loading, deals]) // Form Validation const validateForm = (): boolean => { const errors: Record = {} if (!formData.name || formData.name.trim().length < 3) { errors.name = t('crm.dealNameMin') } if (!formData.contactId) { errors.contactId = t('crm.contactRequired') } if (!formData.structure) { errors.structure = t('crm.structureRequired') } if (!formData.pipelineId) { errors.pipelineId = t('crm.pipelineRequired') } if (!formData.stage) { errors.stage = t('crm.stageRequired') } if (!formData.estimatedValue || formData.estimatedValue <= 0) { errors.estimatedValue = t('crm.valueRequired') } setFormErrors(errors) return Object.keys(errors).length === 0 } // Create Deal const handleCreate = async (e: React.FormEvent) => { e.preventDefault() if (!validateForm()) { toast.error(t('crm.fixFormErrors')) return } setSubmitting(true) try { await dealsAPI.create(formData) toast.success(t('crm.createSuccess')) setShowCreateModal(false) resetForm() fetchDeals() } catch (err: any) { const message = err.response?.data?.message || 'Failed to create deal' toast.error(message) if (err.response?.data?.errors) { setFormErrors(err.response.data.errors) } } finally { setSubmitting(false) } } // Edit Deal const handleEdit = async (e: React.FormEvent) => { e.preventDefault() if (!selectedDeal || !validateForm()) { toast.error(t('crm.fixFormErrors')) return } setSubmitting(true) try { await dealsAPI.update(selectedDeal.id, formData as UpdateDealData) toast.success(t('crm.updateSuccess')) setShowEditModal(false) resetForm() fetchDeals() } catch (err: any) { const message = err.response?.data?.message || 'Failed to update deal' toast.error(message) } finally { setSubmitting(false) } } // Delete Deal (Archive) const handleDelete = async () => { if (!selectedDeal) return setSubmitting(true) try { await dealsAPI.lose(selectedDeal.id, 'Deal deleted by user') toast.success('Deal marked as lost!') setShowDeleteDialog(false) setSelectedDeal(null) fetchDeals() } catch (err: any) { const message = err.response?.data?.message || 'Failed to delete deal' toast.error(message) } finally { setSubmitting(false) } } // Win Deal const handleWin = async () => { if (!selectedDeal || !winData.actualValue || !winData.wonReason) { toast.error('Please fill all fields') return } setSubmitting(true) try { await dealsAPI.win(selectedDeal.id, winData.actualValue, winData.wonReason) toast.success(t('crm.winSuccess')) setShowWinDialog(false) setSelectedDeal(null) setWinData({ actualValue: 0, wonReason: '' }) fetchDeals() } catch (err: any) { const message = err.response?.data?.message || 'Failed to mark deal as won' toast.error(message) } finally { setSubmitting(false) } } // Lose Deal const handleLose = async () => { if (!selectedDeal || !loseData.lostReason) { toast.error('Please provide a reason') return } setSubmitting(true) try { await dealsAPI.lose(selectedDeal.id, loseData.lostReason) toast.success(t('crm.loseSuccess')) setShowLoseDialog(false) setSelectedDeal(null) setLoseData({ lostReason: '' }) fetchDeals() } catch (err: any) { const message = err.response?.data?.message || 'Failed to mark deal as lost' toast.error(message) } finally { setSubmitting(false) } } // Pipelines filtered by selected structure (or all if no match) const filteredPipelines = formData.structure ? pipelines.filter(p => p.structure === formData.structure) : pipelines const displayPipelines = filteredPipelines.length > 0 ? filteredPipelines : pipelines // Utility Functions const resetForm = () => { const defaultStructure = 'B2B' const matchingPipelines = pipelines.filter(p => p.structure === defaultStructure) const firstPipeline = matchingPipelines[0] || pipelines[0] const firstStage = firstPipeline?.stages?.length ? (firstPipeline.stages as { name: string }[])[0].name : 'LEAD' setFormData({ name: '', contactId: '', structure: defaultStructure, pipelineId: firstPipeline?.id ?? '', stage: firstStage, estimatedValue: 0, probability: 50, expectedCloseDate: '' }) setFormErrors({}) setSelectedDeal(null) } const openEditModal = (deal: Deal) => { setSelectedDeal(deal) setFormData({ name: deal.name, contactId: deal.contactId, structure: deal.structure, pipelineId: deal.pipelineId, stage: deal.stage, estimatedValue: deal.estimatedValue, probability: deal.probability, expectedCloseDate: deal.expectedCloseDate?.split('T')[0] || '' }) setShowEditModal(true) } const openDeleteDialog = (deal: Deal) => { setSelectedDeal(deal) setShowDeleteDialog(true) } const openWinDialog = (deal: Deal) => { setSelectedDeal(deal) setWinData({ actualValue: deal.estimatedValue, wonReason: '' }) setShowWinDialog(true) } const openLoseDialog = (deal: Deal) => { setSelectedDeal(deal) setLoseData({ lostReason: '' }) setShowLoseDialog(true) } const getStageColor = (stage: string) => { const colors: Record = { LEAD: 'bg-gray-100 text-gray-700', QUALIFIED: 'bg-blue-100 text-blue-700', PROPOSAL: 'bg-purple-100 text-purple-700', NEGOTIATION: 'bg-orange-100 text-orange-700', WON: 'bg-green-100 text-green-700', LOST: 'bg-red-100 text-red-700' } return colors[stage] || 'bg-gray-100 text-gray-700' } const getStageLabel = (stage: string) => { const labels: Record = { LEAD: 'عميل محتمل', QUALIFIED: 'مؤهل', PROPOSAL: 'عرض', NEGOTIATION: 'تفاوض', WON: 'فوز', LOST: 'خسارة' } return labels[stage] || stage } const getStructureLabel = (structure: string) => { const labels: Record = { B2B: 'شركة لشركة', B2C: 'شركة لفرد', B2G: 'شركة لحكومة', PARTNERSHIP: 'شراكة' } return labels[structure] || structure } // Calculate stats const totalValue = deals.reduce((sum, deal) => sum + deal.estimatedValue, 0) const expectedValue = deals.reduce((sum, deal) => sum + (deal.estimatedValue * (deal.probability || 0) / 100), 0) const wonDeals = deals.filter(d => d.status === 'WON').length const activeDeals = deals.filter(d => d.status === 'ACTIVE').length // Render Form Fields Component const FormFields = ({ isEdit = false }: { isEdit?: boolean }) => (
{/* Deal Structure */}
{formErrors.structure &&

{formErrors.structure}

}
{/* Pipeline */}
{formErrors.pipelineId &&

{formErrors.pipelineId}

}
{/* Contact */}
{formErrors.contactId &&

{formErrors.contactId}

}
{/* Deal Name */}
setFormData({ ...formData, name: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500" placeholder={t('crm.enterDealName')} /> {formErrors.name &&

{formErrors.name}

}
{/* Stage */}
{formErrors.stage &&

{formErrors.stage}

}
{/* Probability */}
setFormData({ ...formData, probability: parseInt(e.target.value) })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500" />
{/* Estimated Value */}
setFormData({ ...formData, estimatedValue: parseFloat(e.target.value) })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500" placeholder="0.00" /> {formErrors.estimatedValue &&

{formErrors.estimatedValue}

}
{/* Expected Close Date */}
setFormData({ ...formData, expectedCloseDate: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500" />
{/* Expected Value Display */}
Expected Value: {(formData.estimatedValue * (formData.probability || 0) / 100).toLocaleString()} SAR

Calculated as: Estimated Value × Probability

{/* Form Actions */}
) return (
{/* Header */}

{t('crm.title')}

{t('crm.subtitle')}

{/* Stats Cards */}

{t('crm.totalValue')}

{(totalValue / 1000).toFixed(0)}K

SAR

{t('crm.expectedValue')}

{(expectedValue / 1000).toFixed(0)}K

{totalValue > 0 ? ((expectedValue / totalValue) * 100).toFixed(0) : 0}% {t('crm.conversion')}

{t('crm.activeDeals')}

{activeDeals}

{t('crm.inPipeline')}

{t('crm.wonDeals')}

{wonDeals}

{total > 0 ? ((wonDeals / total) * 100).toFixed(0) : 0}% {t('crm.winRate')}

{/* Filters and Search */}
{/* Search */}
setSearchTerm(e.target.value)} className="w-full pr-10 pl-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500" />
{/* Structure Filter */} {/* Stage Filter */} {/* Status Filter */}
{/* Deals Table */}
{loading ? (
) : error ? (

{error}

) : deals.length === 0 ? (

{t('crm.noDealsFound')}

) : ( <>
{deals.map((deal) => ( ))}
{t('crm.deal')} {t('crm.contact')} {t('crm.structure')} {t('crm.value')} {t('crm.probability')} {t('crm.stage')} {t('common.actions')}
{deal.name}

{deal.dealNumber}

{deal.contact?.name || 'N/A'}
{getStructureLabel(deal.structure)}
{(deal.estimatedValue ?? 0).toLocaleString()} SAR {(deal.actualValue ?? 0) > 0 && (

Actual: {(deal.actualValue ?? 0).toLocaleString()}

)}
{deal.probability || 0}%
{getStageLabel(deal.stage)}
{deal.status === 'ACTIVE' && ( <> )}
{/* Pagination */}

Showing {((currentPage - 1) * pageSize) + 1} to{' '} {Math.min(currentPage * pageSize, total)} of{' '} {total} deals

{Array.from({ length: Math.min(5, totalPages) }, (_, i) => { const page = i + 1 return ( ) })} {totalPages > 5 && ...}
)}
{/* Create Modal */} { setShowCreateModal(false) resetForm() }} title={t('crm.createNewDeal')} size="xl" >
{/* Edit Modal */} { setShowEditModal(false) resetForm() }} title={t('crm.editDeal')} size="xl" >
{/* Win Dialog */} {showWinDialog && selectedDeal && (
setShowWinDialog(false)} />

{t('crm.markDealWon')}

{selectedDeal.name}

setWinData({ ...winData, actualValue: parseFloat(e.target.value) })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500" />