Files
ihaleist/src/components/KanbanBoard.jsx
T

205 lines
10 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 { PreparationOverview } from './PreparationOverview';
import { useDeadlineClock } from '../hooks/useDeadlineClock';
import { deadlineInfo } from '../services/tenderCalendar';
import React, { useState } from 'react';
import { useApp } from '../context/AppContext';
import { Kanban, User, Calendar, MapPin, Eye } from 'lucide-react';
export const KanbanBoard = () => {
const now = useDeadlineClock();
const { tenders, updateKanbanStatus, setActiveTenderDetail, teamMembers, savingCount, saveError } = 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),
])];
// Panoda yalnızca takibe alınan ya da bir kolona taşınmış ihaleler durur.
// Sunucu HER ilana varsayılan 'incelenecek' durumu veriyor; onu "panoda"
// saymak 8 bin+ kartı tek seferde çizdirip sekmeyi saniyelerce donduruyordu.
const filteredTenders = tenders.filter(t => {
const panoda = t.takipte || (t.kanbanStatus && t.kanbanStatus !== 'incelenecek');
if (!panoda) return false;
if (selectedPersonel && t.atananPersonel !== selectedPersonel) return false;
return true;
});
const handleDragStart = (e, id) => {
if(savingCount > 0){e.preventDefault();return;}
e.dataTransfer.setData('tenderId', id);
setDraggedTenderId(id);
};
const handleDragOver = (e) => {
e.preventDefault();
};
const handleDrop = (e, columnId) => {
e.preventDefault();
if(savingCount > 0)return;
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">Kartın durumunu seçerek veya kartı kolonlar arasında sürükleyerek süreci yönetin.</p>
</div>
</div>
{/* Personnel filter */}
<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
aria-label="Sorumluya göre filtrele"
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>
<p className="text-xs text-slate-500">Slack/Teams gönderimi bu panoda desteklenmiyor.</p>
</div>
</div>
{savingCount > 0 && <p role="status" className="text-sm text-blue-800">Değişiklik kaydediliyor…</p>}
{saveError && <p role="alert" className="text-sm text-red-700">{saveError}</p>}
<PreparationOverview tenders={filteredTenders} now={now}/>
{/* 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={savingCount === 0}
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>
{t.gorev && (() => {
const info = deadlineInfo(t, now);
const kalan = info.days;
const renk = info.expired || kalan === null ? 'bg-slate-200 text-slate-600' : kalan <= 2 ? 'bg-red-100 text-red-800' : kalan <= 7 ? 'bg-amber-100 text-amber-800' : 'bg-emerald-100 text-emerald-800';
return <span className={`text-[10px] font-extrabold px-2 py-0.5 rounded ${renk}`} title={`İhale: ${t.tarih} ${t.saat || 'saat bilinmiyor'} (Türkiye saati)`}>
{info.label}
</span>;
})()}
<button
onClick={() => setActiveTenderDetail(t)}
className="text-slate-400 hover:text-blue-600 p-0.5"
aria-label={`${t.ikn} ihale detayını aç`}
title="Detayı Aç"
>
<Eye className="w-3.5 h-3.5" />
</button>
</div>
<button type="button"
onClick={() => setActiveTenderDetail(t)}
className="text-left font-bold text-xs text-slate-900 line-clamp-2 hover:text-blue-600 cursor-pointer leading-snug"
>
{t.baslik}
</button>
<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>
<label className="block text-[10px] font-semibold text-slate-600">İş akışı durumu
<select aria-label={`${t.ikn} iş akışı durumu`} value={col.id} disabled={savingCount > 0}
onChange={e=>updateKanbanStatus(t.id,e.target.value)}
className="block w-full mt-1 border rounded p-1.5 bg-slate-50 disabled:opacity-50">
{columns.map(column=><option key={column.id} value={column.id}>{column.title}</option>)}
</select>
</label>
{/* 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
aria-label={`${t.ikn} sorumlusu`}
disabled={savingCount > 0}
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>
);
};