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

471 lines
14 KiB
TypeScript
Raw Normal View History

2025-09-15 09:31:47 +00:00
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";
// Mock data - replace with actual data fetching
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?.fullName}</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 (
<div className="space-y-3 pt-2">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-gray-900">
Teklif & Fırsat Yönetimi
</h2>
<p className="text-sm 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>
)}
{/* Modals */}
<OpportunityForm
isOpen={isFormOpen}
onClose={handleCloseForm}
onSave={handleSaveOpportunity}
opportunity={selectedOpportunity}
mode={formMode}
/>
<OpportunityDetails
isOpen={isDetailsOpen}
onClose={handleCloseDetails}
onEdit={handleEditFromDetails}
opportunity={selectedOpportunity}
/>
</div>
);
};
export default OpportunityManagement;