1233 lines
48 KiB
TypeScript
1233 lines
48 KiB
TypeScript
'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<Deal[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState<string | null>(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<Deal | null>(null)
|
||
|
||
// Form Data
|
||
const [formData, setFormData] = useState<CreateDealData>({
|
||
name: '',
|
||
contactId: '',
|
||
structure: 'B2B',
|
||
pipelineId: '',
|
||
stage: 'LEAD',
|
||
estimatedValue: 0,
|
||
probability: 50,
|
||
expectedCloseDate: ''
|
||
})
|
||
const [formErrors, setFormErrors] = useState<Record<string, string>>({})
|
||
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<any[]>([])
|
||
const [loadingContacts, setLoadingContacts] = useState(false)
|
||
|
||
// Pipelines for dropdown
|
||
const [pipelines, setPipelines] = useState<Pipeline[]>([])
|
||
const [loadingPipelines, setLoadingPipelines] = useState(false)
|
||
const editHandledRef = useRef<string | null>(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<string, string> = {}
|
||
|
||
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<string, string> = {
|
||
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<string, string> = {
|
||
LEAD: 'عميل محتمل',
|
||
QUALIFIED: 'مؤهل',
|
||
PROPOSAL: 'عرض',
|
||
NEGOTIATION: 'تفاوض',
|
||
WON: 'فوز',
|
||
LOST: 'خسارة'
|
||
}
|
||
return labels[stage] || stage
|
||
}
|
||
|
||
const getStructureLabel = (structure: string) => {
|
||
const labels: Record<string, string> = {
|
||
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 }) => (
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{/* Deal Structure */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
{t('crm.structure')} <span className="text-red-500">*</span>
|
||
</label>
|
||
<select
|
||
value={formData.structure}
|
||
onChange={(e) => {
|
||
const structure = e.target.value
|
||
const matchingPipelines = pipelines.filter(p => p.structure === structure)
|
||
const firstPipeline = matchingPipelines[0] || pipelines[0]
|
||
const firstStage = firstPipeline?.stages?.length
|
||
? (firstPipeline.stages as { name: string }[])[0].name
|
||
: 'LEAD'
|
||
setFormData({ ...formData, structure, pipelineId: firstPipeline?.id ?? '', stage: firstStage })
|
||
}}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500"
|
||
>
|
||
<option value="B2B">{t('crm.structureB2B')}</option>
|
||
<option value="B2C">{t('crm.structureB2C')}</option>
|
||
<option value="B2G">{t('crm.structureB2G')}</option>
|
||
<option value="PARTNERSHIP">{t('crm.structurePartnership')}</option>
|
||
</select>
|
||
{formErrors.structure && <p className="text-red-500 text-xs mt-1">{formErrors.structure}</p>}
|
||
</div>
|
||
|
||
{/* Pipeline */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
{t('crm.pipeline')} <span className="text-red-500">*</span>
|
||
</label>
|
||
<select
|
||
value={formData.pipelineId}
|
||
onChange={(e) => {
|
||
const pipelineId = e.target.value
|
||
const pipeline = displayPipelines.find(p => p.id === pipelineId)
|
||
const firstStage = pipeline?.stages?.length
|
||
? (pipeline.stages as { name: string }[])[0].name
|
||
: 'LEAD'
|
||
setFormData({ ...formData, pipelineId, stage: firstStage })
|
||
}}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500"
|
||
disabled={loadingPipelines || isEdit}
|
||
>
|
||
<option value="">{loadingPipelines ? t('common.loading') : t('crm.selectPipeline')}</option>
|
||
{displayPipelines.map(p => (
|
||
<option key={p.id} value={p.id}>{p.name} {p.structure ? `(${p.structure})` : ''}</option>
|
||
))}
|
||
</select>
|
||
{formErrors.pipelineId && <p className="text-red-500 text-xs mt-1">{formErrors.pipelineId}</p>}
|
||
</div>
|
||
|
||
{/* Contact */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
{t('crm.contact')} <span className="text-red-500">*</span>
|
||
</label>
|
||
<select
|
||
value={formData.contactId}
|
||
onChange={(e) => setFormData({ ...formData, contactId: 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"
|
||
disabled={loadingContacts}
|
||
>
|
||
<option value="">{t('crm.selectContact')}</option>
|
||
{contacts.map(contact => (
|
||
<option key={contact.id} value={contact.id}>{contact.name}</option>
|
||
))}
|
||
</select>
|
||
{formErrors.contactId && <p className="text-red-500 text-xs mt-1">{formErrors.contactId}</p>}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Deal Name */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
{t('crm.dealName')} <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.name}
|
||
onChange={(e) => 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 && <p className="text-red-500 text-xs mt-1">{formErrors.name}</p>}
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{/* Stage */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
{t('crm.stage')} <span className="text-red-500">*</span>
|
||
</label>
|
||
<select
|
||
value={formData.stage}
|
||
onChange={(e) => setFormData({ ...formData, stage: 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"
|
||
>
|
||
{(() => {
|
||
const selectedPipeline = pipelines.find(p => p.id === formData.pipelineId)
|
||
const stages = (selectedPipeline?.stages as { name: string; nameAr?: string }[] | undefined) ?? []
|
||
if (stages.length > 0) {
|
||
const stageNames = new Set(stages.map(s => s.name))
|
||
const options = stages.map(s => (
|
||
<option key={s.name} value={s.name}>{s.nameAr ? `${s.name} - ${s.nameAr}` : s.name}</option>
|
||
))
|
||
if (formData.stage && !stageNames.has(formData.stage)) {
|
||
options.unshift(<option key={formData.stage} value={formData.stage}>{formData.stage}</option>)
|
||
}
|
||
return options
|
||
}
|
||
return (
|
||
<>
|
||
<option value="LEAD">Lead - عميل محتمل</option>
|
||
<option value="QUALIFIED">Qualified - مؤهل</option>
|
||
<option value="PROPOSAL">Proposal - عرض</option>
|
||
<option value="NEGOTIATION">Negotiation - تفاوض</option>
|
||
</>
|
||
)
|
||
})()}
|
||
</select>
|
||
{formErrors.stage && <p className="text-red-500 text-xs mt-1">{formErrors.stage}</p>}
|
||
</div>
|
||
|
||
{/* Probability */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
{t('crm.probability')} (%)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
max="100"
|
||
value={formData.probability || 50}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{/* Estimated Value */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
{t('crm.estimatedValue')} <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
value={formData.estimatedValue}
|
||
onChange={(e) => 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 && <p className="text-red-500 text-xs mt-1">{formErrors.estimatedValue}</p>}
|
||
</div>
|
||
|
||
{/* Expected Close Date */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
{t('crm.expectedCloseDate')}
|
||
</label>
|
||
<input
|
||
type="date"
|
||
value={formData.expectedCloseDate || ''}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Expected Value Display */}
|
||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm font-medium text-blue-900">Expected Value:</span>
|
||
<span className="text-lg font-bold text-blue-900">
|
||
{(formData.estimatedValue * (formData.probability || 0) / 100).toLocaleString()} SAR
|
||
</span>
|
||
</div>
|
||
<p className="text-xs text-blue-700 mt-1">
|
||
Calculated as: Estimated Value × Probability
|
||
</p>
|
||
</div>
|
||
|
||
{/* Form Actions */}
|
||
<div className="flex items-center justify-end gap-3 pt-4 border-t">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
isEdit ? setShowEditModal(false) : setShowCreateModal(false)
|
||
resetForm()
|
||
}}
|
||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||
disabled={submitting}
|
||
>
|
||
{t('common.cancel')}
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
disabled={submitting}
|
||
>
|
||
{submitting ? (
|
||
<>
|
||
<Loader2 className="h-4 w-4 animate-spin" />
|
||
{isEdit ? t('crm.updating') : t('crm.creating')}
|
||
</>
|
||
) : (
|
||
<>
|
||
{isEdit ? t('crm.updateDeal') : t('crm.createDeal')}
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50">
|
||
{/* Header */}
|
||
<header className="bg-white shadow-sm border-b">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-4">
|
||
<Link
|
||
href="/dashboard"
|
||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||
>
|
||
<ArrowLeft className="h-5 w-5 text-gray-600" />
|
||
</Link>
|
||
<div className="flex items-center gap-3">
|
||
<div className="bg-green-100 p-2 rounded-lg">
|
||
<TrendingUp className="h-6 w-6 text-green-600" />
|
||
</div>
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900">{t('crm.title')}</h1>
|
||
<p className="text-sm text-gray-600">{t('crm.subtitle')}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3">
|
||
<button
|
||
onClick={() => {
|
||
resetForm()
|
||
setShowCreateModal(true)
|
||
}}
|
||
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||
>
|
||
<Plus className="h-4 w-4" />
|
||
{t('crm.addDeal')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
{/* Stats Cards */}
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">{t('crm.totalValue')}</p>
|
||
<p className="text-3xl font-bold text-gray-900 mt-1">
|
||
{(totalValue / 1000).toFixed(0)}K
|
||
</p>
|
||
<p className="text-xs text-gray-600 mt-1">SAR</p>
|
||
</div>
|
||
<div className="bg-blue-100 p-3 rounded-lg">
|
||
<DollarSign className="h-8 w-8 text-blue-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">{t('crm.expectedValue')}</p>
|
||
<p className="text-3xl font-bold text-gray-900 mt-1">
|
||
{(expectedValue / 1000).toFixed(0)}K
|
||
</p>
|
||
<p className="text-xs text-green-600 mt-1">
|
||
{totalValue > 0 ? ((expectedValue / totalValue) * 100).toFixed(0) : 0}% {t('crm.conversion')}
|
||
</p>
|
||
</div>
|
||
<div className="bg-green-100 p-3 rounded-lg">
|
||
<Target className="h-8 w-8 text-green-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">{t('crm.activeDeals')}</p>
|
||
<p className="text-3xl font-bold text-gray-900 mt-1">{activeDeals}</p>
|
||
<p className="text-xs text-orange-600 mt-1">{t('crm.inPipeline')}</p>
|
||
</div>
|
||
<div className="bg-orange-100 p-3 rounded-lg">
|
||
<Clock className="h-8 w-8 text-orange-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-gray-600">{t('crm.wonDeals')}</p>
|
||
<p className="text-3xl font-bold text-gray-900 mt-1">{wonDeals}</p>
|
||
<p className="text-xs text-green-600 mt-1">
|
||
{total > 0 ? ((wonDeals / total) * 100).toFixed(0) : 0}% {t('crm.winRate')}
|
||
</p>
|
||
</div>
|
||
<div className="bg-purple-100 p-3 rounded-lg">
|
||
<Award className="h-8 w-8 text-purple-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Filters and Search */}
|
||
<div className="bg-white rounded-xl shadow-sm p-6 border border-gray-200 mb-6">
|
||
<div className="flex flex-col md:flex-row gap-4">
|
||
{/* Search */}
|
||
<div className="flex-1 relative">
|
||
<Search className="absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||
<input
|
||
type="text"
|
||
placeholder={t('crm.searchPlaceholder')}
|
||
value={searchTerm}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
|
||
{/* Structure Filter */}
|
||
<select
|
||
value={selectedStructure}
|
||
onChange={(e) => setSelectedStructure(e.target.value)}
|
||
className="px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500"
|
||
>
|
||
<option value="all">{t('crm.allStructures')}</option>
|
||
<option value="B2B">B2B</option>
|
||
<option value="B2C">B2C</option>
|
||
<option value="B2G">B2G</option>
|
||
<option value="PARTNERSHIP">Partnership</option>
|
||
</select>
|
||
|
||
{/* Stage Filter */}
|
||
<select
|
||
value={selectedStage}
|
||
onChange={(e) => setSelectedStage(e.target.value)}
|
||
className="px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500"
|
||
>
|
||
<option value="all">{t('crm.allStages')}</option>
|
||
<option value="LEAD">Lead</option>
|
||
<option value="QUALIFIED">Qualified</option>
|
||
<option value="PROPOSAL">Proposal</option>
|
||
<option value="NEGOTIATION">Negotiation</option>
|
||
<option value="WON">Won</option>
|
||
<option value="LOST">Lost</option>
|
||
</select>
|
||
|
||
{/* Status Filter */}
|
||
<select
|
||
value={selectedStatus}
|
||
onChange={(e) => setSelectedStatus(e.target.value)}
|
||
className="px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500"
|
||
>
|
||
<option value="all">{t('crm.allStatus')}</option>
|
||
<option value="ACTIVE">Active</option>
|
||
<option value="WON">Won</option>
|
||
<option value="LOST">Lost</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Deals Table */}
|
||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
||
{loading ? (
|
||
<div className="p-12">
|
||
<LoadingSpinner size="lg" message={t('crm.loadingDeals')} />
|
||
</div>
|
||
) : error ? (
|
||
<div className="p-12 text-center">
|
||
<p className="text-red-600 mb-4">{error}</p>
|
||
<button
|
||
onClick={fetchDeals}
|
||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"
|
||
>
|
||
{t('crm.retry')}
|
||
</button>
|
||
</div>
|
||
) : deals.length === 0 ? (
|
||
<div className="p-12 text-center">
|
||
<TrendingUp className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||
<p className="text-gray-600 mb-4">{t('crm.noDealsFound')}</p>
|
||
<button
|
||
onClick={() => setShowCreateModal(true)}
|
||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"
|
||
>
|
||
{t('crm.createFirstDeal')}
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead className="bg-gray-50 border-b border-gray-200">
|
||
<tr>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">{t('crm.deal')}</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">{t('crm.contact')}</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">{t('crm.structure')}</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">{t('crm.value')}</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">{t('crm.probability')}</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">{t('crm.stage')}</th>
|
||
<th className="px-6 py-4 text-right text-xs font-semibold text-gray-700 uppercase">{t('common.actions')}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-200">
|
||
{deals.map((deal) => (
|
||
<tr key={deal.id} className="hover:bg-gray-50 transition-colors">
|
||
<td className="px-6 py-4">
|
||
<div>
|
||
<Link
|
||
href={`/crm/deals/${deal.id}`}
|
||
className="font-semibold text-gray-900 hover:text-green-600 hover:underline"
|
||
>
|
||
{deal.name}
|
||
</Link>
|
||
<p className="text-xs text-gray-600">{deal.dealNumber}</p>
|
||
</div>
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<div className="flex items-center gap-2">
|
||
<User className="h-4 w-4 text-gray-400" />
|
||
<span className="text-sm text-gray-900">
|
||
{deal.contact?.name || 'N/A'}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<span className="text-sm text-gray-900">
|
||
{getStructureLabel(deal.structure)}
|
||
</span>
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<div>
|
||
<span className="text-sm font-semibold text-gray-900">
|
||
{(deal.estimatedValue ?? 0).toLocaleString()} SAR
|
||
</span>
|
||
{(deal.actualValue ?? 0) > 0 && (
|
||
<p className="text-xs text-green-600">
|
||
Actual: {(deal.actualValue ?? 0).toLocaleString()}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-16 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||
<div
|
||
className="h-full bg-green-500 rounded-full"
|
||
style={{ width: `${deal.probability || 0}%` }}
|
||
/>
|
||
</div>
|
||
<span className="text-sm text-gray-600">{deal.probability || 0}%</span>
|
||
</div>
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${getStageColor(deal.stage)}`}>
|
||
{getStageLabel(deal.stage)}
|
||
</span>
|
||
</td>
|
||
<td className="px-6 py-4">
|
||
<div className="flex items-center gap-2">
|
||
{deal.status === 'ACTIVE' && (
|
||
<>
|
||
<button
|
||
onClick={() => openWinDialog(deal)}
|
||
className="p-2 hover:bg-green-50 text-green-600 rounded-lg transition-colors"
|
||
title={t('crm.markWon')}
|
||
>
|
||
<CheckCircle2 className="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => openLoseDialog(deal)}
|
||
className="p-2 hover:bg-red-50 text-red-600 rounded-lg transition-colors"
|
||
title={t('crm.markLost')}
|
||
>
|
||
<XCircle className="h-4 w-4" />
|
||
</button>
|
||
</>
|
||
)}
|
||
<button
|
||
onClick={() => openEditModal(deal)}
|
||
className="p-2 hover:bg-blue-50 text-blue-600 rounded-lg transition-colors"
|
||
title={t('common.edit')}
|
||
>
|
||
<Edit className="h-4 w-4" />
|
||
</button>
|
||
<button
|
||
onClick={() => openDeleteDialog(deal)}
|
||
className="p-2 hover:bg-red-50 text-red-600 rounded-lg transition-colors"
|
||
title={t('crm.deleteDeal')}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Pagination */}
|
||
<div className="px-6 py-4 border-t border-gray-200 flex items-center justify-between">
|
||
<p className="text-sm text-gray-600">
|
||
Showing <span className="font-semibold">{((currentPage - 1) * pageSize) + 1}</span> to{' '}
|
||
<span className="font-semibold">{Math.min(currentPage * pageSize, total)}</span> of{' '}
|
||
<span className="font-semibold">{total}</span> deals
|
||
</p>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => setCurrentPage(currentPage - 1)}
|
||
disabled={currentPage === 1}
|
||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{t('crm.paginationPrevious')}
|
||
</button>
|
||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||
const page = i + 1
|
||
return (
|
||
<button
|
||
key={page}
|
||
onClick={() => setCurrentPage(page)}
|
||
className={`px-4 py-2 rounded-lg transition-colors ${
|
||
currentPage === page
|
||
? 'bg-green-600 text-white'
|
||
: 'border border-gray-300 text-gray-700 hover:bg-gray-50'
|
||
}`}
|
||
>
|
||
{page}
|
||
</button>
|
||
)
|
||
})}
|
||
{totalPages > 5 && <span className="px-2">...</span>}
|
||
<button
|
||
onClick={() => setCurrentPage(currentPage + 1)}
|
||
disabled={currentPage === totalPages}
|
||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{t('crm.paginationNext')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</main>
|
||
|
||
{/* Create Modal */}
|
||
<Modal
|
||
isOpen={showCreateModal}
|
||
onClose={() => {
|
||
setShowCreateModal(false)
|
||
resetForm()
|
||
}}
|
||
title={t('crm.createNewDeal')}
|
||
size="xl"
|
||
>
|
||
<form onSubmit={handleCreate}>
|
||
<FormFields />
|
||
</form>
|
||
</Modal>
|
||
|
||
{/* Edit Modal */}
|
||
<Modal
|
||
isOpen={showEditModal}
|
||
onClose={() => {
|
||
setShowEditModal(false)
|
||
resetForm()
|
||
}}
|
||
title={t('crm.editDeal')}
|
||
size="xl"
|
||
>
|
||
<form onSubmit={handleEdit}>
|
||
<FormFields isEdit />
|
||
</form>
|
||
</Modal>
|
||
|
||
{/* Win Dialog */}
|
||
{showWinDialog && selectedDeal && (
|
||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowWinDialog(false)} />
|
||
<div className="flex min-h-screen items-center justify-center p-4">
|
||
<div className="relative bg-white rounded-xl shadow-2xl p-6 max-w-md w-full">
|
||
<div className="flex items-center gap-4 mb-4">
|
||
<div className="bg-green-100 p-3 rounded-full">
|
||
<Award className="h-6 w-6 text-green-600" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-lg font-bold text-gray-900">{t('crm.markDealWon')}</h3>
|
||
<p className="text-sm text-gray-600">{selectedDeal.name}</p>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
{t('crm.actualValue')} <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
value={winData.actualValue}
|
||
onChange={(e) => 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"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
{t('crm.reasonForWinning')} <span className="text-red-500">*</span>
|
||
</label>
|
||
<textarea
|
||
value={winData.wonReason}
|
||
onChange={(e) => setWinData({ ...winData, wonReason: e.target.value })}
|
||
rows={3}
|
||
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.winPlaceholder')}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center justify-end gap-3 mt-6">
|
||
<button
|
||
onClick={() => setShowWinDialog(false)}
|
||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||
disabled={submitting}
|
||
>
|
||
{t('common.cancel')}
|
||
</button>
|
||
<button
|
||
onClick={handleWin}
|
||
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50"
|
||
disabled={submitting}
|
||
>
|
||
{submitting ? (
|
||
<>
|
||
<Loader2 className="h-4 w-4 animate-spin" />
|
||
{t('crm.processing')}
|
||
</>
|
||
) : (
|
||
t('crm.markWon')
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Lose Dialog */}
|
||
{showLoseDialog && selectedDeal && (
|
||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowLoseDialog(false)} />
|
||
<div className="flex min-h-screen items-center justify-center p-4">
|
||
<div className="relative bg-white rounded-xl shadow-2xl p-6 max-w-md w-full">
|
||
<div className="flex items-center gap-4 mb-4">
|
||
<div className="bg-red-100 p-3 rounded-full">
|
||
<TrendingDown className="h-6 w-6 text-red-600" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-lg font-bold text-gray-900">{t('crm.markDealLost')}</h3>
|
||
<p className="text-sm text-gray-600">{selectedDeal.name}</p>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||
{t('crm.reasonForLosing')} <span className="text-red-500">*</span>
|
||
</label>
|
||
<textarea
|
||
value={loseData.lostReason}
|
||
onChange={(e) => setLoseData({ ...loseData, lostReason: e.target.value })}
|
||
rows={3}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500"
|
||
placeholder={t('crm.losePlaceholder')}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center justify-end gap-3 mt-6">
|
||
<button
|
||
onClick={() => setShowLoseDialog(false)}
|
||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||
disabled={submitting}
|
||
>
|
||
{t('common.cancel')}
|
||
</button>
|
||
<button
|
||
onClick={handleLose}
|
||
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
|
||
disabled={submitting}
|
||
>
|
||
{submitting ? (
|
||
<>
|
||
<Loader2 className="h-4 w-4 animate-spin" />
|
||
{t('crm.processing')}
|
||
</>
|
||
) : (
|
||
t('crm.markLost')
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Delete Confirmation Dialog */}
|
||
{showDeleteDialog && selectedDeal && (
|
||
<div className="fixed inset-0 z-50 overflow-y-auto">
|
||
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowDeleteDialog(false)} />
|
||
<div className="flex min-h-screen items-center justify-center p-4">
|
||
<div className="relative bg-white rounded-xl shadow-2xl p-6 max-w-md w-full">
|
||
<div className="flex items-center gap-4 mb-4">
|
||
<div className="bg-red-100 p-3 rounded-full">
|
||
<Trash2 className="h-6 w-6 text-red-600" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-lg font-bold text-gray-900">{t('crm.deleteDeal')}</h3>
|
||
<p className="text-sm text-gray-600">{t('crm.deleteDealDesc')}</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-gray-700 mb-6">
|
||
{t('crm.deleteDealConfirm')} <span className="font-semibold">{selectedDeal.name}</span>?
|
||
</p>
|
||
<div className="flex items-center justify-end gap-3">
|
||
<button
|
||
onClick={() => {
|
||
setShowDeleteDialog(false)
|
||
setSelectedDeal(null)
|
||
}}
|
||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||
disabled={submitting}
|
||
>
|
||
{t('common.cancel')}
|
||
</button>
|
||
<button
|
||
onClick={handleDelete}
|
||
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
|
||
disabled={submitting}
|
||
>
|
||
{submitting ? (
|
||
<>
|
||
<Loader2 className="h-4 w-4 animate-spin" />
|
||
{t('crm.deleting')}
|
||
</>
|
||
) : (
|
||
t('crm.deleteDeal')
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default function CRMPage() {
|
||
return (
|
||
<ProtectedRoute>
|
||
<CRMContent />
|
||
</ProtectedRoute>
|
||
)
|
||
}
|