erp-platform/ui/src/views/crm/components/OpportunityManagement.tsx
2025-12-04 00:01:00 +03:00

408 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react'
import {
FaBullseye,
FaPlus,
FaEdit,
FaTrash,
FaDollarSign,
FaCalendar,
FaArrowUp,
FaUser,
FaBuilding,
} 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'
import {
getOpportunityProbabilityColor,
getOpportunityStageColor,
getOpportunityStageText,
} from '../../../utils/erp'
import { Container } from '@/components/shared'
const OpportunityManagement: React.FC = () => {
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')
// Modal states
const [isFormOpen, setIsFormOpen] = useState(false)
const [isDetailsOpen, setIsDetailsOpen] = useState(false)
const [selectedOpportunity, setSelectedOpportunity] = useState<CrmOpportunity | null>(null)
const [formMode, setFormMode] = useState<'create' | 'edit'>('create')
const handleAdd = () => {
setSelectedOpportunity(null)
setFormMode('create')
setIsFormOpen(true)
}
const handleEdit = (opportunity: CrmOpportunity) => {
setSelectedOpportunity(opportunity)
setFormMode('edit')
setIsFormOpen(true)
}
const handleDelete = (id: string) => {
if (confirm('Bu fırsatı silmek istediğinizden emin misiniz?')) {
setOpportunities((prev) => prev.filter((opp) => opp.id !== id))
}
}
const handleViewDetails = (opportunity: CrmOpportunity) => {
setSelectedOpportunity(opportunity)
setIsDetailsOpen(true)
}
const handleSaveOpportunity = (opportunity: CrmOpportunity) => {
if (formMode === 'create') {
setOpportunities((prev) => [...prev, opportunity])
} else {
setOpportunities((prev) => prev.map((opp) => (opp.id === opportunity.id ? opportunity : opp)))
}
}
const handleCloseForm = () => {
setIsFormOpen(false)
setSelectedOpportunity(null)
}
const handleCloseDetails = () => {
setIsDetailsOpen(false)
setSelectedOpportunity(null)
}
const handleEditFromDetails = (opportunity: CrmOpportunity) => {
setIsDetailsOpen(false)
handleEdit(opportunity)
}
const filteredOpportunities = opportunities.filter((opportunity) => {
if (
searchTerm &&
!opportunity.title.toLowerCase().includes(searchTerm.toLowerCase()) &&
!opportunity.description?.toLowerCase().includes(searchTerm.toLowerCase())
) {
return false
}
if (selectedStage !== 'all' && opportunity.stage !== selectedStage) {
return false
}
if (selectedCustomer !== 'all' && opportunity.customerId !== selectedCustomer) {
return false
}
return true
})
const columns: Column<CrmOpportunity>[] = [
{
key: 'title',
header: 'Fırsat Başlığı',
sortable: true,
render: (opportunity: CrmOpportunity) => (
<div>
<div className="font-medium text-gray-900">{opportunity.title}</div>
<div className="text-sm text-gray-500">{opportunity.opportunityNumber}</div>
</div>
),
},
{
key: 'customer',
header: 'Müşteri',
render: (opportunity: CrmOpportunity) => {
const customer = customers.find((c) => c.id === opportunity.customerId)
return customer ? (
<div className="flex items-center gap-2">
<FaBuilding className="w-4 h-4 text-gray-400" />
<span>{customer.name}</span>
</div>
) : (
'-'
)
},
},
{
key: 'stage',
header: 'Aşama',
render: (opportunity: CrmOpportunity) => (
<span
className={`px-2 py-1 text-xs font-medium rounded-full ${getOpportunityStageColor(
opportunity.stage,
)}`}
>
{getOpportunityStageText(opportunity.stage)}
</span>
),
},
{
key: 'estimatedValue',
header: 'Tahmini Değer',
render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1">
<FaDollarSign className="w-4 h-4 text-gray-400" />
<span className="font-medium">{opportunity.estimatedValue.toLocaleString()}</span>
</div>
),
},
{
key: 'probability',
header: 'Olasılık',
render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1">
<FaArrowUp className="w-4 h-4 text-gray-400" />
<span
className={`font-medium ${getOpportunityProbabilityColor(opportunity.probability)}`}
>
%{opportunity.probability}
</span>
</div>
),
},
{
key: 'expectedCloseDate',
header: 'Beklenen Kapanış',
render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1 text-sm">
<FaCalendar className="w-3 h-3 text-gray-400" />
<span>{new Date(opportunity.expectedCloseDate).toLocaleDateString('tr-TR')}</span>
</div>
),
},
{
key: 'assignedTo',
header: 'Sorumlu',
render: (opportunity: CrmOpportunity) => (
<div className="flex items-center gap-1">
<FaUser className="w-4 h-4 text-gray-400" />
<span>{opportunity.assigned?.name}</span>
</div>
),
},
{
key: 'actions',
header: 'İşlemler',
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>
),
},
]
// Calculate statistics
const totalOpportunities = opportunities.length
const activeOpportunities = opportunities.filter(
(o) =>
o.stage !== OpportunityStageEnum.ClosedWon && o.stage !== OpportunityStageEnum.ClosedLost,
).length
const totalValue = opportunities.reduce((sum, opp) => sum + opp.estimatedValue, 0)
const averageProbability =
opportunities.reduce((sum, opp) => sum + opp.probability, 0) / opportunities.length || 0
// Stage distribution
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),
}))
return (
<Container>
<div className="space-y-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<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>
</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>
</div>
{/* 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" />
<Widget title="Aktif Fırsat" value={activeOpportunities} color="green" icon="FaArrowUp" />
<Widget
title="Toplam Değer"
value={`${totalValue.toLocaleString()}`}
color="purple"
icon="FaDollarSign"
/>
<Widget
title="Ortalama Olasılık"
value={`%${averageProbability.toFixed(0)}`}
color="orange"
icon="FaArrowUp"
/>
</div>
{/* 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>
</div>
))}
</div>
</div>
{/* 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>
</div>
<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>
</div>
<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>
</div>
</div>
</div>
{/* 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>
<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>
<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>
{/* Data Table */}
<div className="bg-white rounded-lg shadow-sm border">
<DataTable data={filteredOpportunities} columns={columns} />
</div>
{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>
)}
</div>
{/* Modals */}
<OpportunityForm
isOpen={isFormOpen}
onClose={handleCloseForm}
onSave={handleSaveOpportunity}
opportunity={selectedOpportunity}
mode={formMode}
/>
<OpportunityDetails
isOpen={isDetailsOpen}
onClose={handleCloseDetails}
onEdit={handleEditFromDetails}
opportunity={selectedOpportunity}
/>
</Container>
)
}
export default OpportunityManagement