erp-platform/ui/src/views/crm/components/OpportunityManagement.tsx

409 lines
14 KiB
TypeScript
Raw Normal View History

2025-09-15 14:13:20 +00:00
import React, { useState } from 'react'
2025-09-15 09:31:47 +00:00
import {
FaBullseye,
FaPlus,
FaEdit,
FaTrash,
FaDollarSign,
FaCalendar,
FaArrowUp,
FaUser,
FaBuilding,
2025-09-15 14:13:20 +00:00
} from 'react-icons/fa'
import { CrmOpportunity, OpportunityStageEnum } from '../../../types/crm'
import DataTable, { Column } from '../../../components/common/DataTable'
import { mockBusinessParties } from '../../../mocks/mockBusinessParties'
import { mockOpportunities } from '../../../mocks/mockOpportunities'
import OpportunityForm from './OpportunityForm'
import OpportunityDetails from './OpportunityDetails'
import { BusinessParty } from '../../../types/common'
import Widget from '../../../components/common/Widget'
2025-09-15 09:31:47 +00:00
import {
getOpportunityProbabilityColor,
getOpportunityStageColor,
getOpportunityStageText,
2025-09-15 14:13:20 +00:00
} from '../../../utils/erp'
import { Container } from '@/components/shared'
2025-09-15 09:31:47 +00:00
const OpportunityManagement: React.FC = () => {
2025-09-15 14:13:20 +00:00
const [opportunities, setOpportunities] = useState<CrmOpportunity[]>(mockOpportunities)
const [customers] = useState<BusinessParty[]>(mockBusinessParties)
const [searchTerm, setSearchTerm] = useState('')
const [selectedStage, setSelectedStage] = useState<OpportunityStageEnum | 'all'>('all')
const [selectedCustomer, setSelectedCustomer] = useState<string>('all')
2025-09-15 09:31:47 +00:00
// Modal states
2025-09-15 14:13:20 +00:00
const [isFormOpen, setIsFormOpen] = useState(false)
const [isDetailsOpen, setIsDetailsOpen] = useState(false)
const [selectedOpportunity, setSelectedOpportunity] = useState<CrmOpportunity | null>(null)
const [formMode, setFormMode] = useState<'create' | 'edit'>('create')
2025-09-15 09:31:47 +00:00
const handleAdd = () => {
2025-09-15 14:13:20 +00:00
setSelectedOpportunity(null)
setFormMode('create')
setIsFormOpen(true)
}
2025-09-15 09:31:47 +00:00
const handleEdit = (opportunity: CrmOpportunity) => {
2025-09-15 14:13:20 +00:00
setSelectedOpportunity(opportunity)
setFormMode('edit')
setIsFormOpen(true)
}
2025-09-15 09:31:47 +00:00
const handleDelete = (id: string) => {
2025-09-15 14:13:20 +00:00
if (confirm('Bu fırsatı silmek istediğinizden emin misiniz?')) {
setOpportunities((prev) => prev.filter((opp) => opp.id !== id))
2025-09-15 09:31:47 +00:00
}
2025-09-15 14:13:20 +00:00
}
2025-09-15 09:31:47 +00:00
const handleViewDetails = (opportunity: CrmOpportunity) => {
2025-09-15 14:13:20 +00:00
setSelectedOpportunity(opportunity)
setIsDetailsOpen(true)
}
2025-09-15 09:31:47 +00:00
const handleSaveOpportunity = (opportunity: CrmOpportunity) => {
2025-09-15 14:13:20 +00:00
if (formMode === 'create') {
setOpportunities((prev) => [...prev, opportunity])
2025-09-15 09:31:47 +00:00
} else {
2025-09-15 14:13:20 +00:00
setOpportunities((prev) => prev.map((opp) => (opp.id === opportunity.id ? opportunity : opp)))
2025-09-15 09:31:47 +00:00
}
2025-09-15 14:13:20 +00:00
}
2025-09-15 09:31:47 +00:00
const handleCloseForm = () => {
2025-09-15 14:13:20 +00:00
setIsFormOpen(false)
setSelectedOpportunity(null)
}
2025-09-15 09:31:47 +00:00
const handleCloseDetails = () => {
2025-09-15 14:13:20 +00:00
setIsDetailsOpen(false)
setSelectedOpportunity(null)
}
2025-09-15 09:31:47 +00:00
const handleEditFromDetails = (opportunity: CrmOpportunity) => {
2025-09-15 14:13:20 +00:00
setIsDetailsOpen(false)
handleEdit(opportunity)
}
2025-09-15 09:31:47 +00:00
const filteredOpportunities = opportunities.filter((opportunity) => {
if (
searchTerm &&
!opportunity.title.toLowerCase().includes(searchTerm.toLowerCase()) &&
!opportunity.description?.toLowerCase().includes(searchTerm.toLowerCase())
) {
2025-09-15 14:13:20 +00:00
return false
2025-09-15 09:31:47 +00:00
}
2025-09-15 14:13:20 +00:00
if (selectedStage !== 'all' && opportunity.stage !== selectedStage) {
return false
2025-09-15 09:31:47 +00:00
}
2025-09-15 14:13:20 +00:00
if (selectedCustomer !== 'all' && opportunity.customerId !== selectedCustomer) {
return false
2025-09-15 09:31:47 +00:00
}
2025-09-15 14:13:20 +00:00
return true
})
2025-09-15 09:31:47 +00:00
const columns: Column<CrmOpportunity>[] = [
{
2025-09-15 14:13:20 +00:00
key: 'title',
header: 'Fırsat Başlığı',
2025-09-15 09:31:47 +00:00
sortable: true,
render: (opportunity: CrmOpportunity) => (
<div>
<div className="font-medium text-gray-900">{opportunity.title}</div>
2025-09-15 14:13:20 +00:00
<div className="text-sm text-gray-500">{opportunity.opportunityNumber}</div>
2025-09-15 09:31:47 +00:00
</div>
),
},
{
2025-09-15 14:13:20 +00:00
key: 'customer',
header: 'Müşteri',
2025-09-15 09:31:47 +00:00
render: (opportunity: CrmOpportunity) => {
2025-09-15 14:13:20 +00:00
const customer = customers.find((c) => c.id === opportunity.customerId)
2025-09-15 09:31:47 +00:00
return customer ? (
<div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-400" />
<span>{customer.name}</span>
</div>
) : (
2025-09-15 14:13:20 +00:00
'-'
)
2025-09-15 09:31:47 +00:00
},
},
{
2025-09-15 14:13:20 +00:00
key: 'stage',
header: 'Aşama',
2025-09-15 09:31:47 +00:00
render: (opportunity: CrmOpportunity) => (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getOpportunityStageColor(
2025-09-15 14:13:20 +00:00
opportunity.stage,
2025-09-15 09:31:47 +00:00
)}`}
>
{getOpportunityStageText(opportunity.stage)}
</span>
),
},
{
2025-09-15 14:13:20 +00:00
key: 'estimatedValue',
header: 'Tahmini Değer',
2025-09-15 09:31:47 +00:00
render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1">
<FaDollarSign className="w-4 h-4 text-gray-400" />
2025-09-15 14:13:20 +00:00
<span className="font-medium">{opportunity.estimatedValue.toLocaleString()}</span>
2025-09-15 09:31:47 +00:00
</div>
),
},
{
2025-09-15 14:13:20 +00:00
key: 'probability',
header: 'Olasılık',
2025-09-15 09:31:47 +00:00
render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1">
<FaArrowUp className="w-4 h-4 text-gray-400" />
<span
2025-09-15 14:13:20 +00:00
className={`font-medium ${getOpportunityProbabilityColor(opportunity.probability)}`}
2025-09-15 09:31:47 +00:00
>
%{opportunity.probability}
</span>
</div>
),
},
{
2025-09-15 14:13:20 +00:00
key: 'expectedCloseDate',
header: 'Beklenen Kapanış',
2025-09-15 09:31:47 +00:00
render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1 text-sm">
<FaCalendar className="w-3 h-3 text-gray-400" />
2025-09-15 14:13:20 +00:00
<span>{new Date(opportunity.expectedCloseDate).toLocaleDateString('tr-TR')}</span>
2025-09-15 09:31:47 +00:00
</div>
),
},
{
2025-09-15 14:13:20 +00:00
key: 'assignedTo',
header: 'Sorumlu',
2025-09-15 09:31:47 +00:00
render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1">
<FaUser className="w-4 h-4 text-gray-400" />
<span>{opportunity.assigned?.fullName}</span>
</div>
),
},
{
2025-09-15 14:13:20 +00:00
key: 'actions',
header: 'İşlemler',
2025-09-15 09:31:47 +00:00
render: (opportunity: CrmOpportunity) => (
<div className="flex gap-2">
<button
onClick={() => handleViewDetails(opportunity)}
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
title="Detayları Görüntüle"
>
<FaBullseye className="w-4 h-4" />
</button>
<button
onClick={() => handleEdit(opportunity)}
className="p-1 text-green-600 hover:bg-green-50 rounded"
title="Düzenle"
>
<FaEdit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(opportunity.id)}
className="p-1 text-red-600 hover:bg-red-50 rounded"
title="Sil"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
),
},
2025-09-15 14:13:20 +00:00
]
2025-09-15 09:31:47 +00:00
// Calculate statistics
2025-09-15 14:13:20 +00:00
const totalOpportunities = opportunities.length
2025-09-15 09:31:47 +00:00
const activeOpportunities = opportunities.filter(
(o) =>
2025-09-15 14:13:20 +00:00
o.stage !== OpportunityStageEnum.ClosedWon && o.stage !== OpportunityStageEnum.ClosedLost,
).length
const totalValue = opportunities.reduce((sum, opp) => sum + opp.estimatedValue, 0)
2025-09-15 09:31:47 +00:00
const averageProbability =
2025-09-15 14:13:20 +00:00
opportunities.reduce((sum, opp) => sum + opp.probability, 0) / opportunities.length || 0
2025-09-15 09:31:47 +00:00
// Stage distribution
2025-09-15 14:13:20 +00:00
const stageDistribution = Object.values(OpportunityStageEnum).map((stage) => ({
stage,
count: opportunities.filter((o) => o.stage === stage).length,
value: opportunities
.filter((o) => o.stage === stage)
.reduce((sum, o) => sum + o.estimatedValue, 0),
}))
2025-09-15 09:31:47 +00:00
return (
2025-09-15 14:13:20 +00:00
<Container>
2025-09-15 19:22:43 +00:00
<div className="space-y-2">
2025-09-15 14:13:20 +00:00
{/* Header */}
<div className="flex items-center justify-between">
<div>
2025-09-15 21:02:48 +00:00
<h2 className="text-2xl font-bold text-gray-900">Teklif & Fırsat Yönetimi</h2>
<p className="text-gray-600">Satış fırsatları ve teklif takibi</p>
2025-09-15 14:13:20 +00:00
</div>
<button
onClick={handleAdd}
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
<FaPlus className="w-4 h-4" />
Yeni Fırsat
</button>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 14:13:20 +00:00
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<Widget title="Toplam Fırsat" value={totalOpportunities} color="blue" icon="FaBullseye" />
2025-09-15 09:31:47 +00:00
2025-09-15 14:13:20 +00:00
<Widget title="Aktif Fırsat" value={activeOpportunities} color="green" icon="FaArrowUp" />
2025-09-15 09:31:47 +00:00
2025-09-15 14:13:20 +00:00
<Widget
title="Toplam Değer"
value={`${totalValue.toLocaleString()}`}
color="purple"
icon="FaDollarSign"
/>
2025-09-15 09:31:47 +00:00
2025-09-15 14:13:20 +00:00
<Widget
title="Ortalama Olasılık"
value={`%${averageProbability.toFixed(0)}`}
color="orange"
icon="FaArrowUp"
/>
</div>
2025-09-15 09:31:47 +00:00
2025-09-15 14:13:20 +00:00
{/* Stage Pipeline */}
<div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-3">Satış Aşamaları</h3>
<div className="grid grid-cols-2 md:grid-cols-6 gap-2">
{stageDistribution.map(({ stage, count, value }) => (
<div key={stage} className="text-center p-2 border rounded-lg">
<div
className={`inline-block px-2 py-0.5 text-xs font-medium rounded-full mb-1 ${getOpportunityStageColor(
stage,
)}`}
>
{getOpportunityStageText(stage)}
</div>
<div className="text-lg font-bold text-gray-900 mb-1">{count}</div>
<div className="text-xs text-gray-500">{value.toLocaleString()}</div>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 14:13:20 +00:00
))}
</div>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 14:13:20 +00:00
{/* Win Rate Analysis */}
<div className="bg-white rounded-lg shadow-sm border p-3">
<h3 className="text-sm font-semibold text-gray-900 mb-3">Başarı Oranı Analizi</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-green-600 mb-1">
{opportunities.filter((o) => o.stage === OpportunityStageEnum.ClosedWon).length}
</div>
<p className="text-sm text-gray-600">Kazanılan Fırsat</p>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 14:13:20 +00:00
<div className="text-center">
<div className="text-2xl font-bold text-red-600 mb-1">
{opportunities.filter((o) => o.stage === OpportunityStageEnum.ClosedLost).length}
</div>
<p className="text-sm text-gray-600">Kaybedilen Fırsat</p>
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 14:13:20 +00:00
<div className="text-center">
<div className="text-2xl font-bold text-blue-600 mb-1">
{Math.round(
(opportunities.filter((o) => o.stage === OpportunityStageEnum.ClosedWon).length /
Math.max(
opportunities.filter(
(o) =>
o.stage === OpportunityStageEnum.ClosedWon ||
o.stage === OpportunityStageEnum.ClosedLost,
).length,
1,
)) *
100,
)}
%
</div>
<p className="text-sm text-gray-600">Kazanma Oranı</p>
2025-09-15 09:31:47 +00:00
</div>
</div>
</div>
2025-09-15 14:13:20 +00:00
{/* Filters */}
<div className="flex gap-3 items-center">
<div className="flex-1">
<input
type="text"
placeholder="Fırsat başlığı ara..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
2025-09-15 09:31:47 +00:00
2025-09-15 14:13:20 +00:00
<select
value={selectedStage}
onChange={(e) => setSelectedStage(e.target.value as OpportunityStageEnum | 'all')}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">Tüm Aşamalar</option>
{Object.values(OpportunityStageEnum).map((stage) => (
<option key={stage} value={stage}>
{getOpportunityStageText(stage)}
</option>
))}
</select>
2025-09-15 09:31:47 +00:00
2025-09-15 14:13:20 +00:00
<select
value={selectedCustomer}
onChange={(e) => setSelectedCustomer(e.target.value)}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">Tüm Müşteriler</option>
{customers.map((customer) => (
<option key={customer.id} value={customer.id}>
{customer.name}
</option>
))}
</select>
</div>
2025-09-15 09:31:47 +00:00
2025-09-15 14:13:20 +00:00
{/* Data Table */}
<div className="bg-white rounded-lg shadow-sm border">
<DataTable data={filteredOpportunities} columns={columns} />
2025-09-15 09:31:47 +00:00
</div>
2025-09-15 14:13:20 +00:00
{filteredOpportunities.length === 0 && (
<div className="text-center py-12">
<FaBullseye className="w-10 h-10 text-gray-400 mx-auto mb-3" />
<h3 className="text-sm font-medium text-gray-900 mb-2">Fırsat bulunamadı</h3>
<p className="text-gray-500">Arama kriterlerinizi değiştirmeyi deneyin.</p>
</div>
)}
2025-09-15 19:22:43 +00:00
</div>
2025-09-15 14:13:20 +00:00
2025-09-15 19:22:43 +00:00
{/* Modals */}
<OpportunityForm
isOpen={isFormOpen}
onClose={handleCloseForm}
onSave={handleSaveOpportunity}
opportunity={selectedOpportunity}
mode={formMode}
/>
2025-09-15 09:31:47 +00:00
2025-09-15 19:22:43 +00:00
<OpportunityDetails
isOpen={isDetailsOpen}
onClose={handleCloseDetails}
onEdit={handleEditFromDetails}
opportunity={selectedOpportunity}
/>
2025-09-15 14:13:20 +00:00
</Container>
)
}
2025-09-15 09:31:47 +00:00
2025-09-15 14:13:20 +00:00
export default OpportunityManagement