Files
ihaleist/src/components/KanbanBoard.jsx
T
hikmettan 0ab74143c0 feat: demo özelliklerini gerçekleştir — canlı veri + localStorage
- analytics.js: firmStats/itemStats/provinceDensity (gerçek hesaplama)
- Rakip Radar: canlı veriden kazanan firma analizi (mock 3 firma kaldırıldı)
- Fiyat Analizi: gerçek kalem min/medyan/ort/maks + CSV export
- TurkeyHeatMap: 81 il gerçek interaktif yoğunluk haritası (tıkla→filtrele)
- Favori kurumlar: kart yıldızı + hızlı filtre (localStorage)
- Ekip yönetimi: gerçek CRUD (RightSidebar) + Kanban ataması ekipten
- Bildirimler: gerçek Notification API + izin + test + sessiz saatler
- PDF: jsPDF ile gerçek birim fiyat teklif cetveli (kaşe/imza alanı)
2026-08-28 01:58:46 +03:00

179 lines
8.0 KiB
React
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 { useApp } from '../context/AppContext';
import { Kanban, User, Calendar, MapPin, Star, Plus, ArrowRight, Eye } from 'lucide-react';
export const KanbanBoard = () => {
const { tenders, updateKanbanStatus, setActiveTenderDetail, teamMembers } = useApp();
const [selectedPersonel, setSelectedPersonel] = useState('');
const [draggedTenderId, setDraggedTenderId] = useState(null);
const columns = [
{ id: 'incelenecek', title: 'İncelenecek', color: 'bg-slate-100 border-slate-300 text-slate-800' },
{ id: 'inceleniyor', title: 'İnceleniyor', color: 'bg-blue-50 border-blue-200 text-blue-900' },
{ id: 'teklif_hazirlaniyor', title: 'Teklif Hazırlanıyor', color: 'bg-amber-50 border-amber-200 text-amber-900' },
{ id: 'teklif_verildi', title: 'Teklif Verildi', color: 'bg-purple-50 border-purple-200 text-purple-900' },
{ id: 'kazanildi', title: 'Kazanıldı', color: 'bg-emerald-50 border-emerald-200 text-emerald-900' },
{ id: 'kaybedildi', title: 'Kaybedildi', color: 'bg-rose-50 border-rose-200 text-rose-900' }
];
// Gerçek ekip üyelerinden (Ekip Yönetimi paneli) + atanmış eski isimler
const personnelList = [...new Set([
...teamMembers.map((m) => m.name),
...tenders.map((t) => t.atananPersonel).filter(Boolean),
])];
const filteredTenders = tenders.filter(t => {
if (!t.takipte && !t.kanbanStatus) return false;
if (selectedPersonel && t.atananPersonel !== selectedPersonel) return false;
return true;
});
const handleDragStart = (e, id) => {
e.dataTransfer.setData('tenderId', id);
setDraggedTenderId(id);
};
const handleDragOver = (e) => {
e.preventDefault();
};
const handleDrop = (e, columnId) => {
e.preventDefault();
const id = e.dataTransfer.getData('tenderId') || draggedTenderId;
if (id) {
updateKanbanStatus(id, columnId);
setDraggedTenderId(null);
}
};
return (
<div className="space-y-4 my-4 animate-fade-in">
{/* Kanban Header Controls */}
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-xs flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-blue-600 text-white flex items-center justify-center shadow-md">
<Kanban className="w-6 h-6" />
</div>
<div>
<h1 className="text-xl font-black text-slate-900">Ekip Kanban Takip Panosu</h1>
<p className="text-xs text-slate-500">Kartları kolonlar arasında sürükleyerek ihale süreçlerinizi yönetin.</p>
</div>
</div>
{/* Personnel Filter & Slack Notification Buttons */}
<div className="flex items-center gap-3 w-full sm:w-auto">
<div className="flex items-center gap-2 bg-slate-50 border border-slate-300 rounded-xl px-3 py-1.5 text-xs">
<User className="w-4 h-4 text-slate-400" />
<select
value={selectedPersonel}
onChange={(e) => setSelectedPersonel(e.target.value)}
className="bg-transparent font-semibold text-slate-800 outline-none"
>
<option value="">Tüm Personeller</option>
{personnelList.map(p => <option key={p} value={p}>{p}</option>)}
</select>
</div>
<button
onClick={() => alert('Slack & Teams kanalına Kanban güncel durumu başarıyla gönderildi!')}
className="bg-slate-900 hover:bg-slate-800 text-white font-bold text-xs px-3.5 py-2 rounded-xl flex items-center gap-1.5 transition-colors shadow-xs shrink-0"
title="Slack & Teams Ekip Kanalına Bildirim Gönder"
>
<span>💬 Slack/Teams'e Gönder</span>
</button>
</div>
</div>
{/* Kanban Columns Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-3.5 overflow-x-auto pb-4">
{columns.map(col => {
const colTenders = filteredTenders.filter(t => (t.kanbanStatus || 'incelenecek') === col.id);
return (
<div
key={col.id}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, col.id)}
className={`rounded-2xl border ${col.color} p-3 flex flex-col min-h-[550px] shadow-2xs transition-colors`}
>
{/* Column Header */}
<div className="flex items-center justify-between border-b border-black/10 pb-2 mb-3">
<span className="font-extrabold text-xs uppercase tracking-tight">{col.title}</span>
<span className="bg-white/80 font-black text-xs px-2 py-0.5 rounded-full border border-black/10 shadow-2xs">
{colTenders.length}
</span>
</div>
{/* Column Cards */}
<div className="space-y-3 flex-1 overflow-y-auto max-h-[650px] pr-0.5">
{colTenders.length === 0 ? (
<div className="py-10 text-center border-2 border-dashed border-black/10 rounded-xl">
<p className="text-[11px] font-semibold text-slate-400">İhale yok</p>
</div>
) : (
colTenders.map(t => (
<div
key={t.id}
draggable
onDragStart={(e) => handleDragStart(e, t.id)}
className="bg-white rounded-xl p-3 border border-slate-200 shadow-xs hover:shadow-md cursor-grab active:cursor-grabbing space-y-2 transition-all group"
>
<div className="flex items-start justify-between gap-1">
<span className="text-[10px] font-extrabold bg-blue-100 text-blue-800 px-2 py-0.5 rounded">
#{t.ikn}
</span>
<button
onClick={() => setActiveTenderDetail(t)}
className="text-slate-400 hover:text-blue-600 p-0.5"
title="Detayı Aç"
>
<Eye className="w-3.5 h-3.5" />
</button>
</div>
<h4
onClick={() => setActiveTenderDetail(t)}
className="font-bold text-xs text-slate-900 line-clamp-2 hover:text-blue-600 cursor-pointer leading-snug"
>
{t.baslik}
</h4>
<div className="flex items-center justify-between text-[10px] text-slate-500 font-medium">
<span className="flex items-center gap-1">
<MapPin className="w-3 h-3 text-rose-500" /> {t.sehir}
</span>
<span className="flex items-center gap-1 font-bold text-blue-900">
<Calendar className="w-3 h-3 text-blue-500" /> {t.tarih}
</span>
</div>
{/* Personnel Select on Card */}
<div className="pt-1.5 border-t border-slate-100 flex items-center justify-between text-[10px]">
<span className="text-slate-400">Atanan:</span>
<select
value={t.atananPersonel || ''}
onChange={(e) => updateKanbanStatus(t.id, col.id, e.target.value)}
className="bg-slate-50 border border-slate-200 rounded px-1.5 py-0.5 text-[10px] font-bold text-slate-800"
>
<option value="">Atama Yap</option>
{personnelList.map(p => (
<option key={p} value={p}>{p}</option>
))}
</select>
</div>
</div>
))
)}
</div>
</div>
);
})}
</div>
</div>
);
};